mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-01 15:07:17 +08:00
Merge branch 'release/2.2.0' of ssh://git.yunion.io/~quxuan/onecloud into feature/qx-azure
This commit is contained in:
Generated
+15
-6
@@ -1,6 +1,14 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4b94878e125f31ec67c892ecfe38c13211da40653281588598218f807ae72bf7"
|
||||
name = "github.com/360EntSecGroup-Skylar/excelize"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "eb62256d165607c6877ce88efbba10c119137b3d"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f7a3877d7116adfcdcd641f9f6ad18fc2126bd5c080d8854cc3b395a01e0b7f3"
|
||||
name = "github.com/DataDog/dd-trace-go"
|
||||
@@ -1105,11 +1113,11 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:b71bcbb9d425fa8d1d0f1932e275768930808600a24368abd62b5fc4a945c32b"
|
||||
digest = "1:49ffc35ec8d3f7789393cd132acd359e8ac1f5d38c7a2b91c484b041840c62c0"
|
||||
name = "yunion.io/x/jsonutils"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "6dc5b0d8959336346efaacd9c9ab1defa80d5ee9"
|
||||
revision = "d1290e94d4753c1748fc7c89f472a523cc0a5c08"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -1124,7 +1132,7 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e5dd1b8806ddb16ddd31b64c2208d0cc566cd184d12f63844c33581c28d7fa50"
|
||||
digest = "1:211ecaed7d1d87e5d216b598c3cd5b7c47977b73a06308e1b39c08fd03c9c417"
|
||||
name = "yunion.io/x/pkg"
|
||||
packages = [
|
||||
"gotypes",
|
||||
@@ -1158,7 +1166,7 @@
|
||||
"utils",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "98dfb17dd78a596fbe5542e8276f4c717dc752fe"
|
||||
revision = "9d246215b0a167b153bcdbc7d75031ced7138cf8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -1170,16 +1178,17 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:b62e87e9e21e5d6933164761e3f1861c580d0612fcde43cc8c6b2e4db34ce9f8"
|
||||
digest = "1:f07a1ef9758f56186dd9039a8608bc9d537070a7c04479dbab6aeb42501001b7"
|
||||
name = "yunion.io/x/structarg"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "5a0eff15d64f686ee66801ac42284a797115e55f"
|
||||
revision = "ba8620a6258308b3faa4174d4c2d81a6f70b57ea"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/360EntSecGroup-Skylar/excelize",
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/sdk",
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/errors",
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests",
|
||||
|
||||
@@ -105,3 +105,7 @@
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/texttheater/golang-levenshtein"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/360EntSecGroup-Skylar/excelize"
|
||||
version = "v1.3.0"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
auth_url = http://10.168.222.251:35357/v3
|
||||
admin_user = sysadmin
|
||||
admin_passwd = MxqhTC2VKe067jtD
|
||||
admin_project = system
|
||||
port = 9889
|
||||
cors_hosts = *
|
||||
sql_connection = 'mysql+pymysql://yunionconf:PASSWORD@10.168.222.1:3306/yunionconf?charset=utf8'
|
||||
auto_sync_table = True
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Yunion Conf Service
|
||||
Documentation=http://doc.yunionyun.com
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=yunion
|
||||
Group=yunion
|
||||
ExecStart=/opt/yunion/bin/yunionconf --config /etc/yunion/yunionconf.conf
|
||||
WorkingDirectory=/opt/yunion
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1 @@
|
||||
DESCRIPTION="Yunion Conf Service"
|
||||
@@ -194,4 +194,18 @@ func init() {
|
||||
printObject(disk)
|
||||
return nil
|
||||
})
|
||||
type DiskResetOptions struct {
|
||||
DISK string `help:"ID or name of disk"`
|
||||
SNAPSHOT string `help:"snapshots ID of disk`
|
||||
}
|
||||
R(&DiskResetOptions{}, "disk-reset", "Resize a disk", func(s *mcclient.ClientSession, args *DiskResetOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString(args.SNAPSHOT), "snapshot_id")
|
||||
disk, err := modules.Disks.PerformAction(s, args.DISK, "disk-reset", params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(disk)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ func init() {
|
||||
type ElasticipListOptions struct {
|
||||
Manager string `help:"Show servers imported from manager"`
|
||||
Region string `help:"Show servers in cloudregion"`
|
||||
Usable bool `help:"List all zones that is usable"`
|
||||
|
||||
options.BaseListOptions
|
||||
}
|
||||
@@ -30,6 +31,9 @@ func init() {
|
||||
if len(args.Region) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Region), "region")
|
||||
}
|
||||
if args.Usable {
|
||||
params.Add(jsonutils.JSONTrue, "usable")
|
||||
}
|
||||
results, err := modules.Elasticips.List(s, params)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/options"
|
||||
)
|
||||
|
||||
func init() {
|
||||
type ParametersListOptions struct {
|
||||
NamespaceId string `help:"List parameter of specificated namespace id, ADMIN only"`
|
||||
User string `help:"List parameter of specificated user id, ADMIN only"`
|
||||
Service string `help:"List parameter of specificated service id, ADMIN only"`
|
||||
options.BaseListOptions
|
||||
}
|
||||
|
||||
R(&ParametersListOptions{}, "parameter-list", "list parameters", func(s *mcclient.ClientSession, args *ParametersListOptions) error {
|
||||
params, err := options.ListStructToParams(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result *modules.ListResult
|
||||
if len(args.NamespaceId) > 0 {
|
||||
params.Add(jsonutils.NewString(args.NamespaceId), "namespace_id")
|
||||
result, err = modules.Parameters.List(s, params)
|
||||
} else if len(args.User) > 0 {
|
||||
result, err = modules.Parameters.ListInContext(s, params, &modules.UsersV3, args.User)
|
||||
} else if len(args.Service) > 0 {
|
||||
result, err = modules.Parameters.ListInContext(s, params, &modules.ServicesV3, args.Service)
|
||||
} else {
|
||||
result, err = modules.Parameters.List(s, params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printList(result, modules.Parameters.GetColumns(s))
|
||||
return nil
|
||||
})
|
||||
|
||||
type ParametersShowOptions struct {
|
||||
NamespaceId string `help:"Show parameter of specificated namespace id, ADMIN only"`
|
||||
User string `help:"Show parameter of specificated user id, ADMIN only"`
|
||||
Service string `help:"Show parameter of specificated service id, ADMIN only"`
|
||||
NAME string `help:"The name of parameter"`
|
||||
}
|
||||
|
||||
R(&ParametersShowOptions{}, "parameter-show", "show a parameter", func(s *mcclient.ClientSession, args *ParametersShowOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
if len(args.NamespaceId) > 0 {
|
||||
params.Add(jsonutils.JSONTrue, "admin")
|
||||
params.Add(jsonutils.NewString(args.NamespaceId), "namespace_id")
|
||||
}
|
||||
|
||||
var parameter jsonutils.JSONObject
|
||||
var err error
|
||||
if len(args.NamespaceId) > 0 {
|
||||
params.Add(jsonutils.NewString(args.NamespaceId), "namespace_id")
|
||||
parameter, err = modules.Parameters.Get(s, args.NAME, params)
|
||||
} else if len(args.User) > 0 {
|
||||
parameter, err = modules.Parameters.GetInContext(s, args.NAME, params, &modules.UsersV3, args.User)
|
||||
} else if len(args.Service) > 0 {
|
||||
parameter, err = modules.Parameters.GetInContext(s, args.NAME, params, &modules.ServicesV3, args.Service)
|
||||
} else {
|
||||
parameter, err = modules.Parameters.Get(s, args.NAME, params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(parameter)
|
||||
return nil
|
||||
})
|
||||
|
||||
type ParametersCreateOptions struct {
|
||||
User string `help:"Create parameter for specificated user id, ADMIN only"`
|
||||
Service string `help:"Create parameter for specificated service id, ADMIN only"`
|
||||
NAME string `help:"The name of parameter"`
|
||||
VALUE string `help:"The content of parameter"`
|
||||
}
|
||||
|
||||
R(&ParametersCreateOptions{}, "parameter-create", "create a parameter", func(s *mcclient.ClientSession, args *ParametersCreateOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString(args.NAME), "name")
|
||||
params.Add(jsonutils.NewString(args.VALUE), "value")
|
||||
|
||||
if len(args.User) > 0 {
|
||||
params.Add(jsonutils.NewString(args.User), "user_id")
|
||||
} else if len(args.Service) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Service), "service_id")
|
||||
}
|
||||
|
||||
parameter, err := modules.Parameters.Create(s, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(parameter)
|
||||
return nil
|
||||
})
|
||||
|
||||
type ParametersUpdateOptions struct {
|
||||
User string `help:"Update parameter of specificated user id, ADMIN only"`
|
||||
Service string `help:"Update parameter of specificated service id, ADMIN only"`
|
||||
NAME string `help:"The name of parameter"`
|
||||
VALUE string `help:"The content of parameter"`
|
||||
}
|
||||
|
||||
R(&ParametersUpdateOptions{}, "parameter-update", "update parameter", func(s *mcclient.ClientSession, args *ParametersUpdateOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
if len(args.VALUE) > 0 {
|
||||
params.Add(jsonutils.NewString(args.VALUE), "value")
|
||||
}
|
||||
|
||||
var parameter jsonutils.JSONObject
|
||||
var err error
|
||||
if len(args.User) > 0 {
|
||||
parameter, err = modules.Parameters.PutInContext(s, args.NAME, params, &modules.UsersV3, args.User)
|
||||
} else if len(args.Service) > 0 {
|
||||
parameter, err = modules.Parameters.PutInContext(s, args.NAME, params, &modules.ServicesV3, args.Service)
|
||||
} else {
|
||||
parameter, err = modules.Parameters.Put(s, args.NAME, params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(parameter)
|
||||
return nil
|
||||
})
|
||||
|
||||
type ParametersDeleteOptions struct {
|
||||
User string `help:"Delete parameter of specificated user id, ADMIN only"`
|
||||
Service string `help:"Delete parameter of specificated service id, ADMIN only"`
|
||||
NAME string `help:"The name of parameter"`
|
||||
}
|
||||
|
||||
R(&ParametersDeleteOptions{}, "parameter-delete", "delete notice", func(s *mcclient.ClientSession, args *ParametersDeleteOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
|
||||
var parameter jsonutils.JSONObject
|
||||
var err error
|
||||
if len(args.User) > 0 {
|
||||
parameter, err = modules.Parameters.DeleteInContext(s, args.NAME, params, &modules.UsersV3, args.User)
|
||||
} else if len(args.Service) > 0 {
|
||||
parameter, err = modules.Parameters.DeleteInContext(s, args.NAME, params, &modules.ServicesV3, args.Service)
|
||||
} else {
|
||||
parameter, err = modules.Parameters.Delete(s, args.NAME, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(parameter)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type QuotaBaseOptions struct {
|
||||
Ebw int64 `help:"External bandwidth in Mbps"`
|
||||
Image int64 `help:"Template count"`
|
||||
IsolatedDevice int64 `help:"Isolated device count"`
|
||||
Snapshot int64 `help:"Snapshot count"`
|
||||
}
|
||||
|
||||
func quotaArgs2Params(args *QuotaBaseOptions) *jsonutils.JSONDict {
|
||||
@@ -51,6 +52,9 @@ func quotaArgs2Params(args *QuotaBaseOptions) *jsonutils.JSONDict {
|
||||
if args.IsolatedDevice > 0 {
|
||||
params.Add(jsonutils.NewInt(args.IsolatedDevice), "isolated_device")
|
||||
}
|
||||
if args.Snapshot > 0 {
|
||||
params.Add(jsonutils.NewInt(args.Snapshot), "snapshot")
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ func init() {
|
||||
StatMonth string `help:"stat_month of the query"`
|
||||
StartDate string `help:"start_date of the query"`
|
||||
EndDate string `help:"end_date of the query"`
|
||||
ResType string `help:"res_type of the query"`
|
||||
Platform string `help:"platform of the query"`
|
||||
ProjectId string `help:"project_id of the query"`
|
||||
}
|
||||
R(&ResResultsListOptions{}, "resresult-list", "List all res results ", func(s *mcclient.ClientSession, args *ResResultsListOptions) error {
|
||||
@@ -36,6 +38,12 @@ func init() {
|
||||
if len(args.EndDate) > 0 {
|
||||
params.Add(jsonutils.NewString(args.EndDate), "end_date")
|
||||
}
|
||||
if len(args.ResType) > 0 {
|
||||
params.Add(jsonutils.NewString(args.ResType), "res_type")
|
||||
}
|
||||
if len(args.Platform) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Platform), "platform")
|
||||
}
|
||||
if len(args.ProjectId) > 0 {
|
||||
params.Add(jsonutils.NewString(args.ProjectId), "project_id")
|
||||
}
|
||||
@@ -48,43 +56,4 @@ func init() {
|
||||
printList(result, modules.ResResults.GetColumns(s))
|
||||
return nil
|
||||
})
|
||||
|
||||
type ResResultUpdateOptions struct {
|
||||
ID string `help:"ID of the query"`
|
||||
StatMonth string `help:"stat_month of the query"`
|
||||
StartDate string `help:"start_date of the query"`
|
||||
EndDate string `help:"end_date of the query"`
|
||||
ProjectId string `help:"project_id of the query"`
|
||||
|
||||
ItemKey string `help:"item_key of the query"`
|
||||
ItemText string `help:"item_text of the query"`
|
||||
}
|
||||
R(&ResResultUpdateOptions{}, "resresult-export", "Update a resresult export", func(s *mcclient.ClientSession, args *ResResultUpdateOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
if len(args.StatMonth) > 0 {
|
||||
params.Add(jsonutils.NewString(args.StatMonth), "stat_month")
|
||||
}
|
||||
|
||||
if len(args.StartDate) > 0 {
|
||||
params.Add(jsonutils.NewString(args.StartDate), "start_date")
|
||||
}
|
||||
if len(args.EndDate) > 0 {
|
||||
params.Add(jsonutils.NewString(args.EndDate), "end_date")
|
||||
}
|
||||
if len(args.ProjectId) > 0 {
|
||||
params.Add(jsonutils.NewString(args.ProjectId), "project_id")
|
||||
}
|
||||
if len(args.ItemKey) > 0 {
|
||||
params.Add(jsonutils.NewString(args.ItemKey), "item_key")
|
||||
}
|
||||
if len(args.ItemText) > 0 {
|
||||
params.Add(jsonutils.NewString(args.ItemText), "item_text")
|
||||
}
|
||||
resResult, err := modules.ResResults.Put(s, args.ID, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(resResult)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ func init() {
|
||||
type ServerDiskListOptions struct {
|
||||
options.BaseListOptions
|
||||
Server string `help:"ID or Name of Server"`
|
||||
Disk string `help:"ID or name of disk"`
|
||||
Index int64 `help:"disk index" default:"-1"`
|
||||
}
|
||||
R(&ServerDiskListOptions{}, "server-disk-list", "List server disk pairs", func(s *mcclient.ClientSession, args *ServerDiskListOptions) error {
|
||||
var params *jsonutils.JSONDict
|
||||
@@ -23,10 +25,15 @@ func init() {
|
||||
|
||||
}
|
||||
}
|
||||
if args.Index >= 0 {
|
||||
params.Add(jsonutils.NewInt(args.Index), "index")
|
||||
}
|
||||
var result *modules.ListResult
|
||||
var err error
|
||||
if len(args.Server) > 0 {
|
||||
result, err = modules.Serverdisks.ListDescendent(s, args.Server, params)
|
||||
} else if len(args.Disk) > 0 {
|
||||
result, err = modules.Serverdisks.ListDescendent2(s, args.Disk, params)
|
||||
} else {
|
||||
result, err = modules.Serverdisks.List(s, params)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ func init() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printList(result, modules.Servers.GetColumns(s))
|
||||
if len(opts.ExportFile) > 0 {
|
||||
exportList(result, opts.ExportFile, opts.ExportKeys, opts.ExportTexts, modules.Servers.GetColumns(s))
|
||||
} else {
|
||||
printList(result, modules.Servers.GetColumns(s))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -54,4 +54,15 @@ func init() {
|
||||
printObject(result)
|
||||
return nil
|
||||
})
|
||||
type SnapshotShowOptions struct {
|
||||
ID string `help:"ID or Name of snapshot"`
|
||||
}
|
||||
R(&SnapshotShowOptions{}, "snapshot-show", "Show snapshot details", func(s *mcclient.ClientSession, args *SnapshotShowOptions) error {
|
||||
result, err := modules.Snapshots.Get(s, args.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(result)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
"yunion.io/x/onecloud/pkg/util/printutils"
|
||||
"yunion.io/x/onecloud/pkg/util/excelutils"
|
||||
)
|
||||
|
||||
func printList(list *modules.ListResult, columns []string) {
|
||||
@@ -15,6 +17,27 @@ func printObject(obj jsonutils.JSONObject) {
|
||||
printutils.PrintJSONObject(obj)
|
||||
}
|
||||
|
||||
func printObjectRecursive(obj jsonutils.JSONObject) {
|
||||
printutils.PrintJSONObjectRecursive(obj)
|
||||
}
|
||||
|
||||
func printObjectRecursiveEx(obj jsonutils.JSONObject, cb printutils.PrintJSONObjectRecursiveExFunc) {
|
||||
printutils.PrintJSONObjectRecursiveEx(obj, cb)
|
||||
}
|
||||
|
||||
func printBatchResults(results []modules.SubmitResult, columns []string) {
|
||||
printutils.PrintJSONBatchResults(results, columns)
|
||||
}
|
||||
|
||||
func exportList(list *modules.ListResult, file string, exportKeys string, exportTexts string, columns []string) {
|
||||
var keys []string
|
||||
var texts []string
|
||||
if len(exportKeys) > 0 {
|
||||
keys = strings.Split(exportKeys, ",")
|
||||
texts = strings.Split(exportTexts, ",")
|
||||
}else {
|
||||
keys = columns
|
||||
texts = columns
|
||||
}
|
||||
excelutils.ExportFile(list.Data, keys, texts, file)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
Yunion Conf Service
|
||||
“参数服务”在服务器端为指定用户持久化存储和管理个性化参数,例如 控制台的配置,列表的colume配置等,从而实现产品的个性化配置。
|
||||
*/
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/pkg/yunionconf/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
service.StartService()
|
||||
}
|
||||
@@ -25,7 +25,6 @@ func CheckSync(autoSync bool) bool {
|
||||
tableSpec := modelMan.TableSpec()
|
||||
sqls := tableSpec.SyncSQL()
|
||||
for _, sql := range sqls {
|
||||
// fmt.Printf("%s\n", sql)
|
||||
allSqls = append(allSqls, sql)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +62,7 @@ func commitSqlDIffs(sqls []string) error {
|
||||
log.Infof("Exec %s", sql)
|
||||
_, err := db.Exec(sql)
|
||||
if err != nil {
|
||||
log.Errorf("Exec sql %s failed %s", sql, err)
|
||||
log.Errorf("Exec sql failed %s\n%s", sql, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ const (
|
||||
ACT_SNAPSHOT_DELETE_FAIL = "snapshot_del_fail"
|
||||
ACT_SNAPSHOT_UNLINK = "snapshot_unlink"
|
||||
|
||||
ACT_DISK_CLEAN_UP_SNAPSHOTS = "disk_clean_up_snapshots"
|
||||
ACT_DISK_CLEAN_UP_SNAPSHOTS_FAIL = "disk_clean_up_snapshots_fail"
|
||||
|
||||
ACT_ALLOCATING = "allocating"
|
||||
ACT_ALLOCATE = "allocate"
|
||||
ACT_ALLOCATE_FAIL = "alloc_fail"
|
||||
|
||||
@@ -14,7 +14,7 @@ type SResourceBase struct {
|
||||
|
||||
CreatedAt time.Time `nullable:"false" created_at:"true" get:"user"`
|
||||
UpdatedAt time.Time `nullable:"false" updated_at:"true" list:"user"`
|
||||
UpdateVersion int `default:"0" nullable:"false" auto_version:"true"`
|
||||
UpdateVersion int `default:"0" nullable:"false" auto_version:"true" list:"user"`
|
||||
DeletedAt time.Time ``
|
||||
Deleted bool `nullable:"false" default:"false"`
|
||||
}
|
||||
@@ -31,6 +31,10 @@ func (manager *SResourceBaseManager) Query(fields ...string) *sqlchemy.SQuery {
|
||||
return manager.SModelBaseManager.Query(fields...).IsFalse("deleted")
|
||||
}
|
||||
|
||||
func (manager *SResourceBaseManager) RawQuery(fields ...string) *sqlchemy.SQuery {
|
||||
return manager.SModelBaseManager.Query(fields...)
|
||||
}
|
||||
|
||||
func CanDelete(model IModel, ctx context.Context) bool {
|
||||
err := model.ValidateDeleteCondition(ctx)
|
||||
if err == nil {
|
||||
|
||||
@@ -20,7 +20,7 @@ type SStandaloneResourceBase struct {
|
||||
|
||||
Id string `width:"128" charset:"ascii" primary:"true" list:"user"`
|
||||
Name string `width:"128" charset:"utf8" nullable:"false" index:"true" list:"user" update:"user" create:"required"`
|
||||
ExternalId string `width:"128" charset:"ascii" index:"true" list:"admin" create:"admin_optional"`
|
||||
ExternalId string `width:"256" charset:"ascii" index:"true" list:"admin" create:"admin_optional"`
|
||||
|
||||
Description string `width:"256" charset:"utf8" get:"user" update:"user" create:"optional"`
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type SStatusStandaloneResourceBase struct {
|
||||
SStandaloneResourceBase
|
||||
|
||||
Status string `width:"36" charset:"ascii" nullable:"false" default:"init" list:"user"`
|
||||
Status string `width:"36" charset:"ascii" nullable:"false" default:"init" list:"user" create:"optional"`
|
||||
}
|
||||
|
||||
type SStatusStandaloneResourceBaseManager struct {
|
||||
|
||||
@@ -31,7 +31,7 @@ type SVirtualResourceBase struct {
|
||||
|
||||
ProjectId string `name:"tenant_id" width:"128" charset:"ascii" nullable:"false" index:"true" list:"admin"`
|
||||
|
||||
IsSystem bool `nullable:"true" default:"false" list:"admin"`
|
||||
IsSystem bool `nullable:"true" default:"false" list:"admin" create:"optional"`
|
||||
|
||||
PendingDeletedAt time.Time ``
|
||||
PendingDeleted bool `nullable:"false" default:"false" index:"true" get:"admin"`
|
||||
|
||||
@@ -23,7 +23,7 @@ func (cs Choices) Has(choice string) bool {
|
||||
func (cs Choices) String() string {
|
||||
choices := make([]string, len(cs))
|
||||
i := 0
|
||||
for choice := range cs {
|
||||
for choice, _ := range cs {
|
||||
choices[i] = choice
|
||||
i++
|
||||
}
|
||||
|
||||
@@ -293,4 +293,6 @@ type ICloudNetwork interface {
|
||||
GetIsPublic() bool
|
||||
|
||||
Delete() error
|
||||
|
||||
GetAllocTimeoutSeconds() int
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func fetchIVMinfo(desc SAliyunVMCreateConfig, iVM cloudprovider.ICloudVM, guestI
|
||||
|
||||
func (self *SAliyunGuestDriver) RequestDeployGuestOnHost(ctx context.Context, guest *models.SGuest, host *models.SHost, task taskman.ITask) error {
|
||||
config := guest.GetDeployConfigOnHost(ctx, host, task.GetParams())
|
||||
|
||||
log.Debugf("RequestDeployGuestOnHost: %s", config)
|
||||
/* onfinish, err := config.GetString("on_finish")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -399,7 +399,7 @@ func (self *SAliyunGuestDriver) RequestDeployGuestOnHost(ctx context.Context, gu
|
||||
}
|
||||
if len(idisks) < len(desc.DataDisks)+1 {
|
||||
if waited > maxWaitSecs {
|
||||
log.Errorf("inconsistent disk number, wait timeout, must be something wrong one remote")
|
||||
log.Errorf("inconsistent disk number, wait timeout, must be something wrong on remote")
|
||||
return nil, cloudprovider.ErrTimeout
|
||||
}
|
||||
log.Debugf("inconsistent disk number???? %d != %d", len(idisks), len(desc.DataDisks)+1)
|
||||
|
||||
@@ -165,7 +165,7 @@ func (self *SManagedVirtualizedGuestDriver) GetGuestVncInfo(userCred mcclient.To
|
||||
}
|
||||
|
||||
func (self *SManagedVirtualizedGuestDriver) RequestRebuildRootDisk(ctx context.Context, guest *models.SGuest, task taskman.ITask) error {
|
||||
subtask, err := taskman.TaskManager.NewTask(ctx, "ManagedGuestRebuildRootTask", guest, task.GetUserCred(), nil, task.GetTaskId(), "", nil)
|
||||
subtask, err := taskman.TaskManager.NewTask(ctx, "ManagedGuestRebuildRootTask", guest, task.GetUserCred(), task.GetParams(), task.GetTaskId(), "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
type SAzureHostDriver struct {
|
||||
SBaseHostDriver
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/compute/models"
|
||||
)
|
||||
@@ -11,6 +12,14 @@ import (
|
||||
type SBaseHostDriver struct {
|
||||
}
|
||||
|
||||
func (self *SBaseHostDriver) RequestDeleteSnapshotWithStorage(ctx context.Context, host *models.SHost, snapshot *models.SSnapshot, task taskman.ITask) error {
|
||||
func (self *SBaseHostDriver) RequestDeleteSnapshotsWithStorage(ctx context.Context, host *models.SHost, snapshot *models.SSnapshot, task taskman.ITask) error {
|
||||
return fmt.Errorf("Not Implement")
|
||||
}
|
||||
|
||||
func (self *SBaseHostDriver) RequestResetDisk(ctx context.Context, host *models.SHost, disk *models.SDisk, params *jsonutils.JSONDict, task taskman.ITask) error {
|
||||
return fmt.Errorf("Not Implement")
|
||||
}
|
||||
|
||||
func (self *SBaseHostDriver) RequestCleanUpDiskSnapshots(ctx context.Context, host *models.SHost, disk *models.SDisk, params *jsonutils.JSONDict, task taskman.ITask) error {
|
||||
return fmt.Errorf("Not Implement")
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func (self *SKVMHostDriver) RequestSaveUploadImageOnHost(ctx context.Context, ho
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *SKVMHostDriver) RequestDeleteSnapshotWithStorage(ctx context.Context, host *models.SHost, snapshot *models.SSnapshot, task taskman.ITask) error {
|
||||
func (self *SKVMHostDriver) RequestDeleteSnapshotsWithStorage(ctx context.Context, host *models.SHost, snapshot *models.SSnapshot, task taskman.ITask) error {
|
||||
url := fmt.Sprintf("/storages/%s/delete-snapshots", snapshot.StorageId)
|
||||
body := jsonutils.NewDict()
|
||||
body.Set("disk_id", jsonutils.NewString(snapshot.DiskId))
|
||||
@@ -159,3 +159,21 @@ func (self *SKVMHostDriver) RequestDeleteSnapshotWithStorage(ctx context.Context
|
||||
_, err := host.Request(task.GetUserCred(), "POST", url, header, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *SKVMHostDriver) RequestResetDisk(ctx context.Context, host *models.SHost, disk *models.SDisk, params *jsonutils.JSONDict, task taskman.ITask) error {
|
||||
url := fmt.Sprintf("/disks/%s/reset/%s", disk.StorageId, disk.Id)
|
||||
header := http.Header{}
|
||||
header.Add("X-Task-Id", task.GetTaskId())
|
||||
header.Add("X-Region-Version", "v2")
|
||||
_, err := host.Request(task.GetUserCred(), "POST", url, header, params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *SKVMHostDriver) RequestCleanUpDiskSnapshots(ctx context.Context, host *models.SHost, disk *models.SDisk, params *jsonutils.JSONDict, task taskman.ITask) error {
|
||||
url := fmt.Sprintf("/disks/%s/cleanup-snapshots/%s", disk.StorageId, disk.Id)
|
||||
header := http.Header{}
|
||||
header.Add("X-Task-Id", task.GetTaskId())
|
||||
header.Add("X-Region-Version", "v2")
|
||||
_, err := host.Request(task.GetUserCred(), "POST", url, header, params)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ type SDisk struct {
|
||||
StorageId string `width:"128" charset:"ascii" nullable:"false" list:"admin" create:"required"` // Column(VARCHAR(ID_LENGTH, charset='ascii'), nullable=False)
|
||||
|
||||
// # backing template id and type
|
||||
TemplateId string `width:"128" charset:"ascii" nullable:"true" list:"user"` // Column(VARCHAR(ID_LENGTH, charset='ascii'), nullable=True)
|
||||
TemplateId string `width:"256" charset:"ascii" nullable:"true" list:"user"` // Column(VARCHAR(ID_LENGTH, charset='ascii'), nullable=True)
|
||||
// # file system
|
||||
FsFormat string `width:"32" charset:"ascii" nullable:"true" list:"user"` // Column(VARCHAR(32, charset='ascii'), nullable=True)
|
||||
// # disk type, OS, SWAP, DAT
|
||||
@@ -249,6 +249,9 @@ func (manager *SDiskManager) ValidateCreateData(ctx context.Context, userCred mc
|
||||
if !utils.IsInStringArray(storage.Status, []string{STORAGE_ENABLED, STORAGE_ONLINE}) {
|
||||
return nil, httperrors.NewInputParameterError("Cannot create disk with offline storage[%s]", storage.Name)
|
||||
}
|
||||
if len(diskConfig.Backend) == 0 {
|
||||
diskConfig.Backend = storage.StorageType
|
||||
}
|
||||
if storage.StorageType != diskConfig.Backend {
|
||||
return nil, httperrors.NewInputParameterError("Storage type[%s] not match backend %s", storage.StorageType, diskConfig.Backend)
|
||||
}
|
||||
@@ -313,7 +316,7 @@ func (self *SDisk) StartDiskCreateTask(ctx context.Context, userCred mcclient.To
|
||||
func (self *SDisk) GetSnapshotCount() int {
|
||||
q := SnapshotManager.Query()
|
||||
count := q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("disk_id"), self.Id),
|
||||
sqlchemy.Equals(q.Field("out_of_chain"), false))).Count()
|
||||
sqlchemy.Equals(q.Field("fake_deleted"), false))).Count()
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -376,6 +379,79 @@ func (self *SDisk) GetDetailsConvertSnapshot(ctx context.Context, userCred mccli
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// On disk reset, auto delete snapshots after the reset snapshot(reserve manualed snapshot)
|
||||
func (self *SDisk) CleanUpDiskSnapshots(ctx context.Context, userCred mcclient.TokenCredential, snapshot *SSnapshot) error {
|
||||
dest := make([]SSnapshot, 0)
|
||||
query := SnapshotManager.TableSpec().Query()
|
||||
query.Filter(sqlchemy.Equals(query.Field("disk_id"), self.Id)).
|
||||
GT("created_at", snapshot.CreatedAt).Asc("created_at").All(&dest)
|
||||
if len(dest) == 0 {
|
||||
return nil
|
||||
}
|
||||
convertSnapshots := jsonutils.NewArray()
|
||||
deleteSnapshots := jsonutils.NewArray()
|
||||
for i := 0; i < len(dest); i++ {
|
||||
if dest[i].CreatedBy == MANUAL && !dest[i].FakeDeleted {
|
||||
if !dest[i].OutOfChain {
|
||||
convertSnapshots.Add(jsonutils.NewString(dest[i].Id))
|
||||
}
|
||||
} else {
|
||||
deleteSnapshots.Add(jsonutils.NewString(dest[i].Id))
|
||||
}
|
||||
}
|
||||
params := jsonutils.NewDict()
|
||||
params.Set("convert_snapshots", convertSnapshots)
|
||||
params.Set("delete_snapshots", deleteSnapshots)
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "DiskCleanUpSnapshotsTask", self, userCred, params, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
task.ScheduleRun(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SDisk) AllowPerformDiskReset(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
return self.IsOwner(userCred)
|
||||
}
|
||||
|
||||
func (self *SDisk) PerformDiskReset(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
snapshotId, err := data.GetString("snapshot_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
guests := self.GetGuests()
|
||||
if len(guests) > 1 {
|
||||
return nil, httperrors.NewBadRequestError("Disk attach muti guests")
|
||||
} else if len(guests) == 1 {
|
||||
if guests[0].Status != VM_READY {
|
||||
return nil, httperrors.NewServerStatusError("Disk attached guest status must be ready")
|
||||
}
|
||||
}
|
||||
iSnapshot, err := SnapshotManager.FetchById(snapshotId)
|
||||
if err != nil {
|
||||
return nil, httperrors.NewNotFoundError("Snapshot %s not found", snapshotId)
|
||||
}
|
||||
snapshot := iSnapshot.(*SSnapshot)
|
||||
if snapshot.Status != SNAPSHOT_READY {
|
||||
return nil, httperrors.NewBadRequestError("Cannot reset disk with snapshot in status %s", snapshot.Status)
|
||||
}
|
||||
self.StartResetDisk(ctx, userCred, snapshotId)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (self *SDisk) StartResetDisk(ctx context.Context, userCred mcclient.TokenCredential, snapshotId string) error {
|
||||
params := jsonutils.NewDict()
|
||||
params.Set("snapshot_id", jsonutils.NewString(snapshotId))
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "DiskResetTask", self, userCred, params, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
task.ScheduleRun(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SDisk) AllowPerformResize(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
return self.IsOwner(userCred)
|
||||
}
|
||||
@@ -787,7 +863,7 @@ func parseDiskInfo(ctx context.Context, userCred mcclient.TokenCredential, info
|
||||
}
|
||||
|
||||
// default backend and medium type
|
||||
diskConfig.Backend = STORAGE_LOCAL
|
||||
diskConfig.Backend = "" // STORAGE_LOCAL
|
||||
diskConfig.Medium = DISK_TYPE_HYBRID
|
||||
|
||||
diskStr, err := info.GetString()
|
||||
@@ -1160,7 +1236,7 @@ func (manager *SDiskManager) AutoDiskSnapshot(ctx context.Context, userCred mccl
|
||||
}
|
||||
for _, disk := range disks {
|
||||
snapCount := disk.GetSnapshotCount()
|
||||
if snapCount >= DISK_MAX_SNAPSHOT {
|
||||
if snapCount >= options.Options.DefaultMaxSnapshotCount {
|
||||
continue
|
||||
}
|
||||
guests := disk.GetGuests()
|
||||
@@ -1168,10 +1244,10 @@ func (manager *SDiskManager) AutoDiskSnapshot(ctx context.Context, userCred mccl
|
||||
log.Errorln("Disk %s not attach or attached more than one guest", disk.Id)
|
||||
continue
|
||||
}
|
||||
// if !utils.IsInStringArray(guests[0].Status, []string{VM_RUNNING, VM_READY}) {
|
||||
// log.Errorln("Guest(%s) in status(%s) cannot do snapshot action", guests[0].Id, guests[0].Status)
|
||||
// continue
|
||||
// }
|
||||
if !utils.IsInStringArray(guests[0].Status, []string{VM_RUNNING, VM_READY}) {
|
||||
log.Errorln("Guest(%s) in status(%s) cannot do snapshot action", guests[0].Id, guests[0].Status)
|
||||
continue
|
||||
}
|
||||
// name
|
||||
name := disk.Name + time.Now().Format("2006-01-02#15:04:05")
|
||||
snap, err := SnapshotManager.CreateSnapshot(ctx, userCred, AUTO, disk.Id, guests[0].Id, "", name)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"yunion.io/x/pkg/utils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
"strings"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/quotas"
|
||||
@@ -66,7 +65,7 @@ type SElasticip struct {
|
||||
IpAddr string `width:"17" charset:"ascii" list:"user"`
|
||||
|
||||
AssociateType string `width:"32" charset:"ascii" list:"user"`
|
||||
AssociateId string `width:"128" charset:"ascii" list:"user"`
|
||||
AssociateId string `width:"256" charset:"ascii" list:"user"`
|
||||
|
||||
Bandwidth int `list:"user" create:"required"`
|
||||
|
||||
@@ -109,6 +108,14 @@ func (manager *SElasticipManager) ListItemFilter(ctx context.Context, q *sqlchem
|
||||
q = q.Equals("cloudregion_id", regionObj.GetId())
|
||||
}
|
||||
|
||||
if query.Contains("usable") {
|
||||
usable := jsonutils.QueryBoolean(query, "usable", false)
|
||||
if usable {
|
||||
q = q.Equals("status", EIP_STATUS_READY)
|
||||
q = q.Filter(sqlchemy.OR(sqlchemy.IsNull(q.Field("associate_id")), sqlchemy.IsEmpty(q.Field("associate_id"))))
|
||||
}
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
@@ -612,12 +619,12 @@ func (self *SElasticip) AllowPerformSync(ctx context.Context, userCred mcclient.
|
||||
}
|
||||
|
||||
func (self *SElasticip) PerformSync(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
if self.Status != EIP_STATUS_READY && !strings.HasSuffix(self.Status, "_fail") {
|
||||
/*if self.Status != EIP_STATUS_READY && !strings.HasSuffix(self.Status, "_fail") {
|
||||
return nil, httperrors.NewInvalidStatusError("eip cannot syncstatus in status %s", self.Status)
|
||||
}
|
||||
}*/
|
||||
|
||||
if self.Mode == EIP_MODE_INSTANCE_PUBLICIP {
|
||||
return nil, httperrors.NewUnsupportOperationError("fixed eip cannot be dissociated")
|
||||
return nil, httperrors.NewUnsupportOperationError("fixed eip cannot sync status")
|
||||
}
|
||||
|
||||
err := self.StartEipSyncstatusTask(ctx, userCred, "")
|
||||
|
||||
@@ -78,6 +78,7 @@ func (self *SGuestdisk) getExtraInfo(extra *jsonutils.JSONDict) *jsonutils.JSOND
|
||||
disk := self.GetDisk()
|
||||
extra.Add(jsonutils.NewInt(int64(disk.DiskSize)), "disk_size")
|
||||
extra.Add(jsonutils.NewString(disk.Status), "status")
|
||||
extra.Add(jsonutils.NewString(disk.DiskType), "disk_type")
|
||||
return extra
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,8 @@ func (manager *SGuestnetworkManager) newGuestNetwork(ctx context.Context, userCr
|
||||
gn.MacAddr = macAddr
|
||||
if !virtual {
|
||||
addrTable := network.GetUsedAddresses()
|
||||
ipAddr, err := network.GetFreeIP(ctx, userCred, addrTable, address, allocDir, reserved)
|
||||
recentAddrTable := manager.getRecentlyReleasedIPAddresses(network.Id, time.Duration(network.AllocTimoutSeconds)*time.Second)
|
||||
ipAddr, err := network.GetFreeIP(ctx, userCred, addrTable, recentAddrTable, address, allocDir, reserved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -552,3 +553,29 @@ func (self *SGuestnetwork) getJsonDescAtHost(host *SHost) jsonutils.JSONObject {
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
func (manager *SGuestnetworkManager) getRecentlyReleasedIPAddresses(networkId string, recentDuration time.Duration) map[string]bool {
|
||||
if recentDuration == 0 {
|
||||
return nil
|
||||
}
|
||||
since := time.Now().UTC().Add(-recentDuration)
|
||||
q := manager.RawQuery("ip_addr")
|
||||
q = q.Equals("network_id", networkId).IsTrue("deleted")
|
||||
q = q.GT("deleted_at", since).Distinct()
|
||||
rows, err := q.Rows()
|
||||
if err != nil {
|
||||
log.Errorf("GetRecentlyReleasedIPAddresses fail %s", err)
|
||||
return nil
|
||||
}
|
||||
ret := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
err = rows.Scan(&ip)
|
||||
if err != nil {
|
||||
log.Errorf("scan error %s", err)
|
||||
} else {
|
||||
ret[ip] = true
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -587,6 +587,11 @@ func (manager *SGuestManager) ValidateCreateData(ctx context.Context, userCred m
|
||||
return nil, httperrors.NewInputParameterError("Invalid root image: %s", err)
|
||||
}
|
||||
|
||||
if len(diskConfig.Backend) == 0 {
|
||||
diskConfig.Backend = STORAGE_LOCAL
|
||||
}
|
||||
rootStorageType := diskConfig.Backend
|
||||
|
||||
data.Add(jsonutils.Marshal(diskConfig), "disk.0")
|
||||
|
||||
imgProperties := diskConfig.ImageProperties
|
||||
@@ -718,6 +723,7 @@ func (manager *SGuestManager) ValidateCreateData(ctx context.Context, userCred m
|
||||
}
|
||||
|
||||
data.Add(jsonutils.NewString(hypervisor), "hypervisor")
|
||||
// start from data disk
|
||||
for idx := 1; data.Contains(fmt.Sprintf("disk.%d", idx)); idx += 1 {
|
||||
diskJson, err := data.Get(fmt.Sprintf("disk.%d", idx))
|
||||
if err != nil {
|
||||
@@ -727,6 +733,9 @@ func (manager *SGuestManager) ValidateCreateData(ctx context.Context, userCred m
|
||||
if err != nil {
|
||||
return nil, httperrors.NewInputParameterError("parse disk description error %s", err)
|
||||
}
|
||||
if len(diskConfig.Backend) == 0 {
|
||||
diskConfig.Backend = rootStorageType
|
||||
}
|
||||
if len(diskConfig.Driver) == 0 {
|
||||
diskConfig.Driver = osProf.DiskDriver
|
||||
}
|
||||
@@ -984,6 +993,7 @@ func (self *SGuest) GetCustomizeColumns(ctx context.Context, userCred mcclient.T
|
||||
eip, _ := self.GetEip()
|
||||
if eip != nil {
|
||||
extra.Add(jsonutils.NewString(eip.IpAddr), "eip")
|
||||
extra.Add(jsonutils.NewString(eip.Mode), "eip_mode")
|
||||
}
|
||||
extra.Add(jsonutils.NewInt(int64(self.getDiskSize())), "disk")
|
||||
// flavor??
|
||||
@@ -1061,6 +1071,7 @@ func (self *SGuest) GetExtraDetails(ctx context.Context, userCred mcclient.Token
|
||||
eip, _ := self.GetEip()
|
||||
if eip != nil {
|
||||
extra.Add(jsonutils.NewString(eip.IpAddr), "eip")
|
||||
extra.Add(jsonutils.NewString(eip.Mode), "eip_mode")
|
||||
}
|
||||
return self.moreExtraInfo(extra)
|
||||
}
|
||||
@@ -1402,10 +1413,10 @@ func (manager *SGuestManager) newCloudVM(ctx context.Context, userCred mcclient.
|
||||
|
||||
func (manager *SGuestManager) TotalCount(
|
||||
projectId string, rangeObj db.IStandaloneModel,
|
||||
status []string, hypervisor string,
|
||||
status []string, hypervisors []string,
|
||||
includeSystem bool, pendingDelete bool, hostType string,
|
||||
) SGuestCountStat {
|
||||
return totalGuestResourceCount(projectId, rangeObj, status, hypervisor, includeSystem, pendingDelete, hostType)
|
||||
return totalGuestResourceCount(projectId, rangeObj, status, hypervisors, includeSystem, pendingDelete, hostType)
|
||||
}
|
||||
|
||||
func (self *SGuest) detachNetwork(ctx context.Context, userCred mcclient.TokenCredential, network *SNetwork, reserve bool, deploy bool) error {
|
||||
@@ -1918,8 +1929,15 @@ type SGuestCountStat struct {
|
||||
TotalIsolatedCount int
|
||||
}
|
||||
|
||||
func totalGuestResourceCount(projectId string, rangeObj db.IStandaloneModel, status []string, hypervisor string,
|
||||
includeSystem bool, pendingDelete bool, hostType string) SGuestCountStat {
|
||||
func totalGuestResourceCount(
|
||||
projectId string,
|
||||
rangeObj db.IStandaloneModel,
|
||||
status []string,
|
||||
hypervisors []string,
|
||||
includeSystem bool,
|
||||
pendingDelete bool,
|
||||
hostType string,
|
||||
) SGuestCountStat {
|
||||
|
||||
guestdisks := GuestdiskManager.Query().SubQuery()
|
||||
disks := DiskManager.Query().SubQuery()
|
||||
@@ -1959,8 +1977,8 @@ func totalGuestResourceCount(projectId string, rangeObj db.IStandaloneModel, sta
|
||||
if len(status) > 0 {
|
||||
q = q.Filter(sqlchemy.In(guests.Field("status"), status))
|
||||
}
|
||||
if len(hypervisor) > 0 {
|
||||
q = q.Filter(sqlchemy.Equals(guests.Field("hypervisor"), hypervisor))
|
||||
if len(hypervisors) > 0 {
|
||||
q = q.Filter(sqlchemy.In(guests.Field("hypervisor"), hypervisors))
|
||||
}
|
||||
if !includeSystem {
|
||||
q = q.Filter(sqlchemy.OR(sqlchemy.IsNull(guests.Field("is_system")), sqlchemy.IsFalse(guests.Field("is_system"))))
|
||||
@@ -2561,6 +2579,9 @@ func (self *SGuest) PerformCreatedisk(ctx context.Context, userCred mcclient.Tok
|
||||
logclient.AddActionLog(self, logclient.ACT_CREATE, err.Error(), userCred, false)
|
||||
return nil, httperrors.NewBadRequestError(err.Error())
|
||||
}
|
||||
if len(diskInfo.Backend) == 0 {
|
||||
diskInfo.Backend = self.getDefaultStorageType()
|
||||
}
|
||||
disksConf.Set(diskSeq, jsonutils.Marshal(diskInfo))
|
||||
if _, ok := diskSizes[diskInfo.Backend]; !ok {
|
||||
diskSizes[diskInfo.Backend] = diskInfo.Size
|
||||
@@ -2910,12 +2931,18 @@ func (self *SGuest) PerformChangeConfig(ctx context.Context, userCred mcclient.T
|
||||
if err != nil {
|
||||
return nil, httperrors.NewBadRequestError("Parse disk info error: %s", err)
|
||||
}
|
||||
if len(diskConf.Backend) == 0 {
|
||||
diskConf.Backend = self.getDefaultStorageType()
|
||||
}
|
||||
if diskConf.Size > 0 {
|
||||
if diskIdx >= len(disks) {
|
||||
newDisks.Add(jsonutils.Marshal(diskConf), fmt.Sprintf("disk.%d", newDiskIdx))
|
||||
newDiskIdx += 1
|
||||
addDisk += diskConf.Size
|
||||
storage := host.GetLeastUsedStorage(diskConf.Backend)
|
||||
if storage == nil {
|
||||
return nil, httperrors.NewResourceNotReadyError("host not connect storage %s", diskConf.Backend)
|
||||
}
|
||||
_, ok := diskSizes[storage.Id]
|
||||
if !ok {
|
||||
diskSizes[storage.Id] = 0
|
||||
@@ -3858,7 +3885,7 @@ func (self *SGuest) PerformDiskSnapshot(ctx context.Context, userCred mcclient.T
|
||||
}
|
||||
snapshots := SnapshotManager.GetDiskSnapshotsByCreate(diskId, MANUAL)
|
||||
if snapshots != nil {
|
||||
if len(snapshots) >= DISK_MAX_MANUAL_SNAPSHOT {
|
||||
if len(snapshots) >= options.Options.DefaultMaxManualSnapshotCount {
|
||||
return nil, httperrors.NewBadRequestError("Disk %s snapshot full, cannot take any more", diskId)
|
||||
}
|
||||
for _, snapshot := range snapshots {
|
||||
@@ -3867,10 +3894,17 @@ func (self *SGuest) PerformDiskSnapshot(ctx context.Context, userCred mcclient.T
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingUsage := &SQuota{Snapshot: 1}
|
||||
err = QuotaManager.CheckSetPendingQuota(ctx, userCred, self.ProjectId, pendingUsage)
|
||||
if err != nil {
|
||||
return nil, httperrors.NewBadRequestError("Check set pending quota error %s", err)
|
||||
}
|
||||
snapshot, err := SnapshotManager.CreateSnapshot(ctx, userCred, MANUAL, diskId, self.Id, "", name)
|
||||
QuotaManager.CancelPendingUsage(ctx, userCred, self.ProjectId, nil, pendingUsage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = self.StartDiskSnapshot(ctx, userCred, diskId, snapshot.Id)
|
||||
return nil, err
|
||||
}
|
||||
@@ -4352,6 +4386,9 @@ func (self *SGuest) PerformCreateEip(ctx context.Context, userCred mcclient.Toke
|
||||
if err != nil {
|
||||
return nil, httperrors.NewGeneralError(err)
|
||||
}
|
||||
|
||||
self.SetStatus(userCred, VM_ASSOCIATE_EIP, "allocate and associate EIP")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -4383,3 +4420,14 @@ func (self *SGuest) SetDisableDelete(val bool) error {
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *SGuest) getDefaultStorageType() string {
|
||||
diskCat := self.CategorizeDisks()
|
||||
if diskCat.Root != nil {
|
||||
rootStorage := diskCat.Root.GetStorage()
|
||||
if rootStorage != nil {
|
||||
return rootStorage.StorageType
|
||||
}
|
||||
}
|
||||
return STORAGE_LOCAL
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ type IHostDriver interface {
|
||||
RequestDeallocateDiskOnHost(host *SHost, storage *SStorage, disk *SDisk, task taskman.ITask) error
|
||||
RequestResizeDiskOnHostOnline(host *SHost, storage *SStorage, disk *SDisk, size int64, task taskman.ITask) error
|
||||
RequestResizeDiskOnHost(host *SHost, storage *SStorage, disk *SDisk, size int64, task taskman.ITask) error
|
||||
RequestDeleteSnapshotWithStorage(ctx context.Context, host *SHost, snapshot *SSnapshot, task taskman.ITask) error
|
||||
RequestDeleteSnapshotsWithStorage(ctx context.Context, host *SHost, snapshot *SSnapshot, task taskman.ITask) error
|
||||
RequestResetDisk(ctx context.Context, host *SHost, disk *SDisk, params *jsonutils.JSONDict, task taskman.ITask) error
|
||||
RequestCleanUpDiskSnapshots(ctx context.Context, host *SHost, disk *SDisk, params *jsonutils.JSONDict, task taskman.ITask) error
|
||||
}
|
||||
|
||||
var hostDrivers map[string]IHostDriver
|
||||
|
||||
@@ -101,6 +101,8 @@ type SNetwork struct {
|
||||
ServerType string `width:"16" charset:"ascii" nullable:"true" list:"user" update:"user" create:"optional"` // Column(VARCHAR(16, charset='ascii'), nullable=True)
|
||||
|
||||
AllocPolicy string `width:"16" charset:"ascii" nullable:"true" get:"user" update:"user" create:"optional"` // Column(VARCHAR(16, charset='ascii'), nullable=True)
|
||||
|
||||
AllocTimoutSeconds int `default:"0" nullable:"true" get:"admin"`
|
||||
}
|
||||
|
||||
func (manager *SNetworkManager) GetContextManager() []db.IModelManager {
|
||||
@@ -188,7 +190,22 @@ func (self *SNetwork) getIPRange() netutils.IPV4AddrRange {
|
||||
return netutils.NewIPV4AddrRange(start, end)
|
||||
}
|
||||
|
||||
func (self *SNetwork) getFreeIP(addrTable map[string]bool, candidate string, allocDir IPAddlocationDirection) (string, error) {
|
||||
func isIpUsed(ipstr string, addrTable map[string]bool, recentUsedAddrTable map[string]bool) bool {
|
||||
_, ok := addrTable[ipstr]
|
||||
if !ok {
|
||||
recentUsed := false
|
||||
if recentUsedAddrTable != nil {
|
||||
if _, ok := recentUsedAddrTable[ipstr]; ok {
|
||||
recentUsed = true
|
||||
}
|
||||
}
|
||||
return recentUsed
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SNetwork) getFreeIP(addrTable map[string]bool, recentUsedAddrTable map[string]bool, candidate string, allocDir IPAddlocationDirection) (string, error) {
|
||||
iprange := self.getIPRange()
|
||||
if len(candidate) > 0 {
|
||||
candIP, err := netutils.NewIPV4Addr(candidate)
|
||||
@@ -208,7 +225,7 @@ func (self *SNetwork) getFreeIP(addrTable map[string]bool, candidate string, all
|
||||
if len(allocDir) == 0 || allocDir == IPAllocationStepdown {
|
||||
ip, _ := netutils.NewIPV4Addr(self.GuestIpEnd)
|
||||
for iprange.Contains(ip) {
|
||||
if _, ok := addrTable[ip.String()]; !ok {
|
||||
if !isIpUsed(ip.String(), addrTable, recentUsedAddrTable) {
|
||||
return ip.String(), nil
|
||||
}
|
||||
ip = ip.StepDown()
|
||||
@@ -219,7 +236,7 @@ func (self *SNetwork) getFreeIP(addrTable map[string]bool, candidate string, all
|
||||
const MAX_TRIES = 5
|
||||
for i := 0; i < MAX_TRIES; i += 1 {
|
||||
ip := iprange.Random()
|
||||
if _, ok := addrTable[ip.String()]; !ok {
|
||||
if !isIpUsed(ip.String(), addrTable, recentUsedAddrTable) {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
@@ -227,7 +244,7 @@ func (self *SNetwork) getFreeIP(addrTable map[string]bool, candidate string, all
|
||||
}
|
||||
ip, _ := netutils.NewIPV4Addr(self.GuestIpStart)
|
||||
for iprange.Contains(ip) {
|
||||
if _, ok := addrTable[ip.String()]; !ok {
|
||||
if !isIpUsed(ip.String(), addrTable, recentUsedAddrTable) {
|
||||
return ip.String(), nil
|
||||
}
|
||||
ip = ip.StepUp()
|
||||
@@ -236,7 +253,7 @@ func (self *SNetwork) getFreeIP(addrTable map[string]bool, candidate string, all
|
||||
return "", httperrors.NewInsufficientResourceError("Out of IP address")
|
||||
}
|
||||
|
||||
func (self *SNetwork) GetFreeIP(ctx context.Context, userCred mcclient.TokenCredential, addrTable map[string]bool, candidate string, allocDir IPAddlocationDirection, reserved bool) (string, error) {
|
||||
func (self *SNetwork) GetFreeIP(ctx context.Context, userCred mcclient.TokenCredential, addrTable map[string]bool, recentUsedAddrTable map[string]bool, candidate string, allocDir IPAddlocationDirection, reserved bool) (string, error) {
|
||||
if reserved {
|
||||
rip := ReservedipManager.GetReservedIP(self, candidate)
|
||||
if rip == nil {
|
||||
@@ -245,7 +262,7 @@ func (self *SNetwork) GetFreeIP(ctx context.Context, userCred mcclient.TokenCred
|
||||
rip.Release(ctx, userCred, self)
|
||||
return candidate, nil
|
||||
} else {
|
||||
cand, err := self.getFreeIP(addrTable, candidate, allocDir)
|
||||
cand, err := self.getFreeIP(addrTable, recentUsedAddrTable, candidate, allocDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -471,6 +488,8 @@ func (self *SNetwork) SyncWithCloudNetwork(userCred mcclient.TokenCredential, ex
|
||||
self.ServerType = extNet.GetServerType()
|
||||
self.IsPublic = extNet.GetIsPublic()
|
||||
|
||||
self.AllocTimoutSeconds = extNet.GetAllocTimeoutSeconds()
|
||||
|
||||
self.ProjectId = userCred.GetProjectId()
|
||||
return nil
|
||||
})
|
||||
@@ -495,6 +514,8 @@ func (manager *SNetworkManager) newFromCloudNetwork(userCred mcclient.TokenCrede
|
||||
net.ServerType = extNet.GetServerType()
|
||||
net.IsPublic = extNet.GetIsPublic()
|
||||
|
||||
net.AllocTimoutSeconds = extNet.GetAllocTimeoutSeconds()
|
||||
|
||||
net.ProjectId = userCred.GetProjectId()
|
||||
|
||||
err := manager.TableSpec().Insert(&net)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/quotas"
|
||||
"yunion.io/x/onecloud/pkg/compute/options"
|
||||
"yunion.io/x/pkg/tristate"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
)
|
||||
|
||||
var QuotaManager *quotas.SQuotaManager
|
||||
@@ -33,6 +35,7 @@ var (
|
||||
ErrOutOfGroup = errors.New("out of group quota")
|
||||
ErrOutOfSecgroup = errors.New("out of secgroup quota")
|
||||
ErrOutOfIsolatedDevice = errors.New("out of isolated device quota")
|
||||
ErrOutOfSnapshot = errors.New("out of snapshot quota")
|
||||
)
|
||||
|
||||
type SQuota struct {
|
||||
@@ -49,6 +52,7 @@ type SQuota struct {
|
||||
Group int
|
||||
Secgroup int
|
||||
IsolatedDevice int
|
||||
Snapshot int
|
||||
}
|
||||
|
||||
func (self *SQuota) FetchSystemQuota() {
|
||||
@@ -65,13 +69,15 @@ func (self *SQuota) FetchSystemQuota() {
|
||||
self.Group = options.Options.DefaultGroupQuota
|
||||
self.Secgroup = options.Options.DefaultSecgroupQuota
|
||||
self.IsolatedDevice = options.Options.DefaultIsolatedDeviceQuota
|
||||
self.Snapshot = options.Options.DefaultSnapshotQuota
|
||||
}
|
||||
|
||||
func (self *SQuota) FetchUsage(projectId string) error {
|
||||
diskSize := totalDiskSize(projectId, tristate.None, tristate.None, false)
|
||||
net := totalGuestNicCount(projectId, nil, false)
|
||||
guest := totalGuestResourceCount(projectId, nil, nil, "", false, false, "")
|
||||
guest := totalGuestResourceCount(projectId, nil, nil, nil, false, false, "")
|
||||
eipUsage := ElasticipManager.TotalCount(projectId, nil, nil)
|
||||
snapshotCount := totalSnapshotCount(projectId)
|
||||
// XXX
|
||||
// keypair belongs to user
|
||||
// keypair := totalKeypairCount(projectId)
|
||||
@@ -85,11 +91,12 @@ func (self *SQuota) FetchUsage(projectId string) error {
|
||||
self.Bw = net.InternalBandwidth
|
||||
self.Ebw = net.ExternalBandwidth
|
||||
self.Keypair = 0 // keypair
|
||||
self.Image = 0
|
||||
s := auth.GetAdminSession("", "")
|
||||
self.Image, _ = modules.Images.GetPrivateImageCount(s, projectId, true)
|
||||
self.Group = 0
|
||||
self.Secgroup = totalSecurityGroupCount(projectId)
|
||||
self.IsolatedDevice = guest.TotalIsolatedCount
|
||||
|
||||
self.Snapshot = snapshotCount
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -133,6 +140,9 @@ func (self *SQuota) IsEmpty() bool {
|
||||
if self.IsolatedDevice > 0 {
|
||||
return false
|
||||
}
|
||||
if self.Snapshot > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -151,6 +161,7 @@ func (self *SQuota) Add(quota quotas.IQuota) {
|
||||
self.Group = self.Group + squota.Group
|
||||
self.Secgroup = self.Secgroup + squota.Secgroup
|
||||
self.IsolatedDevice = self.IsolatedDevice + squota.IsolatedDevice
|
||||
self.Snapshot = self.Snapshot + squota.Snapshot
|
||||
}
|
||||
|
||||
func nonNegative(val int) int {
|
||||
@@ -176,6 +187,7 @@ func (self *SQuota) Sub(quota quotas.IQuota) {
|
||||
self.Group = nonNegative(self.Group - squota.Group)
|
||||
self.Secgroup = nonNegative(self.Secgroup - squota.Secgroup)
|
||||
self.IsolatedDevice = nonNegative(self.IsolatedDevice - squota.IsolatedDevice)
|
||||
self.Snapshot = nonNegative(self.Snapshot - squota.Snapshot)
|
||||
}
|
||||
|
||||
func (self *SQuota) Update(quota quotas.IQuota) {
|
||||
@@ -219,6 +231,9 @@ func (self *SQuota) Update(quota quotas.IQuota) {
|
||||
if squota.IsolatedDevice > 0 {
|
||||
self.IsolatedDevice = squota.IsolatedDevice
|
||||
}
|
||||
if squota.Snapshot > 0 {
|
||||
self.Snapshot = squota.Snapshot
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SQuota) Exceed(request quotas.IQuota, quota quotas.IQuota) error {
|
||||
@@ -263,6 +278,9 @@ func (self *SQuota) Exceed(request quotas.IQuota, quota quotas.IQuota) error {
|
||||
if sreq.IsolatedDevice > 0 && self.IsolatedDevice > squota.IsolatedDevice {
|
||||
return ErrOutOfIsolatedDevice
|
||||
}
|
||||
if self.Snapshot > squota.Snapshot {
|
||||
return ErrOutOfSnapshot
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -315,5 +333,8 @@ func (self *SQuota) ToJSON(prefix string) jsonutils.JSONObject {
|
||||
if self.IsolatedDevice > 0 {
|
||||
ret.Add(jsonutils.NewInt(int64(self.IsolatedDevice)), keyName(prefix, "isolated_device"))
|
||||
}
|
||||
if self.Snapshot > 0 {
|
||||
ret.Add(jsonutils.NewInt(int64(self.Snapshot)), keyName(prefix, "snapshot"))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
@@ -10,20 +11,19 @@ import (
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/compute/options"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
const (
|
||||
DISK_MAX_SNAPSHOT = 9
|
||||
DISK_MAX_MANUAL_SNAPSHOT = 2
|
||||
|
||||
// create by
|
||||
MANUAL = "manual"
|
||||
AUTO = "auto"
|
||||
|
||||
SNAPSHOT_FAILED = "create_failed"
|
||||
SNAPSHOT_READY = "ready"
|
||||
SNAPSHOT_FAILED = "create_failed"
|
||||
SNAPSHOT_READY = "ready"
|
||||
SNAPSHOT_DELETING = "deleting"
|
||||
)
|
||||
|
||||
type SSnapshotManager struct {
|
||||
@@ -37,7 +37,7 @@ type SSnapshot struct {
|
||||
CreatedBy string `width:"36" charset:"ascii" nullable:"false" default:"manual" list:"admin"`
|
||||
Location string `charset:"ascii" nullable:"false" list:"admin"`
|
||||
Size int `nullable:"false" list:"user"` // MB
|
||||
OutOfChain bool `nullable:"false" default:"false" index:"true" get:"admin"`
|
||||
OutOfChain bool `nullable:"false" default:"false" index:"true" list:"admin"`
|
||||
FakeDeleted bool `nullable:"false" default:"false" index:"true"`
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (self *SSnapshot) AllowCreateItem(ctx context.Context, userCred mcclient.To
|
||||
}
|
||||
|
||||
func (self *SSnapshot) AllowGetDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *SSnapshot) AllowUpdateItem(ctx context.Context, userCred mcclient.TokenCredential) bool {
|
||||
@@ -92,10 +92,19 @@ func (self *SSnapshot) GetGuest() (*SGuest, error) {
|
||||
} else if len(guests) == 1 {
|
||||
return &guests[0], nil
|
||||
} else {
|
||||
return nil, nil
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SSnapshot) GetDisk() (*SDisk, error) {
|
||||
iDisk, err := DiskManager.FetchById(self.DiskId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
disk := iDisk.(*SDisk)
|
||||
return disk, nil
|
||||
}
|
||||
|
||||
func (self *SSnapshot) GetHost() *SHost {
|
||||
iStorage, err := StorageManager.FetchById(self.StorageId)
|
||||
if err != nil {
|
||||
@@ -140,7 +149,7 @@ func (self *SSnapshotManager) GetDiskFirstSnapshot(diskId string) *SSnapshot {
|
||||
dest := &SSnapshot{}
|
||||
q := self.Query().SubQuery()
|
||||
err := q.Query().Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("disk_id"), diskId),
|
||||
sqlchemy.Equals(q.Field("status"), SNAPSHOT_READY),
|
||||
sqlchemy.In(q.Field("status"), []string{SNAPSHOT_READY, SNAPSHOT_DELETING}),
|
||||
sqlchemy.Equals(q.Field("out_of_chain"), false))).Asc("created_at").First(dest)
|
||||
if err != nil {
|
||||
log.Errorf("Get Disk First snapshot error: %s", err.Error())
|
||||
@@ -200,14 +209,23 @@ func (self *SSnapshot) ValidateDeleteCondition(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (self *SSnapshot) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
|
||||
if self.Status == SNAPSHOT_DELETING {
|
||||
return fmt.Errorf("Cannot delete snapshot in status %s", self.Status)
|
||||
} else if self.Status == VM_SNAPSHOT_FAILED {
|
||||
return self.RealDelete(ctx, userCred)
|
||||
}
|
||||
if self.CreatedBy == MANUAL {
|
||||
if !self.FakeDeleted {
|
||||
return self.FakeDelete()
|
||||
} else {
|
||||
_, err := SnapshotManager.GetConvertSnapshot(self)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot delete snapshot: %s, disk need at least one of snapshot as backing file", err.Error())
|
||||
}
|
||||
return self.StartSnapshotDeleteTask(ctx, userCred, false, "")
|
||||
}
|
||||
} else {
|
||||
return httperrors.NewBadRequestError("Cannot delete snapshot created by %s", self.CreatedBy)
|
||||
return fmt.Errorf("Cannot delete snapshot created by %s", self.CreatedBy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,27 +248,22 @@ func (self *SSnapshotManager) AllowGetPropertyMaxCount(ctx context.Context, user
|
||||
|
||||
func (self *SSnapshotManager) GetPropertyMaxCount(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
ret := jsonutils.NewDict()
|
||||
ret.Set("max_count", jsonutils.NewInt(int64(DISK_MAX_SNAPSHOT)))
|
||||
ret.Set("max_count", jsonutils.NewInt(int64(options.Options.DefaultMaxSnapshotCount)))
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (self *SSnapshotManager) GetConvertSnapshot(deleteSnapshot *SSnapshot) (*SSnapshot, error) {
|
||||
dest := make([]SSnapshot, 0)
|
||||
dest := &SSnapshot{}
|
||||
q := self.Query().SubQuery()
|
||||
err := q.Query().Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("disk_id"), deleteSnapshot.DiskId),
|
||||
sqlchemy.Equals(q.Field("status"), SNAPSHOT_READY),
|
||||
sqlchemy.Equals(q.Field("out_of_chain"), false))).
|
||||
Asc("created_at").Limit(2).All(&dest)
|
||||
sqlchemy.In(q.Field("status"), []string{SNAPSHOT_READY, SNAPSHOT_DELETING}),
|
||||
sqlchemy.Equals(q.Field("out_of_chain"), false),
|
||||
sqlchemy.GT(q.Field("created_at"), deleteSnapshot.CreatedAt))).
|
||||
Asc("created_at").First(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dest) == 2 && dest[0].Id == deleteSnapshot.Id {
|
||||
dest[1].SetModelManager(self)
|
||||
return &dest[1], nil
|
||||
} else if len(dest) == 1 && dest[0].Id == deleteSnapshot.Id {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Snapshot %s cannot convert", deleteSnapshot.Id)
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
func (self *SSnapshotManager) AllowPerformDeleteDiskSnapshots(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
@@ -280,7 +293,7 @@ func (self *SSnapshotManager) PerformDeleteDiskSnapshots(ctx context.Context, us
|
||||
}
|
||||
|
||||
func (self *SSnapshot) StartSnapshotsDeleteTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "BatchSnapshostDeleteTask", self, userCred, nil, parentTaskId, "", nil)
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "BatchSnapshotsDeleteTask", self, userCred, nil, parentTaskId, "", nil)
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return err
|
||||
@@ -305,3 +318,9 @@ func (self *SSnapshot) FakeDelete() error {
|
||||
func (self *SSnapshot) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func totalSnapshotCount(projectId string) int {
|
||||
q := SnapshotManager.Query()
|
||||
count := q.Equals("tenant_id", projectId).Equals("fake_deleted", false).Count()
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -44,13 +44,17 @@ type ComputeOptions struct {
|
||||
DefaultGroupQuota int `default:"50" help:"Common group quota per tenant, default 50"`
|
||||
DefaultSecgroupQuota int `default:"50" help:"Common security group quota per tenant, default 50"`
|
||||
DefaultIsolatedDeviceQuota int `default:"50" help:"Common isolated device quota per tenant, default 50"`
|
||||
DefaultSnapshotQuota int `default:"10" help:"Common snapshot quota per tenant, default 10"`
|
||||
|
||||
SystemAdminQuotaCheck bool `help:"Enable quota check for system admin, default False" default:"false"`
|
||||
|
||||
BaremetalPreparePackageUrl string `help:"Baremetal online register package"`
|
||||
|
||||
AutoSnapshotDay int `default:"1" help:"Days auto snapshot disks, default 1 day"`
|
||||
AutoSnapshotHour int `default:"2" help:"What hour take sanpshot, default 02:00"`
|
||||
// snapshot options
|
||||
AutoSnapshotDay int `default:"1" help:"Days auto snapshot disks, default 1 day"`
|
||||
AutoSnapshotHour int `default:"2" help:"What hour take sanpshot, default 02:00"`
|
||||
DefaultMaxSnapshotCount int `default:"9" help:"Per Disk max snapshot count, default 9"`
|
||||
DefaultMaxManualSnapshotCount int `default:"2" help:"Per Disk max manual snapshot count, default 2"`
|
||||
|
||||
cloudcommon.DBOptions
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/compute/models"
|
||||
)
|
||||
|
||||
type DiskResetTask struct {
|
||||
SDiskBaseTask
|
||||
}
|
||||
|
||||
func init() {
|
||||
taskman.RegisterTask(DiskResetTask{})
|
||||
taskman.RegisterTask(DiskCleanUpSnapshotsTask{})
|
||||
}
|
||||
|
||||
func (self *DiskResetTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
disk := obj.(*models.SDisk)
|
||||
storage := disk.GetStorage()
|
||||
if storage == nil {
|
||||
self.SetStageFailed(ctx, "Disk storage not found")
|
||||
return
|
||||
}
|
||||
host := storage.GetMasterHost()
|
||||
if host == nil {
|
||||
self.SetStageFailed(ctx, "Storage master host not found")
|
||||
return
|
||||
}
|
||||
self.RequestResetDisk(ctx, disk, host)
|
||||
}
|
||||
|
||||
func (self *DiskResetTask) RequestResetDisk(ctx context.Context, disk *models.SDisk, host *models.SHost) {
|
||||
snapshotId, err := self.Params.GetString("snapshot_id")
|
||||
if err != nil {
|
||||
self.SetStageFailed(ctx, fmt.Sprintf("Get snapshotId error %s", err.Error()))
|
||||
return
|
||||
}
|
||||
iSnapshot, _ := models.SnapshotManager.FetchById(snapshotId)
|
||||
snapshot := iSnapshot.(*models.SSnapshot)
|
||||
params := jsonutils.NewDict()
|
||||
params.Set("snapshot_id", jsonutils.NewString(snapshot.Id))
|
||||
if snapshot.OutOfChain {
|
||||
params.Set("out_of_chain", jsonutils.JSONTrue)
|
||||
} else {
|
||||
params.Set("out_of_chain", jsonutils.JSONFalse)
|
||||
}
|
||||
self.SetStage("OnRequestResetDisk", nil)
|
||||
err = host.GetHostDriver().RequestResetDisk(ctx, host, disk, params, self)
|
||||
if err != nil {
|
||||
self.SetStageFailed(ctx, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DiskResetTask) OnRequestResetDisk(ctx context.Context, disk *models.SDisk, data jsonutils.JSONObject) {
|
||||
snapshotId, _ := self.Params.GetString("snapshot_id")
|
||||
iSnapshot, _ := models.SnapshotManager.FetchById(snapshotId)
|
||||
snapshot := iSnapshot.(*models.SSnapshot)
|
||||
if disk.DiskSize != snapshot.Size {
|
||||
_, err := models.DiskManager.TableSpec().Update(disk, func() error {
|
||||
disk.DiskSize = snapshot.Size
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
err := disk.CleanUpDiskSnapshots(ctx, self.UserCred, snapshot)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
self.SetStageFailed(ctx, fmt.Sprintf("OnRequestResetDisk %s", err.Error()))
|
||||
return
|
||||
}
|
||||
self.SetStageComplete(ctx, nil)
|
||||
}
|
||||
|
||||
type DiskCleanUpSnapshotsTask struct {
|
||||
SDiskBaseTask
|
||||
}
|
||||
|
||||
func (self *DiskCleanUpSnapshotsTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
disk := obj.(*models.SDisk)
|
||||
self.StartCleanUpSnapshots(ctx, disk)
|
||||
}
|
||||
|
||||
func (self *DiskCleanUpSnapshotsTask) StartCleanUpSnapshots(ctx context.Context, disk *models.SDisk) {
|
||||
db.OpsLog.LogEvent(disk, db.ACT_DISK_CLEAN_UP_SNAPSHOTS,
|
||||
fmt.Sprintf("start clean up disk snapshots: %s", self.Params.String()), self.UserCred)
|
||||
host := disk.GetStorage().GetMasterHost()
|
||||
self.SetStage("OnCleanUpSnapshots", nil)
|
||||
err := host.GetHostDriver().RequestCleanUpDiskSnapshots(ctx, host, disk, self.Params, self)
|
||||
if err != nil {
|
||||
self.SetStageFailed(ctx, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DiskCleanUpSnapshotsTask) OnCleanUpSnapshots(ctx context.Context, disk *models.SDisk, data jsonutils.JSONObject) {
|
||||
convertSnapshots, _ := self.Params.GetArray("convert_snapshots")
|
||||
for i := 0; i < len(convertSnapshots); i++ {
|
||||
snapshot_id, _ := convertSnapshots[i].GetString()
|
||||
iSnapshot, err := models.SnapshotManager.FetchById(snapshot_id)
|
||||
if err != nil {
|
||||
log.Errorf("OnCleanUpSnapshots Fetch snapshot by id(%s) error:%s", snapshot_id, err.Error())
|
||||
continue
|
||||
}
|
||||
snapshot := iSnapshot.(*models.SSnapshot)
|
||||
models.SnapshotManager.TableSpec().Update(snapshot, func() error {
|
||||
snapshot.OutOfChain = true
|
||||
return nil
|
||||
})
|
||||
}
|
||||
deleteSnapshots, _ := self.Params.GetArray("delete_snapshots")
|
||||
for i := 0; i < len(deleteSnapshots); i++ {
|
||||
snapshot_id, _ := convertSnapshots[i].GetString()
|
||||
iSnapshot, err := models.SnapshotManager.FetchById(snapshot_id)
|
||||
if err != nil {
|
||||
log.Errorf("OnCleanUpSnapshots Fetch snapshot by id(%s) error:%s", snapshot_id, err.Error())
|
||||
continue
|
||||
}
|
||||
snapshot := iSnapshot.(*models.SSnapshot)
|
||||
snapshot.RealDelete(ctx, self.UserCred)
|
||||
}
|
||||
self.SetStageComplete(ctx, nil)
|
||||
}
|
||||
|
||||
func (self *DiskCleanUpSnapshotsTask) OnCleanUpSnapshotsFailed(ctx context.Context, disk *models.SDisk, data jsonutils.JSONObject) {
|
||||
db.OpsLog.LogEvent(disk, db.ACT_DISK_CLEAN_UP_SNAPSHOTS_FAIL, data.String(), self.UserCred)
|
||||
self.SetStageFailed(ctx, data.String())
|
||||
}
|
||||
@@ -19,14 +19,21 @@ func init() {
|
||||
taskman.RegisterTask(EipAssociateTask{})
|
||||
}
|
||||
|
||||
func (self *EipAssociateTask) TaskFail(ctx context.Context, eip *models.SElasticip, msg string, vm *models.SGuest) {
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_ASSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
if vm != nil {
|
||||
vm.StartSyncstatus(ctx, self.UserCred, "")
|
||||
}
|
||||
}
|
||||
|
||||
func (self *EipAssociateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
eip := obj.(*models.SElasticip)
|
||||
|
||||
extEip, err := eip.GetIEip()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("fail to find iEIP for eip %s", err)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_ASSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,24 +46,21 @@ func (self *EipAssociateTask) OnInit(ctx context.Context, obj db.IStandaloneMode
|
||||
|
||||
if server == nil {
|
||||
msg := fmt.Sprintf("fail to find server for instanceId %s", instanceId)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_ASSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = extEip.Associate(server.ExternalId)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("fail to remote associate EIP %s", err)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_ASSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, server)
|
||||
return
|
||||
}
|
||||
|
||||
err = eip.AssociateVM(self.UserCred, server)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("fail to local associate EIP %s", err)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_ASSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, server)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ func (self *EipChangeBandwidthTask) OnInit(ctx context.Context, obj db.IStandalo
|
||||
|
||||
extEip, err := eip.GetIEip()
|
||||
if err != nil {
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_READY, "fail to change bandwidth")
|
||||
msg := fmt.Sprintf("fail to find iEip %s", err)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
return
|
||||
@@ -31,6 +32,7 @@ func (self *EipChangeBandwidthTask) OnInit(ctx context.Context, obj db.IStandalo
|
||||
|
||||
bandwidth, _ := self.Params.Int("bandwidth")
|
||||
if bandwidth <= 0 {
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_READY, "fail to change bandwidth")
|
||||
msg := fmt.Sprintf("invalid bandwidth %d", bandwidth)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
return
|
||||
@@ -39,6 +41,7 @@ func (self *EipChangeBandwidthTask) OnInit(ctx context.Context, obj db.IStandalo
|
||||
err = extEip.ChangeBandwidth(int(bandwidth))
|
||||
|
||||
if err != nil {
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_READY, "fail to change bandwidth")
|
||||
msg := fmt.Sprintf("fail to find iEip %s", err)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
return
|
||||
|
||||
@@ -19,6 +19,14 @@ func init() {
|
||||
taskman.RegisterTask(EipDissociateTask{})
|
||||
}
|
||||
|
||||
func (self *EipDissociateTask) TaskFail(ctx context.Context, eip *models.SElasticip, msg string, vm *models.SGuest) {
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_DISSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
if vm != nil {
|
||||
vm.StartSyncstatus(ctx, self.UserCred, "")
|
||||
}
|
||||
}
|
||||
|
||||
func (self *EipDissociateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
eip := obj.(*models.SElasticip)
|
||||
|
||||
@@ -32,8 +40,7 @@ func (self *EipDissociateTask) OnInit(ctx context.Context, obj db.IStandaloneMod
|
||||
extEip, err := eip.GetIEip()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("fail to find iEIP for eip %s", err)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_DISSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, server)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,8 +48,7 @@ func (self *EipDissociateTask) OnInit(ctx context.Context, obj db.IStandaloneMod
|
||||
err = extEip.Dissociate()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("fail to remote dissociate eip %s", err)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_DISSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, server)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -50,8 +56,7 @@ func (self *EipDissociateTask) OnInit(ctx context.Context, obj db.IStandaloneMod
|
||||
err = eip.Dissociate(ctx, self.UserCred)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("fail to local dissociate eip %s", err)
|
||||
eip.SetStatus(self.UserCred, models.EIP_STATUS_DISSOCIATE_FAIL, msg)
|
||||
self.SetStageFailed(ctx, msg)
|
||||
self.TaskFail(ctx, eip, msg, server)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,11 @@ func (self *GuestChangeConfigTask) DoCreateDisksTask(ctx context.Context, guest
|
||||
|
||||
}
|
||||
|
||||
func (self *GuestChangeConfigTask) OnCreateDisksCompleteFailed(ctx context.Context, obj db.IStandaloneModel, err jsonutils.JSONObject) {
|
||||
self.markStageFailed(obj, ctx, err.String())
|
||||
logclient.AddActionLog(obj, logclient.ACT_VM_CHANGE_FLAVOR, err, self.UserCred, false)
|
||||
}
|
||||
|
||||
func (self *GuestChangeConfigTask) OnCreateDisksComplete(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
iVcpuCount, errCpu := self.Params.Get("vcpu_count")
|
||||
iVmemSize, errMem := self.Params.Get("vmem_size")
|
||||
|
||||
@@ -19,7 +19,7 @@ type GuestDiskSnapshotTask struct {
|
||||
func init() {
|
||||
taskman.RegisterTask(GuestDiskSnapshotTask{})
|
||||
taskman.RegisterTask(SnapshotDeleteTask{})
|
||||
taskman.RegisterTask(BatchSnapshostDeleteTask{})
|
||||
taskman.RegisterTask(BatchSnapshotsDeleteTask{})
|
||||
}
|
||||
|
||||
func (self *GuestDiskSnapshotTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
@@ -114,6 +114,7 @@ func (self *SnapshotDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneMo
|
||||
self.SetStageFailed(ctx, err.Error())
|
||||
return
|
||||
} else {
|
||||
// if snapshot is not used
|
||||
self.DeleteStaticSnapshot(ctx, snapshot)
|
||||
return
|
||||
}
|
||||
@@ -137,6 +138,7 @@ func (self *SnapshotDeleteTask) StartReloadDisk(ctx context.Context, snapshot *m
|
||||
}
|
||||
|
||||
func (self *SnapshotDeleteTask) StartDeleteSnapshot(ctx context.Context, snapshot *models.SSnapshot, guest *models.SGuest) {
|
||||
snapshot.SetStatus(self.UserCred, models.SNAPSHOT_DELETING, "On SnapshotDeleteTask StartDeleteSnapshot")
|
||||
convertSnapshot, err := models.SnapshotManager.GetConvertSnapshot(snapshot)
|
||||
if err != nil {
|
||||
self.TaskFailed(ctx, snapshot, err.Error())
|
||||
@@ -169,14 +171,6 @@ func (self *SnapshotDeleteTask) StartDeleteSnapshot(ctx context.Context, snapsho
|
||||
}
|
||||
|
||||
func (self *SnapshotDeleteTask) DeleteStaticSnapshot(ctx context.Context, snapshot *models.SSnapshot) {
|
||||
// convertSnapshot, err := models.SnapshotManager.GetConvertSnapshot(snapshot)
|
||||
// if err != nil {
|
||||
// self.TaskFailed(ctx, snapshot, err.Error())
|
||||
// return
|
||||
// }
|
||||
// if convertSnapshot == nil {
|
||||
// self.TaskFailed(ctx, snapshot, "Snapshot dose not have convert snapshot")
|
||||
// }
|
||||
err := snapshot.FakeDelete()
|
||||
if err != nil {
|
||||
self.TaskFailed(ctx, snapshot, err.Error())
|
||||
@@ -190,21 +184,21 @@ func (self *SnapshotDeleteTask) OnDeleteSnapshot(ctx context.Context, snapshot *
|
||||
log.Infof("OnDeleteSnapshot with no deleted")
|
||||
return
|
||||
}
|
||||
snapshot.SetStatus(self.UserCred, models.SNAPSHOT_READY, "OnDeleteSnapshot")
|
||||
if snapshot.OutOfChain {
|
||||
snapshot.RealDelete(ctx, self.UserCred)
|
||||
self.TaskComplete(ctx, snapshot, nil)
|
||||
} else {
|
||||
guest, err := snapshot.GetGuest()
|
||||
if err != nil {
|
||||
self.SetStageFailed(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
guest, _ := snapshot.GetGuest()
|
||||
var FakeDelete = false
|
||||
if snapshot.CreatedBy == models.MANUAL && snapshot.FakeDeleted == false {
|
||||
FakeDelete = true
|
||||
}
|
||||
if FakeDelete {
|
||||
snapshot.FakeDelete()
|
||||
models.SnapshotManager.TableSpec().Update(snapshot, func() error {
|
||||
snapshot.OutOfChain = true
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
snapshot.RealDelete(ctx, self.UserCred)
|
||||
}
|
||||
@@ -214,7 +208,7 @@ func (self *SnapshotDeleteTask) OnDeleteSnapshot(ctx context.Context, snapshot *
|
||||
}
|
||||
|
||||
func (self *SnapshotDeleteTask) OnDeleteSnapshotFailed(ctx context.Context, snapshot *models.SSnapshot, data jsonutils.JSONObject) {
|
||||
self.SetStageFailed(ctx, data.String())
|
||||
self.TaskFailed(ctx, snapshot, data.String())
|
||||
}
|
||||
|
||||
func (self *SnapshotDeleteTask) OnReloadDiskSnapshot(ctx context.Context, snapshot *models.SSnapshot, data jsonutils.JSONObject) {
|
||||
@@ -249,6 +243,9 @@ func (self *SnapshotDeleteTask) TaskComplete(ctx context.Context, snapshot *mode
|
||||
}
|
||||
|
||||
func (self *SnapshotDeleteTask) TaskFailed(ctx context.Context, snapshot *models.SSnapshot, reason string) {
|
||||
if snapshot.Status == models.SNAPSHOT_DELETING {
|
||||
snapshot.SetStatus(self.UserCred, models.SNAPSHOT_READY, "On SnapshotDeleteTask TaskFailed")
|
||||
}
|
||||
self.SetStageFailed(ctx, reason)
|
||||
guest, err := snapshot.GetGuest()
|
||||
if err != nil {
|
||||
@@ -258,29 +255,31 @@ func (self *SnapshotDeleteTask) TaskFailed(ctx context.Context, snapshot *models
|
||||
guest.StartSyncstatus(ctx, self.UserCred, "")
|
||||
}
|
||||
|
||||
type BatchSnapshostDeleteTask struct {
|
||||
/***************************** Batch Snapshots Delete Task *****************************/
|
||||
|
||||
type BatchSnapshotsDeleteTask struct {
|
||||
taskman.STask
|
||||
}
|
||||
|
||||
func (self *BatchSnapshostDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
func (self *BatchSnapshotsDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
snapshot := obj.(*models.SSnapshot)
|
||||
self.StartStorageDeleteSnapshot(ctx, snapshot)
|
||||
}
|
||||
|
||||
func (self *BatchSnapshostDeleteTask) StartStorageDeleteSnapshot(ctx context.Context, snapshot *models.SSnapshot) {
|
||||
func (self *BatchSnapshotsDeleteTask) StartStorageDeleteSnapshot(ctx context.Context, snapshot *models.SSnapshot) {
|
||||
host := snapshot.GetHost()
|
||||
if host == nil {
|
||||
self.SetStageFailed(ctx, "Cannot found snapshot host")
|
||||
return
|
||||
}
|
||||
self.SetStage("OnStorageDeleteSnapshot", nil)
|
||||
err := host.GetHostDriver().RequestDeleteSnapshotWithStorage(ctx, host, snapshot, self)
|
||||
err := host.GetHostDriver().RequestDeleteSnapshotsWithStorage(ctx, host, snapshot, self)
|
||||
if err != nil {
|
||||
self.SetStageFailed(ctx, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BatchSnapshostDeleteTask) OnStorageDeleteSnapshot(ctx context.Context, snapshot *models.SSnapshot, data jsonutils.JSONObject) {
|
||||
func (self *BatchSnapshotsDeleteTask) OnStorageDeleteSnapshot(ctx context.Context, snapshot *models.SSnapshot, data jsonutils.JSONObject) {
|
||||
snapshots := models.SnapshotManager.GetDiskSnapshots(snapshot.DiskId)
|
||||
for i := 0; i < len(snapshots); i++ {
|
||||
snapshots[i].RealDelete(ctx, self.UserCred)
|
||||
|
||||
@@ -59,6 +59,14 @@ func (self *GuestRebuildRootTask) StartRebuildRootDisk(ctx context.Context, gues
|
||||
|
||||
self.SetStage("OnRebuildRootDiskComplete", nil)
|
||||
guest.SetStatus(self.UserCred, models.VM_REBUILD_ROOT, "")
|
||||
|
||||
// clear logininfo
|
||||
loginParams := make(map[string]interface{})
|
||||
loginParams["login_account"] = "none"
|
||||
loginParams["login_key"] = "none"
|
||||
loginParams["login_key_timestamp"] = "none"
|
||||
guest.SetAllMetadata(ctx, loginParams, self.UserCred)
|
||||
|
||||
guest.GetDriver().RequestRebuildRootDisk(ctx, guest, self)
|
||||
}
|
||||
|
||||
@@ -141,14 +149,6 @@ func (self *KVMGuestRebuildRootTask) OnRebuildRootDiskComplete(ctx context.Conte
|
||||
guest.SetStatus(self.UserCred, models.VM_DEPLOYING, "")
|
||||
// params := jsonutils.NewDict()
|
||||
// params.Set("reset_password", jsonutils.JSONTrue)
|
||||
|
||||
// clear logininfo
|
||||
loginParams := make(map[string]interface{})
|
||||
loginParams["login_account"] = "none"
|
||||
loginParams["login_key"] = "none"
|
||||
loginParams["login_key_timestamp"] = "none"
|
||||
guest.SetAllMetadata(ctx, loginParams, self.UserCred)
|
||||
|
||||
guest.StartGuestDeployTask(ctx, self.UserCred, self.GetParams(), "deploy", self.GetTaskId())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
|
||||
json "yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/tristate"
|
||||
"yunion.io/x/pkg/util/sets"
|
||||
"yunion.io/x/pkg/utils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/appctx"
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
@@ -14,8 +18,6 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
"yunion.io/x/pkg/tristate"
|
||||
"yunion.io/x/pkg/utils"
|
||||
)
|
||||
|
||||
type Usage map[string]interface{}
|
||||
@@ -160,12 +162,8 @@ func ReportCloudRegionUsage(userCred mcclient.TokenCredential, cloudRegion db.IS
|
||||
|
||||
//func ReportGuestUsage()
|
||||
|
||||
func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) (count Usage, err error) {
|
||||
func getAdminGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) (count Usage, err error) {
|
||||
count = ZoneUsage()
|
||||
if !userCred.IsSystemAdmin() {
|
||||
return
|
||||
}
|
||||
|
||||
var pmemTotal float64
|
||||
var pcpuTotal float64
|
||||
|
||||
@@ -181,9 +179,14 @@ func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandalo
|
||||
count.Add("memory.virtual", host.GetVirtualMemorySize())
|
||||
count.Add("cpu.virtual", host.GetVirtualCPUCount())
|
||||
}
|
||||
guestRunningUsage := GuestRunningUsage(userCred, rangeObj, hostTypes)
|
||||
guestRunningUsage := GuestRunningUsage("all.running_servers", nil, rangeObj, hostTypes)
|
||||
runningMem := guestRunningUsage.Get("all.running_servers.memory").(int)
|
||||
runningCpu := guestRunningUsage.Get("all.running_servers.cpu").(int)
|
||||
containerRunningUsage := containerUsage("all.containers", nil, rangeObj)
|
||||
containerRunningMem := containerRunningUsage.Get("all.containers.memory").(int)
|
||||
containerRunningCpu := containerRunningUsage.Get("all.containers.cpu").(int)
|
||||
runningMem += containerRunningMem
|
||||
runningCpu += containerRunningCpu
|
||||
runningCpuCmtRate := 0.0
|
||||
runningMemCmtRate := 0.0
|
||||
if pmemTotal > 0 {
|
||||
@@ -209,10 +212,11 @@ func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandalo
|
||||
hostEnabledUsage,
|
||||
BaremetalUsage(userCred, rangeObj, hostTypes),
|
||||
storageUsage,
|
||||
GuestNormalUsage(userCred, rangeObj, hostTypes),
|
||||
GuestPendingDeleteUsage(userCred, rangeObj, hostTypes),
|
||||
GuestReadyUsage(userCred, rangeObj, hostTypes),
|
||||
GuestNormalUsage("all.servers", nil, rangeObj, hostTypes),
|
||||
GuestPendingDeleteUsage("all.pending_delete_servers", nil, rangeObj, hostTypes),
|
||||
GuestReadyUsage("all.ready_servers", nil, rangeObj, hostTypes),
|
||||
guestRunningUsage,
|
||||
containerRunningUsage,
|
||||
IsolatedDeviceUsage(rangeObj, hostTypes),
|
||||
WireUsage(rangeObj, hostTypes),
|
||||
NetworkUsage(userCred, rangeObj),
|
||||
@@ -222,6 +226,37 @@ func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandalo
|
||||
return
|
||||
}
|
||||
|
||||
func getCommonGeneralUsage(cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) (count Usage, err error) {
|
||||
guestNormalUsage := GuestNormalUsage("servers", cred, rangeObj, hostTypes)
|
||||
guestRunningUsage := GuestRunningUsage("running_servers", cred, rangeObj, hostTypes)
|
||||
guestPendingDeleteUsage := GuestPendingDeleteUsage("pending_delete_servers", cred, rangeObj, hostTypes)
|
||||
guestReadyUsage := GuestReadyUsage("ready_servers", cred, rangeObj, hostTypes)
|
||||
containerUsage := containerUsage("containers", cred, rangeObj)
|
||||
count = guestNormalUsage.Include(
|
||||
guestRunningUsage,
|
||||
guestPendingDeleteUsage,
|
||||
guestReadyUsage,
|
||||
containerUsage,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func ReportGeneralUsage(userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) (count Usage, err error) {
|
||||
count = make(map[string]interface{})
|
||||
if userCred.IsSystemAdmin() {
|
||||
count, err = getAdminGeneralUsage(userCred, rangeObj, hostTypes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
commonUsage, err := getCommonGeneralUsage(userCred, rangeObj, hostTypes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
count.Include(commonUsage)
|
||||
return
|
||||
}
|
||||
|
||||
func ZoneUsage() Usage {
|
||||
count := make(map[string]interface{})
|
||||
count["zones"] = models.ZoneManager.Count()
|
||||
@@ -294,27 +329,29 @@ func hostUsage(
|
||||
return count
|
||||
}
|
||||
|
||||
func GuestNormalUsage(cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
prefix := "all.servers"
|
||||
func GuestNormalUsage(prefix string, cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
return guestUsage(prefix, cred, rangeObj, hostTypes, nil, false)
|
||||
}
|
||||
|
||||
func GuestPendingDeleteUsage(cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
prefix := "all.pending_delete_servers"
|
||||
func GuestPendingDeleteUsage(prefix string, cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
return guestUsage(prefix, cred, rangeObj, hostTypes, nil, true)
|
||||
}
|
||||
|
||||
func GuestRunningUsage(cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
prefix := "all.running_servers"
|
||||
func GuestRunningUsage(prefix string, cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
return guestUsage(prefix, cred, rangeObj, hostTypes, []string{models.VM_RUNNING}, false)
|
||||
}
|
||||
|
||||
func GuestReadyUsage(cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
prefix := "all.ready_servers"
|
||||
func GuestReadyUsage(prefix string, cred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes []string) Usage {
|
||||
return guestUsage(prefix, cred, rangeObj, hostTypes, []string{models.VM_READY}, false)
|
||||
}
|
||||
|
||||
func guestUsage(prefix string, userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes, status []string, pendingDelete bool) Usage {
|
||||
func guestHypervisorsUsage(
|
||||
prefix string,
|
||||
userCred mcclient.TokenCredential,
|
||||
rangeObj db.IStandaloneModel,
|
||||
hostTypes, status, hypervisors []string,
|
||||
pendingDelete bool,
|
||||
) Usage {
|
||||
hostType := ""
|
||||
if len(hostTypes) != 0 {
|
||||
hostType = hostTypes[0]
|
||||
@@ -323,17 +360,33 @@ func guestUsage(prefix string, userCred mcclient.TokenCredential, rangeObj db.IS
|
||||
if userCred != nil {
|
||||
projectId = userCred.GetProjectId()
|
||||
}
|
||||
guest := models.GuestManager.TotalCount(projectId, rangeObj, status, "", true, pendingDelete, hostType)
|
||||
guest := models.GuestManager.TotalCount(projectId, rangeObj, status, hypervisors, true, pendingDelete, hostType)
|
||||
count := make(map[string]interface{})
|
||||
count[prefix] = guest.TotalGuestCount
|
||||
count[fmt.Sprintf("%s.cpu", prefix)] = guest.TotalCpuCount
|
||||
count[fmt.Sprintf("%s.memory", prefix)] = guest.TotalMemSize
|
||||
|
||||
if len(hypervisors) == 1 && hypervisors[0] == models.HYPERVISOR_CONTAINER {
|
||||
return count
|
||||
}
|
||||
|
||||
count[fmt.Sprintf("%s.disk", prefix)] = guest.TotalDiskSize
|
||||
count[fmt.Sprintf("%s.isolated_devices", prefix)] = guest.TotalIsolatedCount
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func guestUsage(prefix string, userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel, hostTypes, status []string, pendingDelete bool) Usage {
|
||||
hypervisors := sets.NewString(models.HYPERVISORS...)
|
||||
hypervisors.Delete(models.HYPERVISOR_CONTAINER)
|
||||
return guestHypervisorsUsage(prefix, userCred, rangeObj, hostTypes, status, hypervisors.List(), pendingDelete)
|
||||
}
|
||||
|
||||
func containerUsage(prefix string, userCred mcclient.TokenCredential, rangeObj db.IStandaloneModel) Usage {
|
||||
hypervisors := []string{models.HYPERVISOR_CONTAINER}
|
||||
return guestHypervisorsUsage(prefix, userCred, rangeObj, nil, nil, hypervisors, false)
|
||||
}
|
||||
|
||||
func IsolatedDeviceUsage(rangeObj db.IStandaloneModel, hostType []string) Usage {
|
||||
prefix := "isolated_devices"
|
||||
ret := models.IsolatedDeviceManager.TotalCount(hostType, rangeObj)
|
||||
|
||||
@@ -115,3 +115,11 @@ func NewYunionAgentManager(keyword, keywordPlural string, columns, adminColumns
|
||||
serviceType: "yunionagent"},
|
||||
Keyword: keyword, KeywordPlural: keywordPlural}
|
||||
}
|
||||
|
||||
func NewYunionConfManager(keyword, keywordPlural string, columns, adminColumns []string) ResourceManager {
|
||||
return ResourceManager{
|
||||
BaseManager: BaseManager{columns: columns,
|
||||
adminColumns: adminColumns,
|
||||
serviceType: "yunionconf"},
|
||||
Keyword: keyword, KeywordPlural: keywordPlural}
|
||||
}
|
||||
@@ -128,6 +128,21 @@ func (this *ImageManager) List(session *mcclient.ClientSession, params jsonutils
|
||||
return this._list(session, path, this.KeywordPlural)
|
||||
}
|
||||
|
||||
func (this *ImageManager) GetPrivateImageCount(s *mcclient.ClientSession, ownerId string, isAdmin bool) (int, error) {
|
||||
params := jsonutils.NewDict()
|
||||
params.Add(jsonutils.NewString("none"), "is_public")
|
||||
params.Add(jsonutils.NewString(ownerId), "owner")
|
||||
if isAdmin {
|
||||
params.Add(jsonutils.JSONTrue, "admin")
|
||||
}
|
||||
|
||||
result, err := this.List(s, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(result.Data), nil
|
||||
}
|
||||
|
||||
type ImageUsageCount struct {
|
||||
Count int64
|
||||
Size int64
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package modules
|
||||
|
||||
type ParametersManager struct {
|
||||
ResourceManager
|
||||
}
|
||||
|
||||
var (
|
||||
Parameters ParametersManager
|
||||
)
|
||||
|
||||
func init() {
|
||||
Parameters = ParametersManager{NewYunionConfManager("parameter", "parameters",
|
||||
[]string{"id", "created_at", "update_at", "name", "value"},
|
||||
[]string{"namespace", "namespace_id", "created_by", "updated_by"},
|
||||
)}
|
||||
register(&Parameters)
|
||||
}
|
||||
@@ -6,7 +6,8 @@ var (
|
||||
|
||||
func init() {
|
||||
ResResults = NewMeterManager("res_result", "res_results",
|
||||
[]string{"stat_month", "start_date", "end_date", "filter", "project_id"},
|
||||
[]string{"res_id", "res_name", "cpu", "mem", "sys_disk", "data_disk", "ips", "res_type", "band_width", "os_distribution", "os_version", "platform", "region_id",
|
||||
"project_name", "user_name", "start_time", "end_time", "time_length", "cpu_amount", "mem_amount", "disk_amount", "baremetal_amount", "gpu_amount", "res_fee"},
|
||||
[]string{},
|
||||
)
|
||||
register(&ResResults)
|
||||
|
||||
@@ -10,7 +10,7 @@ func init() {
|
||||
"guestdisks",
|
||||
[]string{"Guest_ID", "Guest",
|
||||
"Disk_ID", "Disk", "Disk_size",
|
||||
"Driver", "Cache_mode", "Index", "Status"},
|
||||
"Driver", "Cache_mode", "Index", "Status", "Disk_type"},
|
||||
[]string{},
|
||||
&Servers,
|
||||
&Disks)
|
||||
|
||||
@@ -8,7 +8,7 @@ func init() {
|
||||
Snapshots = NewComputeManager("snapshot", "snapshots",
|
||||
[]string{"ID", "Name", "Size", "Status",
|
||||
"Disk_id", "Guest_id", "Created_at"},
|
||||
[]string{"Storage_id", "Create_by", "Location"})
|
||||
[]string{"Storage_id", "Create_by", "Location", "Out_of_chain"})
|
||||
|
||||
registerCompute(&Snapshots)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,9 @@ type BaseListOptions struct {
|
||||
PendingDeleteAll *bool `help:"Show all resources including pending deleted" json:"-"`
|
||||
Field []string `help:"Show only specified fields"`
|
||||
ShowEmulated *bool `help:"Show all resources including the emulated resources"`
|
||||
ExportFile string `help:"Export to file" metavar:"<EXPORT_FILE_PATH>" json:"-"`
|
||||
ExportKeys string `help:"Export field keys"`
|
||||
ExportTexts string `help:"Export field displayname texts" json:"-"`
|
||||
}
|
||||
|
||||
func (opts *BaseListOptions) Params() (*jsonutils.JSONDict, error) {
|
||||
|
||||
@@ -155,6 +155,10 @@ func (this *ClientSession) GetRegion() string {
|
||||
return this.region
|
||||
}
|
||||
|
||||
func (this *ClientSession) GetUserId() string {
|
||||
return this.token.GetUserId()
|
||||
}
|
||||
|
||||
func (this *ClientSession) GetTenantId() string {
|
||||
return this.token.GetTenantId()
|
||||
}
|
||||
|
||||
@@ -314,11 +314,27 @@ func (self *SInstance) GetHypervisor() string {
|
||||
}
|
||||
|
||||
func (self *SInstance) StartVM() error {
|
||||
err := self.host.zone.region.StartVM(self.InstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
timeout := 300*time.Second
|
||||
interval := 15*time.Second
|
||||
|
||||
startTime := time.Now()
|
||||
for time.Now().Sub(startTime) < timeout {
|
||||
err := self.Refresh()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("status %s expect %s", self.GetStatus(), models.VM_RUNNING)
|
||||
if self.GetStatus() == models.VM_RUNNING {
|
||||
return nil
|
||||
} else if self.GetStatus() == models.VM_READY {
|
||||
err := self.host.zone.region.StartVM(self.InstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
time.Sleep(interval)
|
||||
}
|
||||
return cloudprovider.WaitStatus(self, models.VM_RUNNING, 5*time.Second, 180*time.Second) // 3minutes
|
||||
return cloudprovider.ErrTimeout
|
||||
}
|
||||
|
||||
func (self *SInstance) StopVM(isForce bool) error {
|
||||
|
||||
@@ -2,13 +2,13 @@ package provider
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudprovider"
|
||||
// "yunion.io/x/log"
|
||||
"yunion.io/x/onecloud/pkg/util/aliyun"
|
||||
)
|
||||
|
||||
type SAliyunProviderFactory struct {
|
||||
providerTable map[string]*SAliyunProvider
|
||||
// providerTable map[string]*SAliyunProvider
|
||||
}
|
||||
|
||||
func (self *SAliyunProviderFactory) GetId() string {
|
||||
@@ -16,26 +16,33 @@ func (self *SAliyunProviderFactory) GetId() string {
|
||||
}
|
||||
|
||||
func (self *SAliyunProviderFactory) GetProvider(providerId, providerName, url, account, secret string) (cloudprovider.ICloudProvider, error) {
|
||||
provider, ok := self.providerTable[providerId]
|
||||
if ok {
|
||||
err := provider.client.UpdateAccount(account, secret)
|
||||
/* provider, ok := self.providerTable[providerId]
|
||||
if ok {
|
||||
err := provider.client.UpdateAccount(account, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return provider, nil
|
||||
}
|
||||
}
|
||||
client, err := aliyun.NewAliyunClient(providerId, providerName, account, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return provider, nil
|
||||
}
|
||||
}
|
||||
self.providerTable[providerId] = &SAliyunProvider{client: client}
|
||||
return self.providerTable[providerId], nil
|
||||
*/
|
||||
|
||||
client, err := aliyun.NewAliyunClient(providerId, providerName, account, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
self.providerTable[providerId] = &SAliyunProvider{client: client}
|
||||
return self.providerTable[providerId], nil
|
||||
return &SAliyunProvider{client: client}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
factory := SAliyunProviderFactory{
|
||||
providerTable: make(map[string]*SAliyunProvider),
|
||||
// providerTable: make(map[string]*SAliyunProvider),
|
||||
}
|
||||
cloudprovider.RegisterFactory(&factory)
|
||||
}
|
||||
|
||||
@@ -281,7 +281,8 @@ func (listener *OssProgressListener) ProgressChanged(event *oss.ProgressEvent) {
|
||||
}
|
||||
|
||||
func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error) {
|
||||
tmpImageFile := fmt.Sprintf("/tmp/%s", extId)
|
||||
tmpImageFile := fmt.Sprintf("/opt/cloud/workspace/data/glance/image-cache/%s", extId)
|
||||
defer os.Remove(tmpImageFile)
|
||||
bucketName := strings.ToLower(fmt.Sprintf("imgcache-%s", self.region.GetId()))
|
||||
if bucket, err := self.region.checkBucket(bucketName); err != nil {
|
||||
return nil, err
|
||||
@@ -305,7 +306,6 @@ func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imag
|
||||
} else if result, err := modules.Images.Upload(s, params, file, imageList.Objects[0].Size); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
os.Remove(tmpImageFile)
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,3 +152,7 @@ func (self *SRegion) deleteVSwitch(vswitchId string) error {
|
||||
func (self *SVSwitch) Delete() error {
|
||||
return self.wire.zone.region.deleteVSwitch(self.VSwitchId)
|
||||
}
|
||||
|
||||
func (self *SVSwitch) GetAllocTimeoutSeconds() int {
|
||||
return 120 // 2 minutes
|
||||
}
|
||||
@@ -222,8 +222,7 @@ func (self *SDisk) GetName() string {
|
||||
}
|
||||
|
||||
func (self *SDisk) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, DISK_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SDisk) IsEmulated() bool {
|
||||
@@ -267,8 +266,7 @@ func (self *SDisk) GetIsAutoDelete() bool {
|
||||
}
|
||||
|
||||
func (self *SDisk) GetTemplateId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.Properties.CreationData.ImageReference.ID, IMAGE_RESOURCE)
|
||||
return globalId
|
||||
return self.Properties.CreationData.ImageReference.ID
|
||||
}
|
||||
|
||||
func (self *SDisk) GetDiskType() string {
|
||||
|
||||
@@ -262,8 +262,7 @@ func (self *SEipAddress) GetAssociationExternalId() string {
|
||||
if nic, err := self.region.GetNetworkInterfaceDetail(self.Properties.IPConfiguration.ID); err != nil {
|
||||
log.Errorf("Failt to find NetworkInterface for eip %s", self.Name)
|
||||
} else if len(nic.Properties.VirtualMachine.ID) > 0 {
|
||||
globalId, _, _ := pareResourceGroupWithName(nic.Properties.VirtualMachine.ID, INSTANCE_RESOURCE)
|
||||
return globalId
|
||||
return nic.Properties.VirtualMachine.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -277,8 +276,7 @@ func (self *SEipAddress) GetBandwidth() int {
|
||||
}
|
||||
|
||||
func (self *SEipAddress) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, EIP_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SEipAddress) GetId() string {
|
||||
|
||||
@@ -86,8 +86,7 @@ func (self *SImage) IsEmulated() bool {
|
||||
}
|
||||
|
||||
func (self *SImage) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, IMAGE_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SImage) GetStatus() string {
|
||||
|
||||
@@ -196,12 +196,12 @@ func (self *SRegion) GetInstances() ([]SInstance, error) {
|
||||
return instances, err
|
||||
} else {
|
||||
for _, _instance := range instanceList.Values() {
|
||||
instance := SInstance{}
|
||||
if *_instance.Location == self.Name {
|
||||
if err := jsonutils.Update(&instance, _instance); err != nil {
|
||||
return instances, err
|
||||
if instance, err := self.GetInstance(*_instance.ID); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
instances = append(instances, *instance)
|
||||
}
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -618,8 +618,7 @@ func (self *SInstance) GetName() string {
|
||||
}
|
||||
|
||||
func (self *SInstance) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, INSTANCE_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SRegion) GetInstanceStatus(instanceId string) (string, error) {
|
||||
|
||||
@@ -35,8 +35,7 @@ func (self *SNetwork) GetName() string {
|
||||
}
|
||||
|
||||
func (self *SNetwork) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, VPC_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SNetwork) IsEmulated() bool {
|
||||
@@ -121,11 +120,14 @@ func (self *SNetwork) GetServerType() string {
|
||||
}
|
||||
|
||||
func (self *SNetwork) Refresh() error {
|
||||
// log.Debugf("vsiwtch refresh %s", self.VSwitchId)
|
||||
// new, err := self.wire.zone.region.getVSwitch(self.VSwitchId)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return jsonutils.Update(self, new)
|
||||
if new, err := self.wire.zone.region.GetNetworkDetail(self.ID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return jsonutils.Update(self, new)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SNetwork) GetAllocTimeoutSeconds() int {
|
||||
return 120 // 2 minutes
|
||||
}
|
||||
|
||||
@@ -153,8 +153,7 @@ func (self *SSecurityGroup) GetMetadata() *jsonutils.JSONDict {
|
||||
}
|
||||
|
||||
func (self *SSecurityGroup) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, SECGRP_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SSecurityGroup) GetDescription() string {
|
||||
|
||||
@@ -31,8 +31,7 @@ func (self *SSnapshot) GetId() string {
|
||||
}
|
||||
|
||||
func (self *SSnapshot) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, SNAPSHOT_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SSnapshot) GetMetadata() *jsonutils.JSONDict {
|
||||
|
||||
@@ -187,8 +187,8 @@ func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imag
|
||||
return nil, err
|
||||
} else {
|
||||
_, _, snapshot := pareResourceGroupWithName(snapshotId, SNAPSHOT_RESOURCE)
|
||||
tmpImageFile := fmt.Sprintf("/tmp/%s", snapshot)
|
||||
//resp.ContentLength =
|
||||
tmpImageFile := fmt.Sprintf("/opt/cloud/workspace/data/glance/image-cache/%s", snapshot)
|
||||
defer os.Remove(tmpImageFile)
|
||||
if f, err := os.Create(tmpImageFile); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
@@ -221,7 +221,6 @@ func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imag
|
||||
} else if result, err := modules.Images.Upload(s, params, file, resp.ContentLength); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
os.Remove(tmpImageFile)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -2,6 +2,7 @@ package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
@@ -64,8 +65,7 @@ func (self *SVpc) GetName() string {
|
||||
}
|
||||
|
||||
func (self *SVpc) GetGlobalId() string {
|
||||
globalId, _, _ := pareResourceGroupWithName(self.ID, VPC_RESOURCE)
|
||||
return globalId
|
||||
return self.ID
|
||||
}
|
||||
|
||||
func (self *SVpc) IsEmulated() bool {
|
||||
@@ -241,3 +241,18 @@ func (self *SVpc) addWire(wire *SWire) {
|
||||
func (self *SVpc) GetNetworks() []Subnet {
|
||||
return self.Properties.Subnets
|
||||
}
|
||||
|
||||
func (self *SRegion) GetNetworkDetail(networkId string) (*Subnet, error) {
|
||||
valid := regexp.MustCompile("resourceGroups/(.+)/providers/Microsoft.Network/virtualNetworks/(.+)/subnets/(.+)$")
|
||||
if data := valid.FindStringSubmatch(networkId); len(data) == 4 {
|
||||
sunet := Subnet{}
|
||||
networkClient := network.NewSubnetsClientWithBaseURI(self.client.baseUrl, self.SubscriptionID)
|
||||
if result, err := networkClient.Get(context.Background(), data[1], data[2], data[3], ""); err != nil {
|
||||
return nil, err
|
||||
} else if err := jsonutils.Update(&sunet, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sunet, nil
|
||||
}
|
||||
return nil, cloudprovider.ErrNotFound
|
||||
}
|
||||
|
||||
+15
-19
@@ -60,7 +60,7 @@ func (self *SWire) addNetwork(network *SNetwork) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SRegion) createNetwork(vpc *SVpc, subnetName string, cidr string, desc string) (string, error) {
|
||||
func (self *SRegion) createNetwork(vpc *SVpc, subnetName string, cidr string, desc string) (*SNetwork, error) {
|
||||
addressSpace := network.AddressSpace{AddressPrefixes: &vpc.Properties.AddressSpace.AddressPrefixes}
|
||||
subnets := []network.Subnet{}
|
||||
for i := 0; i < len(vpc.Properties.Subnets); i++ {
|
||||
@@ -79,27 +79,25 @@ func (self *SRegion) createNetwork(vpc *SVpc, subnetName string, cidr string, de
|
||||
networkClient := network.NewVirtualNetworksClientWithBaseURI(self.client.baseUrl, self.SubscriptionID)
|
||||
networkClient.Authorizer = self.client.authorizer
|
||||
_, resourceGroup, vpcName := pareResourceGroupWithName(vpc.ID, VPC_RESOURCE)
|
||||
networkId, _, _ := pareResourceGroupWithName(subnetName, VPC_RESOURCE)
|
||||
if result, err := networkClient.CreateOrUpdate(context.Background(), resourceGroup, vpcName, params); err != nil {
|
||||
return "", err
|
||||
} else if err := result.WaitForCompletion(context.Background(), networkClient.Client); err != nil {
|
||||
return "", err
|
||||
result := SNetwork{}
|
||||
if resp, err := networkClient.CreateOrUpdate(context.Background(), resourceGroup, vpcName, params); err != nil {
|
||||
return nil, err
|
||||
} else if err := resp.WaitForCompletion(context.Background(), networkClient.Client); err != nil {
|
||||
return nil, err
|
||||
} else if net, err := resp.Result(networkClient); err != nil {
|
||||
return nil, err
|
||||
} else if err := jsonutils.Update(&result, net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return networkId, nil
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (self *SWire) CreateINetwork(name string, cidr string, desc string) (cloudprovider.ICloudNetwork, error) {
|
||||
if networkId, err := self.zone.region.createNetwork(self.vpc, name, cidr, desc); err != nil {
|
||||
log.Errorf("createNetwork error %s", err)
|
||||
if network, err := self.zone.region.createNetwork(self.vpc, name, cidr, desc); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
self.inetworks = nil
|
||||
if network := self.getNetworkById(networkId); network == nil {
|
||||
log.Errorf("cannot find network after create????")
|
||||
return nil, cloudprovider.ErrNotFound
|
||||
} else {
|
||||
return network, nil
|
||||
}
|
||||
network.wire = self
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,12 +138,10 @@ func (self *SWire) getNetworkById(networkId string) *SNetwork {
|
||||
log.Errorf("getNetworkById error: %v", err)
|
||||
return nil
|
||||
} else {
|
||||
globalId, _, _ := pareResourceGroupWithName(networkId, VPC_RESOURCE)
|
||||
log.Debugf("search for networks %d", len(networks))
|
||||
for i := 0; i < len(networks); i++ {
|
||||
network := networks[i].(*SNetwork)
|
||||
_globalId, _, _ := pareResourceGroupWithName(network.ID, VPC_RESOURCE)
|
||||
if globalId == _globalId {
|
||||
if networkId == network.ID {
|
||||
return network
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package excelutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_SHEET = "Sheet1"
|
||||
)
|
||||
|
||||
func decimalBaseMaxWidth(decNum int, base int) int {
|
||||
if decNum == 0 {
|
||||
return 1
|
||||
}
|
||||
width := 0
|
||||
for decNum > 0 {
|
||||
decNum = decNum/base
|
||||
width += 1
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func decimalBaseN(decNum int, base int, width int) (int, int) {
|
||||
b := 1
|
||||
for i := 0; i < width - 1; i += 1 {
|
||||
decNum = decNum/base
|
||||
b = b*base
|
||||
}
|
||||
return decNum, b
|
||||
}
|
||||
|
||||
func decimal2Base(decNum int, base int) []int {
|
||||
width := decimalBaseMaxWidth(decNum, base)
|
||||
ret := make([]int, width)
|
||||
for i := width; i > 0; i -= 1 {
|
||||
ith, divider := decimalBaseN(decNum, base, i)
|
||||
decNum -= ith*divider
|
||||
ret[width - i] = ith
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func decimal2Alphabet(decNum int) string {
|
||||
var buf bytes.Buffer
|
||||
b26 := decimal2Base(decNum, 26)
|
||||
for i := 0; i < len(b26); i += 1 {
|
||||
if i == 0 && len(b26) > 1 {
|
||||
buf.WriteByte(byte('A' + b26[i] - 1))
|
||||
} else {
|
||||
buf.WriteByte(byte('A' + b26[i]))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func exportHeader(xlsx *excelize.File, texts []string, rowIndex int) {
|
||||
for i := 0; i < len(texts); i += 1 {
|
||||
cell := fmt.Sprintf("%s%d", decimal2Alphabet(i), rowIndex)
|
||||
xlsx.SetCellValue(DEFAULT_SHEET, cell, texts[i])
|
||||
}
|
||||
}
|
||||
|
||||
func exportRow(xlsx *excelize.File, data jsonutils.JSONObject, keys []string, rowIndex int) {
|
||||
for i := 0; i < len(keys); i += 1 {
|
||||
var valStr string
|
||||
val, _ := data.GetIgnoreCases(keys[i])
|
||||
if val != nil {
|
||||
valStr, _ = val.GetString()
|
||||
}
|
||||
cell := fmt.Sprintf("%s%d", decimal2Alphabet(i), rowIndex)
|
||||
xlsx.SetCellValue(DEFAULT_SHEET, cell, valStr)
|
||||
}
|
||||
}
|
||||
|
||||
func Export(data []jsonutils.JSONObject, keys []string, texts []string, writer io.Writer) error {
|
||||
xlsx := excelize.NewFile()
|
||||
|
||||
exportHeader(xlsx, texts, 1)
|
||||
for i := 0; i < len(data); i += 1 {
|
||||
exportRow(xlsx, data[i], keys, i + 2)
|
||||
}
|
||||
|
||||
return xlsx.Write(writer)
|
||||
}
|
||||
|
||||
func ExportFile(data []jsonutils.JSONObject, keys []string, texts []string, filename string) error {
|
||||
writer, err:= os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
return Export(data, keys, texts, writer)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package excelutils
|
||||
|
||||
import "testing"
|
||||
|
||||
|
||||
func arrayEqual(a1, a2 []int) bool {
|
||||
if len(a1) != len(a2) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a1); i += 1 {
|
||||
if a1[i] != a2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestDecimalBaseMaxWidth(t *testing.T) {
|
||||
cases := []struct {
|
||||
decIn int
|
||||
baseIn int
|
||||
want int
|
||||
} {
|
||||
{100, 10, 3},
|
||||
{16, 16, 2},
|
||||
{15, 16, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := decimalBaseMaxWidth(c.decIn, c.baseIn)
|
||||
if got != c.want {
|
||||
t.Errorf("decimalBaseMaxWidth(%d, %d) = %d != %d", c.decIn, c.baseIn, got, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
cases2 := []struct {
|
||||
decIn int
|
||||
baseIn int
|
||||
width int
|
||||
want int
|
||||
want2 int
|
||||
} {
|
||||
{100, 10, 3, 1, 100},
|
||||
{16, 16, 2, 1, 16},
|
||||
{15, 16, 1, 15, 1},
|
||||
}
|
||||
|
||||
for _, c := range cases2 {
|
||||
got1, got2 := decimalBaseN(c.decIn, c.baseIn, c.width)
|
||||
if got1 != c.want || got2 != c.want2 {
|
||||
t.Errorf("decimalBaseN(%d %d %d) = %d %d != %d %d", c.decIn, c.baseIn, c.width, got1, got2, c.want, c.want2)
|
||||
}
|
||||
}
|
||||
|
||||
cases3 := []struct {
|
||||
decIn int
|
||||
baseIn int
|
||||
want []int
|
||||
} {
|
||||
{100, 10, []int{1, 0, 0}},
|
||||
{16, 16, []int{1, 0}},
|
||||
{0, 16, []int{0}},
|
||||
{15, 16, []int{15}},
|
||||
{0, 26, []int{0}},
|
||||
{1, 26, []int{1}},
|
||||
{25, 26, []int{25}},
|
||||
{26, 26, []int{1, 0}},
|
||||
{27, 26, []int{1, 1}},
|
||||
{676, 26, []int{1, 0, 0}},
|
||||
}
|
||||
for _, c := range cases3 {
|
||||
got := decimal2Base(c.decIn, c.baseIn)
|
||||
if !arrayEqual(got, c.want) {
|
||||
t.Errorf("decimal2Base(%d %d) = %#v != %#v", c.decIn, c.baseIn, got, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
cases4 := []struct {
|
||||
decIn int
|
||||
want string
|
||||
} {
|
||||
{0, "A"},
|
||||
{1, "B"},
|
||||
{25, "Z"},
|
||||
{26, "AA"},
|
||||
{27, "AB"},
|
||||
{676, "AAA"},
|
||||
}
|
||||
for _, c := range cases4 {
|
||||
got := decimal2Alphabet(c.decIn)
|
||||
if got != c.want {
|
||||
t.Errorf("decimal2Alphabet(%d) = %s != %s", c.decIn, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,12 +75,7 @@ func PrintJSONList(list *modules.ListResult, columns []string) {
|
||||
fmt.Println("*** ", title, " ***")
|
||||
}
|
||||
|
||||
func PrintJSONObject(obj jsonutils.JSONObject) {
|
||||
dict, ok := obj.(*jsonutils.JSONDict)
|
||||
if !ok {
|
||||
fmt.Println("Not a valid JSON object:", obj.String())
|
||||
return
|
||||
}
|
||||
func printJSONObject(dict *jsonutils.JSONDict, cb PrintJSONObjectFunc) {
|
||||
keys := dict.SortedKeys()
|
||||
pt := prettytable.NewPrettyTable([]string{"Field", "Value"})
|
||||
rows := make([][]string, 0)
|
||||
@@ -95,7 +90,68 @@ func PrintJSONObject(obj jsonutils.JSONObject) {
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
fmt.Print(pt.GetString(rows))
|
||||
cb(pt.GetString(rows))
|
||||
}
|
||||
|
||||
func PrintJSONObject(obj jsonutils.JSONObject) {
|
||||
dict, ok := obj.(*jsonutils.JSONDict)
|
||||
if !ok {
|
||||
fmt.Println("Not a valid JSON object:", obj.String())
|
||||
return
|
||||
}
|
||||
printJSONObject(dict, func(s string) {
|
||||
fmt.Print(s)
|
||||
})
|
||||
}
|
||||
|
||||
func flattenJSONObjectRecursive(v jsonutils.JSONObject, k string, rootDict *jsonutils.JSONDict) {
|
||||
switch vv := v.(type) {
|
||||
case *jsonutils.JSONString, *jsonutils.JSONInt, *jsonutils.JSONBool, *jsonutils.JSONFloat:
|
||||
rootDict.Set(k, vv)
|
||||
case *jsonutils.JSONArray:
|
||||
arr, _ := vv.GetArray()
|
||||
for i, arrElem := range arr {
|
||||
nextK := fmt.Sprintf("%s.%d", k, i)
|
||||
flattenJSONObjectRecursive(arrElem, nextK, rootDict)
|
||||
}
|
||||
if k != "" {
|
||||
rootDict.Remove(k)
|
||||
}
|
||||
case *jsonutils.JSONDict:
|
||||
m, _ := vv.GetMap()
|
||||
for kk, w := range m {
|
||||
nextK := kk
|
||||
if k != "" {
|
||||
nextK = k + "." + nextK
|
||||
}
|
||||
flattenJSONObjectRecursive(w, nextK, rootDict)
|
||||
}
|
||||
if k != "" {
|
||||
rootDict.Remove(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PrintJSONObjectRecursiveExFunc func(jsonutils.JSONObject)
|
||||
type PrintJSONObjectFunc func(string)
|
||||
|
||||
func printJSONObjectRecursive_(obj jsonutils.JSONObject, cb PrintJSONObjectRecursiveExFunc) {
|
||||
dict, ok := obj.(*jsonutils.JSONDict)
|
||||
if !ok {
|
||||
fmt.Println("Not a valid JSON object:", obj.String())
|
||||
return
|
||||
}
|
||||
dictCopy := jsonutils.DeepCopy(dict).(*jsonutils.JSONDict)
|
||||
flattenJSONObjectRecursive(dictCopy, "", dictCopy)
|
||||
cb(dictCopy)
|
||||
}
|
||||
|
||||
func PrintJSONObjectRecursive(obj jsonutils.JSONObject) {
|
||||
printJSONObjectRecursive_(obj, PrintJSONObject)
|
||||
}
|
||||
|
||||
func PrintJSONObjectRecursiveEx(obj jsonutils.JSONObject, cb PrintJSONObjectRecursiveExFunc) {
|
||||
printJSONObjectRecursive_(obj, cb)
|
||||
}
|
||||
|
||||
func PrintJSONBatchResults(results []modules.SubmitResult, columns []string) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package printutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
)
|
||||
|
||||
func TestPrintJSONObjectRecursive(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
in: `{
|
||||
"k0": {
|
||||
"k00": "v00",
|
||||
"k01": [
|
||||
{
|
||||
"k010": "v010",
|
||||
"k011": "v011"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
out: `+---------------+-------+
|
||||
| Field | Value |
|
||||
+---------------+-------+
|
||||
| k0.k00 | v00 |
|
||||
| k0.k01.0.k010 | v010 |
|
||||
| k0.k01.0.k011 | v011 |
|
||||
+---------------+-------+
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
j, err := jsonutils.ParseString(c.in)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse json error: %s", err)
|
||||
}
|
||||
PrintJSONObjectRecursiveEx(j, func(obj jsonutils.JSONObject) {
|
||||
dict := obj.(*jsonutils.JSONDict)
|
||||
printJSONObject(dict, func(s string) {
|
||||
if s != c.out {
|
||||
t.Errorf("want:\n%s\ngot:\n%s", c.out, s)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
ALL_DIGITS = "0123456789"
|
||||
ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz"
|
||||
ALL_UPPERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
ALL_PUNC = "~`!@#$%^&*()-_=+[]{}|:':\",./<>?"
|
||||
ALL_PUNC = "~`!@#$%^&*()-_=+[]{}|:';\",./<>?"
|
||||
)
|
||||
|
||||
type PasswordStrength struct {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package yunionconf
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/yunionconf/models"
|
||||
)
|
||||
|
||||
func InitHandlers(app *appsrv.Application) {
|
||||
for _, manager := range []db.IModelManager{
|
||||
models.ParameterManager,
|
||||
} {
|
||||
db.RegisterModelManager(manager)
|
||||
handler := db.NewModelHandler(manager)
|
||||
dispatcher.AddModelDispatcher("", app, handler)
|
||||
dispatcher.AddModelDispatcher("/users/<user_id>", app, handler)
|
||||
dispatcher.AddModelDispatcher("/services/<service_id>", app, handler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
)
|
||||
|
||||
func InitDB() error {
|
||||
for _, manager := range []db.IModelManager{
|
||||
ParameterManager,
|
||||
} {
|
||||
err := manager.InitializeData()
|
||||
if err != nil {
|
||||
log.Errorf("Manager %s initializeData fail %s", manager.Keyword(), err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/pkg/util/timeutils"
|
||||
"yunion.io/x/pkg/utils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
)
|
||||
|
||||
const (
|
||||
NAMESPACE_USER = "user"
|
||||
NAMESPACE_SERVICE = "service"
|
||||
)
|
||||
|
||||
type SParameterManager struct {
|
||||
db.SResourceBaseManager
|
||||
}
|
||||
|
||||
type SParameter struct {
|
||||
db.SResourceBase
|
||||
|
||||
Id int64 `primary:"true" auto_increment:"true" list:"user"` // = Column(BigInteger, primary_key=True)
|
||||
CreatedBy string `width:"128" charset:"ascii" nullable:"false" create:"required" list:"user"` // Column(VARCHAR(length=128, charset='ascii'), nullable=False)
|
||||
UpdatedBy string `width:"128" charset:"ascii" nullable:"false" create:"required" update:"user" list:"user"` // Column(VARCHAR(length=128, charset='ascii'), nullable=False) "user"/ serviceName/ "admin"
|
||||
Namespace string `width:"64" charset:"ascii" default:"user" nullable:"false" create:"required" list:"admin"` // Column(VARCHAR(length=128, charset='ascii'), nullable=False) user_id / serviceid
|
||||
NamespaceId string `width:"128" charset:"ascii" nullable:"false" index:"true" create:"required" list:"admin"` // Column(VARCHAR(length=128, charset='ascii'), nullable=False)
|
||||
Name string `width:"128" charset:"ascii" nullable:"false" index:"true" create:"required" list:"user"` // Column(VARCHAR(length=128, charset='ascii'), nullable=false)
|
||||
Value string `charset:"utf8" create:"required" update:"user" update:"user" list:"user"` // Column(VARCHAR(charset='utf-8'))
|
||||
}
|
||||
|
||||
var ParameterManager *SParameterManager
|
||||
|
||||
func init() {
|
||||
ParameterManager = &SParameterManager{SResourceBaseManager: db.NewResourceBaseManager(SParameter{}, "paramters_tbl", "parameter", "parameters")}
|
||||
}
|
||||
|
||||
func isAdminQuery(query jsonutils.JSONObject) bool {
|
||||
admin_fields := [3]string{"namespace_id", "user_id", "service_id"}
|
||||
|
||||
for _, field := range admin_fields {
|
||||
if s, _ := query.GetString(field); len(s) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getNamespaceInContext(userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (namespace string, namespaceId string, err error) {
|
||||
// 优先匹配上线文中的参数, /users/<user_id>/parameters /services/<service_id>/parameters
|
||||
if uid, _ := query.GetString("user_id"); len(uid) > 0 {
|
||||
return NAMESPACE_USER, uid, nil
|
||||
} else if sid, _ := query.GetString("service_id"); len(sid) > 0 {
|
||||
return NAMESPACE_SERVICE, sid, nil
|
||||
}
|
||||
|
||||
// 匹配/parameters中的参数
|
||||
if uid, _ := data.GetString("user_id"); len(uid) > 0 {
|
||||
return NAMESPACE_USER, uid, nil
|
||||
} else if sid, _ := data.GetString("service_id"); len(sid) > 0 {
|
||||
return NAMESPACE_SERVICE, sid, nil
|
||||
} else {
|
||||
return NAMESPACE_USER, userCred.GetUserId(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespace(userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (string, string, error) {
|
||||
var namespace, namespace_id string
|
||||
if userCred.IsSystemAdmin() {
|
||||
if name, nameId, e := getNamespaceInContext(userCred, query, data); e != nil {
|
||||
return "", "", e
|
||||
} else {
|
||||
namespace = name
|
||||
namespace_id = nameId
|
||||
}
|
||||
} else {
|
||||
namespace = NAMESPACE_USER
|
||||
namespace_id = userCred.GetUserId()
|
||||
}
|
||||
|
||||
return namespace, namespace_id, nil
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
|
||||
if !isAdminQuery(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return userCred.IsSystemAdmin()
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
if !isAdminQuery(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return userCred.IsSystemAdmin()
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerProjId string, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
|
||||
// check duplication
|
||||
name, _ := data.GetString("name")
|
||||
uid := userCred.GetUserId()
|
||||
if len(uid) < 0 {
|
||||
return nil, httperrors.NewUserNotFoundError("user not found")
|
||||
}
|
||||
|
||||
namespace, namespace_id, e := getNamespace(userCred, query, data)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// check duplication, 同一个namespace下,name不能 重复
|
||||
q := manager.Query().Equals("name", name).Equals("namespace_id", namespace_id)
|
||||
if q.Count() > 0 {
|
||||
return nil, httperrors.NewDuplicateNameError("paramter %s has been created", name)
|
||||
}
|
||||
|
||||
data.Add(jsonutils.NewString(uid), "created_by")
|
||||
data.Add(jsonutils.NewString(uid), "updated_by")
|
||||
data.Add(jsonutils.NewString(namespace), "namespace")
|
||||
data.Add(jsonutils.NewString(namespace_id), "namespace_id")
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) GetOwnerId(userCred mcclient.TokenCredential) string {
|
||||
return userCred.GetUserId()
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) FilterByOwner(q *sqlchemy.SQuery, owner string) *sqlchemy.SQuery {
|
||||
return q.Equals("created_by", owner)
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
|
||||
return q.Equals("name", idStr)
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) FilterByName(q *sqlchemy.SQuery, name string) *sqlchemy.SQuery {
|
||||
return q.Equals("name", name)
|
||||
}
|
||||
|
||||
func (manager *SParameterManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) {
|
||||
if userCred.IsSystemAdmin() {
|
||||
if id, _ := query.GetString("namespace_id"); len(id) > 0 {
|
||||
q = q.Equals("namespace_id", id)
|
||||
} else if id, _ := query.GetString("service_id"); len(id) > 0 {
|
||||
q = q.Equals("namespace_id", id).Equals("namespace", NAMESPACE_SERVICE)
|
||||
} else if id, _ := query.GetString("user_id"); len(id) > 0 {
|
||||
q = q.Equals("namespace_id", id).Equals("namespace", NAMESPACE_USER)
|
||||
} else {
|
||||
// not admin
|
||||
admin, _ := query.GetString("admin")
|
||||
if !utils.ToBool(admin) {
|
||||
q = q.Equals("namespace_id", userCred.GetUserId()).Equals("namespace", NAMESPACE_USER)
|
||||
}
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
return q.Equals("namespace_id", userCred.GetUserId()).Equals("namespace", NAMESPACE_USER), nil
|
||||
}
|
||||
|
||||
func (model *SParameter) IsOwner(userCred mcclient.TokenCredential) bool {
|
||||
return model.CreatedBy == userCred.GetUserId() || (model.NamespaceId == userCred.GetUserId() && model.Namespace == NAMESPACE_USER)
|
||||
}
|
||||
|
||||
func (model *SParameter) AllowUpdateItem(ctx context.Context, userCred mcclient.TokenCredential) bool {
|
||||
return model.IsOwner(userCred) || userCred.IsSystemAdmin()
|
||||
}
|
||||
|
||||
func (model *SParameter) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
|
||||
uid := userCred.GetUserId()
|
||||
if len(uid) < 0 {
|
||||
return nil, httperrors.NewUserNotFoundError("user not found")
|
||||
}
|
||||
|
||||
namespace, namespace_id, e := getNamespace(userCred, query, data)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
data.Add(jsonutils.NewString(uid), "updated_by")
|
||||
data.Add(jsonutils.NewString(namespace_id), "namespace_id")
|
||||
data.Add(jsonutils.NewString(namespace), "namespace")
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (model *SParameter) AllowDeleteItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
return model.IsOwner(userCred) || userCred.IsSystemAdmin()
|
||||
}
|
||||
|
||||
func (model *SParameter) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
|
||||
return model.Delete(ctx, userCred)
|
||||
}
|
||||
|
||||
func (model *SParameter) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
|
||||
_, err := model.GetModelManager().TableSpec().Update(model, func() error {
|
||||
model.Deleted = true
|
||||
model.DeletedAt = timeutils.UtcNow()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("PendingDelete fail %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (model *SParameter) AllowGetDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
|
||||
return model.IsOwner(userCred) || userCred.IsSystemAdmin()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package options
|
||||
|
||||
import "yunion.io/x/onecloud/pkg/cloudcommon"
|
||||
|
||||
type YunionConfOptions struct {
|
||||
cloudcommon.DBOptions
|
||||
}
|
||||
|
||||
var (
|
||||
Options YunionConfOptions
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/yunionconf"
|
||||
"yunion.io/x/onecloud/pkg/yunionconf/models"
|
||||
"yunion.io/x/onecloud/pkg/yunionconf/options"
|
||||
)
|
||||
|
||||
func StartService() {
|
||||
cloudcommon.ParseOptions(&options.Options, &options.Options.Options, os.Args, "yunionconf.conf")
|
||||
cloudcommon.InitAuth(&options.Options.Options, func() {
|
||||
log.Infof("Auth complete!!")
|
||||
})
|
||||
|
||||
if options.Options.GlobalVirtualResourceNamespace {
|
||||
db.EnableGlobalVirtualResourceNamespace()
|
||||
}
|
||||
|
||||
cloudcommon.InitDB(&options.Options.DBOptions)
|
||||
defer cloudcommon.CloseDB()
|
||||
|
||||
app := cloudcommon.InitApp(&options.Options.Options)
|
||||
yunionconf.InitHandlers(app)
|
||||
|
||||
if db.CheckSync(options.Options.AutoSyncTable) {
|
||||
err := models.InitDB()
|
||||
if err == nil {
|
||||
cloudcommon.ServeForever(app, &options.Options.Options)
|
||||
} else {
|
||||
log.Errorf("InitDB fail: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
language: go
|
||||
|
||||
install:
|
||||
- go get -d -t -v ./... && go build -v ./...
|
||||
|
||||
go:
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test ./... -v -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [xuri.me](https://xuri.me). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
# Contributing to excelize
|
||||
|
||||
Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and
|
||||
guidelines useful to experienced open source contributors. Finally, make sure
|
||||
you read our [community guidelines](#community-guidelines) before you
|
||||
start participating.
|
||||
|
||||
## Topics
|
||||
|
||||
* [Reporting Security Issues](#reporting-security-issues)
|
||||
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
|
||||
* [Reporting Issues](#reporting-other-issues)
|
||||
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
|
||||
* [Community Guidelines](#community-guidelines)
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
The excelize maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
Please **DO NOT** file a public issue, instead send your report privately to
|
||||
[xuri.me](https://xuri.me).
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||
We currently do not offer a paid security bounty program, but are not
|
||||
ruling it out in the future.
|
||||
|
||||
|
||||
## Reporting other issues
|
||||
|
||||
A great way to contribute to the project is to send a detailed report when you
|
||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
||||
and will thank you for it!
|
||||
|
||||
Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues)
|
||||
doesn't already include that problem or suggestion before submitting an issue.
|
||||
If you find a match, you can use the "subscribe" button to get notified on
|
||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||
only clutter the discussion, and don't help resolving it. However, if you
|
||||
have ways to reproduce the issue or have additional information that may help
|
||||
resolving the issue, please leave a comment.
|
||||
|
||||
When reporting issues, always include the output of `go env`.
|
||||
|
||||
Also include the steps required to reproduce the problem if possible and
|
||||
applicable. This information will help us review and fix your issue faster.
|
||||
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
|
||||
Don't forget to remove sensitive data from your logfiles before posting (you can
|
||||
replace those parts with "REDACTED").
|
||||
|
||||
## Quick contribution tips and guidelines
|
||||
|
||||
This section gives the experienced contributor some tips and guidelines.
|
||||
|
||||
### Pull requests are always welcome
|
||||
|
||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
||||
it? Do it! We will appreciate it. Any significant improvement should be
|
||||
documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before
|
||||
anybody starts working on it.
|
||||
|
||||
We are always thrilled to receive pull requests. We do our best to process them
|
||||
quickly. If your pull request is not accepted on the first try,
|
||||
don't get discouraged!
|
||||
|
||||
### Design and cleanup proposals
|
||||
|
||||
You can propose new designs for existing excelize features. You can also design
|
||||
entirely new features. We really appreciate contributors who want to refactor or
|
||||
otherwise cleanup our project.
|
||||
|
||||
We try hard to keep excelize lean and focused. Excelize can't do everything for
|
||||
everybody. This means that we might decide against incorporating a new feature.
|
||||
However, there might be a way to implement that feature *on top of* excelize.
|
||||
|
||||
### Conventions
|
||||
|
||||
Fork the repository and make changes on your fork in a feature branch:
|
||||
|
||||
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
||||
the issue.
|
||||
- If it's a feature branch, create an enhancement issue to announce
|
||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
||||
issue.
|
||||
|
||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
||||
it! Take a look at existing tests for inspiration. Run the full test on your branch before
|
||||
submitting a pull request.
|
||||
|
||||
Update the documentation when creating or modifying features. Test your
|
||||
documentation changes for clarity, concision, and correctness, as well as a
|
||||
clean documentation build.
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
committing your changes. Most editors have plug-ins that do this automatically.
|
||||
|
||||
Pull request descriptions should be as clear as possible and include a reference
|
||||
to all the issues that they address.
|
||||
|
||||
### Successful Changes
|
||||
|
||||
Before contributing large or high impact changes, make the effort to coordinate
|
||||
with the maintainers of the project before submitting a pull request. This
|
||||
prevents you from doing extra work that may or may not be merged.
|
||||
|
||||
Large PRs that are just submitted without any prior communication are unlikely
|
||||
to be successful.
|
||||
|
||||
While pull requests are the methodology for submitting changes to code, changes
|
||||
are much more likely to be accepted if they are accompanied by additional
|
||||
engineering work. While we don't define this explicitly, most of these goals
|
||||
are accomplished through communication of the design goals and subsequent
|
||||
solutions. Often times, it helps to first state the problem before presenting
|
||||
solutions.
|
||||
|
||||
Typically, the best methods of accomplishing this are to submit an issue,
|
||||
stating the problem. This issue can include a problem statement and a
|
||||
checklist with requirements. If solutions are proposed, alternatives should be
|
||||
listed and eliminated. Even if the criteria for elimination of a solution is
|
||||
frivolous, say so.
|
||||
|
||||
Larger changes typically work best with design documents. These are focused on
|
||||
providing context to the design at the time the feature was conceived and can
|
||||
inform future documentation contributions.
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Commit messages must start with a capitalized and short summary
|
||||
written in the imperative, followed by an optional, more detailed explanatory
|
||||
text which is separated from the summary by an empty line.
|
||||
|
||||
Commit messages should follow best practices, including explaining the context
|
||||
of the problem and how it was solved, including in caveats or follow up changes
|
||||
required. They should tell the story of the change and provide readers
|
||||
understanding of what led to it.
|
||||
|
||||
In practice, the best approach to maintaining a nice commit message is to
|
||||
leverage a `git add -p` and `git commit --amend` to formulate a solid
|
||||
changeset. This allows one to piece together a change, as information becomes
|
||||
available.
|
||||
|
||||
If you squash a series of commits, don't just submit that. Re-write the commit
|
||||
message, as if the series of commits was a single stroke of brilliance.
|
||||
|
||||
That said, there is no requirement to have a single commit for a PR, as long as
|
||||
each commit tells the story. For example, if there is a feature that requires a
|
||||
package, it might make sense to have the package in a separate commit then have
|
||||
a subsequent commit that uses it.
|
||||
|
||||
Remember, you're telling part of the story with the commit message. Don't make
|
||||
your chapter weird.
|
||||
|
||||
### Review
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Post
|
||||
a comment after pushing. New commits show up in the pull request automatically,
|
||||
but the reviewers are notified only when you comment.
|
||||
|
||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
||||
mixed into the PR.
|
||||
|
||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
||||
feature branch to update your pull request rather than `merge master`.
|
||||
|
||||
Before you make a pull request, squash your commits into logical units of work
|
||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
||||
set of patches that should be reviewed together: for example, upgrading the
|
||||
version of a vendored dependency and taking advantage of its now available new
|
||||
feature constitute two separate units of work. Implementing a new function and
|
||||
calling it in another file constitute a single logical unit of work. The very
|
||||
high majority of submissions should have a single commit, so if in doubt: squash
|
||||
down to one.
|
||||
|
||||
After every commit, make sure the test passes. Include documentation
|
||||
changes in the same pull request so that a revert would remove all traces of
|
||||
the feature or fix.
|
||||
|
||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that
|
||||
close an issue. Including references automatically closes the issue on a merge.
|
||||
|
||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
||||
|
||||
### Merge approval
|
||||
|
||||
The excelize maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
||||
indicate acceptance.
|
||||
|
||||
### Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
1 Letterman Drive
|
||||
Suite D4700
|
||||
San Francisco, CA, 94129
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Ri Xu https://xuri.me
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
### How can I become a maintainer?
|
||||
|
||||
First, all maintainers have 3 things
|
||||
|
||||
- They share responsibility in the project's success.
|
||||
- They have made a long-term, recurring time investment to improve the project.
|
||||
- They spend that time doing whatever needs to be done, not necessarily what
|
||||
is the most interesting or fun.
|
||||
|
||||
Maintainers are often under-appreciated, because their work is harder to appreciate.
|
||||
It's easy to appreciate a really cool and technically advanced feature. It's harder
|
||||
to appreciate the absence of bugs, the slow but steady improvement in stability,
|
||||
or the reliability of a release process. But those things distinguish a good
|
||||
project from a great one.
|
||||
|
||||
Don't forget: being a maintainer is a time investment. Make sure you
|
||||
will have time to make yourself available. You don't have to be a
|
||||
maintainer to make a difference on the project!
|
||||
|
||||
If you want to become a meintainer, contact [xuri.me](https://xuri.me) and given a introduction of you.
|
||||
|
||||
## Community guidelines
|
||||
|
||||
We want to keep the community awesome, growing and collaborative. We need
|
||||
your help to keep it that way. To help with this we've come up with some general
|
||||
guidelines for the community as a whole:
|
||||
|
||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
||||
no regional, racial, gender, or other abuse will be tolerated. We like
|
||||
nice people way better than mean ones!
|
||||
|
||||
* Encourage diversity and participation: Make everyone in our community feel
|
||||
welcome, regardless of their background and the extent of their
|
||||
contributions, and do everything possible to encourage participation in
|
||||
our community.
|
||||
|
||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
||||
you own, do not share private or sensitive information, and don't break
|
||||
the law.
|
||||
|
||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||
avoid off-topic discussions. Remember when you update an issue or respond
|
||||
to an email you are potentially sending to a large number of people. Please
|
||||
consider this before you update. Also remember that nobody likes spam.
|
||||
|
||||
* Don't send email to the maintainers: There's no need to send email to the
|
||||
maintainers to ask them to investigate an issue or to take a look at a
|
||||
pull request. Instead of sending an email, GitHub mentions should be
|
||||
used to ping maintainers to review a pull request, a proposal or an
|
||||
issue.
|
||||
|
||||
### Guideline violations — 3 strikes method
|
||||
|
||||
The point of this section is not to find opportunities to punish people, but we
|
||||
do need a fair way to deal with people who are making our community suck.
|
||||
|
||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
||||
behavior is inappropriate according to our guidelines.
|
||||
|
||||
2. Second occurrence: We will send you a private message with a warning that
|
||||
any additional violations will result in removal from the community.
|
||||
|
||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
||||
your account.
|
||||
|
||||
**Notes:**
|
||||
|
||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
||||
have spam all over the place.
|
||||
|
||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
||||
grudge.
|
||||
|
||||
* People who commit minor infractions will get some education, rather than
|
||||
hammering them in the 3 strikes process.
|
||||
|
||||
* The rules apply equally to everyone in the community, no matter how much
|
||||
you've contributed.
|
||||
|
||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
||||
|
||||
* Contact [xuri.me](https://xuri.me) to report abuse or appeal violations. In the case of
|
||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
||||
fair solution if there has been a misunderstanding.
|
||||
|
||||
## Coding Style
|
||||
|
||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
||||
community. While some of these standards may seem arbitrary, they somehow seem
|
||||
to result in a solid, consistent codebase.
|
||||
|
||||
It is possible that the code base does not currently comply with these
|
||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
||||
goes against the spirit of the guidelines. All new contributions should make a
|
||||
best effort to clean up and make the code base better than they left it.
|
||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
||||
code base easier for humans to navigate and understand. Always keep that in
|
||||
mind when nudging others to comply.
|
||||
|
||||
The rules:
|
||||
|
||||
1. All code should be formatted with `gofmt -s`.
|
||||
2. All code should pass the default levels of
|
||||
[`golint`](https://github.com/golang/lint).
|
||||
3. All code should follow the guidelines covered in [Effective
|
||||
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
||||
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
4. Comment the code. Tell us the why, the history and the context.
|
||||
5. Document _all_ declarations and methods, even private ones. Declare
|
||||
expectations, caveats and anything else that may be important. If a type
|
||||
gets exported, having the comments already there will ensure it's ready.
|
||||
6. Variable name length should be proportional to its context and no longer.
|
||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
||||
In practice, short methods will have short variable names and globals will
|
||||
have longer names.
|
||||
7. No underscores in package names. If you need a compound name, step back,
|
||||
and re-examine why you need a compound name. If you still think you need a
|
||||
compound name, lose the underscore.
|
||||
8. No utils or helpers packages. If a function is not general enough to
|
||||
warrant its own package, it has not been written generally enough to be a
|
||||
part of a util package. Just leave it unexported and well-documented.
|
||||
9. All tests should run with `go test` and outside tooling should not be
|
||||
required. No, we don't need another unit testing framework. Assertion
|
||||
packages are acceptable if they provide _real_ incremental value.
|
||||
10. Even though we call these "rules" above, they are actually just
|
||||
guidelines. Since you've read all the rules, you now know that.
|
||||
|
||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
||||
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
|
||||
kool-aid is a lot easier than going thirsty.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2016 - 2018 360 Enterprise Security Group, Endpoint Security,
|
||||
inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of Excelize nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+175
@@ -0,0 +1,175 @@
|
||||

|
||||
|
||||
# Excelize
|
||||
|
||||
[](https://travis-ci.org/360EntSecGroup-Skylar/excelize)
|
||||
[](https://codecov.io/gh/360EntSecGroup-Skylar/excelize)
|
||||
[](https://goreportcard.com/report/github.com/360EntSecGroup-Skylar/excelize)
|
||||
[](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize)
|
||||
[](https://opensource.org/licenses/BSD-3-Clause)
|
||||
[](https://www.paypal.me/xuri)
|
||||
|
||||
## Introduction
|
||||
|
||||
Excelize is a library written in pure Golang and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [Chinese translation](https://xuri.me/excelize/zh_cn).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```go
|
||||
go get github.com/360EntSecGroup-Skylar/excelize
|
||||
```
|
||||
|
||||
### Create XLSX file
|
||||
|
||||
Here is a minimal example usage that will create XLSX file.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xlsx := excelize.NewFile()
|
||||
// Create a new sheet.
|
||||
index := xlsx.NewSheet("Sheet2")
|
||||
// Set value of a cell.
|
||||
xlsx.SetCellValue("Sheet2", "A2", "Hello world.")
|
||||
xlsx.SetCellValue("Sheet1", "B2", 100)
|
||||
// Set active sheet of the workbook.
|
||||
xlsx.SetActiveSheet(index)
|
||||
// Save xlsx file by the given path.
|
||||
err := xlsx.SaveAs("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reading XLSX file
|
||||
|
||||
The following constitutes the bare to read a XLSX document.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
// Get value from cell by given worksheet name and axis.
|
||||
cell := xlsx.GetCellValue("Sheet1", "B2")
|
||||
fmt.Println(cell)
|
||||
// Get all the rows in the Sheet1.
|
||||
rows := xlsx.GetRows("Sheet1")
|
||||
for _, row := range rows {
|
||||
for _, colCell := range row {
|
||||
fmt.Print(colCell, "\t")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add chart to XLSX file
|
||||
|
||||
With Excelize chart generation and management is as easy as a few lines of code. You can build charts based off data in your worksheet or generate charts without any data in your worksheet at all.
|
||||
|
||||

|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
|
||||
values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
|
||||
xlsx := excelize.NewFile()
|
||||
for k, v := range categories {
|
||||
xlsx.SetCellValue("Sheet1", k, v)
|
||||
}
|
||||
for k, v := range values {
|
||||
xlsx.SetCellValue("Sheet1", k, v)
|
||||
}
|
||||
xlsx.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`)
|
||||
// Save xlsx file by the given path.
|
||||
err := xlsx.SaveAs("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Add picture to XLSX file
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
// Insert a picture.
|
||||
err = xlsx.AddPicture("Sheet1", "A2", "./image1.png", "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// Insert a picture to worksheet with scaling.
|
||||
err = xlsx.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// Insert a picture offset in the cell with printing support.
|
||||
err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// Save the xlsx file with the origin path.
|
||||
err = xlsx.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm).
|
||||
|
||||
## Credits
|
||||
|
||||
Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx).
|
||||
|
||||
## Licenses
|
||||
|
||||
This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause).
|
||||
+175
@@ -0,0 +1,175 @@
|
||||

|
||||
|
||||
# Excelize
|
||||
|
||||
[](https://travis-ci.org/360EntSecGroup-Skylar/excelize)
|
||||
[](https://codecov.io/gh/360EntSecGroup-Skylar/excelize)
|
||||
[](https://goreportcard.com/report/github.com/360EntSecGroup-Skylar/excelize)
|
||||
[](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize)
|
||||
[](https://opensource.org/licenses/BSD-3-Clause)
|
||||
[](https://www.paypal.me/xuri)
|
||||
|
||||
## 简介
|
||||
|
||||
Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [中文翻译](https://xuri.me/excelize/zh_cn)。
|
||||
|
||||
## 快速上手
|
||||
|
||||
### 安装
|
||||
|
||||
```go
|
||||
go get github.com/360EntSecGroup-Skylar/excelize
|
||||
```
|
||||
|
||||
### 创建 Excel 文档
|
||||
|
||||
下面是一个创建 Excel 文档的简单例子:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xlsx := excelize.NewFile()
|
||||
// 创建一个工作表
|
||||
index := xlsx.NewSheet("Sheet2")
|
||||
// 设置单元格的值
|
||||
xlsx.SetCellValue("Sheet2", "A2", "Hello world.")
|
||||
xlsx.SetCellValue("Sheet1", "B2", 100)
|
||||
// 设置工作簿的默认工作表
|
||||
xlsx.SetActiveSheet(index)
|
||||
// 根据指定路径保存文件
|
||||
err := xlsx.SaveAs("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 读取 Excel 文档
|
||||
|
||||
下面是读取 Excel 文档的例子:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
// 获取工作表中指定单元格的值
|
||||
cell := xlsx.GetCellValue("Sheet1", "B2")
|
||||
fmt.Println(cell)
|
||||
// 获取 Sheet1 上所有单元格
|
||||
rows := xlsx.GetRows("Sheet1")
|
||||
for _, row := range rows {
|
||||
for _, colCell := range row {
|
||||
fmt.Print(colCell, "\t")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Excel 文档中创建图表
|
||||
|
||||
使用 Excelize 生成图表十分简单,仅需几行代码。您可以根据工作表中的已有数据构建图表,或向工作表中添加数据并创建图表。
|
||||
|
||||

|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
|
||||
values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
|
||||
xlsx := excelize.NewFile()
|
||||
for k, v := range categories {
|
||||
xlsx.SetCellValue("Sheet1", k, v)
|
||||
}
|
||||
for k, v := range values {
|
||||
xlsx.SetCellValue("Sheet1", k, v)
|
||||
}
|
||||
xlsx.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`)
|
||||
// 根据指定路径保存文件
|
||||
err := xlsx.SaveAs("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 向 Excel 文档中插入图片
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
"github.com/360EntSecGroup-Skylar/excelize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
// 插入图片
|
||||
err = xlsx.AddPicture("Sheet1", "A2", "./image1.png", "")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// 在工作表中插入图片,并设置图片的缩放比例
|
||||
err = xlsx.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// 在工作表中插入图片,并设置图片的打印属性
|
||||
err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// 保存文件
|
||||
err = xlsx.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 社区合作
|
||||
|
||||
欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm)。
|
||||
|
||||
## 致谢
|
||||
|
||||
本类库中部分 XML 结构体的定义参考了开源项目:[tealeg/xlsx](https://github.com/tealeg/xlsx).
|
||||
|
||||
## 开源许可
|
||||
|
||||
本项目遵循 BSD 3-Clause 开源许可协议,访问 [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) 查看许可协议文件。
|
||||
+547
@@ -0,0 +1,547 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mergeCellsParser provides function to check merged cells in worksheet by
|
||||
// given axis.
|
||||
func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string {
|
||||
axis = strings.ToUpper(axis)
|
||||
if xlsx.MergeCells != nil {
|
||||
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
|
||||
if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) {
|
||||
axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return axis
|
||||
}
|
||||
|
||||
// SetCellValue provides function to set value of a cell. The following shows
|
||||
// the supported data types:
|
||||
//
|
||||
// int
|
||||
// int8
|
||||
// int16
|
||||
// int32
|
||||
// int64
|
||||
// uint
|
||||
// uint8
|
||||
// uint16
|
||||
// uint32
|
||||
// uint64
|
||||
// float32
|
||||
// float64
|
||||
// string
|
||||
// []byte
|
||||
// time.Duration
|
||||
// time.Time
|
||||
// bool
|
||||
// nil
|
||||
//
|
||||
// Note that default date format is m/d/yy h:mm of time.Time type value. You can
|
||||
// set numbers format by SetCellStyle() method.
|
||||
func (f *File) SetCellValue(sheet, axis string, value interface{}) {
|
||||
switch t := value.(type) {
|
||||
case float32:
|
||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32))
|
||||
case float64:
|
||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64))
|
||||
case string:
|
||||
f.SetCellStr(sheet, axis, t)
|
||||
case []byte:
|
||||
f.SetCellStr(sheet, axis, string(t))
|
||||
case time.Duration:
|
||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(time.Duration).Seconds()/86400), 'f', -1, 32))
|
||||
f.setDefaultTimeStyle(sheet, axis, 21)
|
||||
case time.Time:
|
||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(timeToExcelTime(timeToUTCTime(value.(time.Time)))), 'f', -1, 64))
|
||||
f.setDefaultTimeStyle(sheet, axis, 22)
|
||||
case nil:
|
||||
f.SetCellStr(sheet, axis, "")
|
||||
case bool:
|
||||
f.SetCellBool(sheet, axis, bool(value.(bool)))
|
||||
default:
|
||||
f.setCellIntValue(sheet, axis, value)
|
||||
}
|
||||
}
|
||||
|
||||
// setCellIntValue provides function to set int value of a cell.
|
||||
func (f *File) setCellIntValue(sheet, axis string, value interface{}) {
|
||||
switch value.(type) {
|
||||
case int:
|
||||
f.SetCellInt(sheet, axis, value.(int))
|
||||
case int8:
|
||||
f.SetCellInt(sheet, axis, int(value.(int8)))
|
||||
case int16:
|
||||
f.SetCellInt(sheet, axis, int(value.(int16)))
|
||||
case int32:
|
||||
f.SetCellInt(sheet, axis, int(value.(int32)))
|
||||
case int64:
|
||||
f.SetCellInt(sheet, axis, int(value.(int64)))
|
||||
case uint:
|
||||
f.SetCellInt(sheet, axis, int(value.(uint)))
|
||||
case uint8:
|
||||
f.SetCellInt(sheet, axis, int(value.(uint8)))
|
||||
case uint16:
|
||||
f.SetCellInt(sheet, axis, int(value.(uint16)))
|
||||
case uint32:
|
||||
f.SetCellInt(sheet, axis, int(value.(uint32)))
|
||||
case uint64:
|
||||
f.SetCellInt(sheet, axis, int(value.(uint64)))
|
||||
default:
|
||||
f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value))
|
||||
}
|
||||
}
|
||||
|
||||
// SetCellBool provides function to set bool type value of a cell by given
|
||||
// worksheet name, cell coordinates and cell value.
|
||||
func (f *File) SetCellBool(sheet, axis string, value bool) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = "b"
|
||||
if value {
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = "1"
|
||||
} else {
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = "0"
|
||||
}
|
||||
}
|
||||
|
||||
// GetCellValue provides function to get formatted value from cell by given
|
||||
// worksheet name and axis in XLSX file. If it is possible to apply a format to
|
||||
// the cell value, it will do so, if not then an error will be returned, along
|
||||
// with the raw value of the cell.
|
||||
func (f *File) GetCellValue(sheet, axis string) string {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
xAxis := row - 1
|
||||
rows := len(xlsx.SheetData.Row)
|
||||
if rows > 1 {
|
||||
lastRow := xlsx.SheetData.Row[rows-1].R
|
||||
if lastRow >= rows {
|
||||
rows = lastRow
|
||||
}
|
||||
}
|
||||
if rows < xAxis {
|
||||
return ""
|
||||
}
|
||||
for k := range xlsx.SheetData.Row {
|
||||
if xlsx.SheetData.Row[k].R == row {
|
||||
for i := range xlsx.SheetData.Row[k].C {
|
||||
if axis == xlsx.SheetData.Row[k].C[i].R {
|
||||
val, _ := xlsx.SheetData.Row[k].C[i].getValueFrom(f, f.sharedStringsReader())
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// formattedValue provides function to returns a value after formatted. If it is
|
||||
// possible to apply a format to the cell value, it will do so, if not then an
|
||||
// error will be returned, along with the raw value of the cell.
|
||||
func (f *File) formattedValue(s int, v string) string {
|
||||
if s == 0 {
|
||||
return v
|
||||
}
|
||||
styleSheet := f.stylesReader()
|
||||
ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID]
|
||||
if ok != nil {
|
||||
return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// GetCellStyle provides function to get cell style index by given worksheet
|
||||
// name and cell coordinates.
|
||||
func (f *File) GetCellStyle(sheet, axis string) int {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
return f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
||||
}
|
||||
|
||||
// GetCellFormula provides function to get formula from cell by given worksheet
|
||||
// name and axis in XLSX file.
|
||||
func (f *File) GetCellFormula(sheet, axis string) string {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
xAxis := row - 1
|
||||
rows := len(xlsx.SheetData.Row)
|
||||
if rows > 1 {
|
||||
lastRow := xlsx.SheetData.Row[rows-1].R
|
||||
if lastRow >= rows {
|
||||
rows = lastRow
|
||||
}
|
||||
}
|
||||
if rows < xAxis {
|
||||
return ""
|
||||
}
|
||||
for k := range xlsx.SheetData.Row {
|
||||
if xlsx.SheetData.Row[k].R == row {
|
||||
for i := range xlsx.SheetData.Row[k].C {
|
||||
if axis == xlsx.SheetData.Row[k].C[i].R {
|
||||
if xlsx.SheetData.Row[k].C[i].F != nil {
|
||||
return xlsx.SheetData.Row[k].C[i].F.Content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetCellFormula provides function to set cell formula by given string and
|
||||
// worksheet name.
|
||||
func (f *File) SetCellFormula(sheet, axis, formula string) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
if xlsx.SheetData.Row[xAxis].C[yAxis].F != nil {
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].F.Content = formula
|
||||
} else {
|
||||
f := xlsxF{
|
||||
Content: formula,
|
||||
}
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].F = &f
|
||||
}
|
||||
}
|
||||
|
||||
// SetCellHyperLink provides function to set cell hyperlink by given worksheet
|
||||
// name and link URL address. LinkType defines two types of hyperlink "External"
|
||||
// for web site or "Location" for moving to one of cell in this workbook. The
|
||||
// below is example for external link.
|
||||
//
|
||||
// xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
|
||||
// // Set underline and font color style for the cell.
|
||||
// style, _ := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`)
|
||||
// xlsx.SetCellStyle("Sheet1", "A3", "A3", style)
|
||||
//
|
||||
// A this is another example for "Location":
|
||||
//
|
||||
// xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location")
|
||||
//
|
||||
func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
linkTypes := map[string]xlsxHyperlink{
|
||||
"External": {},
|
||||
"Location": {Location: link},
|
||||
}
|
||||
hyperlink, ok := linkTypes[linkType]
|
||||
if !ok || axis == "" {
|
||||
return
|
||||
}
|
||||
hyperlink.Ref = axis
|
||||
if linkType == "External" {
|
||||
rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType)
|
||||
hyperlink.RID = "rId" + strconv.Itoa(rID)
|
||||
}
|
||||
if xlsx.Hyperlinks == nil {
|
||||
xlsx.Hyperlinks = &xlsxHyperlinks{}
|
||||
}
|
||||
xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, hyperlink)
|
||||
}
|
||||
|
||||
// GetCellHyperLink provides function to get cell hyperlink by given worksheet
|
||||
// name and axis. Boolean type value link will be ture if the cell has a
|
||||
// hyperlink and the target is the address of the hyperlink. Otherwise, the
|
||||
// value of link will be false and the value of the target will be a blank
|
||||
// string. For example get hyperlink of Sheet1!H6:
|
||||
//
|
||||
// link, target := xlsx.GetCellHyperLink("Sheet1", "H6")
|
||||
//
|
||||
func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) {
|
||||
var link bool
|
||||
var target string
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
if xlsx.Hyperlinks == nil || axis == "" {
|
||||
return link, target
|
||||
}
|
||||
for h := range xlsx.Hyperlinks.Hyperlink {
|
||||
if xlsx.Hyperlinks.Hyperlink[h].Ref == axis {
|
||||
link = true
|
||||
target = xlsx.Hyperlinks.Hyperlink[h].Location
|
||||
if xlsx.Hyperlinks.Hyperlink[h].RID != "" {
|
||||
target = f.getSheetRelationshipsTargetByID(sheet, xlsx.Hyperlinks.Hyperlink[h].RID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return link, target
|
||||
}
|
||||
|
||||
// MergeCell provides function to merge cells by given coordinate area and sheet
|
||||
// name. For example create a merged cell of D3:E9 on Sheet1:
|
||||
//
|
||||
// xlsx.MergeCell("Sheet1", "D3", "E9")
|
||||
//
|
||||
// If you create a merged cell that overlaps with another existing merged cell,
|
||||
// those merged cells that already exist will be removed.
|
||||
func (f *File) MergeCell(sheet, hcell, vcell string) {
|
||||
if hcell == vcell {
|
||||
return
|
||||
}
|
||||
|
||||
hcell = strings.ToUpper(hcell)
|
||||
vcell = strings.ToUpper(vcell)
|
||||
|
||||
// Coordinate conversion, convert C1:B3 to 2,0,1,2.
|
||||
hcol := string(strings.Map(letterOnlyMapF, hcell))
|
||||
hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell))
|
||||
hyAxis := hrow - 1
|
||||
hxAxis := TitleToNumber(hcol)
|
||||
|
||||
vcol := string(strings.Map(letterOnlyMapF, vcell))
|
||||
vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell))
|
||||
vyAxis := vrow - 1
|
||||
vxAxis := TitleToNumber(vcol)
|
||||
|
||||
if vxAxis < hxAxis {
|
||||
hcell, vcell = vcell, hcell
|
||||
vxAxis, hxAxis = hxAxis, vxAxis
|
||||
}
|
||||
|
||||
if vyAxis < hyAxis {
|
||||
hcell, vcell = vcell, hcell
|
||||
vyAxis, hyAxis = hyAxis, vyAxis
|
||||
}
|
||||
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
if xlsx.MergeCells != nil {
|
||||
mergeCell := xlsxMergeCell{}
|
||||
// Correct the coordinate area, such correct C1:B3 to B1:C3.
|
||||
mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1)
|
||||
// Delete the merged cells of the overlapping area.
|
||||
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
|
||||
if checkCellInArea(hcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0], mergeCell.Ref) {
|
||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
|
||||
} else if checkCellInArea(vcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[1], mergeCell.Ref) {
|
||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
|
||||
}
|
||||
}
|
||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &mergeCell)
|
||||
} else {
|
||||
mergeCell := xlsxMergeCell{}
|
||||
// Correct the coordinate area, such correct C1:B3 to B1:C3.
|
||||
mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1)
|
||||
mergeCells := xlsxMergeCells{}
|
||||
mergeCells.Cells = append(mergeCells.Cells, &mergeCell)
|
||||
xlsx.MergeCells = &mergeCells
|
||||
}
|
||||
}
|
||||
|
||||
// SetCellInt provides function to set int type value of a cell by given
|
||||
// worksheet name, cell coordinates and cell value.
|
||||
func (f *File) SetCellInt(sheet, axis string, value int) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = ""
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value)
|
||||
}
|
||||
|
||||
// prepareCellStyle provides function to prepare style index of cell in
|
||||
// worksheet by given column index and style index.
|
||||
func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int {
|
||||
if xlsx.Cols != nil && style == 0 {
|
||||
for _, v := range xlsx.Cols.Col {
|
||||
if v.Min <= col && col <= v.Max {
|
||||
style = v.Style
|
||||
}
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// SetCellStr provides function to set string type value of a cell. Total number
|
||||
// of characters that a cell can contain 32767 characters.
|
||||
func (f *File) SetCellStr(sheet, axis, value string) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
if len(value) > 32767 {
|
||||
value = value[0:32767]
|
||||
}
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
// Leading space(s) character detection.
|
||||
if len(value) > 0 {
|
||||
if value[0] == 32 {
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].XMLSpace = xml.Attr{
|
||||
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
|
||||
Value: "preserve",
|
||||
}
|
||||
}
|
||||
}
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = "str"
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = value
|
||||
}
|
||||
|
||||
// SetCellDefault provides function to set string type value of a cell as
|
||||
// default format without escaping the cell.
|
||||
func (f *File) SetCellDefault(sheet, axis, value string) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = ""
|
||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = value
|
||||
}
|
||||
|
||||
// SetSheetRow writes an array to row by given worksheet name, starting
|
||||
// coordinate and a pointer to array type 'slice'. For example, writes an
|
||||
// array to row 6 start with the cell B6 on Sheet1:
|
||||
//
|
||||
// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2})
|
||||
//
|
||||
func (f *File) SetSheetRow(sheet, axis string, slice interface{}) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
axis = f.mergeCellsParser(xlsx, axis)
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Make sure 'slice' is a Ptr to Slice
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Ptr {
|
||||
return
|
||||
}
|
||||
v = v.Elem()
|
||||
if v.Kind() != reflect.Slice {
|
||||
return
|
||||
}
|
||||
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
|
||||
rows := xAxis + 1
|
||||
cell := yAxis + 1
|
||||
|
||||
completeRow(xlsx, rows, cell)
|
||||
completeCol(xlsx, rows, cell)
|
||||
|
||||
idx := 0
|
||||
for i := cell - 1; i < v.Len()+cell-1; i++ {
|
||||
c := ToAlphaString(i) + strconv.Itoa(row)
|
||||
f.SetCellValue(sheet, c, v.Index(idx).Interface())
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
// checkCellInArea provides function to determine if a given coordinate is
|
||||
// within an area.
|
||||
func checkCellInArea(cell, area string) bool {
|
||||
cell = strings.ToUpper(cell)
|
||||
area = strings.ToUpper(area)
|
||||
|
||||
ref := strings.Split(area, ":")
|
||||
if len(ref) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
from := ref[0]
|
||||
to := ref[1]
|
||||
|
||||
col, row := getCellColRow(cell)
|
||||
fromCol, fromRow := getCellColRow(from)
|
||||
toCol, toRow := getCellColRow(to)
|
||||
|
||||
return axisLowerOrEqualThan(fromCol, col) && axisLowerOrEqualThan(col, toCol) && axisLowerOrEqualThan(fromRow, row) && axisLowerOrEqualThan(row, toRow)
|
||||
}
|
||||
+1161
File diff suppressed because it is too large
Load Diff
+366
@@ -0,0 +1,366 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Define the default cell size and EMU unit of measurement.
|
||||
const (
|
||||
defaultColWidthPixels float64 = 64
|
||||
defaultRowHeightPixels float64 = 20
|
||||
EMU int = 9525
|
||||
)
|
||||
|
||||
// GetColVisible provides a function to get visible of a single column by given
|
||||
// worksheet name and column name. For example, get visible state of column D
|
||||
// in Sheet1:
|
||||
//
|
||||
// xlsx.GetColVisible("Sheet1", "D")
|
||||
//
|
||||
func (f *File) GetColVisible(sheet, column string) bool {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
col := TitleToNumber(strings.ToUpper(column)) + 1
|
||||
visible := true
|
||||
if xlsx.Cols == nil {
|
||||
return visible
|
||||
}
|
||||
for c := range xlsx.Cols.Col {
|
||||
if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max {
|
||||
visible = !xlsx.Cols.Col[c].Hidden
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
// SetColVisible provides a function to set visible of a single column by given
|
||||
// worksheet name and column name. For example, hide column D in Sheet1:
|
||||
//
|
||||
// xlsx.SetColVisible("Sheet1", "D", false)
|
||||
//
|
||||
func (f *File) SetColVisible(sheet, column string, visible bool) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
c := TitleToNumber(strings.ToUpper(column)) + 1
|
||||
col := xlsxCol{
|
||||
Min: c,
|
||||
Max: c,
|
||||
Hidden: !visible,
|
||||
CustomWidth: true,
|
||||
}
|
||||
if xlsx.Cols == nil {
|
||||
cols := xlsxCols{}
|
||||
cols.Col = append(cols.Col, col)
|
||||
xlsx.Cols = &cols
|
||||
return
|
||||
}
|
||||
for v := range xlsx.Cols.Col {
|
||||
if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max {
|
||||
col = xlsx.Cols.Col[v]
|
||||
}
|
||||
}
|
||||
col.Min = c
|
||||
col.Max = c
|
||||
col.Hidden = !visible
|
||||
col.CustomWidth = true
|
||||
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
|
||||
}
|
||||
|
||||
// GetColOutlineLevel provides a function to get outline level of a single
|
||||
// column by given worksheet name and column name. For example, get outline
|
||||
// level of column D in Sheet1:
|
||||
//
|
||||
// xlsx.GetColOutlineLevel("Sheet1", "D")
|
||||
//
|
||||
func (f *File) GetColOutlineLevel(sheet, column string) uint8 {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
col := TitleToNumber(strings.ToUpper(column)) + 1
|
||||
level := uint8(0)
|
||||
if xlsx.Cols == nil {
|
||||
return level
|
||||
}
|
||||
for c := range xlsx.Cols.Col {
|
||||
if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max {
|
||||
level = xlsx.Cols.Col[c].OutlineLevel
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// SetColOutlineLevel provides a function to set outline level of a single
|
||||
// column by given worksheet name and column name. For example, set outline
|
||||
// level of column D in Sheet1 to 2:
|
||||
//
|
||||
// xlsx.SetColOutlineLevel("Sheet1", "D", 2)
|
||||
//
|
||||
func (f *File) SetColOutlineLevel(sheet, column string, level uint8) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
c := TitleToNumber(strings.ToUpper(column)) + 1
|
||||
col := xlsxCol{
|
||||
Min: c,
|
||||
Max: c,
|
||||
OutlineLevel: level,
|
||||
CustomWidth: true,
|
||||
}
|
||||
if xlsx.Cols == nil {
|
||||
cols := xlsxCols{}
|
||||
cols.Col = append(cols.Col, col)
|
||||
xlsx.Cols = &cols
|
||||
return
|
||||
}
|
||||
for v := range xlsx.Cols.Col {
|
||||
if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max {
|
||||
col = xlsx.Cols.Col[v]
|
||||
}
|
||||
}
|
||||
col.Min = c
|
||||
col.Max = c
|
||||
col.OutlineLevel = level
|
||||
col.CustomWidth = true
|
||||
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
|
||||
}
|
||||
|
||||
// SetColWidth provides function to set the width of a single column or multiple
|
||||
// columns. For example:
|
||||
//
|
||||
// xlsx := excelize.NewFile()
|
||||
// xlsx.SetColWidth("Sheet1", "A", "H", 20)
|
||||
// err := xlsx.Save()
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
//
|
||||
func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) {
|
||||
min := TitleToNumber(strings.ToUpper(startcol)) + 1
|
||||
max := TitleToNumber(strings.ToUpper(endcol)) + 1
|
||||
if min > max {
|
||||
min, max = max, min
|
||||
}
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
col := xlsxCol{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Width: width,
|
||||
CustomWidth: true,
|
||||
}
|
||||
if xlsx.Cols != nil {
|
||||
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
|
||||
} else {
|
||||
cols := xlsxCols{}
|
||||
cols.Col = append(cols.Col, col)
|
||||
xlsx.Cols = &cols
|
||||
}
|
||||
}
|
||||
|
||||
// positionObjectPixels calculate the vertices that define the position of a
|
||||
// graphical object within the worksheet in pixels.
|
||||
//
|
||||
// +------------+------------+
|
||||
// | A | B |
|
||||
// +-----+------------+------------+
|
||||
// | |(x1,y1) | |
|
||||
// | 1 |(A1)._______|______ |
|
||||
// | | | | |
|
||||
// | | | | |
|
||||
// +-----+----| OBJECT |-----+
|
||||
// | | | | |
|
||||
// | 2 | |______________. |
|
||||
// | | | (B2)|
|
||||
// | | | (x2,y2)|
|
||||
// +-----+------------+------------+
|
||||
//
|
||||
// Example of an object that covers some of the area from cell A1 to B2.
|
||||
//
|
||||
// Based on the width and height of the object we need to calculate 8 vars:
|
||||
//
|
||||
// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2.
|
||||
//
|
||||
// We also calculate the absolute x and y position of the top left vertex of
|
||||
// the object. This is required for images.
|
||||
//
|
||||
// The width and height of the cells that the object occupies can be
|
||||
// variable and have to be taken into account.
|
||||
//
|
||||
// The values of col_start and row_start are passed in from the calling
|
||||
// function. The values of col_end and row_end are calculated by
|
||||
// subtracting the width and height of the object from the width and
|
||||
// height of the underlying cells.
|
||||
//
|
||||
// colStart # Col containing upper left corner of object.
|
||||
// x1 # Distance to left side of object.
|
||||
//
|
||||
// rowStart # Row containing top left corner of object.
|
||||
// y1 # Distance to top of object.
|
||||
//
|
||||
// colEnd # Col containing lower right corner of object.
|
||||
// x2 # Distance to right side of object.
|
||||
//
|
||||
// rowEnd # Row containing bottom right corner of object.
|
||||
// y2 # Distance to bottom of object.
|
||||
//
|
||||
// width # Width of object frame.
|
||||
// height # Height of object frame.
|
||||
//
|
||||
// xAbs # Absolute distance to left side of object.
|
||||
// yAbs # Absolute distance to top side of object.
|
||||
//
|
||||
func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, width, height int) (int, int, int, int, int, int, int, int) {
|
||||
xAbs := 0
|
||||
yAbs := 0
|
||||
|
||||
// Calculate the absolute x offset of the top-left vertex.
|
||||
for colID := 1; colID <= colStart; colID++ {
|
||||
xAbs += f.getColWidth(sheet, colID)
|
||||
}
|
||||
xAbs += x1
|
||||
|
||||
// Calculate the absolute y offset of the top-left vertex.
|
||||
// Store the column change to allow optimisations.
|
||||
for rowID := 1; rowID <= rowStart; rowID++ {
|
||||
yAbs += f.getRowHeight(sheet, rowID)
|
||||
}
|
||||
yAbs += y1
|
||||
|
||||
// Adjust start column for offsets that are greater than the col width.
|
||||
for x1 >= f.getColWidth(sheet, colStart) {
|
||||
x1 -= f.getColWidth(sheet, colStart)
|
||||
colStart++
|
||||
}
|
||||
|
||||
// Adjust start row for offsets that are greater than the row height.
|
||||
for y1 >= f.getRowHeight(sheet, rowStart) {
|
||||
y1 -= f.getRowHeight(sheet, rowStart)
|
||||
rowStart++
|
||||
}
|
||||
|
||||
// Initialise end cell to the same as the start cell.
|
||||
colEnd := colStart
|
||||
rowEnd := rowStart
|
||||
|
||||
width += x1
|
||||
height += y1
|
||||
|
||||
// Subtract the underlying cell widths to find end cell of the object.
|
||||
for width >= f.getColWidth(sheet, colEnd) {
|
||||
colEnd++
|
||||
width -= f.getColWidth(sheet, colEnd)
|
||||
}
|
||||
|
||||
// Subtract the underlying cell heights to find end cell of the object.
|
||||
for height >= f.getRowHeight(sheet, rowEnd) {
|
||||
rowEnd++
|
||||
height -= f.getRowHeight(sheet, rowEnd)
|
||||
}
|
||||
|
||||
// The end vertices are whatever is left from the width and height.
|
||||
x2 := width
|
||||
y2 := height
|
||||
return colStart, rowStart, xAbs, yAbs, colEnd, rowEnd, x2, y2
|
||||
}
|
||||
|
||||
// getColWidth provides function to get column width in pixels by given sheet
|
||||
// name and column index.
|
||||
func (f *File) getColWidth(sheet string, col int) int {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
if xlsx.Cols != nil {
|
||||
var width float64
|
||||
for _, v := range xlsx.Cols.Col {
|
||||
if v.Min <= col && col <= v.Max {
|
||||
width = v.Width
|
||||
}
|
||||
}
|
||||
if width != 0 {
|
||||
return int(convertColWidthToPixels(width))
|
||||
}
|
||||
}
|
||||
// Optimisation for when the column widths haven't changed.
|
||||
return int(defaultColWidthPixels)
|
||||
}
|
||||
|
||||
// GetColWidth provides function to get column width by given worksheet name and
|
||||
// column index.
|
||||
func (f *File) GetColWidth(sheet, column string) float64 {
|
||||
col := TitleToNumber(strings.ToUpper(column)) + 1
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
if xlsx.Cols != nil {
|
||||
var width float64
|
||||
for _, v := range xlsx.Cols.Col {
|
||||
if v.Min <= col && col <= v.Max {
|
||||
width = v.Width
|
||||
}
|
||||
}
|
||||
if width != 0 {
|
||||
return width
|
||||
}
|
||||
}
|
||||
// Optimisation for when the column widths haven't changed.
|
||||
return defaultColWidthPixels
|
||||
}
|
||||
|
||||
// InsertCol provides function to insert a new column before given column index.
|
||||
// For example, create a new column before column C in Sheet1:
|
||||
//
|
||||
// xlsx.InsertCol("Sheet1", "C")
|
||||
//
|
||||
func (f *File) InsertCol(sheet, column string) {
|
||||
col := TitleToNumber(strings.ToUpper(column))
|
||||
f.adjustHelper(sheet, col, -1, 1)
|
||||
}
|
||||
|
||||
// RemoveCol provides function to remove single column by given worksheet name
|
||||
// and column index. For example, remove column C in Sheet1:
|
||||
//
|
||||
// xlsx.RemoveCol("Sheet1", "C")
|
||||
//
|
||||
func (f *File) RemoveCol(sheet, column string) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
for r := range xlsx.SheetData.Row {
|
||||
for k, v := range xlsx.SheetData.Row[r].C {
|
||||
axis := v.R
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
if col == column {
|
||||
xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C[:k], xlsx.SheetData.Row[r].C[k+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
col := TitleToNumber(strings.ToUpper(column))
|
||||
f.adjustHelper(sheet, col, -1, -1)
|
||||
}
|
||||
|
||||
// Completion column element tags of XML in a sheet.
|
||||
func completeCol(xlsx *xlsxWorksheet, row, cell int) {
|
||||
buffer := bytes.Buffer{}
|
||||
for r := range xlsx.SheetData.Row {
|
||||
if len(xlsx.SheetData.Row[r].C) < cell {
|
||||
start := len(xlsx.SheetData.Row[r].C)
|
||||
for iii := start; iii < cell; iii++ {
|
||||
buffer.WriteString(ToAlphaString(iii))
|
||||
buffer.WriteString(strconv.Itoa(r + 1))
|
||||
xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C, xlsxC{
|
||||
R: buffer.String(),
|
||||
})
|
||||
buffer.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convertColWidthToPixels provieds function to convert the width of a cell from
|
||||
// user's units to pixels. Excel rounds the column width to the nearest pixel.
|
||||
// If the width hasn't been set by the user we use the default value. If the
|
||||
// column is hidden it has a value of zero.
|
||||
func convertColWidthToPixels(width float64) float64 {
|
||||
var padding float64 = 5
|
||||
var pixels float64
|
||||
var maxDigitWidth float64 = 7
|
||||
if width == 0 {
|
||||
return pixels
|
||||
}
|
||||
if width < 1 {
|
||||
pixels = (width * 12) + 0.5
|
||||
return math.Ceil(pixels)
|
||||
}
|
||||
pixels = (width*maxDigitWidth + 0.5) + padding
|
||||
return math.Ceil(pixels)
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseFormatCommentsSet provides function to parse the format settings of the
|
||||
// comment with default value.
|
||||
func parseFormatCommentsSet(formatSet string) *formatComment {
|
||||
format := formatComment{
|
||||
Author: "Author:",
|
||||
Text: " ",
|
||||
}
|
||||
json.Unmarshal([]byte(formatSet), &format)
|
||||
return &format
|
||||
}
|
||||
|
||||
// AddComment provides the method to add comment in a sheet by given worksheet
|
||||
// index, cell and format set (such as author and text). Note that the max
|
||||
// author length is 255 and the max text length is 32512. For example, add a
|
||||
// comment in Sheet1!$A$30:
|
||||
//
|
||||
// xlsx.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
|
||||
//
|
||||
func (f *File) AddComment(sheet, cell, format string) {
|
||||
formatSet := parseFormatCommentsSet(format)
|
||||
// Read sheet data.
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
commentID := f.countComments() + 1
|
||||
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
|
||||
sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml"
|
||||
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
|
||||
if xlsx.LegacyDrawing != nil {
|
||||
// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
|
||||
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, xlsx.LegacyDrawing.RID)
|
||||
commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
|
||||
drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
|
||||
} else {
|
||||
// Add first comment for given sheet.
|
||||
rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
|
||||
f.addSheetRelationships(sheet, SourceRelationshipComments, sheetRelationshipsComments, "")
|
||||
f.addSheetLegacyDrawing(sheet, rID)
|
||||
}
|
||||
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
|
||||
f.addComment(commentsXML, cell, formatSet)
|
||||
f.addDrawingVML(commentID, drawingVML, cell)
|
||||
f.addContentTypePart(commentID, "comments")
|
||||
}
|
||||
|
||||
// addDrawingVML provides function to create comment as
|
||||
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
|
||||
func (f *File) addDrawingVML(commentID int, drawingVML, cell string) {
|
||||
col := string(strings.Map(letterOnlyMapF, cell))
|
||||
row, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
|
||||
xAxis := row - 1
|
||||
yAxis := TitleToNumber(col)
|
||||
vml := vmlDrawing{
|
||||
XMLNSv: "urn:schemas-microsoft-com:vml",
|
||||
XMLNSo: "urn:schemas-microsoft-com:office:office",
|
||||
XMLNSx: "urn:schemas-microsoft-com:office:excel",
|
||||
XMLNSmv: "http://macVmlSchemaUri",
|
||||
Shapelayout: &xlsxShapelayout{
|
||||
Ext: "edit",
|
||||
IDmap: &xlsxIDmap{
|
||||
Ext: "edit",
|
||||
Data: commentID,
|
||||
},
|
||||
},
|
||||
Shapetype: &xlsxShapetype{
|
||||
ID: "_x0000_t202",
|
||||
Coordsize: "21600,21600",
|
||||
Spt: 202,
|
||||
Path: "m0,0l0,21600,21600,21600,21600,0xe",
|
||||
Stroke: &xlsxStroke{
|
||||
Joinstyle: "miter",
|
||||
},
|
||||
VPath: &vPath{
|
||||
Gradientshapeok: "t",
|
||||
Connecttype: "rect",
|
||||
},
|
||||
},
|
||||
}
|
||||
sp := encodeShape{
|
||||
Fill: &vFill{
|
||||
Color2: "#fbfe82",
|
||||
Angle: -180,
|
||||
Type: "gradient",
|
||||
Fill: &oFill{
|
||||
Ext: "view",
|
||||
Type: "gradientUnscaled",
|
||||
},
|
||||
},
|
||||
Shadow: &vShadow{
|
||||
On: "t",
|
||||
Color: "black",
|
||||
Obscured: "t",
|
||||
},
|
||||
Path: &vPath{
|
||||
Connecttype: "none",
|
||||
},
|
||||
Textbox: &vTextbox{
|
||||
Style: "mso-direction-alt:auto",
|
||||
Div: &xlsxDiv{
|
||||
Style: "text-align:left",
|
||||
},
|
||||
},
|
||||
ClientData: &xClientData{
|
||||
ObjectType: "Note",
|
||||
Anchor: "3, 15, 8, 6, 4, 54, 13, 2",
|
||||
AutoFill: "False",
|
||||
Row: xAxis,
|
||||
Column: yAxis,
|
||||
},
|
||||
}
|
||||
s, _ := xml.Marshal(sp)
|
||||
shape := xlsxShape{
|
||||
ID: "_x0000_s1025",
|
||||
Type: "#_x0000_t202",
|
||||
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
|
||||
Fillcolor: "#fbf6d6",
|
||||
Strokecolor: "#edeaa1",
|
||||
Val: string(s[13 : len(s)-14]),
|
||||
}
|
||||
c, ok := f.XLSX[drawingVML]
|
||||
if ok {
|
||||
d := decodeVmlDrawing{}
|
||||
xml.Unmarshal([]byte(c), &d)
|
||||
for _, v := range d.Shape {
|
||||
s := xlsxShape{
|
||||
ID: "_x0000_s1025",
|
||||
Type: "#_x0000_t202",
|
||||
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
|
||||
Fillcolor: "#fbf6d6",
|
||||
Strokecolor: "#edeaa1",
|
||||
Val: v.Val,
|
||||
}
|
||||
vml.Shape = append(vml.Shape, s)
|
||||
}
|
||||
}
|
||||
vml.Shape = append(vml.Shape, shape)
|
||||
v, _ := xml.Marshal(vml)
|
||||
f.XLSX[drawingVML] = v
|
||||
}
|
||||
|
||||
// addComment provides function to create chart as xl/comments%d.xml by given
|
||||
// cell and format sets.
|
||||
func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
|
||||
a := formatSet.Author
|
||||
t := formatSet.Text
|
||||
if len(a) > 255 {
|
||||
a = a[0:255]
|
||||
}
|
||||
if len(t) > 32512 {
|
||||
t = t[0:32512]
|
||||
}
|
||||
comments := xlsxComments{
|
||||
Authors: []xlsxAuthor{
|
||||
{
|
||||
Author: formatSet.Author,
|
||||
},
|
||||
},
|
||||
}
|
||||
cmt := xlsxComment{
|
||||
Ref: cell,
|
||||
AuthorID: 0,
|
||||
Text: xlsxText{
|
||||
R: []xlsxR{
|
||||
{
|
||||
RPr: &xlsxRPr{
|
||||
B: " ",
|
||||
Sz: &attrValFloat{Val: 9},
|
||||
Color: &xlsxColor{
|
||||
Indexed: 81,
|
||||
},
|
||||
RFont: &attrValString{Val: "Calibri"},
|
||||
Family: &attrValInt{Val: 2},
|
||||
},
|
||||
T: a,
|
||||
},
|
||||
{
|
||||
RPr: &xlsxRPr{
|
||||
Sz: &attrValFloat{Val: 9},
|
||||
Color: &xlsxColor{
|
||||
Indexed: 81,
|
||||
},
|
||||
RFont: &attrValString{Val: "Calibri"},
|
||||
Family: &attrValInt{Val: 2},
|
||||
},
|
||||
T: t,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
c, ok := f.XLSX[commentsXML]
|
||||
if ok {
|
||||
d := xlsxComments{}
|
||||
xml.Unmarshal([]byte(c), &d)
|
||||
comments.CommentList.Comment = append(comments.CommentList.Comment, d.CommentList.Comment...)
|
||||
}
|
||||
comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
|
||||
v, _ := xml.Marshal(comments)
|
||||
f.saveFileList(commentsXML, v)
|
||||
}
|
||||
|
||||
// countComments provides function to get comments files count storage in the
|
||||
// folder xl.
|
||||
func (f *File) countComments() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/comments") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// timeLocationUTC defined the UTC time location.
|
||||
var timeLocationUTC, _ = time.LoadLocation("UTC")
|
||||
|
||||
// timeToUTCTime provides function to convert time to UTC time.
|
||||
func timeToUTCTime(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), timeLocationUTC)
|
||||
}
|
||||
|
||||
// timeToExcelTime provides function to convert time to Excel time.
|
||||
func timeToExcelTime(t time.Time) float64 {
|
||||
return float64(t.UnixNano())/8.64e13 + 25569.0
|
||||
}
|
||||
|
||||
// shiftJulianToNoon provides function to process julian date to noon.
|
||||
func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) {
|
||||
switch {
|
||||
case -0.5 < julianFraction && julianFraction < 0.5:
|
||||
julianFraction += 0.5
|
||||
case julianFraction >= 0.5:
|
||||
julianDays++
|
||||
julianFraction -= 0.5
|
||||
case julianFraction <= -0.5:
|
||||
julianDays--
|
||||
julianFraction += 1.5
|
||||
}
|
||||
return julianDays, julianFraction
|
||||
}
|
||||
|
||||
// fractionOfADay provides function to return the integer values for hour,
|
||||
// minutes, seconds and nanoseconds that comprised a given fraction of a day.
|
||||
// values would round to 1 us.
|
||||
func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) {
|
||||
|
||||
const (
|
||||
c1us = 1e3
|
||||
c1s = 1e9
|
||||
c1day = 24 * 60 * 60 * c1s
|
||||
)
|
||||
|
||||
frac := int64(c1day*fraction + c1us/2)
|
||||
nanoseconds = int((frac%c1s)/c1us) * c1us
|
||||
frac /= c1s
|
||||
seconds = int(frac % 60)
|
||||
frac /= 60
|
||||
minutes = int(frac % 60)
|
||||
hours = int(frac / 60)
|
||||
return
|
||||
}
|
||||
|
||||
// julianDateToGregorianTime provides function to convert julian date to
|
||||
// gregorian time.
|
||||
func julianDateToGregorianTime(part1, part2 float64) time.Time {
|
||||
part1I, part1F := math.Modf(part1)
|
||||
part2I, part2F := math.Modf(part2)
|
||||
julianDays := part1I + part2I
|
||||
julianFraction := part1F + part2F
|
||||
julianDays, julianFraction = shiftJulianToNoon(julianDays, julianFraction)
|
||||
day, month, year := doTheFliegelAndVanFlandernAlgorithm(int(julianDays))
|
||||
hours, minutes, seconds, nanoseconds := fractionOfADay(julianFraction)
|
||||
return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC)
|
||||
}
|
||||
|
||||
// By this point generations of programmers have repeated the algorithm sent to
|
||||
// the editor of "Communications of the ACM" in 1968 (published in CACM, volume
|
||||
// 11, number 10, October 1968, p.657). None of those programmers seems to have
|
||||
// found it necessary to explain the constants or variable names set out by
|
||||
// Henry F. Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that
|
||||
// jounal and expand an explanation here - that day is not today.
|
||||
func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
|
||||
l := jd + 68569
|
||||
n := (4 * l) / 146097
|
||||
l = l - (146097*n+3)/4
|
||||
i := (4000 * (l + 1)) / 1461001
|
||||
l = l - (1461*i)/4 + 31
|
||||
j := (80 * l) / 2447
|
||||
d := l - (2447*j)/80
|
||||
l = j / 11
|
||||
m := j + 2 - (12 * l)
|
||||
y := 100*(n-49) + i + l
|
||||
return d, m, y
|
||||
}
|
||||
|
||||
// timeFromExcelTime provides function to convert an excelTime representation
|
||||
// (stored as a floating point number) to a time.Time.
|
||||
func timeFromExcelTime(excelTime float64, date1904 bool) time.Time {
|
||||
var date time.Time
|
||||
var intPart = int64(excelTime)
|
||||
// Excel uses Julian dates prior to March 1st 1900, and Gregorian
|
||||
// thereafter.
|
||||
if intPart <= 61 {
|
||||
const OFFSET1900 = 15018.0
|
||||
const OFFSET1904 = 16480.0
|
||||
const MJD0 float64 = 2400000.5
|
||||
var date time.Time
|
||||
if date1904 {
|
||||
date = julianDateToGregorianTime(MJD0, excelTime+OFFSET1904)
|
||||
} else {
|
||||
date = julianDateToGregorianTime(MJD0, excelTime+OFFSET1900)
|
||||
}
|
||||
return date
|
||||
}
|
||||
var floatPart = excelTime - float64(intPart)
|
||||
var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000
|
||||
if date1904 {
|
||||
date = time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
} else {
|
||||
date = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
durationDays := time.Duration(intPart) * time.Hour * 24
|
||||
durationPart := time.Duration(dayNanoSeconds * floatPart)
|
||||
return date.Add(durationDays).Add(durationPart)
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// File define a populated XLSX file struct.
|
||||
type File struct {
|
||||
checked map[string]bool
|
||||
sheetMap map[string]string
|
||||
ContentTypes *xlsxTypes
|
||||
Path string
|
||||
SharedStrings *xlsxSST
|
||||
Sheet map[string]*xlsxWorksheet
|
||||
SheetCount int
|
||||
Styles *xlsxStyleSheet
|
||||
WorkBook *xlsxWorkbook
|
||||
WorkBookRels *xlsxWorkbookRels
|
||||
XLSX map[string][]byte
|
||||
}
|
||||
|
||||
// OpenFile take the name of an XLSX file and returns a populated XLSX file
|
||||
// struct for it.
|
||||
func OpenFile(filename string) (*File, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
f, err := OpenReader(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Path = filename
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// OpenReader take an io.Reader and return a populated XLSX file.
|
||||
func OpenReader(r io.Reader) (*File, error) {
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, sheetCount, err := ReadZipReader(zr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := &File{
|
||||
checked: make(map[string]bool),
|
||||
Sheet: make(map[string]*xlsxWorksheet),
|
||||
SheetCount: sheetCount,
|
||||
XLSX: file,
|
||||
}
|
||||
f.sheetMap = f.getSheetMap()
|
||||
f.Styles = f.stylesReader()
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// setDefaultTimeStyle provides function to set default numbers format for
|
||||
// time.Time type cell value by given worksheet name, cell coordinates and
|
||||
// number format code.
|
||||
func (f *File) setDefaultTimeStyle(sheet, axis string, format int) {
|
||||
if f.GetCellStyle(sheet, axis) == 0 {
|
||||
style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`)
|
||||
f.SetCellStyle(sheet, axis, axis, style)
|
||||
}
|
||||
}
|
||||
|
||||
// workSheetReader provides function to get the pointer to the structure after
|
||||
// deserialization by given worksheet name.
|
||||
func (f *File) workSheetReader(sheet string) *xlsxWorksheet {
|
||||
name, ok := f.sheetMap[trimSheetName(sheet)]
|
||||
if !ok {
|
||||
name = "xl/worksheets/" + strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
if f.Sheet[name] == nil {
|
||||
var xlsx xlsxWorksheet
|
||||
xml.Unmarshal(f.readXML(name), &xlsx)
|
||||
if f.checked == nil {
|
||||
f.checked = make(map[string]bool)
|
||||
}
|
||||
ok := f.checked[name]
|
||||
if !ok {
|
||||
checkSheet(&xlsx)
|
||||
checkRow(&xlsx)
|
||||
f.checked[name] = true
|
||||
}
|
||||
f.Sheet[name] = &xlsx
|
||||
}
|
||||
return f.Sheet[name]
|
||||
}
|
||||
|
||||
// checkSheet provides function to fill each row element and make that is
|
||||
// continuous in a worksheet of XML.
|
||||
func checkSheet(xlsx *xlsxWorksheet) {
|
||||
row := len(xlsx.SheetData.Row)
|
||||
if row >= 1 {
|
||||
lastRow := xlsx.SheetData.Row[row-1].R
|
||||
if lastRow >= row {
|
||||
row = lastRow
|
||||
}
|
||||
}
|
||||
sheetData := xlsxSheetData{}
|
||||
existsRows := map[int]int{}
|
||||
for k := range xlsx.SheetData.Row {
|
||||
existsRows[xlsx.SheetData.Row[k].R] = k
|
||||
}
|
||||
for i := 0; i < row; i++ {
|
||||
_, ok := existsRows[i+1]
|
||||
if ok {
|
||||
sheetData.Row = append(sheetData.Row, xlsx.SheetData.Row[existsRows[i+1]])
|
||||
} else {
|
||||
sheetData.Row = append(sheetData.Row, xlsxRow{
|
||||
R: i + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
xlsx.SheetData = sheetData
|
||||
}
|
||||
|
||||
// replaceWorkSheetsRelationshipsNameSpaceBytes provides function to replace
|
||||
// xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft
|
||||
// Office Excel 2007.
|
||||
func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte {
|
||||
var oldXmlns = []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
|
||||
var newXmlns = []byte(`<worksheet xr:uid="{00000000-0001-0000-0000-000000000000}" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" mc:Ignorable="x14ac xr xr2 xr3" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
|
||||
workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
|
||||
return workbookMarshal
|
||||
}
|
||||
|
||||
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
|
||||
// Office Excel 2007 and 2010. This function will be remove value tag when met a
|
||||
// cell have a linked value. Reference
|
||||
// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating?forum=excel
|
||||
//
|
||||
// Notice: after open XLSX file Excel will be update linked value and generate
|
||||
// new value and will prompt save file or not.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// <row r="19" spans="2:2">
|
||||
// <c r="B19">
|
||||
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
||||
// <v>100</v>
|
||||
// </c>
|
||||
// </row>
|
||||
//
|
||||
// to
|
||||
//
|
||||
// <row r="19" spans="2:2">
|
||||
// <c r="B19">
|
||||
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
||||
// </c>
|
||||
// </row>
|
||||
//
|
||||
func (f *File) UpdateLinkedValue() {
|
||||
for _, name := range f.GetSheetMap() {
|
||||
xlsx := f.workSheetReader(name)
|
||||
for indexR := range xlsx.SheetData.Row {
|
||||
for indexC, col := range xlsx.SheetData.Row[indexR].C {
|
||||
if col.F != nil && col.V != "" {
|
||||
xlsx.SheetData.Row[indexR].C[indexC].V = ""
|
||||
xlsx.SheetData.Row[indexR].C[indexC].T = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustHelper provides function to adjust rows and columns dimensions,
|
||||
// hyperlinks, merged cells and auto filter when inserting or deleting rows or
|
||||
// columns.
|
||||
//
|
||||
// sheet: Worksheet name that we're editing
|
||||
// column: Index number of the column we're inserting/deleting before
|
||||
// row: Index number of the row we're inserting/deleting before
|
||||
// offset: Number of rows/column to insert/delete negative values indicate deletion
|
||||
//
|
||||
// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells
|
||||
//
|
||||
func (f *File) adjustHelper(sheet string, column, row, offset int) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
f.adjustRowDimensions(xlsx, row, offset)
|
||||
f.adjustColDimensions(xlsx, column, offset)
|
||||
f.adjustHyperlinks(sheet, column, row, offset)
|
||||
f.adjustMergeCells(xlsx, column, row, offset)
|
||||
f.adjustAutoFilter(xlsx, column, row, offset)
|
||||
checkSheet(xlsx)
|
||||
checkRow(xlsx)
|
||||
}
|
||||
|
||||
// adjustColDimensions provides function to update column dimensions when
|
||||
// inserting or deleting rows or columns.
|
||||
func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, column, offset int) {
|
||||
for i, r := range xlsx.SheetData.Row {
|
||||
for k, v := range r.C {
|
||||
axis := v.R
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
yAxis := TitleToNumber(col)
|
||||
if yAxis >= column && column != -1 {
|
||||
xlsx.SheetData.Row[i].C[k].R = ToAlphaString(yAxis+offset) + strconv.Itoa(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustRowDimensions provides function to update row dimensions when inserting
|
||||
// or deleting rows or columns.
|
||||
func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) {
|
||||
if rowIndex == -1 {
|
||||
return
|
||||
}
|
||||
for i, r := range xlsx.SheetData.Row {
|
||||
if r.R >= rowIndex {
|
||||
xlsx.SheetData.Row[i].R += offset
|
||||
for k, v := range xlsx.SheetData.Row[i].C {
|
||||
axis := v.R
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
xAxis := row + offset
|
||||
xlsx.SheetData.Row[i].C[k].R = col + strconv.Itoa(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustHyperlinks provides function to update hyperlinks when inserting or
|
||||
// deleting rows or columns.
|
||||
func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
|
||||
// order is important
|
||||
if xlsx.Hyperlinks != nil && offset < 0 {
|
||||
for i, v := range xlsx.Hyperlinks.Hyperlink {
|
||||
axis := v.Ref
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
yAxis := TitleToNumber(col)
|
||||
if row == rowIndex || yAxis == column {
|
||||
f.deleteSheetRelationships(sheet, v.RID)
|
||||
if len(xlsx.Hyperlinks.Hyperlink) > 1 {
|
||||
xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i], xlsx.Hyperlinks.Hyperlink[i+1:]...)
|
||||
} else {
|
||||
xlsx.Hyperlinks = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if xlsx.Hyperlinks != nil {
|
||||
for i, v := range xlsx.Hyperlinks.Hyperlink {
|
||||
axis := v.Ref
|
||||
col := string(strings.Map(letterOnlyMapF, axis))
|
||||
row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
||||
xAxis := row + offset
|
||||
yAxis := TitleToNumber(col)
|
||||
if rowIndex != -1 && row >= rowIndex {
|
||||
xlsx.Hyperlinks.Hyperlink[i].Ref = col + strconv.Itoa(xAxis)
|
||||
}
|
||||
if column != -1 && yAxis >= column {
|
||||
xlsx.Hyperlinks.Hyperlink[i].Ref = ToAlphaString(yAxis+offset) + strconv.Itoa(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustMergeCellsHelper provides function to update merged cells when inserting or
|
||||
// deleting rows or columns.
|
||||
func (f *File) adjustMergeCellsHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) {
|
||||
if xlsx.MergeCells != nil {
|
||||
for k, v := range xlsx.MergeCells.Cells {
|
||||
beg := strings.Split(v.Ref, ":")[0]
|
||||
end := strings.Split(v.Ref, ":")[1]
|
||||
|
||||
begcol := string(strings.Map(letterOnlyMapF, beg))
|
||||
begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg))
|
||||
begxAxis := begrow + offset
|
||||
begyAxis := TitleToNumber(begcol)
|
||||
|
||||
endcol := string(strings.Map(letterOnlyMapF, end))
|
||||
endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end))
|
||||
endxAxis := endrow + offset
|
||||
endyAxis := TitleToNumber(endcol)
|
||||
|
||||
if rowIndex != -1 {
|
||||
if begrow > 1 && begrow >= rowIndex {
|
||||
beg = begcol + strconv.Itoa(begxAxis)
|
||||
}
|
||||
if endrow > 1 && endrow >= rowIndex {
|
||||
end = endcol + strconv.Itoa(endxAxis)
|
||||
}
|
||||
}
|
||||
|
||||
if column != -1 {
|
||||
if begyAxis >= column {
|
||||
beg = ToAlphaString(begyAxis+offset) + strconv.Itoa(endrow)
|
||||
}
|
||||
if endyAxis >= column {
|
||||
end = ToAlphaString(endyAxis+offset) + strconv.Itoa(endrow)
|
||||
}
|
||||
}
|
||||
|
||||
xlsx.MergeCells.Cells[k].Ref = beg + ":" + end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustMergeCells provides function to update merged cells when inserting or
|
||||
// deleting rows or columns.
|
||||
func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, column, rowIndex, offset int) {
|
||||
f.adjustMergeCellsHelper(xlsx, column, rowIndex, offset)
|
||||
|
||||
if xlsx.MergeCells != nil && offset < 0 {
|
||||
for k, v := range xlsx.MergeCells.Cells {
|
||||
beg := strings.Split(v.Ref, ":")[0]
|
||||
end := strings.Split(v.Ref, ":")[1]
|
||||
if beg == end {
|
||||
xlsx.MergeCells.Count += offset
|
||||
if len(xlsx.MergeCells.Cells) > 1 {
|
||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:k], xlsx.MergeCells.Cells[k+1:]...)
|
||||
} else {
|
||||
xlsx.MergeCells = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustAutoFilter provides function to update the auto filter when inserting
|
||||
// or deleting rows or columns.
|
||||
func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, column, rowIndex, offset int) {
|
||||
f.adjustAutoFilterHelper(xlsx, column, rowIndex, offset)
|
||||
|
||||
if xlsx.AutoFilter != nil {
|
||||
beg := strings.Split(xlsx.AutoFilter.Ref, ":")[0]
|
||||
end := strings.Split(xlsx.AutoFilter.Ref, ":")[1]
|
||||
|
||||
begcol := string(strings.Map(letterOnlyMapF, beg))
|
||||
begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg))
|
||||
begxAxis := begrow + offset
|
||||
|
||||
endcol := string(strings.Map(letterOnlyMapF, end))
|
||||
endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end))
|
||||
endxAxis := endrow + offset
|
||||
endyAxis := TitleToNumber(endcol)
|
||||
|
||||
if rowIndex != -1 {
|
||||
if begrow >= rowIndex {
|
||||
beg = begcol + strconv.Itoa(begxAxis)
|
||||
}
|
||||
if endrow >= rowIndex {
|
||||
end = endcol + strconv.Itoa(endxAxis)
|
||||
}
|
||||
}
|
||||
|
||||
if column != -1 && endyAxis >= column {
|
||||
end = ToAlphaString(endyAxis+offset) + strconv.Itoa(endrow)
|
||||
}
|
||||
xlsx.AutoFilter.Ref = beg + ":" + end
|
||||
}
|
||||
}
|
||||
|
||||
// adjustAutoFilterHelper provides function to update the auto filter when
|
||||
// inserting or deleting rows or columns.
|
||||
func (f *File) adjustAutoFilterHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) {
|
||||
if xlsx.AutoFilter != nil {
|
||||
beg := strings.Split(xlsx.AutoFilter.Ref, ":")[0]
|
||||
end := strings.Split(xlsx.AutoFilter.Ref, ":")[1]
|
||||
|
||||
begcol := string(strings.Map(letterOnlyMapF, beg))
|
||||
begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg))
|
||||
begyAxis := TitleToNumber(begcol)
|
||||
|
||||
endcol := string(strings.Map(letterOnlyMapF, end))
|
||||
endyAxis := TitleToNumber(endcol)
|
||||
endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end))
|
||||
|
||||
if (begrow == rowIndex && offset < 0) || (column == begyAxis && column == endyAxis) {
|
||||
xlsx.AutoFilter = nil
|
||||
for i, r := range xlsx.SheetData.Row {
|
||||
if begrow < r.R && r.R <= endrow {
|
||||
xlsx.SheetData.Row[i].Hidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
+90
@@ -0,0 +1,90 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// NewFile provides function to create new file by default template. For
|
||||
// example:
|
||||
//
|
||||
// xlsx := NewFile()
|
||||
//
|
||||
func NewFile() *File {
|
||||
file := make(map[string][]byte)
|
||||
file["_rels/.rels"] = []byte(XMLHeader + templateRels)
|
||||
file["docProps/app.xml"] = []byte(XMLHeader + templateDocpropsApp)
|
||||
file["docProps/core.xml"] = []byte(XMLHeader + templateDocpropsCore)
|
||||
file["xl/_rels/workbook.xml.rels"] = []byte(XMLHeader + templateWorkbookRels)
|
||||
file["xl/theme/theme1.xml"] = []byte(XMLHeader + templateTheme)
|
||||
file["xl/worksheets/sheet1.xml"] = []byte(XMLHeader + templateSheet)
|
||||
file["xl/styles.xml"] = []byte(XMLHeader + templateStyles)
|
||||
file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook)
|
||||
file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes)
|
||||
f := &File{
|
||||
sheetMap: make(map[string]string),
|
||||
Sheet: make(map[string]*xlsxWorksheet),
|
||||
SheetCount: 1,
|
||||
XLSX: file,
|
||||
}
|
||||
f.ContentTypes = f.contentTypesReader()
|
||||
f.Styles = f.stylesReader()
|
||||
f.WorkBook = f.workbookReader()
|
||||
f.WorkBookRels = f.workbookRelsReader()
|
||||
f.Sheet["xl/worksheets/sheet1.xml"] = f.workSheetReader("Sheet1")
|
||||
f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml"
|
||||
return f
|
||||
}
|
||||
|
||||
// Save provides function to override the xlsx file with origin path.
|
||||
func (f *File) Save() error {
|
||||
if f.Path == "" {
|
||||
return fmt.Errorf("No path defined for file, consider File.WriteTo or File.Write")
|
||||
}
|
||||
return f.SaveAs(f.Path)
|
||||
}
|
||||
|
||||
// SaveAs provides function to create or update to an xlsx file at the provided
|
||||
// path.
|
||||
func (f *File) SaveAs(name string) error {
|
||||
file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return f.Write(file)
|
||||
}
|
||||
|
||||
// Write provides function to write to an io.Writer.
|
||||
func (f *File) Write(w io.Writer) error {
|
||||
buf := new(bytes.Buffer)
|
||||
zw := zip.NewWriter(buf)
|
||||
f.contentTypesWriter()
|
||||
f.workbookWriter()
|
||||
f.workbookRelsWriter()
|
||||
f.worksheetWriter()
|
||||
f.styleSheetWriter()
|
||||
for path, content := range f.XLSX {
|
||||
fi, err := zw.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fi.Write([]byte(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := zw.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.WriteTo(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// ReadZipReader can be used to read an XLSX in memory without touching the
|
||||
// filesystem.
|
||||
func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
|
||||
fileList := make(map[string][]byte)
|
||||
worksheets := 0
|
||||
for _, v := range r.File {
|
||||
fileList[v.Name] = readFile(v)
|
||||
if len(v.Name) > 18 {
|
||||
if v.Name[0:19] == "xl/worksheets/sheet" {
|
||||
worksheets++
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileList, worksheets, nil
|
||||
}
|
||||
|
||||
// readXML provides function to read XML content as string.
|
||||
func (f *File) readXML(name string) []byte {
|
||||
if content, ok := f.XLSX[name]; ok {
|
||||
return content
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// saveFileList provides function to update given file content in file list of
|
||||
// XLSX.
|
||||
func (f *File) saveFileList(name string, content []byte) {
|
||||
newContent := make([]byte, 0, len(XMLHeader)+len(content))
|
||||
newContent = append(newContent, []byte(XMLHeader)...)
|
||||
newContent = append(newContent, content...)
|
||||
f.XLSX[name] = newContent
|
||||
}
|
||||
|
||||
// Read file content as string in a archive file.
|
||||
func readFile(file *zip.File) []byte {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
buff := bytes.NewBuffer(nil)
|
||||
io.Copy(buff, rc)
|
||||
rc.Close()
|
||||
return buff.Bytes()
|
||||
}
|
||||
|
||||
// ToAlphaString provides function to convert integer to Excel sheet column
|
||||
// title. For example convert 36 to column title AK:
|
||||
//
|
||||
// excelize.ToAlphaString(36)
|
||||
//
|
||||
func ToAlphaString(value int) string {
|
||||
if value < 0 {
|
||||
return ""
|
||||
}
|
||||
var ans string
|
||||
i := value + 1
|
||||
for i > 0 {
|
||||
ans = string((i-1)%26+65) + ans
|
||||
i = (i - 1) / 26
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// TitleToNumber provides function to convert Excel sheet column title to int
|
||||
// (this function doesn't do value check currently). For example convert AK
|
||||
// and ak to column title 36:
|
||||
//
|
||||
// excelize.TitleToNumber("AK")
|
||||
// excelize.TitleToNumber("ak")
|
||||
//
|
||||
func TitleToNumber(s string) int {
|
||||
weight := 0.0
|
||||
sum := 0
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
ch := int(s[i])
|
||||
if int(s[i]) >= int('a') && int(s[i]) <= int('z') {
|
||||
ch = int(s[i]) - 32
|
||||
}
|
||||
sum = sum + (ch-int('A')+1)*int(math.Pow(26, weight))
|
||||
weight++
|
||||
}
|
||||
return sum - 1
|
||||
}
|
||||
|
||||
// letterOnlyMapF is used in conjunction with strings.Map to return only the
|
||||
// characters A-Z and a-z in a string.
|
||||
func letterOnlyMapF(rune rune) rune {
|
||||
switch {
|
||||
case 'A' <= rune && rune <= 'Z':
|
||||
return rune
|
||||
case 'a' <= rune && rune <= 'z':
|
||||
return rune - 32
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// intOnlyMapF is used in conjunction with strings.Map to return only the
|
||||
// numeric portions of a string.
|
||||
func intOnlyMapF(rune rune) rune {
|
||||
if rune >= 48 && rune < 58 {
|
||||
return rune
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// deepCopy provides method to creates a deep copy of whatever is passed to it
|
||||
// and returns the copy in an interface. The returned value will need to be
|
||||
// asserted to the correct type.
|
||||
func deepCopy(dst, src interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
|
||||
}
|
||||
|
||||
// boolPtr returns a pointer to a bool with the given value.
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
// defaultTrue returns true if b is nil, or the pointed value.
|
||||
func defaultTrue(b *bool) bool {
|
||||
if b == nil {
|
||||
return true
|
||||
}
|
||||
return *b
|
||||
}
|
||||
|
||||
// axisLowerOrEqualThan returns true if axis1 <= axis2
|
||||
// axis1/axis2 can be either a column or a row axis, e.g. "A", "AAE", "42", "1", etc.
|
||||
//
|
||||
// For instance, the following comparisons are all true:
|
||||
//
|
||||
// "A" <= "B"
|
||||
// "A" <= "AA"
|
||||
// "B" <= "AA"
|
||||
// "BC" <= "ABCD" (in a XLSX sheet, the BC col comes before the ABCD col)
|
||||
// "1" <= "2"
|
||||
// "2" <= "11" (in a XLSX sheet, the row 2 comes before the row 11)
|
||||
// and so on
|
||||
func axisLowerOrEqualThan(axis1, axis2 string) bool {
|
||||
if len(axis1) < len(axis2) {
|
||||
return true
|
||||
} else if len(axis1) > len(axis2) {
|
||||
return false
|
||||
} else {
|
||||
return axis1 <= axis2
|
||||
}
|
||||
}
|
||||
|
||||
// getCellColRow returns the two parts of a cell identifier (its col and row) as strings
|
||||
//
|
||||
// For instance:
|
||||
//
|
||||
// "C220" => "C", "220"
|
||||
// "aaef42" => "aaef", "42"
|
||||
// "" => "", ""
|
||||
func getCellColRow(cell string) (col, row string) {
|
||||
for index, rune := range cell {
|
||||
if unicode.IsDigit(rune) {
|
||||
return cell[:index], cell[index:]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return cell, ""
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
+478
@@ -0,0 +1,478 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseFormatPictureSet provides function to parse the format settings of the
|
||||
// picture with default value.
|
||||
func parseFormatPictureSet(formatSet string) *formatPicture {
|
||||
format := formatPicture{
|
||||
FPrintsWithSheet: true,
|
||||
FLocksWithSheet: false,
|
||||
NoChangeAspect: false,
|
||||
OffsetX: 0,
|
||||
OffsetY: 0,
|
||||
XScale: 1.0,
|
||||
YScale: 1.0,
|
||||
}
|
||||
json.Unmarshal([]byte(formatSet), &format)
|
||||
return &format
|
||||
}
|
||||
|
||||
// AddPicture provides the method to add picture in a sheet by given picture
|
||||
// format set (such as offset, scale, aspect ratio setting and print settings)
|
||||
// and file path. For example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "fmt"
|
||||
// _ "image/gif"
|
||||
// _ "image/jpeg"
|
||||
// _ "image/png"
|
||||
//
|
||||
// "github.com/360EntSecGroup-Skylar/excelize"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// xlsx := excelize.NewFile()
|
||||
// // Insert a picture.
|
||||
// err := xlsx.AddPicture("Sheet1", "A2", "./image1.jpg", "")
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// // Insert a picture scaling in the cell with location hyperlink.
|
||||
// err = xlsx.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// // Insert a picture offset in the cell with external hyperlink, printing and positioning support.
|
||||
// err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// err = xlsx.SaveAs("./Book1.xlsx")
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// LinkType defines two types of hyperlink "External" for web site or
|
||||
// "Location" for moving to one of cell in this workbook. When the
|
||||
// "hyperlink_type" is "Location", coordinates need to start with "#".
|
||||
//
|
||||
// Positioning defines two types of the position of a picture in an Excel
|
||||
// spreadsheet, "oneCell" (Move but don't size with cells) or "absolute"
|
||||
// (Don't move or size with cells). If you don't set this parameter, default
|
||||
// positioning is move and size with cells.
|
||||
func (f *File) AddPicture(sheet, cell, picture, format string) error {
|
||||
var err error
|
||||
var drawingHyperlinkRID int
|
||||
var hyperlinkType string
|
||||
// Check picture exists first.
|
||||
if _, err = os.Stat(picture); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
ext, ok := supportImageTypes[path.Ext(picture)]
|
||||
if !ok {
|
||||
return errors.New("Unsupported image extension")
|
||||
}
|
||||
readFile, _ := os.Open(picture)
|
||||
image, _, err := image.DecodeConfig(readFile)
|
||||
_, file := filepath.Split(picture)
|
||||
formatSet := parseFormatPictureSet(format)
|
||||
// Read sheet data.
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
// Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
|
||||
drawingID := f.countDrawings() + 1
|
||||
pictureID := f.countMedia() + 1
|
||||
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
|
||||
drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML)
|
||||
drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, hyperlinkType)
|
||||
// Add picture with hyperlink.
|
||||
if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" {
|
||||
if formatSet.HyperlinkType == "External" {
|
||||
hyperlinkType = formatSet.HyperlinkType
|
||||
}
|
||||
drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType)
|
||||
}
|
||||
f.addDrawingPicture(sheet, drawingXML, cell, file, image.Width, image.Height, drawingRID, drawingHyperlinkRID, formatSet)
|
||||
f.addMedia(picture, ext)
|
||||
f.addContentTypePart(drawingID, "drawings")
|
||||
return err
|
||||
}
|
||||
|
||||
// addSheetRelationships provides function to add
|
||||
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name, relationship
|
||||
// type and target.
|
||||
func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int {
|
||||
name, ok := f.sheetMap[trimSheetName(sheet)]
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
var sheetRels xlsxWorkbookRels
|
||||
var rID = 1
|
||||
var ID bytes.Buffer
|
||||
ID.WriteString("rId")
|
||||
ID.WriteString(strconv.Itoa(rID))
|
||||
_, ok = f.XLSX[rels]
|
||||
if ok {
|
||||
ID.Reset()
|
||||
xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels)
|
||||
rID = len(sheetRels.Relationships) + 1
|
||||
ID.WriteString("rId")
|
||||
ID.WriteString(strconv.Itoa(rID))
|
||||
}
|
||||
sheetRels.Relationships = append(sheetRels.Relationships, xlsxWorkbookRelation{
|
||||
ID: ID.String(),
|
||||
Type: relType,
|
||||
Target: target,
|
||||
TargetMode: targetMode,
|
||||
})
|
||||
output, _ := xml.Marshal(sheetRels)
|
||||
f.saveFileList(rels, output)
|
||||
return rID
|
||||
}
|
||||
|
||||
// deleteSheetRelationships provides function to delete relationships in
|
||||
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and relationship
|
||||
// index.
|
||||
func (f *File) deleteSheetRelationships(sheet, rID string) {
|
||||
name, ok := f.sheetMap[trimSheetName(sheet)]
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
var sheetRels xlsxWorkbookRels
|
||||
xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels)
|
||||
for k, v := range sheetRels.Relationships {
|
||||
if v.ID == rID {
|
||||
sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
|
||||
}
|
||||
}
|
||||
output, _ := xml.Marshal(sheetRels)
|
||||
f.saveFileList(rels, output)
|
||||
}
|
||||
|
||||
// addSheetLegacyDrawing provides function to add legacy drawing element to
|
||||
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
||||
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
xlsx.LegacyDrawing = &xlsxLegacyDrawing{
|
||||
RID: "rId" + strconv.Itoa(rID),
|
||||
}
|
||||
}
|
||||
|
||||
// addSheetDrawing provides function to add drawing element to
|
||||
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
||||
func (f *File) addSheetDrawing(sheet string, rID int) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
xlsx.Drawing = &xlsxDrawing{
|
||||
RID: "rId" + strconv.Itoa(rID),
|
||||
}
|
||||
}
|
||||
|
||||
// addSheetPicture provides function to add picture element to
|
||||
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
||||
func (f *File) addSheetPicture(sheet string, rID int) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
xlsx.Picture = &xlsxPicture{
|
||||
RID: "rId" + strconv.Itoa(rID),
|
||||
}
|
||||
}
|
||||
|
||||
// countDrawings provides function to get drawing files count storage in the
|
||||
// folder xl/drawings.
|
||||
func (f *File) countDrawings() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/drawings/drawing") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// addDrawingPicture provides function to add picture by given sheet,
|
||||
// drawingXML, cell, file name, width, height relationship index and format
|
||||
// sets.
|
||||
func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) {
|
||||
cell = strings.ToUpper(cell)
|
||||
fromCol := string(strings.Map(letterOnlyMapF, cell))
|
||||
fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
|
||||
row := fromRow - 1
|
||||
col := TitleToNumber(fromCol)
|
||||
width = int(float64(width) * formatSet.XScale)
|
||||
height = int(float64(height) * formatSet.YScale)
|
||||
colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height)
|
||||
content := xlsxWsDr{}
|
||||
content.A = NameSpaceDrawingML
|
||||
content.Xdr = NameSpaceDrawingMLSpreadSheet
|
||||
cNvPrID := f.drawingParser(drawingXML, &content)
|
||||
twoCellAnchor := xdrCellAnchor{}
|
||||
twoCellAnchor.EditAs = formatSet.Positioning
|
||||
from := xlsxFrom{}
|
||||
from.Col = colStart
|
||||
from.ColOff = formatSet.OffsetX * EMU
|
||||
from.Row = rowStart
|
||||
from.RowOff = formatSet.OffsetY * EMU
|
||||
to := xlsxTo{}
|
||||
to.Col = colEnd
|
||||
to.ColOff = x2 * EMU
|
||||
to.Row = rowEnd
|
||||
to.RowOff = y2 * EMU
|
||||
twoCellAnchor.From = &from
|
||||
twoCellAnchor.To = &to
|
||||
pic := xlsxPic{}
|
||||
pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect
|
||||
pic.NvPicPr.CNvPr.ID = f.countCharts() + f.countMedia() + 1
|
||||
pic.NvPicPr.CNvPr.Descr = file
|
||||
pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID)
|
||||
if hyperlinkRID != 0 {
|
||||
pic.NvPicPr.CNvPr.HlinkClick = &xlsxHlinkClick{
|
||||
R: SourceRelationship,
|
||||
RID: "rId" + strconv.Itoa(hyperlinkRID),
|
||||
}
|
||||
}
|
||||
pic.BlipFill.Blip.R = SourceRelationship
|
||||
pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID)
|
||||
pic.SpPr.PrstGeom.Prst = "rect"
|
||||
|
||||
twoCellAnchor.Pic = &pic
|
||||
twoCellAnchor.ClientData = &xdrClientData{
|
||||
FLocksWithSheet: formatSet.FLocksWithSheet,
|
||||
FPrintsWithSheet: formatSet.FPrintsWithSheet,
|
||||
}
|
||||
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
|
||||
output, _ := xml.Marshal(content)
|
||||
f.saveFileList(drawingXML, output)
|
||||
}
|
||||
|
||||
// addDrawingRelationships provides function to add image part relationships in
|
||||
// the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index,
|
||||
// relationship type and target.
|
||||
func (f *File) addDrawingRelationships(index int, relType, target, targetMode string) int {
|
||||
var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels"
|
||||
var drawingRels xlsxWorkbookRels
|
||||
var rID = 1
|
||||
var ID bytes.Buffer
|
||||
ID.WriteString("rId")
|
||||
ID.WriteString(strconv.Itoa(rID))
|
||||
_, ok := f.XLSX[rels]
|
||||
if ok {
|
||||
ID.Reset()
|
||||
xml.Unmarshal([]byte(f.readXML(rels)), &drawingRels)
|
||||
rID = len(drawingRels.Relationships) + 1
|
||||
ID.WriteString("rId")
|
||||
ID.WriteString(strconv.Itoa(rID))
|
||||
}
|
||||
drawingRels.Relationships = append(drawingRels.Relationships, xlsxWorkbookRelation{
|
||||
ID: ID.String(),
|
||||
Type: relType,
|
||||
Target: target,
|
||||
TargetMode: targetMode,
|
||||
})
|
||||
output, _ := xml.Marshal(drawingRels)
|
||||
f.saveFileList(rels, output)
|
||||
return rID
|
||||
}
|
||||
|
||||
// countMedia provides function to get media files count storage in the folder
|
||||
// xl/media/image.
|
||||
func (f *File) countMedia() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/media/image") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// addMedia provides function to add picture into folder xl/media/image by given
|
||||
// file name and extension name.
|
||||
func (f *File) addMedia(file, ext string) {
|
||||
count := f.countMedia()
|
||||
dat, _ := ioutil.ReadFile(file)
|
||||
media := "xl/media/image" + strconv.Itoa(count+1) + ext
|
||||
f.XLSX[media] = dat
|
||||
}
|
||||
|
||||
// setContentTypePartImageExtensions provides function to set the content type
|
||||
// for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartImageExtensions() {
|
||||
var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false}
|
||||
content := f.contentTypesReader()
|
||||
for _, v := range content.Defaults {
|
||||
_, ok := imageTypes[v.Extension]
|
||||
if ok {
|
||||
imageTypes[v.Extension] = true
|
||||
}
|
||||
}
|
||||
for k, v := range imageTypes {
|
||||
if !v {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: k,
|
||||
ContentType: "image/" + k,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setContentTypePartVMLExtensions provides function to set the content type
|
||||
// for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartVMLExtensions() {
|
||||
vml := false
|
||||
content := f.contentTypesReader()
|
||||
for _, v := range content.Defaults {
|
||||
if v.Extension == "vml" {
|
||||
vml = true
|
||||
}
|
||||
}
|
||||
if !vml {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: "vml",
|
||||
ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// addContentTypePart provides function to add content type part relationships
|
||||
// in the file [Content_Types].xml by given index.
|
||||
func (f *File) addContentTypePart(index int, contentType string) {
|
||||
setContentType := map[string]func(){
|
||||
"comments": f.setContentTypePartVMLExtensions,
|
||||
"drawings": f.setContentTypePartImageExtensions,
|
||||
}
|
||||
partNames := map[string]string{
|
||||
"chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
|
||||
"comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
|
||||
"drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
|
||||
"table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
|
||||
}
|
||||
contentTypes := map[string]string{
|
||||
"chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
|
||||
"comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml",
|
||||
"drawings": "application/vnd.openxmlformats-officedocument.drawing+xml",
|
||||
"table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml",
|
||||
}
|
||||
s, ok := setContentType[contentType]
|
||||
if ok {
|
||||
s()
|
||||
}
|
||||
content := f.contentTypesReader()
|
||||
for _, v := range content.Overrides {
|
||||
if v.PartName == partNames[contentType] {
|
||||
return
|
||||
}
|
||||
}
|
||||
content.Overrides = append(content.Overrides, xlsxOverride{
|
||||
PartName: partNames[contentType],
|
||||
ContentType: contentTypes[contentType],
|
||||
})
|
||||
}
|
||||
|
||||
// getSheetRelationshipsTargetByID provides function to get Target attribute
|
||||
// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
||||
// relationship index.
|
||||
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
|
||||
name, ok := f.sheetMap[trimSheetName(sheet)]
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
var sheetRels xlsxWorkbookRels
|
||||
xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels)
|
||||
for _, v := range sheetRels.Relationships {
|
||||
if v.ID == rID {
|
||||
return v.Target
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPicture provides function to get picture base name and raw content embed
|
||||
// in XLSX by given worksheet and cell name. This function returns the file name
|
||||
// in XLSX and file contents as []byte data types. For example:
|
||||
//
|
||||
// xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
// }
|
||||
// file, raw := xlsx.GetPicture("Sheet1", "A2")
|
||||
// if file == "" {
|
||||
// return
|
||||
// }
|
||||
// err := ioutil.WriteFile(file, raw, 0644)
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
//
|
||||
func (f *File) GetPicture(sheet, cell string) (string, []byte) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
if xlsx.Drawing == nil {
|
||||
return "", []byte{}
|
||||
}
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
|
||||
drawingXML := strings.Replace(target, "..", "xl", -1)
|
||||
|
||||
_, ok := f.XLSX[drawingXML]
|
||||
if !ok {
|
||||
return "", []byte{}
|
||||
}
|
||||
decodeWsDr := decodeWsDr{}
|
||||
xml.Unmarshal([]byte(f.readXML(drawingXML)), &decodeWsDr)
|
||||
|
||||
cell = strings.ToUpper(cell)
|
||||
fromCol := string(strings.Map(letterOnlyMapF, cell))
|
||||
fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
|
||||
row := fromRow - 1
|
||||
col := TitleToNumber(fromCol)
|
||||
|
||||
drawingRelationships := strings.Replace(strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1)
|
||||
|
||||
for _, anchor := range decodeWsDr.TwoCellAnchor {
|
||||
decodeTwoCellAnchor := decodeTwoCellAnchor{}
|
||||
xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+anchor.Content+"</decodeTwoCellAnchor>"), &decodeTwoCellAnchor)
|
||||
if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil {
|
||||
if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row {
|
||||
xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed)
|
||||
_, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)]
|
||||
if ok {
|
||||
return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", []byte{}
|
||||
}
|
||||
|
||||
// getDrawingRelationships provides function to get drawing relationships from
|
||||
// xl/drawings/_rels/drawing%s.xml.rels by given file name and relationship ID.
|
||||
func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation {
|
||||
_, ok := f.XLSX[rels]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var drawingRels xlsxWorkbookRels
|
||||
xml.Unmarshal([]byte(f.readXML(rels)), &drawingRels)
|
||||
for _, v := range drawingRels.Relationships {
|
||||
if v.ID == rID {
|
||||
return &v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+461
@@ -0,0 +1,461 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetRows return all the rows in a sheet by given worksheet name (case
|
||||
// sensitive). For example:
|
||||
//
|
||||
// for _, row := range xlsx.GetRows("Sheet1") {
|
||||
// for _, colCell := range row {
|
||||
// fmt.Print(colCell, "\t")
|
||||
// }
|
||||
// fmt.Println()
|
||||
// }
|
||||
//
|
||||
func (f *File) GetRows(sheet string) [][]string {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
rows := [][]string{}
|
||||
name, ok := f.sheetMap[trimSheetName(sheet)]
|
||||
if !ok {
|
||||
return rows
|
||||
}
|
||||
if xlsx != nil {
|
||||
output, _ := xml.Marshal(f.Sheet[name])
|
||||
f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output))
|
||||
}
|
||||
decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name)))
|
||||
d := f.sharedStringsReader()
|
||||
var inElement string
|
||||
var r xlsxRow
|
||||
var row []string
|
||||
tr, tc := f.getTotalRowsCols(name)
|
||||
for i := 0; i < tr; i++ {
|
||||
row = []string{}
|
||||
for j := 0; j <= tc; j++ {
|
||||
row = append(row, "")
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
decoder = xml.NewDecoder(bytes.NewReader(f.readXML(name)))
|
||||
for {
|
||||
token, _ := decoder.Token()
|
||||
if token == nil {
|
||||
break
|
||||
}
|
||||
switch startElement := token.(type) {
|
||||
case xml.StartElement:
|
||||
inElement = startElement.Name.Local
|
||||
if inElement == "row" {
|
||||
r = xlsxRow{}
|
||||
decoder.DecodeElement(&r, &startElement)
|
||||
cr := r.R - 1
|
||||
for _, colCell := range r.C {
|
||||
c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R))
|
||||
val, _ := colCell.getValueFrom(f, d)
|
||||
rows[cr][c] = val
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// Rows defines an iterator to a sheet
|
||||
type Rows struct {
|
||||
decoder *xml.Decoder
|
||||
token xml.Token
|
||||
err error
|
||||
f *File
|
||||
}
|
||||
|
||||
// Next will return true if find the next row element.
|
||||
func (rows *Rows) Next() bool {
|
||||
for {
|
||||
rows.token, rows.err = rows.decoder.Token()
|
||||
if rows.err == io.EOF {
|
||||
rows.err = nil
|
||||
}
|
||||
if rows.token == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch startElement := rows.token.(type) {
|
||||
case xml.StartElement:
|
||||
inElement := startElement.Name.Local
|
||||
if inElement == "row" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error will return the error when the find next row element
|
||||
func (rows *Rows) Error() error {
|
||||
return rows.err
|
||||
}
|
||||
|
||||
// Columns return the current row's column values
|
||||
func (rows *Rows) Columns() []string {
|
||||
if rows.token == nil {
|
||||
return []string{}
|
||||
}
|
||||
startElement := rows.token.(xml.StartElement)
|
||||
r := xlsxRow{}
|
||||
rows.decoder.DecodeElement(&r, &startElement)
|
||||
d := rows.f.sharedStringsReader()
|
||||
row := make([]string, len(r.C), len(r.C))
|
||||
for _, colCell := range r.C {
|
||||
c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R))
|
||||
val, _ := colCell.getValueFrom(rows.f, d)
|
||||
row[c] = val
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
// ErrSheetNotExist defines an error of sheet is not exist
|
||||
type ErrSheetNotExist struct {
|
||||
SheetName string
|
||||
}
|
||||
|
||||
func (err ErrSheetNotExist) Error() string {
|
||||
return fmt.Sprintf("Sheet %s is not exist", string(err.SheetName))
|
||||
}
|
||||
|
||||
// Rows return a rows iterator. For example:
|
||||
//
|
||||
// rows, err := xlsx.GetRows("Sheet1")
|
||||
// for rows.Next() {
|
||||
// for _, colCell := range rows.Columns() {
|
||||
// fmt.Print(colCell, "\t")
|
||||
// }
|
||||
// fmt.Println()
|
||||
// }
|
||||
//
|
||||
func (f *File) Rows(sheet string) (*Rows, error) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
name, ok := f.sheetMap[trimSheetName(sheet)]
|
||||
if !ok {
|
||||
return nil, ErrSheetNotExist{sheet}
|
||||
}
|
||||
if xlsx != nil {
|
||||
output, _ := xml.Marshal(f.Sheet[name])
|
||||
f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output))
|
||||
}
|
||||
return &Rows{
|
||||
f: f,
|
||||
decoder: xml.NewDecoder(bytes.NewReader(f.readXML(name))),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getTotalRowsCols provides a function to get total columns and rows in a
|
||||
// worksheet.
|
||||
func (f *File) getTotalRowsCols(name string) (int, int) {
|
||||
decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name)))
|
||||
var inElement string
|
||||
var r xlsxRow
|
||||
var tr, tc int
|
||||
for {
|
||||
token, _ := decoder.Token()
|
||||
if token == nil {
|
||||
break
|
||||
}
|
||||
switch startElement := token.(type) {
|
||||
case xml.StartElement:
|
||||
inElement = startElement.Name.Local
|
||||
if inElement == "row" {
|
||||
r = xlsxRow{}
|
||||
decoder.DecodeElement(&r, &startElement)
|
||||
tr = r.R
|
||||
for _, colCell := range r.C {
|
||||
col := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R))
|
||||
if col > tc {
|
||||
tc = col
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
return tr, tc
|
||||
}
|
||||
|
||||
// SetRowHeight provides a function to set the height of a single row. For
|
||||
// example, set the height of the first row in Sheet1:
|
||||
//
|
||||
// xlsx.SetRowHeight("Sheet1", 1, 50)
|
||||
//
|
||||
func (f *File) SetRowHeight(sheet string, row int, height float64) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
cells := 0
|
||||
rowIdx := row - 1
|
||||
completeRow(xlsx, row, cells)
|
||||
xlsx.SheetData.Row[rowIdx].Ht = height
|
||||
xlsx.SheetData.Row[rowIdx].CustomHeight = true
|
||||
}
|
||||
|
||||
// getRowHeight provides function to get row height in pixels by given sheet
|
||||
// name and row index.
|
||||
func (f *File) getRowHeight(sheet string, row int) int {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
for _, v := range xlsx.SheetData.Row {
|
||||
if v.R == row+1 && v.Ht != 0 {
|
||||
return int(convertRowHeightToPixels(v.Ht))
|
||||
}
|
||||
}
|
||||
// Optimisation for when the row heights haven't changed.
|
||||
return int(defaultRowHeightPixels)
|
||||
}
|
||||
|
||||
// GetRowHeight provides function to get row height by given worksheet name
|
||||
// and row index. For example, get the height of the first row in Sheet1:
|
||||
//
|
||||
// xlsx.GetRowHeight("Sheet1", 1)
|
||||
//
|
||||
func (f *File) GetRowHeight(sheet string, row int) float64 {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
for _, v := range xlsx.SheetData.Row {
|
||||
if v.R == row && v.Ht != 0 {
|
||||
return v.Ht
|
||||
}
|
||||
}
|
||||
// Optimisation for when the row heights haven't changed.
|
||||
return defaultRowHeightPixels
|
||||
}
|
||||
|
||||
// sharedStringsReader provides function to get the pointer to the structure
|
||||
// after deserialization of xl/sharedStrings.xml.
|
||||
func (f *File) sharedStringsReader() *xlsxSST {
|
||||
if f.SharedStrings == nil {
|
||||
var sharedStrings xlsxSST
|
||||
ss := f.readXML("xl/sharedStrings.xml")
|
||||
if len(ss) == 0 {
|
||||
ss = f.readXML("xl/SharedStrings.xml")
|
||||
}
|
||||
xml.Unmarshal([]byte(ss), &sharedStrings)
|
||||
f.SharedStrings = &sharedStrings
|
||||
}
|
||||
return f.SharedStrings
|
||||
}
|
||||
|
||||
// getValueFrom return a value from a column/row cell, this function is inteded
|
||||
// to be used with for range on rows an argument with the xlsx opened file.
|
||||
func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) {
|
||||
switch xlsx.T {
|
||||
case "s":
|
||||
xlsxSI := 0
|
||||
xlsxSI, _ = strconv.Atoi(xlsx.V)
|
||||
if len(d.SI[xlsxSI].R) > 0 {
|
||||
value := ""
|
||||
for _, v := range d.SI[xlsxSI].R {
|
||||
value += v.T
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
return f.formattedValue(xlsx.S, d.SI[xlsxSI].T), nil
|
||||
case "str":
|
||||
return f.formattedValue(xlsx.S, xlsx.V), nil
|
||||
case "inlineStr":
|
||||
return f.formattedValue(xlsx.S, xlsx.IS.T), nil
|
||||
default:
|
||||
return f.formattedValue(xlsx.S, xlsx.V), nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetRowVisible provides a function to set visible of a single row by given
|
||||
// worksheet name and row index. For example, hide row 2 in Sheet1:
|
||||
//
|
||||
// xlsx.SetRowVisible("Sheet1", 2, false)
|
||||
//
|
||||
func (f *File) SetRowVisible(sheet string, rowIndex int, visible bool) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
rows := rowIndex + 1
|
||||
cells := 0
|
||||
completeRow(xlsx, rows, cells)
|
||||
if visible {
|
||||
xlsx.SheetData.Row[rowIndex].Hidden = false
|
||||
return
|
||||
}
|
||||
xlsx.SheetData.Row[rowIndex].Hidden = true
|
||||
}
|
||||
|
||||
// GetRowVisible provides a function to get visible of a single row by given
|
||||
// worksheet name and row index. For example, get visible state of row 2 in
|
||||
// Sheet1:
|
||||
//
|
||||
// xlsx.GetRowVisible("Sheet1", 2)
|
||||
//
|
||||
func (f *File) GetRowVisible(sheet string, rowIndex int) bool {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
rows := rowIndex + 1
|
||||
cells := 0
|
||||
completeRow(xlsx, rows, cells)
|
||||
return !xlsx.SheetData.Row[rowIndex].Hidden
|
||||
}
|
||||
|
||||
// SetRowOutlineLevel provides a function to set outline level number of a
|
||||
// single row by given worksheet name and row index. For example, outline row
|
||||
// 2 in Sheet1 to level 1:
|
||||
//
|
||||
// xlsx.SetRowOutlineLevel("Sheet1", 2, 1)
|
||||
//
|
||||
func (f *File) SetRowOutlineLevel(sheet string, rowIndex int, level uint8) {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
rows := rowIndex + 1
|
||||
cells := 0
|
||||
completeRow(xlsx, rows, cells)
|
||||
xlsx.SheetData.Row[rowIndex].OutlineLevel = level
|
||||
}
|
||||
|
||||
// GetRowOutlineLevel provides a function to get outline level number of a single row by given
|
||||
// worksheet name and row index. For example, get outline number of row 2 in
|
||||
// Sheet1:
|
||||
//
|
||||
// xlsx.GetRowOutlineLevel("Sheet1", 2)
|
||||
//
|
||||
func (f *File) GetRowOutlineLevel(sheet string, rowIndex int) uint8 {
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
rows := rowIndex + 1
|
||||
cells := 0
|
||||
completeRow(xlsx, rows, cells)
|
||||
return xlsx.SheetData.Row[rowIndex].OutlineLevel
|
||||
}
|
||||
|
||||
// RemoveRow provides function to remove single row by given worksheet name and
|
||||
// row index. For example, remove row 3 in Sheet1:
|
||||
//
|
||||
// xlsx.RemoveRow("Sheet1", 2)
|
||||
//
|
||||
func (f *File) RemoveRow(sheet string, row int) {
|
||||
if row < 0 {
|
||||
return
|
||||
}
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
row++
|
||||
for i, r := range xlsx.SheetData.Row {
|
||||
if r.R == row {
|
||||
xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...)
|
||||
f.adjustHelper(sheet, -1, row, -1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InsertRow provides function to insert a new row before given row index. For
|
||||
// example, create a new row before row 3 in Sheet1:
|
||||
//
|
||||
// xlsx.InsertRow("Sheet1", 2)
|
||||
//
|
||||
func (f *File) InsertRow(sheet string, row int) {
|
||||
if row < 0 {
|
||||
return
|
||||
}
|
||||
row++
|
||||
f.adjustHelper(sheet, -1, row, 1)
|
||||
}
|
||||
|
||||
// checkRow provides function to check and fill each column element for all rows
|
||||
// and make that is continuous in a worksheet of XML. For example:
|
||||
//
|
||||
// <row r="15" spans="1:22" x14ac:dyDescent="0.2">
|
||||
// <c r="A15" s="2" />
|
||||
// <c r="B15" s="2" />
|
||||
// <c r="F15" s="1" />
|
||||
// <c r="G15" s="1" />
|
||||
// </row>
|
||||
//
|
||||
// in this case, we should to change it to
|
||||
//
|
||||
// <row r="15" spans="1:22" x14ac:dyDescent="0.2">
|
||||
// <c r="A15" s="2" />
|
||||
// <c r="B15" s="2" />
|
||||
// <c r="C15" s="2" />
|
||||
// <c r="D15" s="2" />
|
||||
// <c r="E15" s="2" />
|
||||
// <c r="F15" s="1" />
|
||||
// <c r="G15" s="1" />
|
||||
// </row>
|
||||
//
|
||||
// Noteice: this method could be very slow for large spreadsheets (more than
|
||||
// 3000 rows one sheet).
|
||||
func checkRow(xlsx *xlsxWorksheet) {
|
||||
buffer := bytes.Buffer{}
|
||||
for k := range xlsx.SheetData.Row {
|
||||
lenCol := len(xlsx.SheetData.Row[k].C)
|
||||
if lenCol > 0 {
|
||||
endR := string(strings.Map(letterOnlyMapF, xlsx.SheetData.Row[k].C[lenCol-1].R))
|
||||
endRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, xlsx.SheetData.Row[k].C[lenCol-1].R))
|
||||
endCol := TitleToNumber(endR) + 1
|
||||
if lenCol < endCol {
|
||||
oldRow := xlsx.SheetData.Row[k].C
|
||||
xlsx.SheetData.Row[k].C = xlsx.SheetData.Row[k].C[:0]
|
||||
tmp := []xlsxC{}
|
||||
for i := 0; i < endCol; i++ {
|
||||
buffer.WriteString(ToAlphaString(i))
|
||||
buffer.WriteString(strconv.Itoa(endRow))
|
||||
tmp = append(tmp, xlsxC{
|
||||
R: buffer.String(),
|
||||
})
|
||||
buffer.Reset()
|
||||
}
|
||||
xlsx.SheetData.Row[k].C = tmp
|
||||
for _, y := range oldRow {
|
||||
colAxis := TitleToNumber(string(strings.Map(letterOnlyMapF, y.R)))
|
||||
xlsx.SheetData.Row[k].C[colAxis] = y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// completeRow provides function to check and fill each column element for a
|
||||
// single row and make that is continuous in a worksheet of XML by given row
|
||||
// index and axis.
|
||||
func completeRow(xlsx *xlsxWorksheet, row, cell int) {
|
||||
currentRows := len(xlsx.SheetData.Row)
|
||||
if currentRows > 1 {
|
||||
lastRow := xlsx.SheetData.Row[currentRows-1].R
|
||||
if lastRow >= row {
|
||||
row = lastRow
|
||||
}
|
||||
}
|
||||
for i := currentRows; i < row; i++ {
|
||||
xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{
|
||||
R: i + 1,
|
||||
})
|
||||
}
|
||||
buffer := bytes.Buffer{}
|
||||
for ii := currentRows; ii < row; ii++ {
|
||||
start := len(xlsx.SheetData.Row[ii].C)
|
||||
if start == 0 {
|
||||
for iii := start; iii < cell; iii++ {
|
||||
buffer.WriteString(ToAlphaString(iii))
|
||||
buffer.WriteString(strconv.Itoa(ii + 1))
|
||||
xlsx.SheetData.Row[ii].C = append(xlsx.SheetData.Row[ii].C, xlsxC{
|
||||
R: buffer.String(),
|
||||
})
|
||||
buffer.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convertRowHeightToPixels provides function to convert the height of a cell
|
||||
// from user's units to pixels. If the height hasn't been set by the user we use
|
||||
// the default value. If the row is hidden it has a value of zero.
|
||||
func convertRowHeightToPixels(height float64) float64 {
|
||||
var pixels float64
|
||||
if height == 0 {
|
||||
return pixels
|
||||
}
|
||||
pixels = math.Ceil(4.0 / 3.0 * height)
|
||||
return pixels
|
||||
}
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseFormatShapeSet provides function to parse the format settings of the
|
||||
// shape with default value.
|
||||
func parseFormatShapeSet(formatSet string) *formatShape {
|
||||
format := formatShape{
|
||||
Width: 160,
|
||||
Height: 160,
|
||||
Format: formatPicture{
|
||||
FPrintsWithSheet: true,
|
||||
FLocksWithSheet: false,
|
||||
NoChangeAspect: false,
|
||||
OffsetX: 0,
|
||||
OffsetY: 0,
|
||||
XScale: 1.0,
|
||||
YScale: 1.0,
|
||||
},
|
||||
}
|
||||
json.Unmarshal([]byte(formatSet), &format)
|
||||
return &format
|
||||
}
|
||||
|
||||
// AddShape provides the method to add shape in a sheet by given worksheet
|
||||
// index, shape format set (such as offset, scale, aspect ratio setting and
|
||||
// print settings) and properties set. For example, add text box (rect shape) in
|
||||
// Sheet1:
|
||||
//
|
||||
// xlsx.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`)
|
||||
//
|
||||
// The following shows the type of shape supported by excelize:
|
||||
//
|
||||
// accentBorderCallout1 (Callout 1 with Border and Accent Shape)
|
||||
// accentBorderCallout2 (Callout 2 with Border and Accent Shape)
|
||||
// accentBorderCallout3 (Callout 3 with Border and Accent Shape)
|
||||
// accentCallout1 (Callout 1 Shape)
|
||||
// accentCallout2 (Callout 2 Shape)
|
||||
// accentCallout3 (Callout 3 Shape)
|
||||
// actionButtonBackPrevious (Back or Previous Button Shape)
|
||||
// actionButtonBeginning (Beginning Button Shape)
|
||||
// actionButtonBlank (Blank Button Shape)
|
||||
// actionButtonDocument (Document Button Shape)
|
||||
// actionButtonEnd (End Button Shape)
|
||||
// actionButtonForwardNext (Forward or Next Button Shape)
|
||||
// actionButtonHelp (Help Button Shape)
|
||||
// actionButtonHome (Home Button Shape)
|
||||
// actionButtonInformation (Information Button Shape)
|
||||
// actionButtonMovie (Movie Button Shape)
|
||||
// actionButtonReturn (Return Button Shape)
|
||||
// actionButtonSound (Sound Button Shape)
|
||||
// arc (Curved Arc Shape)
|
||||
// bentArrow (Bent Arrow Shape)
|
||||
// bentConnector2 (Bent Connector 2 Shape)
|
||||
// bentConnector3 (Bent Connector 3 Shape)
|
||||
// bentConnector4 (Bent Connector 4 Shape)
|
||||
// bentConnector5 (Bent Connector 5 Shape)
|
||||
// bentUpArrow (Bent Up Arrow Shape)
|
||||
// bevel (Bevel Shape)
|
||||
// blockArc (Block Arc Shape)
|
||||
// borderCallout1 (Callout 1 with Border Shape)
|
||||
// borderCallout2 (Callout 2 with Border Shape)
|
||||
// borderCallout3 (Callout 3 with Border Shape)
|
||||
// bracePair (Brace Pair Shape)
|
||||
// bracketPair (Bracket Pair Shape)
|
||||
// callout1 (Callout 1 Shape)
|
||||
// callout2 (Callout 2 Shape)
|
||||
// callout3 (Callout 3 Shape)
|
||||
// can (Can Shape)
|
||||
// chartPlus (Chart Plus Shape)
|
||||
// chartStar (Chart Star Shape)
|
||||
// chartX (Chart X Shape)
|
||||
// chevron (Chevron Shape)
|
||||
// chord (Chord Shape)
|
||||
// circularArrow (Circular Arrow Shape)
|
||||
// cloud (Cloud Shape)
|
||||
// cloudCallout (Callout Cloud Shape)
|
||||
// corner (Corner Shape)
|
||||
// cornerTabs (Corner Tabs Shape)
|
||||
// cube (Cube Shape)
|
||||
// curvedConnector2 (Curved Connector 2 Shape)
|
||||
// curvedConnector3 (Curved Connector 3 Shape)
|
||||
// curvedConnector4 (Curved Connector 4 Shape)
|
||||
// curvedConnector5 (Curved Connector 5 Shape)
|
||||
// curvedDownArrow (Curved Down Arrow Shape)
|
||||
// curvedLeftArrow (Curved Left Arrow Shape)
|
||||
// curvedRightArrow (Curved Right Arrow Shape)
|
||||
// curvedUpArrow (Curved Up Arrow Shape)
|
||||
// decagon (Decagon Shape)
|
||||
// diagStripe (Diagonal Stripe Shape)
|
||||
// diamond (Diamond Shape)
|
||||
// dodecagon (Dodecagon Shape)
|
||||
// donut (Donut Shape)
|
||||
// doubleWave (Double Wave Shape)
|
||||
// downArrow (Down Arrow Shape)
|
||||
// downArrowCallout (Callout Down Arrow Shape)
|
||||
// ellipse (Ellipse Shape)
|
||||
// ellipseRibbon (Ellipse Ribbon Shape)
|
||||
// ellipseRibbon2 (Ellipse Ribbon 2 Shape)
|
||||
// flowChartAlternateProcess (Alternate Process Flow Shape)
|
||||
// flowChartCollate (Collate Flow Shape)
|
||||
// flowChartConnector (Connector Flow Shape)
|
||||
// flowChartDecision (Decision Flow Shape)
|
||||
// flowChartDelay (Delay Flow Shape)
|
||||
// flowChartDisplay (Display Flow Shape)
|
||||
// flowChartDocument (Document Flow Shape)
|
||||
// flowChartExtract (Extract Flow Shape)
|
||||
// flowChartInputOutput (Input Output Flow Shape)
|
||||
// flowChartInternalStorage (Internal Storage Flow Shape)
|
||||
// flowChartMagneticDisk (Magnetic Disk Flow Shape)
|
||||
// flowChartMagneticDrum (Magnetic Drum Flow Shape)
|
||||
// flowChartMagneticTape (Magnetic Tape Flow Shape)
|
||||
// flowChartManualInput (Manual Input Flow Shape)
|
||||
// flowChartManualOperation (Manual Operation Flow Shape)
|
||||
// flowChartMerge (Merge Flow Shape)
|
||||
// flowChartMultidocument (Multi-Document Flow Shape)
|
||||
// flowChartOfflineStorage (Offline Storage Flow Shape)
|
||||
// flowChartOffpageConnector (Off-Page Connector Flow Shape)
|
||||
// flowChartOnlineStorage (Online Storage Flow Shape)
|
||||
// flowChartOr (Or Flow Shape)
|
||||
// flowChartPredefinedProcess (Predefined Process Flow Shape)
|
||||
// flowChartPreparation (Preparation Flow Shape)
|
||||
// flowChartProcess (Process Flow Shape)
|
||||
// flowChartPunchedCard (Punched Card Flow Shape)
|
||||
// flowChartPunchedTape (Punched Tape Flow Shape)
|
||||
// flowChartSort (Sort Flow Shape)
|
||||
// flowChartSummingJunction (Summing Junction Flow Shape)
|
||||
// flowChartTerminator (Terminator Flow Shape)
|
||||
// foldedCorner (Folded Corner Shape)
|
||||
// frame (Frame Shape)
|
||||
// funnel (Funnel Shape)
|
||||
// gear6 (Gear 6 Shape)
|
||||
// gear9 (Gear 9 Shape)
|
||||
// halfFrame (Half Frame Shape)
|
||||
// heart (Heart Shape)
|
||||
// heptagon (Heptagon Shape)
|
||||
// hexagon (Hexagon Shape)
|
||||
// homePlate (Home Plate Shape)
|
||||
// horizontalScroll (Horizontal Scroll Shape)
|
||||
// irregularSeal1 (Irregular Seal 1 Shape)
|
||||
// irregularSeal2 (Irregular Seal 2 Shape)
|
||||
// leftArrow (Left Arrow Shape)
|
||||
// leftArrowCallout (Callout Left Arrow Shape)
|
||||
// leftBrace (Left Brace Shape)
|
||||
// leftBracket (Left Bracket Shape)
|
||||
// leftCircularArrow (Left Circular Arrow Shape)
|
||||
// leftRightArrow (Left Right Arrow Shape)
|
||||
// leftRightArrowCallout (Callout Left Right Arrow Shape)
|
||||
// leftRightCircularArrow (Left Right Circular Arrow Shape)
|
||||
// leftRightRibbon (Left Right Ribbon Shape)
|
||||
// leftRightUpArrow (Left Right Up Arrow Shape)
|
||||
// leftUpArrow (Left Up Arrow Shape)
|
||||
// lightningBolt (Lightning Bolt Shape)
|
||||
// line (Line Shape)
|
||||
// lineInv (Line Inverse Shape)
|
||||
// mathDivide (Divide Math Shape)
|
||||
// mathEqual (Equal Math Shape)
|
||||
// mathMinus (Minus Math Shape)
|
||||
// mathMultiply (Multiply Math Shape)
|
||||
// mathNotEqual (Not Equal Math Shape)
|
||||
// mathPlus (Plus Math Shape)
|
||||
// moon (Moon Shape)
|
||||
// nonIsoscelesTrapezoid (Non-Isosceles Trapezoid Shape)
|
||||
// noSmoking (No Smoking Shape)
|
||||
// notchedRightArrow (Notched Right Arrow Shape)
|
||||
// octagon (Octagon Shape)
|
||||
// parallelogram (Parallelogram Shape)
|
||||
// pentagon (Pentagon Shape)
|
||||
// pie (Pie Shape)
|
||||
// pieWedge (Pie Wedge Shape)
|
||||
// plaque (Plaque Shape)
|
||||
// plaqueTabs (Plaque Tabs Shape)
|
||||
// plus (Plus Shape)
|
||||
// quadArrow (Quad-Arrow Shape)
|
||||
// quadArrowCallout (Callout Quad-Arrow Shape)
|
||||
// rect (Rectangle Shape)
|
||||
// ribbon (Ribbon Shape)
|
||||
// ribbon2 (Ribbon 2 Shape)
|
||||
// rightArrow (Right Arrow Shape)
|
||||
// rightArrowCallout (Callout Right Arrow Shape)
|
||||
// rightBrace (Right Brace Shape)
|
||||
// rightBracket (Right Bracket Shape)
|
||||
// round1Rect (One Round Corner Rectangle Shape)
|
||||
// round2DiagRect (Two Diagonal Round Corner Rectangle Shape)
|
||||
// round2SameRect (Two Same-side Round Corner Rectangle Shape)
|
||||
// roundRect (Round Corner Rectangle Shape)
|
||||
// rtTriangle (Right Triangle Shape)
|
||||
// smileyFace (Smiley Face Shape)
|
||||
// snip1Rect (One Snip Corner Rectangle Shape)
|
||||
// snip2DiagRect (Two Diagonal Snip Corner Rectangle Shape)
|
||||
// snip2SameRect (Two Same-side Snip Corner Rectangle Shape)
|
||||
// snipRoundRect (One Snip One Round Corner Rectangle Shape)
|
||||
// squareTabs (Square Tabs Shape)
|
||||
// star10 (Ten Pointed Star Shape)
|
||||
// star12 (Twelve Pointed Star Shape)
|
||||
// star16 (Sixteen Pointed Star Shape)
|
||||
// star24 (Twenty Four Pointed Star Shape)
|
||||
// star32 (Thirty Two Pointed Star Shape)
|
||||
// star4 (Four Pointed Star Shape)
|
||||
// star5 (Five Pointed Star Shape)
|
||||
// star6 (Six Pointed Star Shape)
|
||||
// star7 (Seven Pointed Star Shape)
|
||||
// star8 (Eight Pointed Star Shape)
|
||||
// straightConnector1 (Straight Connector 1 Shape)
|
||||
// stripedRightArrow (Striped Right Arrow Shape)
|
||||
// sun (Sun Shape)
|
||||
// swooshArrow (Swoosh Arrow Shape)
|
||||
// teardrop (Teardrop Shape)
|
||||
// trapezoid (Trapezoid Shape)
|
||||
// triangle (Triangle Shape)
|
||||
// upArrow (Up Arrow Shape)
|
||||
// upArrowCallout (Callout Up Arrow Shape)
|
||||
// upDownArrow (Up Down Arrow Shape)
|
||||
// upDownArrowCallout (Callout Up Down Arrow Shape)
|
||||
// uturnArrow (U-Turn Arrow Shape)
|
||||
// verticalScroll (Vertical Scroll Shape)
|
||||
// wave (Wave Shape)
|
||||
// wedgeEllipseCallout (Callout Wedge Ellipse Shape)
|
||||
// wedgeRectCallout (Callout Wedge Rectangle Shape)
|
||||
// wedgeRoundRectCallout (Callout Wedge Round Rectangle Shape)
|
||||
//
|
||||
// The following shows the type of text underline supported by excelize:
|
||||
//
|
||||
// none
|
||||
// words
|
||||
// sng
|
||||
// dbl
|
||||
// heavy
|
||||
// dotted
|
||||
// dottedHeavy
|
||||
// dash
|
||||
// dashHeavy
|
||||
// dashLong
|
||||
// dashLongHeavy
|
||||
// dotDash
|
||||
// dotDashHeavy
|
||||
// dotDotDash
|
||||
// dotDotDashHeavy
|
||||
// wavy
|
||||
// wavyHeavy
|
||||
// wavyDbl
|
||||
//
|
||||
func (f *File) AddShape(sheet, cell, format string) {
|
||||
formatSet := parseFormatShapeSet(format)
|
||||
// Read sheet data.
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
// Add first shape for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
|
||||
drawingID := f.countDrawings() + 1
|
||||
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
|
||||
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
|
||||
|
||||
if xlsx.Drawing != nil {
|
||||
// The worksheet already has a shape or chart relationships, use the relationships drawing ../drawings/drawing%d.xml.
|
||||
sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
|
||||
drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
|
||||
drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1)
|
||||
} else {
|
||||
// Add first shape for given sheet.
|
||||
rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
|
||||
f.addSheetDrawing(sheet, rID)
|
||||
}
|
||||
f.addDrawingShape(sheet, drawingXML, cell, formatSet)
|
||||
f.addContentTypePart(drawingID, "drawings")
|
||||
}
|
||||
|
||||
// addDrawingShape provides function to add preset geometry by given sheet,
|
||||
// drawingXMLand format sets.
|
||||
func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) {
|
||||
textUnderlineType := map[string]bool{"none": true, "words": true, "sng": true, "dbl": true, "heavy": true, "dotted": true, "dottedHeavy": true, "dash": true, "dashHeavy": true, "dashLong": true, "dashLongHeavy": true, "dotDash": true, "dotDashHeavy": true, "dotDotDash": true, "dotDotDashHeavy": true, "wavy": true, "wavyHeavy": true, "wavyDbl": true}
|
||||
cell = strings.ToUpper(cell)
|
||||
fromCol := string(strings.Map(letterOnlyMapF, cell))
|
||||
fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
|
||||
row := fromRow - 1
|
||||
col := TitleToNumber(fromCol)
|
||||
width := int(float64(formatSet.Width) * formatSet.Format.XScale)
|
||||
height := int(float64(formatSet.Height) * formatSet.Format.YScale)
|
||||
colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.Format.OffsetX, formatSet.Format.OffsetY, width, height)
|
||||
content := xlsxWsDr{}
|
||||
content.A = NameSpaceDrawingML
|
||||
content.Xdr = NameSpaceDrawingMLSpreadSheet
|
||||
cNvPrID := f.drawingParser(drawingXML, &content)
|
||||
twoCellAnchor := xdrCellAnchor{}
|
||||
twoCellAnchor.EditAs = formatSet.Format.Positioning
|
||||
from := xlsxFrom{}
|
||||
from.Col = colStart
|
||||
from.ColOff = formatSet.Format.OffsetX * EMU
|
||||
from.Row = rowStart
|
||||
from.RowOff = formatSet.Format.OffsetY * EMU
|
||||
to := xlsxTo{}
|
||||
to.Col = colEnd
|
||||
to.ColOff = x2 * EMU
|
||||
to.Row = rowEnd
|
||||
to.RowOff = y2 * EMU
|
||||
twoCellAnchor.From = &from
|
||||
twoCellAnchor.To = &to
|
||||
shape := xdrSp{
|
||||
NvSpPr: &xdrNvSpPr{
|
||||
CNvPr: &xlsxCNvPr{
|
||||
ID: cNvPrID,
|
||||
Name: "Shape " + strconv.Itoa(cNvPrID),
|
||||
},
|
||||
CNvSpPr: &xdrCNvSpPr{
|
||||
TxBox: true,
|
||||
},
|
||||
},
|
||||
SpPr: &xlsxSpPr{
|
||||
PrstGeom: xlsxPrstGeom{
|
||||
Prst: formatSet.Type,
|
||||
},
|
||||
},
|
||||
Style: &xdrStyle{
|
||||
LnRef: setShapeRef(formatSet.Color.Line, 2),
|
||||
FillRef: setShapeRef(formatSet.Color.Fill, 1),
|
||||
EffectRef: setShapeRef(formatSet.Color.Effect, 0),
|
||||
FontRef: &aFontRef{
|
||||
Idx: "minor",
|
||||
SchemeClr: &attrValString{
|
||||
Val: "tx1",
|
||||
},
|
||||
},
|
||||
},
|
||||
TxBody: &xdrTxBody{
|
||||
BodyPr: &aBodyPr{
|
||||
VertOverflow: "clip",
|
||||
HorzOverflow: "clip",
|
||||
Wrap: "none",
|
||||
RtlCol: false,
|
||||
Anchor: "t",
|
||||
},
|
||||
},
|
||||
}
|
||||
if len(formatSet.Paragraph) < 1 {
|
||||
formatSet.Paragraph = []formatShapeParagraph{
|
||||
{
|
||||
Font: formatFont{
|
||||
Bold: false,
|
||||
Italic: false,
|
||||
Underline: "none",
|
||||
Family: "Calibri",
|
||||
Size: 11,
|
||||
Color: "#000000",
|
||||
},
|
||||
Text: " ",
|
||||
},
|
||||
}
|
||||
}
|
||||
for _, p := range formatSet.Paragraph {
|
||||
u := p.Font.Underline
|
||||
_, ok := textUnderlineType[u]
|
||||
if !ok {
|
||||
u = "none"
|
||||
}
|
||||
text := p.Text
|
||||
if text == "" {
|
||||
text = " "
|
||||
}
|
||||
paragraph := &aP{
|
||||
R: &aR{
|
||||
RPr: aRPr{
|
||||
I: p.Font.Italic,
|
||||
B: p.Font.Bold,
|
||||
Lang: "en-US",
|
||||
AltLang: "en-US",
|
||||
U: u,
|
||||
Sz: p.Font.Size * 100,
|
||||
Latin: &aLatin{Typeface: p.Font.Family},
|
||||
SolidFill: &aSolidFill{
|
||||
SrgbClr: &attrValString{
|
||||
Val: strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1),
|
||||
},
|
||||
},
|
||||
},
|
||||
T: text,
|
||||
},
|
||||
EndParaRPr: &aEndParaRPr{
|
||||
Lang: "en-US",
|
||||
},
|
||||
}
|
||||
shape.TxBody.P = append(shape.TxBody.P, paragraph)
|
||||
}
|
||||
twoCellAnchor.Sp = &shape
|
||||
twoCellAnchor.ClientData = &xdrClientData{
|
||||
FLocksWithSheet: formatSet.Format.FLocksWithSheet,
|
||||
FPrintsWithSheet: formatSet.Format.FPrintsWithSheet,
|
||||
}
|
||||
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
|
||||
output, _ := xml.Marshal(content)
|
||||
f.saveFileList(drawingXML, output)
|
||||
}
|
||||
|
||||
// setShapeRef provides function to set color with hex model by given actual
|
||||
// color value.
|
||||
func setShapeRef(color string, i int) *aRef {
|
||||
if color == "" {
|
||||
return &aRef{
|
||||
Idx: 0,
|
||||
ScrgbClr: &aScrgbClr{
|
||||
R: 0,
|
||||
G: 0,
|
||||
B: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
return &aRef{
|
||||
Idx: i,
|
||||
SrgbClr: &attrValString{
|
||||
Val: strings.Replace(strings.ToUpper(color), "#", "", -1),
|
||||
},
|
||||
}
|
||||
}
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// NewSheet provides function to create a new sheet by given worksheet name,
|
||||
// when creating a new XLSX file, the default sheet will be create, when you
|
||||
// create a new file.
|
||||
func (f *File) NewSheet(name string) int {
|
||||
// Check if the worksheet already exists
|
||||
if f.GetSheetIndex(name) != 0 {
|
||||
return f.SheetCount
|
||||
}
|
||||
f.SheetCount++
|
||||
// Update docProps/app.xml
|
||||
f.setAppXML()
|
||||
// Update [Content_Types].xml
|
||||
f.setContentTypes(f.SheetCount)
|
||||
// Create new sheet /xl/worksheets/sheet%d.xml
|
||||
f.setSheet(f.SheetCount, name)
|
||||
// Update xl/_rels/workbook.xml.rels
|
||||
rID := f.addXlsxWorkbookRels(f.SheetCount)
|
||||
// Update xl/workbook.xml
|
||||
f.setWorkbook(name, rID)
|
||||
return f.SheetCount
|
||||
}
|
||||
|
||||
// contentTypesReader provides function to get the pointer to the
|
||||
// [Content_Types].xml structure after deserialization.
|
||||
func (f *File) contentTypesReader() *xlsxTypes {
|
||||
if f.ContentTypes == nil {
|
||||
var content xlsxTypes
|
||||
xml.Unmarshal([]byte(f.readXML("[Content_Types].xml")), &content)
|
||||
f.ContentTypes = &content
|
||||
}
|
||||
return f.ContentTypes
|
||||
}
|
||||
|
||||
// contentTypesWriter provides function to save [Content_Types].xml after
|
||||
// serialize structure.
|
||||
func (f *File) contentTypesWriter() {
|
||||
if f.ContentTypes != nil {
|
||||
output, _ := xml.Marshal(f.ContentTypes)
|
||||
f.saveFileList("[Content_Types].xml", output)
|
||||
}
|
||||
}
|
||||
|
||||
// workbookReader provides function to get the pointer to the xl/workbook.xml
|
||||
// structure after deserialization.
|
||||
func (f *File) workbookReader() *xlsxWorkbook {
|
||||
if f.WorkBook == nil {
|
||||
var content xlsxWorkbook
|
||||
xml.Unmarshal([]byte(f.readXML("xl/workbook.xml")), &content)
|
||||
f.WorkBook = &content
|
||||
}
|
||||
return f.WorkBook
|
||||
}
|
||||
|
||||
// workbookWriter provides function to save xl/workbook.xml after serialize
|
||||
// structure.
|
||||
func (f *File) workbookWriter() {
|
||||
if f.WorkBook != nil {
|
||||
output, _ := xml.Marshal(f.WorkBook)
|
||||
f.saveFileList("xl/workbook.xml", replaceRelationshipsNameSpaceBytes(output))
|
||||
}
|
||||
}
|
||||
|
||||
// worksheetWriter provides function to save xl/worksheets/sheet%d.xml after
|
||||
// serialize structure.
|
||||
func (f *File) worksheetWriter() {
|
||||
for path, sheet := range f.Sheet {
|
||||
if sheet != nil {
|
||||
for k, v := range sheet.SheetData.Row {
|
||||
f.Sheet[path].SheetData.Row[k].C = trimCell(v.C)
|
||||
}
|
||||
output, _ := xml.Marshal(sheet)
|
||||
f.saveFileList(path, replaceWorkSheetsRelationshipsNameSpaceBytes(output))
|
||||
ok := f.checked[path]
|
||||
if ok {
|
||||
f.checked[path] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trimCell provides function to trim blank cells which created by completeCol.
|
||||
func trimCell(column []xlsxC) []xlsxC {
|
||||
col := []xlsxC{}
|
||||
for _, c := range column {
|
||||
if c.S != 0 || c.V != "" || c.F != nil || c.T != "" {
|
||||
col = append(col, c)
|
||||
}
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
// Read and update property of contents type of XLSX.
|
||||
func (f *File) setContentTypes(index int) {
|
||||
content := f.contentTypesReader()
|
||||
content.Overrides = append(content.Overrides, xlsxOverride{
|
||||
PartName: "/xl/worksheets/sheet" + strconv.Itoa(index) + ".xml",
|
||||
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
||||
})
|
||||
}
|
||||
|
||||
// Update sheet property by given index.
|
||||
func (f *File) setSheet(index int, name string) {
|
||||
var xlsx xlsxWorksheet
|
||||
xlsx.Dimension.Ref = "A1"
|
||||
xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{
|
||||
WorkbookViewID: 0,
|
||||
})
|
||||
path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
|
||||
f.sheetMap[trimSheetName(name)] = path
|
||||
f.Sheet[path] = &xlsx
|
||||
}
|
||||
|
||||
// setWorkbook update workbook property of XLSX. Maximum 31 characters are
|
||||
// allowed in sheet title.
|
||||
func (f *File) setWorkbook(name string, rid int) {
|
||||
content := f.workbookReader()
|
||||
content.Sheets.Sheet = append(content.Sheets.Sheet, xlsxSheet{
|
||||
Name: trimSheetName(name),
|
||||
SheetID: strconv.Itoa(rid),
|
||||
ID: "rId" + strconv.Itoa(rid),
|
||||
})
|
||||
}
|
||||
|
||||
// workbookRelsReader provides function to read and unmarshal workbook
|
||||
// relationships of XLSX file.
|
||||
func (f *File) workbookRelsReader() *xlsxWorkbookRels {
|
||||
if f.WorkBookRels == nil {
|
||||
var content xlsxWorkbookRels
|
||||
xml.Unmarshal([]byte(f.readXML("xl/_rels/workbook.xml.rels")), &content)
|
||||
f.WorkBookRels = &content
|
||||
}
|
||||
return f.WorkBookRels
|
||||
}
|
||||
|
||||
// workbookRelsWriter provides function to save xl/_rels/workbook.xml.rels after
|
||||
// serialize structure.
|
||||
func (f *File) workbookRelsWriter() {
|
||||
if f.WorkBookRels != nil {
|
||||
output, _ := xml.Marshal(f.WorkBookRels)
|
||||
f.saveFileList("xl/_rels/workbook.xml.rels", output)
|
||||
}
|
||||
}
|
||||
|
||||
// addXlsxWorkbookRels update workbook relationships property of XLSX.
|
||||
func (f *File) addXlsxWorkbookRels(sheet int) int {
|
||||
content := f.workbookRelsReader()
|
||||
rID := 0
|
||||
for _, v := range content.Relationships {
|
||||
t, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId"))
|
||||
if t > rID {
|
||||
rID = t
|
||||
}
|
||||
}
|
||||
rID++
|
||||
ID := bytes.Buffer{}
|
||||
ID.WriteString("rId")
|
||||
ID.WriteString(strconv.Itoa(rID))
|
||||
target := bytes.Buffer{}
|
||||
target.WriteString("worksheets/sheet")
|
||||
target.WriteString(strconv.Itoa(sheet))
|
||||
target.WriteString(".xml")
|
||||
content.Relationships = append(content.Relationships, xlsxWorkbookRelation{
|
||||
ID: ID.String(),
|
||||
Target: target.String(),
|
||||
Type: SourceRelationshipWorkSheet,
|
||||
})
|
||||
return rID
|
||||
}
|
||||
|
||||
// setAppXML update docProps/app.xml file of XML.
|
||||
func (f *File) setAppXML() {
|
||||
f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp))
|
||||
}
|
||||
|
||||
// Some tools that read XLSX files have very strict requirements about the
|
||||
// structure of the input XML. In particular both Numbers on the Mac and SAS
|
||||
// dislike inline XML namespace declarations, or namespace prefixes that don't
|
||||
// match the ones that Excel itself uses. This is a problem because the Go XML
|
||||
// library doesn't multiple namespace declarations in a single element of a
|
||||
// document. This function is a horrible hack to fix that after the XML
|
||||
// marshalling is completed.
|
||||
func replaceRelationshipsNameSpace(workbookMarshal string) string {
|
||||
oldXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`
|
||||
newXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x15" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main">`
|
||||
return strings.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
|
||||
}
|
||||
|
||||
func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte {
|
||||
oldXmlns := []byte(`<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
|
||||
newXmlns := []byte(`<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x15" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main">`)
|
||||
return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
|
||||
}
|
||||
|
||||
// SetActiveSheet provides function to set default active worksheet of XLSX by
|
||||
// given index. Note that active index is different with the index that got by
|
||||
// function GetSheetMap, and it should be greater than 0 and less than total
|
||||
// worksheet numbers.
|
||||
func (f *File) SetActiveSheet(index int) {
|
||||
if index < 1 {
|
||||
index = 1
|
||||
}
|
||||
index--
|
||||
content := f.workbookReader()
|
||||
if len(content.BookViews.WorkBookView) > 0 {
|
||||
content.BookViews.WorkBookView[0].ActiveTab = index
|
||||
} else {
|
||||
content.BookViews.WorkBookView = append(content.BookViews.WorkBookView, xlsxWorkBookView{
|
||||
ActiveTab: index,
|
||||
})
|
||||
}
|
||||
index++
|
||||
for idx, name := range f.GetSheetMap() {
|
||||
xlsx := f.workSheetReader(name)
|
||||
if index == idx {
|
||||
if len(xlsx.SheetViews.SheetView) > 0 {
|
||||
xlsx.SheetViews.SheetView[0].TabSelected = true
|
||||
} else {
|
||||
xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{
|
||||
TabSelected: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if len(xlsx.SheetViews.SheetView) > 0 {
|
||||
xlsx.SheetViews.SheetView[0].TabSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveSheetIndex provides function to get active sheet of XLSX. If not
|
||||
// found the active sheet will be return integer 0.
|
||||
func (f *File) GetActiveSheetIndex() int {
|
||||
buffer := bytes.Buffer{}
|
||||
content := f.workbookReader()
|
||||
for _, v := range content.Sheets.Sheet {
|
||||
xlsx := xlsxWorksheet{}
|
||||
buffer.WriteString("xl/worksheets/sheet")
|
||||
buffer.WriteString(strings.TrimPrefix(v.ID, "rId"))
|
||||
buffer.WriteString(".xml")
|
||||
xml.Unmarshal([]byte(f.readXML(buffer.String())), &xlsx)
|
||||
for _, sheetView := range xlsx.SheetViews.SheetView {
|
||||
if sheetView.TabSelected {
|
||||
ID, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId"))
|
||||
return ID
|
||||
}
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetSheetName provides function to set the worksheet name be given old and new
|
||||
// worksheet name. Maximum 31 characters are allowed in sheet title and this
|
||||
// function only changes the name of the sheet and will not update the sheet
|
||||
// name in the formula or reference associated with the cell. So there may be
|
||||
// problem formula error or reference missing.
|
||||
func (f *File) SetSheetName(oldName, newName string) {
|
||||
oldName = trimSheetName(oldName)
|
||||
newName = trimSheetName(newName)
|
||||
content := f.workbookReader()
|
||||
for k, v := range content.Sheets.Sheet {
|
||||
if v.Name == oldName {
|
||||
content.Sheets.Sheet[k].Name = newName
|
||||
f.sheetMap[newName] = f.sheetMap[oldName]
|
||||
delete(f.sheetMap, oldName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSheetName provides function to get worksheet name of XLSX by given
|
||||
// worksheet index. If given sheet index is invalid, will return an empty
|
||||
// string.
|
||||
func (f *File) GetSheetName(index int) string {
|
||||
content := f.workbookReader()
|
||||
rels := f.workbookRelsReader()
|
||||
for _, rel := range rels.Relationships {
|
||||
rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml"))
|
||||
if rID == index {
|
||||
for _, v := range content.Sheets.Sheet {
|
||||
if v.ID == rel.ID {
|
||||
return v.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSheetIndex provides function to get worksheet index of XLSX by given sheet
|
||||
// name. If given worksheet name is invalid, will return an integer type value
|
||||
// 0.
|
||||
func (f *File) GetSheetIndex(name string) int {
|
||||
content := f.workbookReader()
|
||||
rels := f.workbookRelsReader()
|
||||
for _, v := range content.Sheets.Sheet {
|
||||
if v.Name == name {
|
||||
for _, rel := range rels.Relationships {
|
||||
if v.ID == rel.ID {
|
||||
rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml"))
|
||||
return rID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetSheetMap provides function to get worksheet name and index map of XLSX.
|
||||
// For example:
|
||||
//
|
||||
// xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// for index, name := range xlsx.GetSheetMap() {
|
||||
// fmt.Println(index, name)
|
||||
// }
|
||||
//
|
||||
func (f *File) GetSheetMap() map[int]string {
|
||||
content := f.workbookReader()
|
||||
rels := f.workbookRelsReader()
|
||||
sheetMap := map[int]string{}
|
||||
for _, v := range content.Sheets.Sheet {
|
||||
for _, rel := range rels.Relationships {
|
||||
if rel.ID == v.ID {
|
||||
rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml"))
|
||||
sheetMap[rID] = v.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return sheetMap
|
||||
}
|
||||
|
||||
// getSheetMap provides function to get worksheet name and XML file path map of
|
||||
// XLSX.
|
||||
func (f *File) getSheetMap() map[string]string {
|
||||
maps := make(map[string]string)
|
||||
for idx, name := range f.GetSheetMap() {
|
||||
maps[name] = "xl/worksheets/sheet" + strconv.Itoa(idx) + ".xml"
|
||||
}
|
||||
return maps
|
||||
}
|
||||
|
||||
// SetSheetBackground provides function to set background picture by given
|
||||
// worksheet name.
|
||||
func (f *File) SetSheetBackground(sheet, picture string) error {
|
||||
var err error
|
||||
// Check picture exists first.
|
||||
if _, err = os.Stat(picture); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
ext, ok := supportImageTypes[path.Ext(picture)]
|
||||
if !ok {
|
||||
return errors.New("Unsupported image extension")
|
||||
}
|
||||
pictureID := f.countMedia() + 1
|
||||
rID := f.addSheetRelationships(sheet, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, "")
|
||||
f.addSheetPicture(sheet, rID)
|
||||
f.addMedia(picture, ext)
|
||||
f.setContentTypePartImageExtensions()
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSheet provides function to delete worksheet in a workbook by given
|
||||
// worksheet name. Use this method with caution, which will affect changes in
|
||||
// references such as formulas, charts, and so on. If there is any referenced
|
||||
// value of the deleted worksheet, it will cause a file error when you open it.
|
||||
// This function will be invalid when only the one worksheet is left.
|
||||
func (f *File) DeleteSheet(name string) {
|
||||
content := f.workbookReader()
|
||||
for k, v := range content.Sheets.Sheet {
|
||||
if v.Name == trimSheetName(name) && len(content.Sheets.Sheet) > 1 {
|
||||
content.Sheets.Sheet = append(content.Sheets.Sheet[:k], content.Sheets.Sheet[k+1:]...)
|
||||
sheet := "xl/worksheets/sheet" + strings.TrimPrefix(v.ID, "rId") + ".xml"
|
||||
rels := "xl/worksheets/_rels/sheet" + strings.TrimPrefix(v.ID, "rId") + ".xml.rels"
|
||||
target := f.deleteSheetFromWorkbookRels(v.ID)
|
||||
f.deleteSheetFromContentTypes(target)
|
||||
delete(f.sheetMap, name)
|
||||
delete(f.XLSX, sheet)
|
||||
delete(f.XLSX, rels)
|
||||
delete(f.Sheet, sheet)
|
||||
f.SheetCount--
|
||||
}
|
||||
}
|
||||
f.SetActiveSheet(len(f.GetSheetMap()))
|
||||
}
|
||||
|
||||
// deleteSheetFromWorkbookRels provides function to remove worksheet
|
||||
// relationships by given relationships ID in the file
|
||||
// xl/_rels/workbook.xml.rels.
|
||||
func (f *File) deleteSheetFromWorkbookRels(rID string) string {
|
||||
content := f.workbookRelsReader()
|
||||
for k, v := range content.Relationships {
|
||||
if v.ID == rID {
|
||||
content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...)
|
||||
return v.Target
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// deleteSheetFromContentTypes provides function to remove worksheet
|
||||
// relationships by given target name in the file [Content_Types].xml.
|
||||
func (f *File) deleteSheetFromContentTypes(target string) {
|
||||
content := f.contentTypesReader()
|
||||
for k, v := range content.Overrides {
|
||||
if v.PartName == "/xl/"+target {
|
||||
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopySheet provides function to duplicate a worksheet by gave source and
|
||||
// target worksheet index. Note that currently doesn't support duplicate
|
||||
// workbooks that contain tables, charts or pictures. For Example:
|
||||
//
|
||||
// // Sheet1 already exists...
|
||||
// index := xlsx.NewSheet("Sheet2")
|
||||
// err := xlsx.CopySheet(1, index)
|
||||
// return err
|
||||
//
|
||||
func (f *File) CopySheet(from, to int) error {
|
||||
if from < 1 || to < 1 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" {
|
||||
return errors.New("Invalid worksheet index")
|
||||
}
|
||||
f.copySheet(from, to)
|
||||
return nil
|
||||
}
|
||||
|
||||
// copySheet provides function to duplicate a worksheet by gave source and
|
||||
// target worksheet name.
|
||||
func (f *File) copySheet(from, to int) {
|
||||
sheet := f.workSheetReader("sheet" + strconv.Itoa(from))
|
||||
worksheet := xlsxWorksheet{}
|
||||
deepCopy(&worksheet, &sheet)
|
||||
path := "xl/worksheets/sheet" + strconv.Itoa(to) + ".xml"
|
||||
if len(worksheet.SheetViews.SheetView) > 0 {
|
||||
worksheet.SheetViews.SheetView[0].TabSelected = false
|
||||
}
|
||||
worksheet.Drawing = nil
|
||||
worksheet.TableParts = nil
|
||||
worksheet.PageSetUp = nil
|
||||
f.Sheet[path] = &worksheet
|
||||
toRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(to) + ".xml.rels"
|
||||
fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(from) + ".xml.rels"
|
||||
_, ok := f.XLSX[fromRels]
|
||||
if ok {
|
||||
f.XLSX[toRels] = f.XLSX[fromRels]
|
||||
}
|
||||
}
|
||||
|
||||
// SetSheetVisible provides function to set worksheet visible by given worksheet
|
||||
// name. A workbook must contain at least one visible worksheet. If the given
|
||||
// worksheet has been activated, this setting will be invalidated. Sheet state
|
||||
// values as defined by http://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetstatevalues.aspx
|
||||
//
|
||||
// visible
|
||||
// hidden
|
||||
// veryHidden
|
||||
//
|
||||
// For example, hide Sheet1:
|
||||
//
|
||||
// xlsx.SetSheetVisible("Sheet1", false)
|
||||
//
|
||||
func (f *File) SetSheetVisible(name string, visible bool) {
|
||||
name = trimSheetName(name)
|
||||
content := f.workbookReader()
|
||||
if visible {
|
||||
for k, v := range content.Sheets.Sheet {
|
||||
if v.Name == name {
|
||||
content.Sheets.Sheet[k].State = ""
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
count := 0
|
||||
for _, v := range content.Sheets.Sheet {
|
||||
if v.State != "hidden" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
for k, v := range content.Sheets.Sheet {
|
||||
xlsx := f.workSheetReader(f.GetSheetMap()[k])
|
||||
tabSelected := false
|
||||
if len(xlsx.SheetViews.SheetView) > 0 {
|
||||
tabSelected = xlsx.SheetViews.SheetView[0].TabSelected
|
||||
}
|
||||
if v.Name == name && count > 1 && !tabSelected {
|
||||
content.Sheets.Sheet[k].State = "hidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseFormatPanesSet provides function to parse the panes settings.
|
||||
func parseFormatPanesSet(formatSet string) *formatPanes {
|
||||
format := formatPanes{}
|
||||
json.Unmarshal([]byte(formatSet), &format)
|
||||
return &format
|
||||
}
|
||||
|
||||
// SetPanes provides function to create and remove freeze panes and split panes
|
||||
// by given worksheet name and panes format set.
|
||||
//
|
||||
// activePane defines the pane that is active. The possible values for this
|
||||
// attribute are defined in the following table:
|
||||
//
|
||||
// Enumeration Value | Description
|
||||
// --------------------------------+-------------------------------------------------------------
|
||||
// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal
|
||||
// | splits are applied.
|
||||
// |
|
||||
// | This value is also used when only a horizontal split has
|
||||
// | been applied, dividing the pane into upper and lower
|
||||
// | regions. In that case, this value specifies the bottom
|
||||
// | pane.
|
||||
// |
|
||||
// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal
|
||||
// | splits are applied.
|
||||
// |
|
||||
// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits
|
||||
// | are applied.
|
||||
// |
|
||||
// | This value is also used when only a horizontal split has
|
||||
// | been applied, dividing the pane into upper and lower
|
||||
// | regions. In that case, this value specifies the top pane.
|
||||
// |
|
||||
// | This value is also used when only a vertical split has
|
||||
// | been applied, dividing the pane into right and left
|
||||
// | regions. In that case, this value specifies the left pane
|
||||
// |
|
||||
// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal
|
||||
// | splits are applied.
|
||||
// |
|
||||
// | This value is also used when only a vertical split has
|
||||
// | been applied, dividing the pane into right and left
|
||||
// | regions. In that case, this value specifies the right
|
||||
// | pane.
|
||||
//
|
||||
// Pane state type is restricted to the values supported currently listed in the following table:
|
||||
//
|
||||
// Enumeration Value | Description
|
||||
// --------------------------------+-------------------------------------------------------------
|
||||
// frozen (Frozen) | Panes are frozen, but were not split being frozen. In
|
||||
// | this state, when the panes are unfrozen again, a single
|
||||
// | pane results, with no split.
|
||||
// |
|
||||
// | In this state, the split bars are not adjustable.
|
||||
// |
|
||||
// split (Split) | Panes are split, but not frozen. In this state, the split
|
||||
// | bars are adjustable by the user.
|
||||
//
|
||||
// x_split (Horizontal Split Position): Horizontal position of the split, in
|
||||
// 1/20th of a point; 0 (zero) if none. If the pane is frozen, this value
|
||||
// indicates the number of columns visible in the top pane.
|
||||
//
|
||||
// y_split (Vertical Split Position): Vertical position of the split, in 1/20th
|
||||
// of a point; 0 (zero) if none. If the pane is frozen, this value indicates the
|
||||
// number of rows visible in the left pane. The possible values for this
|
||||
// attribute are defined by the W3C XML Schema double datatype.
|
||||
//
|
||||
// top_left_cell: Location of the top left visible cell in the bottom right pane
|
||||
// (when in Left-To-Right mode).
|
||||
//
|
||||
// sqref (Sequence of References): Range of the selection. Can be non-contiguous
|
||||
// set of ranges.
|
||||
//
|
||||
// An example of how to freeze column A in the Sheet1 and set the active cell on
|
||||
// Sheet1!K16:
|
||||
//
|
||||
// xlsx.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)
|
||||
//
|
||||
// An example of how to freeze rows 1 to 9 in the Sheet1 and set the active cell
|
||||
// ranges on Sheet1!A11:XFD11:
|
||||
//
|
||||
// xlsx.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)
|
||||
//
|
||||
// An example of how to create split panes in the Sheet1 and set the active cell
|
||||
// on Sheet1!J60:
|
||||
//
|
||||
// xlsx.SetPanes("Sheet1", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`)
|
||||
//
|
||||
// An example of how to unfreeze and remove all panes on Sheet1:
|
||||
//
|
||||
// xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`)
|
||||
//
|
||||
func (f *File) SetPanes(sheet, panes string) {
|
||||
fs := parseFormatPanesSet(panes)
|
||||
xlsx := f.workSheetReader(sheet)
|
||||
p := &xlsxPane{
|
||||
ActivePane: fs.ActivePane,
|
||||
TopLeftCell: fs.TopLeftCell,
|
||||
XSplit: float64(fs.XSplit),
|
||||
YSplit: float64(fs.YSplit),
|
||||
}
|
||||
if fs.Freeze {
|
||||
p.State = "frozen"
|
||||
}
|
||||
xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Pane = p
|
||||
if !(fs.Freeze) && !(fs.Split) {
|
||||
if len(xlsx.SheetViews.SheetView) > 0 {
|
||||
xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Pane = nil
|
||||
}
|
||||
}
|
||||
s := []*xlsxSelection{}
|
||||
for _, p := range fs.Panes {
|
||||
s = append(s, &xlsxSelection{
|
||||
ActiveCell: p.ActiveCell,
|
||||
Pane: p.Pane,
|
||||
SQRef: p.SQRef,
|
||||
})
|
||||
}
|
||||
xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Selection = s
|
||||
}
|
||||
|
||||
// GetSheetVisible provides function to get worksheet visible by given worksheet
|
||||
// name. For example, get visible state of Sheet1:
|
||||
//
|
||||
// xlsx.GetSheetVisible("Sheet1")
|
||||
//
|
||||
func (f *File) GetSheetVisible(name string) bool {
|
||||
content := f.workbookReader()
|
||||
visible := false
|
||||
for k, v := range content.Sheets.Sheet {
|
||||
if v.Name == trimSheetName(name) {
|
||||
if content.Sheets.Sheet[k].State == "" || content.Sheets.Sheet[k].State == "visible" {
|
||||
visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
// trimSheetName provides function to trim invaild characters by given worksheet
|
||||
// name.
|
||||
func trimSheetName(name string) string {
|
||||
r := strings.NewReplacer(":", "", "\\", "", "/", "", "?", "", "*", "", "[", "", "]", "")
|
||||
name = r.Replace(name)
|
||||
if utf8.RuneCountInString(name) > 31 {
|
||||
name = string([]rune(name)[0:31])
|
||||
}
|
||||
return name
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package excelize
|
||||
|
||||
// SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions().
|
||||
type SheetPrOption interface {
|
||||
setSheetPrOption(view *xlsxSheetPr)
|
||||
}
|
||||
|
||||
// SheetPrOptionPtr is a writable SheetPrOption. See GetSheetPrOptions().
|
||||
type SheetPrOptionPtr interface {
|
||||
SheetPrOption
|
||||
getSheetPrOption(view *xlsxSheetPr)
|
||||
}
|
||||
|
||||
type (
|
||||
// CodeName is a SheetPrOption
|
||||
CodeName string
|
||||
// EnableFormatConditionsCalculation is a SheetPrOption
|
||||
EnableFormatConditionsCalculation bool
|
||||
// Published is a SheetPrOption
|
||||
Published bool
|
||||
// FitToPage is a SheetPrOption
|
||||
FitToPage bool
|
||||
// AutoPageBreaks is a SheetPrOption
|
||||
AutoPageBreaks bool
|
||||
)
|
||||
|
||||
func (o CodeName) setSheetPrOption(pr *xlsxSheetPr) {
|
||||
pr.CodeName = string(o)
|
||||
}
|
||||
|
||||
func (o *CodeName) getSheetPrOption(pr *xlsxSheetPr) {
|
||||
if pr == nil {
|
||||
*o = ""
|
||||
return
|
||||
}
|
||||
*o = CodeName(pr.CodeName)
|
||||
}
|
||||
|
||||
func (o EnableFormatConditionsCalculation) setSheetPrOption(pr *xlsxSheetPr) {
|
||||
pr.EnableFormatConditionsCalculation = boolPtr(bool(o))
|
||||
}
|
||||
|
||||
func (o *EnableFormatConditionsCalculation) getSheetPrOption(pr *xlsxSheetPr) {
|
||||
if pr == nil {
|
||||
*o = true
|
||||
return
|
||||
}
|
||||
*o = EnableFormatConditionsCalculation(defaultTrue(pr.EnableFormatConditionsCalculation))
|
||||
}
|
||||
|
||||
func (o Published) setSheetPrOption(pr *xlsxSheetPr) {
|
||||
pr.Published = boolPtr(bool(o))
|
||||
}
|
||||
|
||||
func (o *Published) getSheetPrOption(pr *xlsxSheetPr) {
|
||||
if pr == nil {
|
||||
*o = true
|
||||
return
|
||||
}
|
||||
*o = Published(defaultTrue(pr.Published))
|
||||
}
|
||||
|
||||
func (o FitToPage) setSheetPrOption(pr *xlsxSheetPr) {
|
||||
if pr.PageSetUpPr == nil {
|
||||
if !o {
|
||||
return
|
||||
}
|
||||
pr.PageSetUpPr = new(xlsxPageSetUpPr)
|
||||
}
|
||||
pr.PageSetUpPr.FitToPage = bool(o)
|
||||
}
|
||||
|
||||
func (o *FitToPage) getSheetPrOption(pr *xlsxSheetPr) {
|
||||
// Excel default: false
|
||||
if pr == nil || pr.PageSetUpPr == nil {
|
||||
*o = false
|
||||
return
|
||||
}
|
||||
*o = FitToPage(pr.PageSetUpPr.FitToPage)
|
||||
}
|
||||
|
||||
func (o AutoPageBreaks) setSheetPrOption(pr *xlsxSheetPr) {
|
||||
if pr.PageSetUpPr == nil {
|
||||
if !o {
|
||||
return
|
||||
}
|
||||
pr.PageSetUpPr = new(xlsxPageSetUpPr)
|
||||
}
|
||||
pr.PageSetUpPr.AutoPageBreaks = bool(o)
|
||||
}
|
||||
|
||||
func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) {
|
||||
// Excel default: false
|
||||
if pr == nil || pr.PageSetUpPr == nil {
|
||||
*o = false
|
||||
return
|
||||
}
|
||||
*o = AutoPageBreaks(pr.PageSetUpPr.AutoPageBreaks)
|
||||
}
|
||||
|
||||
// SetSheetPrOptions provides function to sets worksheet properties.
|
||||
//
|
||||
// Available options:
|
||||
// CodeName(string)
|
||||
// EnableFormatConditionsCalculation(bool)
|
||||
// Published(bool)
|
||||
// FitToPage(bool)
|
||||
// AutoPageBreaks(bool)
|
||||
func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error {
|
||||
sheet := f.workSheetReader(name)
|
||||
pr := sheet.SheetPr
|
||||
if pr == nil {
|
||||
pr = new(xlsxSheetPr)
|
||||
sheet.SheetPr = pr
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.setSheetPrOption(pr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSheetPrOptions provides function to gets worksheet properties.
|
||||
//
|
||||
// Available options:
|
||||
// CodeName(string)
|
||||
// EnableFormatConditionsCalculation(bool)
|
||||
// Published(bool)
|
||||
// FitToPage(bool)
|
||||
// AutoPageBreaks(bool)
|
||||
func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error {
|
||||
sheet := f.workSheetReader(name)
|
||||
pr := sheet.SheetPr
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.getSheetPrOption(pr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package excelize
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SheetViewOption is an option of a view of a worksheet. See SetSheetViewOptions().
|
||||
type SheetViewOption interface {
|
||||
setSheetViewOption(view *xlsxSheetView)
|
||||
}
|
||||
|
||||
// SheetViewOptionPtr is a writable SheetViewOption. See GetSheetViewOptions().
|
||||
type SheetViewOptionPtr interface {
|
||||
SheetViewOption
|
||||
getSheetViewOption(view *xlsxSheetView)
|
||||
}
|
||||
|
||||
type (
|
||||
// DefaultGridColor is a SheetViewOption.
|
||||
DefaultGridColor bool
|
||||
// RightToLeft is a SheetViewOption.
|
||||
RightToLeft bool
|
||||
// ShowFormulas is a SheetViewOption.
|
||||
ShowFormulas bool
|
||||
// ShowGridLines is a SheetViewOption.
|
||||
ShowGridLines bool
|
||||
// ShowRowColHeaders is a SheetViewOption.
|
||||
ShowRowColHeaders bool
|
||||
// ZoomScale is a SheetViewOption.
|
||||
ZoomScale float64
|
||||
/* TODO
|
||||
// ShowWhiteSpace is a SheetViewOption.
|
||||
ShowWhiteSpace bool
|
||||
// ShowZeros is a SheetViewOption.
|
||||
ShowZeros bool
|
||||
// WindowProtection is a SheetViewOption.
|
||||
WindowProtection bool
|
||||
*/
|
||||
)
|
||||
|
||||
// Defaults for each option are described in XML schema for CT_SheetView
|
||||
|
||||
func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) {
|
||||
view.DefaultGridColor = boolPtr(bool(o))
|
||||
}
|
||||
|
||||
func (o *DefaultGridColor) getSheetViewOption(view *xlsxSheetView) {
|
||||
*o = DefaultGridColor(defaultTrue(view.DefaultGridColor)) // Excel default: true
|
||||
}
|
||||
|
||||
func (o RightToLeft) setSheetViewOption(view *xlsxSheetView) {
|
||||
view.RightToLeft = bool(o) // Excel default: false
|
||||
}
|
||||
|
||||
func (o *RightToLeft) getSheetViewOption(view *xlsxSheetView) {
|
||||
*o = RightToLeft(view.RightToLeft)
|
||||
}
|
||||
|
||||
func (o ShowFormulas) setSheetViewOption(view *xlsxSheetView) {
|
||||
view.ShowFormulas = bool(o) // Excel default: false
|
||||
}
|
||||
|
||||
func (o *ShowFormulas) getSheetViewOption(view *xlsxSheetView) {
|
||||
*o = ShowFormulas(view.ShowFormulas) // Excel default: false
|
||||
}
|
||||
|
||||
func (o ShowGridLines) setSheetViewOption(view *xlsxSheetView) {
|
||||
view.ShowGridLines = boolPtr(bool(o))
|
||||
}
|
||||
|
||||
func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) {
|
||||
*o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true
|
||||
}
|
||||
|
||||
func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) {
|
||||
view.ShowRowColHeaders = boolPtr(bool(o))
|
||||
}
|
||||
|
||||
func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) {
|
||||
*o = ShowRowColHeaders(defaultTrue(view.ShowRowColHeaders)) // Excel default: true
|
||||
}
|
||||
|
||||
func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) {
|
||||
//This attribute is restricted to values ranging from 10 to 400.
|
||||
if float64(o) >= 10 && float64(o) <= 400 {
|
||||
view.ZoomScale = float64(o)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ZoomScale) getSheetViewOption(view *xlsxSheetView) {
|
||||
*o = ZoomScale(view.ZoomScale)
|
||||
}
|
||||
|
||||
// getSheetView returns the SheetView object
|
||||
func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, error) {
|
||||
xlsx := f.workSheetReader(sheetName)
|
||||
if viewIndex < 0 {
|
||||
if viewIndex < -len(xlsx.SheetViews.SheetView) {
|
||||
return nil, fmt.Errorf("view index %d out of range", viewIndex)
|
||||
}
|
||||
viewIndex = len(xlsx.SheetViews.SheetView) + viewIndex
|
||||
} else if viewIndex >= len(xlsx.SheetViews.SheetView) {
|
||||
return nil, fmt.Errorf("view index %d out of range", viewIndex)
|
||||
}
|
||||
|
||||
return &(xlsx.SheetViews.SheetView[viewIndex]), nil
|
||||
}
|
||||
|
||||
// SetSheetViewOptions sets sheet view options.
|
||||
// The viewIndex may be negative and if so is counted backward (-1 is the last view).
|
||||
//
|
||||
// Available options:
|
||||
// DefaultGridColor(bool)
|
||||
// RightToLeft(bool)
|
||||
// ShowFormulas(bool)
|
||||
// ShowGridLines(bool)
|
||||
// ShowRowColHeaders(bool)
|
||||
// Example:
|
||||
// err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false))
|
||||
func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error {
|
||||
view, err := f.getSheetView(name, viewIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.setSheetViewOption(view)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSheetViewOptions gets the value of sheet view options.
|
||||
// The viewIndex may be negative and if so is counted backward (-1 is the last view).
|
||||
//
|
||||
// Available options:
|
||||
// DefaultGridColor(bool)
|
||||
// RightToLeft(bool)
|
||||
// ShowFormulas(bool)
|
||||
// ShowGridLines(bool)
|
||||
// ShowRowColHeaders(bool)
|
||||
// Example:
|
||||
// var showGridLines excelize.ShowGridLines
|
||||
// err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines)
|
||||
func (f *File) GetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOptionPtr) error {
|
||||
view, err := f.getSheetView(name, viewIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.getSheetViewOption(view)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user