diff --git a/CODEOWNERS b/CODEOWNERS index cbc814bea..524078797 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,3 +5,4 @@ /backend/remote-state/azure @hashicorp/terraform-azure /backend/remote-state/gcs @hashicorp/terraform-google /backend/remote-state/s3 @hashicorp/terraform-aws +/backend/remote-state/s3 @likexian \ No newline at end of file diff --git a/backend/init/init.go b/backend/init/init.go index 5142c4193..d4a239791 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -16,6 +16,7 @@ import ( backendArtifactory "github.com/hashicorp/terraform/backend/remote-state/artifactory" backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure" backendConsul "github.com/hashicorp/terraform/backend/remote-state/consul" + backendCos "github.com/hashicorp/terraform/backend/remote-state/cos" backendEtcdv2 "github.com/hashicorp/terraform/backend/remote-state/etcdv2" backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3" backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs" @@ -57,6 +58,7 @@ func Init(services *disco.Disco) { "atlas": func() backend.Backend { return backendAtlas.New() }, "azurerm": func() backend.Backend { return backendAzure.New() }, "consul": func() backend.Backend { return backendConsul.New() }, + "cos": func() backend.Backend { return backendCos.New() }, "etcd": func() backend.Backend { return backendEtcdv2.New() }, "etcdv3": func() backend.Backend { return backendEtcdv3.New() }, "gcs": func() backend.Backend { return backendGCS.New() }, diff --git a/backend/init/init_test.go b/backend/init/init_test.go index 74dbf53aa..2b7571b54 100644 --- a/backend/init/init_test.go +++ b/backend/init/init_test.go @@ -18,6 +18,7 @@ func TestInit_backend(t *testing.T) { {"atlas", "*atlas.Backend"}, {"azurerm", "*azure.Backend"}, {"consul", "*consul.Backend"}, + {"cos", "*cos.Backend"}, {"etcdv3", "*etcd.Backend"}, {"gcs", "*gcs.Backend"}, {"inmem", "*inmem.Backend"}, diff --git a/backend/remote-state/cos/backend.go b/backend/remote-state/cos/backend.go new file mode 100644 index 000000000..ce502e5cc --- /dev/null +++ b/backend/remote-state/cos/backend.go @@ -0,0 +1,169 @@ +package cos + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/helper/schema" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" + "github.com/tencentyun/cos-go-sdk-v5" +) + +// Default value from environment variable +const ( + PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" + PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" + PROVIDER_REGION = "TENCENTCLOUD_REGION" +) + +// Backend implements "backend".Backend for tencentCloud cos +type Backend struct { + *schema.Backend + + cosContext context.Context + cosClient *cos.Client + tagClient *tag.Client + + region string + bucket string + prefix string + key string + encrypt bool + acl string +} + +// New creates a new backend for TencentCloud cos remote state. +func New() backend.Backend { + s := &schema.Backend{ + Schema: map[string]*schema.Schema{ + "secret_id": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil), + Description: "Secret id of Tencent Cloud", + }, + "secret_key": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil), + Description: "Secret key of Tencent Cloud", + Sensitive: true, + }, + "region": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_REGION, nil), + Description: "The region of the COS bucket", + InputDefault: "ap-guangzhou", + }, + "bucket": { + Type: schema.TypeString, + Required: true, + Description: "The name of the COS bucket", + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + Description: "The directory for saving the state file in bucket", + ValidateFunc: func(v interface{}, s string) ([]string, []error) { + prefix := v.(string) + if strings.HasPrefix(prefix, "/") || strings.HasPrefix(prefix, "./") { + return nil, []error{fmt.Errorf("prefix must not start with '/' or './'")} + } + return nil, nil + }, + }, + "key": { + Type: schema.TypeString, + Optional: true, + Description: "The path for saving the state file in bucket", + Default: "terraform.tfstate", + ValidateFunc: func(v interface{}, s string) ([]string, []error) { + if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") { + return nil, []error{fmt.Errorf("key can not start and end with '/'")} + } + return nil, nil + }, + }, + "encrypt": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to enable server side encryption of the state file", + Default: true, + }, + "acl": { + Type: schema.TypeString, + Optional: true, + Description: "Object ACL to be applied to the state file", + Default: "private", + ValidateFunc: func(v interface{}, s string) ([]string, []error) { + value := v.(string) + if value != "private" && value != "public-read" { + return nil, []error{fmt.Errorf( + "acl value invalid, expected %s or %s, got %s", + "private", "public-read", value)} + } + return nil, nil + }, + }, + }, + } + + result := &Backend{Backend: s} + result.Backend.ConfigureFunc = result.configure + + return result +} + +// configure init cos client +func (b *Backend) configure(ctx context.Context) error { + if b.cosClient != nil { + return nil + } + + b.cosContext = ctx + data := schema.FromContextBackendConfig(b.cosContext) + + b.region = data.Get("region").(string) + b.bucket = data.Get("bucket").(string) + b.prefix = data.Get("prefix").(string) + b.key = data.Get("key").(string) + b.encrypt = data.Get("encrypt").(bool) + b.acl = data.Get("acl").(string) + + u, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", b.bucket, b.region)) + if err != nil { + return err + } + + b.cosClient = cos.NewClient( + &cos.BaseURL{BucketURL: u}, + &http.Client{ + Timeout: 60 * time.Second, + Transport: &cos.AuthorizationTransport{ + SecretID: data.Get("secret_id").(string), + SecretKey: data.Get("secret_key").(string), + }, + }, + ) + + credential := common.NewCredential( + data.Get("secret_id").(string), + data.Get("secret_key").(string), + ) + + cpf := profile.NewClientProfile() + cpf.HttpProfile.ReqMethod = "POST" + cpf.HttpProfile.ReqTimeout = 300 + cpf.Language = "en-US" + b.tagClient, err = tag.NewClient(credential, b.region, cpf) + + return err +} diff --git a/backend/remote-state/cos/backend_state.go b/backend/remote-state/cos/backend_state.go new file mode 100644 index 000000000..2bc3f2428 --- /dev/null +++ b/backend/remote-state/cos/backend_state.go @@ -0,0 +1,178 @@ +package cos + +import ( + "fmt" + "log" + "path" + "sort" + "strings" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" + "github.com/hashicorp/terraform/states" + "github.com/likexian/gokit/assert" +) + +// Define file suffix +const ( + stateFileSuffix = ".tfstate" + lockFileSuffix = ".tflock" +) + +// Workspaces returns a list of names for the workspaces +func (b *Backend) Workspaces() ([]string, error) { + c, err := b.client("tencentcloud") + if err != nil { + return nil, err + } + + obs, err := c.getBucket(b.prefix) + log.Printf("[DEBUG] list all workspaces, objects: %v, error: %v", obs, err) + if err != nil { + return nil, err + } + + ws := []string{backend.DefaultStateName} + for _, vv := range obs { + // .tfstate + if !strings.HasSuffix(vv.Key, stateFileSuffix) { + continue + } + // default worksapce + if path.Join(b.prefix, b.key) == vv.Key { + continue + } + // // + prefix := strings.TrimRight(b.prefix, "/") + "/" + parts := strings.Split(strings.TrimPrefix(vv.Key, prefix), "/") + if len(parts) > 0 && parts[0] != "" { + ws = append(ws, parts[0]) + } + } + + sort.Strings(ws[1:]) + log.Printf("[DEBUG] list all workspaces, workspaces: %v", ws) + + return ws, nil +} + +// DeleteWorkspace deletes the named workspaces. The "default" state cannot be deleted. +func (b *Backend) DeleteWorkspace(name string) error { + log.Printf("[DEBUG] delete workspace, workspace: %v", name) + + if name == backend.DefaultStateName || name == "" { + return fmt.Errorf("default state is not allow to delete") + } + + c, err := b.client(name) + if err != nil { + return err + } + + return c.Delete() +} + +// StateMgr manage the state, if the named state not exists, a new file will created +func (b *Backend) StateMgr(name string) (state.State, error) { + log.Printf("[DEBUG] state manager, current workspace: %v", name) + + c, err := b.client(name) + if err != nil { + return nil, err + } + stateMgr := &remote.State{Client: c} + + ws, err := b.Workspaces() + if err != nil { + return nil, err + } + + if !assert.IsContains(ws, name) { + log.Printf("[DEBUG] workspace %v not exists", name) + + // take a lock on this state while we write it + lockInfo := state.NewLockInfo() + lockInfo.Operation = "init" + lockId, err := c.Lock(lockInfo) + if err != nil { + return nil, fmt.Errorf("Failed to lock cos state: %s", err) + } + + // Local helper function so we can call it multiple places + lockUnlock := func(e error) error { + if err := stateMgr.Unlock(lockId); err != nil { + return fmt.Errorf(unlockErrMsg, err, lockId) + } + return e + } + + // Grab the value + if err := stateMgr.RefreshState(); err != nil { + err = lockUnlock(err) + return nil, err + } + + // If we have no state, we have to create an empty state + if v := stateMgr.State(); v == nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { + err = lockUnlock(err) + return nil, err + } + if err := stateMgr.PersistState(); err != nil { + err = lockUnlock(err) + return nil, err + } + } + + // Unlock, the state should now be initialized + if err := lockUnlock(nil); err != nil { + return nil, err + } + } + + return stateMgr, nil +} + +// client returns a remoteClient for the named state. +func (b *Backend) client(name string) (*remoteClient, error) { + if strings.TrimSpace(name) == "" { + return nil, fmt.Errorf("state name not allow to be empty") + } + + return &remoteClient{ + cosContext: b.cosContext, + cosClient: b.cosClient, + tagClient: b.tagClient, + bucket: b.bucket, + stateFile: b.stateFile(name), + lockFile: b.lockFile(name), + encrypt: b.encrypt, + acl: b.acl, + }, nil +} + +// stateFile returns state file path by name +func (b *Backend) stateFile(name string) string { + if name == backend.DefaultStateName { + return path.Join(b.prefix, b.key) + } + return path.Join(b.prefix, name, b.key) +} + +// lockFile returns lock file path by name +func (b *Backend) lockFile(name string) string { + return b.stateFile(name) + lockFileSuffix +} + +// unlockErrMsg is error msg for unlock failed +const unlockErrMsg = ` +Unlocking the state file on TencentCloud cos backend failed: + +Error message: %v +Lock ID (gen): %s + +You may have to force-unlock this state in order to use it again. +The TencentCloud backend acquires a lock during initialization +to ensure the initial state file is created. +` diff --git a/backend/remote-state/cos/backend_test.go b/backend/remote-state/cos/backend_test.go new file mode 100644 index 000000000..b372571af --- /dev/null +++ b/backend/remote-state/cos/backend_test.go @@ -0,0 +1,227 @@ +package cos + +import ( + "crypto/md5" + "fmt" + "os" + "testing" + "time" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state/remote" + "github.com/likexian/gokit/assert" +) + +const ( + defaultPrefix = "" + defaultKey = "terraform.tfstate" +) + +// Testing Thanks to GCS + +func TestStateFile(t *testing.T) { + t.Parallel() + + cases := []struct { + prefix string + stateName string + key string + wantStateFile string + wantLockFile string + }{ + {"", "default", "default.tfstate", "default.tfstate", "default.tfstate.tflock"}, + {"", "default", "test.tfstate", "test.tfstate", "test.tfstate.tflock"}, + {"", "dev", "test.tfstate", "dev/test.tfstate", "dev/test.tfstate.tflock"}, + {"terraform/test", "default", "default.tfstate", "terraform/test/default.tfstate", "terraform/test/default.tfstate.tflock"}, + {"terraform/test", "default", "test.tfstate", "terraform/test/test.tfstate", "terraform/test/test.tfstate.tflock"}, + {"terraform/test", "dev", "test.tfstate", "terraform/test/dev/test.tfstate", "terraform/test/dev/test.tfstate.tflock"}, + } + + for _, c := range cases { + b := &Backend{ + prefix: c.prefix, + key: c.key, + } + assert.Equal(t, b.stateFile(c.stateName), c.wantStateFile) + assert.Equal(t, b.lockFile(c.stateName), c.wantLockFile) + } +} + +func TestRemoteClient(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be := setupBackend(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be) + + ss, err := be.StateMgr(backend.DefaultStateName) + assert.Nil(t, err) + + rs, ok := ss.(*remote.State) + assert.True(t, ok) + + remote.TestClient(t, rs.Client) +} + +func TestRemoteClientWithPrefix(t *testing.T) { + t.Parallel() + + prefix := "prefix/test" + bucket := bucketName(t) + + be := setupBackend(t, bucket, prefix, defaultKey, false) + defer teardownBackend(t, be) + + ss, err := be.StateMgr(backend.DefaultStateName) + assert.Nil(t, err) + + rs, ok := ss.(*remote.State) + assert.True(t, ok) + + remote.TestClient(t, rs.Client) +} + +func TestRemoteClientWithEncryption(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be := setupBackend(t, bucket, defaultPrefix, defaultKey, true) + defer teardownBackend(t, be) + + ss, err := be.StateMgr(backend.DefaultStateName) + assert.Nil(t, err) + + rs, ok := ss.(*remote.State) + assert.True(t, ok) + + remote.TestClient(t, rs.Client) +} + +func TestRemoteLocks(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be := setupBackend(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be) + + remoteClient := func() (remote.Client, error) { + ss, err := be.StateMgr(backend.DefaultStateName) + if err != nil { + return nil, err + } + + rs, ok := ss.(*remote.State) + if !ok { + return nil, fmt.Errorf("be.StateMgr(): got a %T, want a *remote.State", ss) + } + + return rs.Client, nil + } + + c0, err := remoteClient() + assert.Nil(t, err) + + c1, err := remoteClient() + assert.Nil(t, err) + + remote.TestRemoteLocks(t, c0, c1) +} + +func TestBackend(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be0 := setupBackend(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be0) + + be1 := setupBackend(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be1) + + backend.TestBackendStates(t, be0) + backend.TestBackendStateLocks(t, be0, be1) + backend.TestBackendStateForceUnlock(t, be0, be1) +} + +func TestBackendWithPrefix(t *testing.T) { + t.Parallel() + + prefix := "prefix/test" + bucket := bucketName(t) + + be0 := setupBackend(t, bucket, prefix, defaultKey, false) + defer teardownBackend(t, be0) + + be1 := setupBackend(t, bucket, prefix+"/", defaultKey, false) + defer teardownBackend(t, be1) + + backend.TestBackendStates(t, be0) + backend.TestBackendStateLocks(t, be0, be1) +} + +func TestBackendWithEncryption(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be0 := setupBackend(t, bucket, defaultPrefix, defaultKey, true) + defer teardownBackend(t, be0) + + be1 := setupBackend(t, bucket, defaultPrefix, defaultKey, true) + defer teardownBackend(t, be1) + + backend.TestBackendStates(t, be0) + backend.TestBackendStateLocks(t, be0, be1) +} + +func setupBackend(t *testing.T, bucket, prefix, key string, encrypt bool) backend.Backend { + t.Helper() + + skip := os.Getenv("TF_COS_APPID") == "" + if skip { + t.Skip("This test require setting TF_COS_APPID environment variables") + } + + if os.Getenv(PROVIDER_REGION) == "" { + os.Setenv(PROVIDER_REGION, "ap-guangzhou") + } + + appId := os.Getenv("TF_COS_APPID") + region := os.Getenv(PROVIDER_REGION) + + config := map[string]interface{}{ + "region": region, + "bucket": bucket + appId, + "prefix": prefix, + "key": key, + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)) + be := b.(*Backend) + + c, err := be.client("tencentcloud") + assert.Nil(t, err) + + err = c.putBucket() + assert.Nil(t, err) + + return b +} + +func teardownBackend(t *testing.T, b backend.Backend) { + t.Helper() + + c, err := b.(*Backend).client("tencentcloud") + assert.Nil(t, err) + + err = c.deleteBucket(true) + assert.Nil(t, err) +} + +func bucketName(t *testing.T) string { + unique := fmt.Sprintf("%s-%x", t.Name(), time.Now().UnixNano()) + return fmt.Sprintf("terraform-test-%s-%s", fmt.Sprintf("%x", md5.Sum([]byte(unique)))[:10], "") +} diff --git a/backend/remote-state/cos/client.go b/backend/remote-state/cos/client.go new file mode 100644 index 000000000..281fd2b7a --- /dev/null +++ b/backend/remote-state/cos/client.go @@ -0,0 +1,403 @@ +package cos + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" + tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" + "github.com/tencentyun/cos-go-sdk-v5" +) + +const ( + lockTagKey = "tencentcloud-terraform-lock" +) + +// RemoteClient implements the client of remote state +type remoteClient struct { + cosContext context.Context + cosClient *cos.Client + tagClient *tag.Client + + bucket string + stateFile string + lockFile string + encrypt bool + acl string +} + +// Get returns remote state file +func (c *remoteClient) Get() (*remote.Payload, error) { + log.Printf("[DEBUG] get remote state file %s", c.stateFile) + + exists, data, checksum, err := c.getObject(c.stateFile) + if err != nil { + return nil, err + } + + if !exists { + return nil, nil + } + + payload := &remote.Payload{ + Data: data, + MD5: []byte(checksum), + } + + return payload, nil +} + +// Put put state file to remote +func (c *remoteClient) Put(data []byte) error { + log.Printf("[DEBUG] put remote state file %s", c.stateFile) + + return c.putObject(c.stateFile, data) +} + +// Delete delete remote state file +func (c *remoteClient) Delete() error { + log.Printf("[DEBUG] delete remote state file %s", c.stateFile) + + return c.deleteObject(c.stateFile) +} + +// Lock lock remote state file for writing +func (c *remoteClient) Lock(info *state.LockInfo) (string, error) { + log.Printf("[DEBUG] lock remote state file %s", c.lockFile) + + err := c.cosLock(c.bucket, c.lockFile) + if err != nil { + return "", c.lockError(err) + } + defer c.cosUnlock(c.bucket, c.lockFile) + + exists, _, _, err := c.getObject(c.lockFile) + if err != nil { + return "", c.lockError(err) + } + + if exists { + return "", c.lockError(fmt.Errorf("lock file %s exists", c.lockFile)) + } + + info.Path = c.lockFile + data, err := json.Marshal(info) + if err != nil { + return "", c.lockError(err) + } + + check := fmt.Sprintf("%x", md5.Sum(data)) + err = c.putObject(c.lockFile, data) + if err != nil { + return "", c.lockError(err) + } + + return check, nil +} + +// Unlock unlock remote state file +func (c *remoteClient) Unlock(check string) error { + log.Printf("[DEBUG] unlock remote state file %s", c.lockFile) + + info, err := c.lockInfo() + if err != nil { + return c.lockError(err) + } + + if info.ID != check { + return c.lockError(fmt.Errorf("lock id mismatch, %v != %v", info.ID, check)) + } + + err = c.deleteObject(c.lockFile) + if err != nil { + return c.lockError(err) + } + + return nil +} + +// lockError returns state.LockError +func (c *remoteClient) lockError(err error) *state.LockError { + log.Printf("[DEBUG] failed to lock or unlock %s: %v", c.lockFile, err) + + lockErr := &state.LockError{ + Err: err, + } + + info, infoErr := c.lockInfo() + if infoErr != nil { + lockErr.Err = multierror.Append(lockErr.Err, infoErr) + } else { + lockErr.Info = info + } + + return lockErr +} + +// lockInfo returns LockInfo from lock file +func (c *remoteClient) lockInfo() (*state.LockInfo, error) { + exists, data, checksum, err := c.getObject(c.lockFile) + if err != nil { + return nil, err + } + + if !exists { + return nil, fmt.Errorf("lock file %s not exists", c.lockFile) + } + + info := &state.LockInfo{} + if err := json.Unmarshal(data, info); err != nil { + return nil, err + } + + info.ID = checksum + + return info, nil +} + +// getObject get remote object +func (c *remoteClient) getObject(cosFile string) (exists bool, data []byte, checksum string, err error) { + rsp, err := c.cosClient.Object.Get(c.cosContext, cosFile, nil) + if rsp == nil { + log.Printf("[DEBUG] getObject %s: error: %v", cosFile, err) + err = fmt.Errorf("failed to open file at %v: %v", cosFile, err) + return + } + defer rsp.Body.Close() + + log.Printf("[DEBUG] getObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) + if err != nil { + if rsp.StatusCode == 404 { + err = nil + } else { + err = fmt.Errorf("failed to open file at %v: %v", cosFile, err) + } + return + } + + checksum = rsp.Header.Get("X-Cos-Meta-Md5") + log.Printf("[DEBUG] getObject %s: checksum: %s", cosFile, checksum) + if len(checksum) != 32 { + err = fmt.Errorf("failed to open file at %v: checksum %s invalid", cosFile, checksum) + return + } + + exists = true + data, err = ioutil.ReadAll(rsp.Body) + log.Printf("[DEBUG] getObject %s: data length: %d", cosFile, len(data)) + if err != nil { + err = fmt.Errorf("failed to open file at %v: %v", cosFile, err) + return + } + + check := fmt.Sprintf("%x", md5.Sum(data)) + log.Printf("[DEBUG] getObject %s: check: %s", cosFile, check) + if check != checksum { + err = fmt.Errorf("failed to open file at %v: checksum mismatch, %s != %s", cosFile, check, checksum) + return + } + + return +} + +// putObject put object to remote +func (c *remoteClient) putObject(cosFile string, data []byte) error { + opt := &cos.ObjectPutOptions{ + ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ + XCosMetaXXX: &http.Header{ + "X-Cos-Meta-Md5": []string{fmt.Sprintf("%x", md5.Sum(data))}, + }, + }, + ACLHeaderOptions: &cos.ACLHeaderOptions{ + XCosACL: c.acl, + }, + } + + if c.encrypt { + opt.ObjectPutHeaderOptions.XCosServerSideEncryption = "AES256" + } + + r := bytes.NewReader(data) + rsp, err := c.cosClient.Object.Put(c.cosContext, cosFile, r, opt) + if rsp == nil { + log.Printf("[DEBUG] putObject %s: error: %v", cosFile, err) + return fmt.Errorf("failed to save file to %v: %v", cosFile, err) + } + defer rsp.Body.Close() + + log.Printf("[DEBUG] putObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) + if err != nil { + return fmt.Errorf("failed to save file to %v: %v", cosFile, err) + } + + return nil +} + +// deleteObject delete remote object +func (c *remoteClient) deleteObject(cosFile string) error { + rsp, err := c.cosClient.Object.Delete(c.cosContext, cosFile) + if rsp == nil { + log.Printf("[DEBUG] deleteObject %s: error: %v", cosFile, err) + return fmt.Errorf("failed to delete file %v: %v", cosFile, err) + } + defer rsp.Body.Close() + + log.Printf("[DEBUG] deleteObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) + if rsp.StatusCode == 404 { + return nil + } + + if err != nil { + return fmt.Errorf("failed to delete file %v: %v", cosFile, err) + } + + return nil +} + +// getBucket list bucket by prefix +func (c *remoteClient) getBucket(prefix string) (obs []cos.Object, err error) { + fs, rsp, err := c.cosClient.Bucket.Get(c.cosContext, &cos.BucketGetOptions{Prefix: prefix}) + if rsp == nil { + log.Printf("[DEBUG] getBucket %s/%s: error: %v", c.bucket, prefix, err) + err = fmt.Errorf("bucket %s not exists", c.bucket) + return + } + defer rsp.Body.Close() + + log.Printf("[DEBUG] getBucket %s/%s: code: %d, error: %v", c.bucket, prefix, rsp.StatusCode, err) + if rsp.StatusCode == 404 { + err = fmt.Errorf("bucket %s not exists", c.bucket) + return + } + + if err != nil { + return + } + + return fs.Contents, nil +} + +// putBucket create cos bucket +func (c *remoteClient) putBucket() error { + rsp, err := c.cosClient.Bucket.Put(c.cosContext, nil) + if rsp == nil { + log.Printf("[DEBUG] putBucket %s: error: %v", c.bucket, err) + return fmt.Errorf("failed to create bucket %v: %v", c.bucket, err) + } + defer rsp.Body.Close() + + log.Printf("[DEBUG] putBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err) + if rsp.StatusCode == 409 { + return nil + } + + if err != nil { + return fmt.Errorf("failed to create bucket %v: %v", c.bucket, err) + } + + return nil +} + +// deleteBucket delete cos bucket +func (c *remoteClient) deleteBucket(recursive bool) error { + if recursive { + obs, err := c.getBucket("") + if err != nil { + if strings.Contains(err.Error(), "not exists") { + return nil + } + log.Printf("[DEBUG] deleteBucket %s: empty bucket error: %v", c.bucket, err) + return fmt.Errorf("failed to empty bucket %v: %v", c.bucket, err) + } + for _, v := range obs { + c.deleteObject(v.Key) + } + } + + rsp, err := c.cosClient.Bucket.Delete(c.cosContext) + if rsp == nil { + log.Printf("[DEBUG] deleteBucket %s: error: %v", c.bucket, err) + return fmt.Errorf("failed to delete bucket %v: %v", c.bucket, err) + } + defer rsp.Body.Close() + + log.Printf("[DEBUG] deleteBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err) + if rsp.StatusCode == 404 { + return nil + } + + if err != nil { + return fmt.Errorf("failed to delete bucket %v: %v", c.bucket, err) + } + + return nil +} + +// cosLock lock cos for writing +func (c *remoteClient) cosLock(bucket, cosFile string) error { + log.Printf("[DEBUG] lock cos file %s:%s", bucket, cosFile) + + cosPath := fmt.Sprintf("%s:%s", bucket, cosFile) + lockTagValue := fmt.Sprintf("%x", md5.Sum([]byte(cosPath))) + + return c.CreateTag(lockTagKey, lockTagValue) +} + +// cosUnlock unlock cos writing +func (c *remoteClient) cosUnlock(bucket, cosFile string) error { + log.Printf("[DEBUG] unlock cos file %s:%s", bucket, cosFile) + + cosPath := fmt.Sprintf("%s:%s", bucket, cosFile) + lockTagValue := fmt.Sprintf("%x", md5.Sum([]byte(cosPath))) + + var err error + for i := 0; i < 30; i++ { + err = c.DeleteTag(lockTagKey, lockTagValue) + if err == nil { + return nil + } + time.Sleep(1 * time.Second) + } + + return err +} + +// CreateTag create tag by key and value +func (c *remoteClient) CreateTag(key, value string) error { + request := tag.NewCreateTagRequest() + request.TagKey = &key + request.TagValue = &value + + _, err := c.tagClient.CreateTag(request) + log.Printf("[DEBUG] create tag %s:%s: error: %v", key, value, err) + if err != nil { + return fmt.Errorf("failed to create tag: %s -> %s: %s", key, value, err) + } + + return nil +} + +// DeleteTag create tag by key and value +func (c *remoteClient) DeleteTag(key, value string) error { + request := tag.NewDeleteTagRequest() + request.TagKey = &key + request.TagValue = &value + + _, err := c.tagClient.DeleteTag(request) + log.Printf("[DEBUG] delete tag %s:%s: error: %v", key, value, err) + if err != nil { + return fmt.Errorf("failed to delete tag: %s -> %s: %s", key, value, err) + } + + return nil +} diff --git a/go.mod b/go.mod index e20dab3d7..f23680977 100644 --- a/go.mod +++ b/go.mod @@ -84,6 +84,7 @@ require ( github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba // indirect github.com/lib/pq v1.0.0 + github.com/likexian/gokit v0.20.15 github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82 github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b github.com/mattn/go-colorable v0.1.1 @@ -113,6 +114,8 @@ require ( github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect github.com/soheilhy/cmux v0.1.4 // indirect github.com/spf13/afero v1.2.1 + github.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible + github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c github.com/terraform-providers/terraform-provider-openstack v1.15.0 github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5 // indirect diff --git a/go.sum b/go.sum index 83f672304..2c5eb21d6 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022 h1:y8Gs8CzNfDF5AZvjr+5UyGQvQEBL7pwo+v+wX6q9JI8= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292 h1:tuQ7w+my8a8mkwN7x2TSd7OzTjkZ7rAeSyH4xncuAMI= github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14= @@ -285,6 +286,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/likexian/gokit v0.0.0-20190309162924-0a377eecf7aa/go.mod h1:QdfYv6y6qPA9pbBA2qXtoT8BMKha6UyNbxWGWl/9Jfk= +github.com/likexian/gokit v0.0.0-20190418170008-ace88ad0983b/go.mod h1:KKqSnk/VVSW8kEyO2vVCXoanzEutKdlBAPohmGXkxCk= +github.com/likexian/gokit v0.0.0-20190501133040-e77ea8b19cdc/go.mod h1:3kvONayqCaj+UgrRZGpgfXzHdMYCAO0KAt4/8n0L57Y= +github.com/likexian/gokit v0.20.15 h1:DgtIqqTRFqtbiLJFzuRESwVrxWxfs8OlY6hnPYBa3BM= +github.com/likexian/gokit v0.20.15/go.mod h1:kn+nTv3tqh6yhor9BC4Lfiu58SmH8NmQ2PmEl+uM6nU= +github.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:Typ1BfnATYtZ/+/shXfFYLrovhFyuKvzwrdOnIDHlmg= +github.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec= +github.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8= github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82 h1:wnfcqULT+N2seWf6y4yHzmi7GD2kNx4Ute0qArktD48= github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9 h1:SmVbOZFWAlyQshuMfOkiAx1f5oUTsOGG5IXplAEYeeM= @@ -336,6 +345,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= @@ -392,6 +403,10 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d h1:Z4EH+5EffvBEhh37F0C0DnpklTMh00JOkjW5zK3ofBI= github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw= +github.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible h1:5Td2b0yfaOvw9M9nZ5Oav6Li9bxUNxt4DgxMfIPpsa0= +github.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4= +github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c h1:iRD1CqtWUjgEVEmjwTMbP1DMzz1HRytOsgx/rlw/vNs= +github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c/go.mod h1:wk2XFUg6egk4tSDNZtXeKfe2G6690UVyt163PuUxBZk= github.com/terraform-providers/terraform-provider-openstack v1.15.0 h1:adpjqej+F8BAX9dHmuPF47sUIkgifeqBu6p7iCsyj0Y= github.com/terraform-providers/terraform-provider-openstack v1.15.0/go.mod h1:2aQ6n/BtChAl1y2S60vebhyJyZXBsuAI5G4+lHrT1Ew= github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc= diff --git a/vendor/github.com/likexian/gokit/LICENSE b/vendor/github.com/likexian/gokit/LICENSE new file mode 100644 index 000000000..42d4ca144 --- /dev/null +++ b/vendor/github.com/likexian/gokit/LICENSE @@ -0,0 +1,205 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2012-2019 Li Kexian + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + APPENDIX: Copyright 2012-2019 Li Kexian + + https://www.likexian.com/ diff --git a/vendor/github.com/likexian/gokit/assert/README.md b/vendor/github.com/likexian/gokit/assert/README.md new file mode 100644 index 000000000..8ecb799ce --- /dev/null +++ b/vendor/github.com/likexian/gokit/assert/README.md @@ -0,0 +1,98 @@ +# GoKit - assert + +Assert kits for Golang development. + +## Installation + + go get -u github.com/likexian/gokit + +## Importing + + import ( + "github.com/likexian/gokit/assert" + ) + +## Documentation + +Visit the docs on [GoDoc](https://godoc.org/github.com/likexian/gokit/assert) + +## Example + +### assert panic + +```go +func willItPanic() { + panic("failed") +} +assert.Panic(t, willItPanic) +``` + +### assert err is nil + +```go +fp, err := os.Open("/data/dev/gokit/LICENSE") +assert.Nil(t, err) +``` + +### assert equal + +```go +x := map[string]int{"a": 1, "b": 2} +y := map[string]int{"a": 1, "b": 2} +assert.Equal(t, x, y, "x shall equal to y") +``` + +### check string in array + +```go +ok := assert.IsContains([]string{"a", "b", "c"}, "b") +if ok { + fmt.Println("value in array") +} else { + fmt.Println("value not in array") +} +``` + +### check string in interface array + +```go +ok := assert.IsContains([]interface{}{0, "1", 2}, "1") +if ok { + fmt.Println("value in array") +} else { + fmt.Println("value not in array") +} +``` + +### check object in struct array + +```go +ok := assert.IsContains([]A{A{0, 1}, A{1, 2}, A{1, 3}}, A{1, 2}) +if ok { + fmt.Println("value in array") +} else { + fmt.Println("value not in array") +} +``` + +### a := c ? x : y + +```go +a := 1 +// b := a == 1 ? true : false +b := assert.If(a == 1, true, false) +``` + +## LICENSE + +Copyright 2012-2019 Li Kexian + +Licensed under the Apache License 2.0 + +## About + +- [Li Kexian](https://www.likexian.com/) + +## DONATE + +- [Help me make perfect](https://www.likexian.com/donate/) diff --git a/vendor/github.com/likexian/gokit/assert/assert.go b/vendor/github.com/likexian/gokit/assert/assert.go new file mode 100644 index 000000000..ddc4a6688 --- /dev/null +++ b/vendor/github.com/likexian/gokit/assert/assert.go @@ -0,0 +1,202 @@ +/* + * Copyright 2012-2019 Li Kexian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * A toolkit for Golang development + * https://www.likexian.com/ + */ + +package assert + +import ( + "fmt" + "reflect" + "runtime" + "testing" +) + +// Version returns package version +func Version() string { + return "0.10.1" +} + +// Author returns package author +func Author() string { + return "[Li Kexian](https://www.likexian.com/)" +} + +// License returns package license +func License() string { + return "Licensed under the Apache License 2.0" +} + +// Equal assert test value to be equal +func Equal(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, got, exp, 1, args...) +} + +// NotEqual assert test value to be not equal +func NotEqual(t *testing.T, got, exp interface{}, args ...interface{}) { + notEqual(t, got, exp, 1, args...) +} + +// Nil assert test value to be nil +func Nil(t *testing.T, got interface{}, args ...interface{}) { + equal(t, got, nil, 1, args...) +} + +// NotNil assert test value to be not nil +func NotNil(t *testing.T, got interface{}, args ...interface{}) { + notEqual(t, got, nil, 1, args...) +} + +// True assert test value to be true +func True(t *testing.T, got interface{}, args ...interface{}) { + equal(t, got, true, 1, args...) +} + +// False assert test value to be false +func False(t *testing.T, got interface{}, args ...interface{}) { + notEqual(t, got, true, 1, args...) +} + +// Zero assert test value to be zero value +func Zero(t *testing.T, got interface{}, args ...interface{}) { + equal(t, IsZero(got), true, 1, args...) +} + +// NotZero assert test value to be not zero value +func NotZero(t *testing.T, got interface{}, args ...interface{}) { + notEqual(t, IsZero(got), true, 1, args...) +} + +// Len assert length of test vaue to be exp +func Len(t *testing.T, got interface{}, exp int, args ...interface{}) { + equal(t, Length(got), exp, 1, args...) +} + +// NotLen assert length of test vaue to be not exp +func NotLen(t *testing.T, got interface{}, exp int, args ...interface{}) { + notEqual(t, Length(got), exp, 1, args...) +} + +// Contains assert test value to be contains +func Contains(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, IsContains(got, exp), true, 1, args...) +} + +// NotContains assert test value to be contains +func NotContains(t *testing.T, got, exp interface{}, args ...interface{}) { + notEqual(t, IsContains(got, exp), true, 1, args...) +} + +// Match assert test value match exp pattern +func Match(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, IsMatch(got, exp), true, 1, args...) +} + +// NotMatch assert test value not match exp pattern +func NotMatch(t *testing.T, got, exp interface{}, args ...interface{}) { + notEqual(t, IsMatch(got, exp), true, 1, args...) +} + +// Lt assert test value less than exp +func Lt(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, IsLt(got, exp), true, 1, args...) +} + +// Le assert test value less than exp or equal +func Le(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, IsLe(got, exp), true, 1, args...) +} + +// Gt assert test value greater than exp +func Gt(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, IsGt(got, exp), true, 1, args...) +} + +// Ge assert test value greater than exp or equal +func Ge(t *testing.T, got, exp interface{}, args ...interface{}) { + equal(t, IsGe(got, exp), true, 1, args...) +} + +// Panic assert testing to be panic +func Panic(t *testing.T, fn func(), args ...interface{}) { + defer func() { + ff := func() { + t.Error("! -", "assert expected to be panic") + if len(args) > 0 { + t.Error("! -", fmt.Sprint(args...)) + } + } + ok := recover() != nil + assert(t, ok, ff, 2) + }() + + fn() +} + +// NotPanic assert testing to be panic +func NotPanic(t *testing.T, fn func(), args ...interface{}) { + defer func() { + ff := func() { + t.Error("! -", "assert expected to be not panic") + if len(args) > 0 { + t.Error("! -", fmt.Sprint(args...)) + } + } + ok := recover() == nil + assert(t, ok, ff, 3) + }() + + fn() +} + +func equal(t *testing.T, got, exp interface{}, step int, args ...interface{}) { + fn := func() { + switch got.(type) { + case error: + t.Errorf("! unexpected error: \"%s\"", got) + default: + t.Errorf("! expected %#v, but got %#v", exp, got) + } + if len(args) > 0 { + t.Error("! -", fmt.Sprint(args...)) + } + } + ok := reflect.DeepEqual(exp, got) + assert(t, ok, fn, step+1) +} + +func notEqual(t *testing.T, got, exp interface{}, step int, args ...interface{}) { + fn := func() { + t.Errorf("! unexpected: %#v", got) + if len(args) > 0 { + t.Error("! -", fmt.Sprint(args...)) + } + } + ok := !reflect.DeepEqual(exp, got) + assert(t, ok, fn, step+1) +} + +func assert(t *testing.T, pass bool, fn func(), step int) { + if !pass { + _, file, line, ok := runtime.Caller(step + 1) + if ok { + t.Errorf("%s:%d", file, line) + } + fn() + t.FailNow() + } +} diff --git a/vendor/github.com/likexian/gokit/assert/values.go b/vendor/github.com/likexian/gokit/assert/values.go new file mode 100644 index 000000000..02c303ab1 --- /dev/null +++ b/vendor/github.com/likexian/gokit/assert/values.go @@ -0,0 +1,334 @@ +/* + * Copyright 2012-2019 Li Kexian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * A toolkit for Golang development + * https://www.likexian.com/ + */ + +package assert + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +// ErrInvalid is value invalid for operation +var ErrInvalid = errors.New("value if invalid") + +// ErrLess is expect to be greater error +var ErrLess = errors.New("left is less the right") + +// ErrGreater is expect to be less error +var ErrGreater = errors.New("left is greater then right") + +// CMP is compare operation +var CMP = struct { + LT string + LE string + GT string + GE string +}{ + "<", + "<=", + ">", + ">=", +} + +// IsZero returns value is zero value +func IsZero(v interface{}) bool { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Invalid: + return true + case reflect.Bool: + return !vv.Bool() + case reflect.Ptr, reflect.Interface: + return vv.IsNil() + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return vv.Len() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return vv.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return vv.Uint() == 0 + case reflect.Float32, reflect.Float64: + return vv.Float() == 0 + default: + return false + } +} + +// IsContains returns whether value is within array +func IsContains(array interface{}, value interface{}) bool { + vv := reflect.ValueOf(array) + if vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface { + if vv.IsNil() { + return false + } + vv = vv.Elem() + } + + switch vv.Kind() { + case reflect.Invalid: + return false + case reflect.Slice: + for i := 0; i < vv.Len(); i++ { + if reflect.DeepEqual(value, vv.Index(i).Interface()) { + return true + } + } + return false + case reflect.Map: + s := vv.MapKeys() + for i := 0; i < len(s); i++ { + if reflect.DeepEqual(value, s[i].Interface()) { + return true + } + } + return false + case reflect.String: + ss := reflect.ValueOf(value) + switch ss.Kind() { + case reflect.String: + return strings.Contains(vv.String(), ss.String()) + } + return false + default: + return reflect.DeepEqual(array, value) + } +} + +// IsMatch returns if value v contains any match of pattern r +// IsMatch(regexp.MustCompile("v\d+"), "v100") +// IsMatch("v\d+", "v100") +// IsMatch("\d+\.\d+", 100.1) +func IsMatch(r interface{}, v interface{}) bool { + var re *regexp.Regexp + + if v, ok := r.(*regexp.Regexp); ok { + re = v + } else { + re = regexp.MustCompile(fmt.Sprint(r)) + } + + return re.MatchString(fmt.Sprint(v)) +} + +// Length returns length of value +func Length(v interface{}) int { + vv := reflect.ValueOf(v) + if vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface { + if vv.IsNil() { + return 0 + } + vv = vv.Elem() + } + + switch vv.Kind() { + case reflect.Invalid: + return 0 + case reflect.Ptr, reflect.Interface: + return 0 + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return vv.Len() + default: + return len(fmt.Sprintf("%#v", v)) + } +} + +// IsLt returns if x less than y, value invalid will returns false +func IsLt(x, y interface{}) bool { + return Compare(x, y, CMP.LT) == nil +} + +// IsLe returns if x less than or equal to y, value invalid will returns false +func IsLe(x, y interface{}) bool { + return Compare(x, y, CMP.LE) == nil +} + +// IsGt returns if x greater than y, value invalid will returns false +func IsGt(x, y interface{}) bool { + return Compare(x, y, CMP.GT) == nil +} + +// IsGe returns if x greater than or equal to y, value invalid will returns false +func IsGe(x, y interface{}) bool { + return Compare(x, y, CMP.GE) == nil +} + +// Compare compare x and y, by operation +// It returns nil for true, ErrInvalid for invalid operation, err for false +// Compare(1, 2, ">") // number compare -> true +// Compare("a", "a", ">=") // string compare -> true +// Compare([]string{"a", "b"}, []string{"a"}, "<") // slice len compare -> false +func Compare(x, y interface{}, op string) error { + if !IsContains([]string{CMP.LT, CMP.LE, CMP.GT, CMP.GE}, op) { + return ErrInvalid + } + + vv := reflect.ValueOf(x) + if vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface { + if vv.IsNil() { + return ErrInvalid + } + vv = vv.Elem() + } + + var c float64 + switch vv.Kind() { + case reflect.Invalid: + return ErrInvalid + case reflect.String: + yy := reflect.ValueOf(y) + switch yy.Kind() { + case reflect.String: + c = float64(strings.Compare(vv.String(), yy.String())) + default: + return ErrInvalid + } + case reflect.Slice, reflect.Map, reflect.Array: + yy := reflect.ValueOf(y) + switch yy.Kind() { + case reflect.Slice, reflect.Map, reflect.Array: + c = float64(vv.Len() - yy.Len()) + default: + return ErrInvalid + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + yy, err := ToInt64(y) + if err != nil { + return ErrInvalid + } + c = float64(vv.Int() - yy) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + yy, err := ToUint64(y) + if err != nil { + return ErrInvalid + } + c = float64(vv.Uint()) - float64(yy) + case reflect.Float32, reflect.Float64: + yy, err := ToFloat64(y) + if err != nil { + return ErrInvalid + } + c = float64(vv.Float() - yy) + default: + return ErrInvalid + } + + switch { + case c < 0: + switch op { + case CMP.LT, CMP.LE: + return nil + default: + return ErrLess + } + case c > 0: + switch op { + case CMP.GT, CMP.GE: + return nil + default: + return ErrGreater + } + default: + switch op { + case CMP.LT: + return ErrGreater + case CMP.GT: + return ErrLess + default: + return nil + } + } +} + +// ToInt64 returns int value for int or uint or float +func ToInt64(v interface{}) (int64, error) { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return int64(vv.Int()), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return int64(vv.Uint()), nil + case reflect.Float32, reflect.Float64: + return int64(vv.Float()), nil + case reflect.String: + r, err := strconv.ParseInt(vv.String(), 10, 64) + if err != nil { + return 0, ErrInvalid + } + return r, nil + default: + return 0, ErrInvalid + } +} + +// ToUint64 returns uint value for int or uint or float +func ToUint64(v interface{}) (uint64, error) { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return uint64(vv.Int()), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return uint64(vv.Uint()), nil + case reflect.Float32, reflect.Float64: + return uint64(vv.Float()), nil + case reflect.String: + r, err := strconv.ParseUint(vv.String(), 10, 64) + if err != nil { + return 0, ErrInvalid + } + return r, nil + default: + return 0, ErrInvalid + } +} + +// ToFloat64 returns float64 value for int or uint or float +func ToFloat64(v interface{}) (float64, error) { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return float64(vv.Int()), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return float64(vv.Uint()), nil + case reflect.Float32, reflect.Float64: + return float64(vv.Float()), nil + case reflect.String: + r, err := strconv.ParseFloat(vv.String(), 64) + if err != nil { + return 0, ErrInvalid + } + return r, nil + default: + return 0, ErrInvalid + } +} + +// If returns x if c is true, else y +// z = If(c, x, y) +// equal to: +// z = c ? x : y +func If(c bool, x, y interface{}) interface{} { + if c { + return x + } + + return y +} diff --git a/vendor/github.com/mozillazg/go-httpheader/.bumpversion.cfg b/vendor/github.com/mozillazg/go-httpheader/.bumpversion.cfg new file mode 100644 index 000000000..18dad225a --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/.bumpversion.cfg @@ -0,0 +1,7 @@ +[bumpversion] +commit = True +tag = True +current_version = 0.2.1 + +[bumpversion:file:encode.go] + diff --git a/vendor/github.com/mozillazg/go-httpheader/.gitignore b/vendor/github.com/mozillazg/go-httpheader/.gitignore new file mode 100644 index 000000000..098e254bc --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +dist/ +cover.html +cover.out diff --git a/vendor/github.com/mozillazg/go-httpheader/.travis.yml b/vendor/github.com/mozillazg/go-httpheader/.travis.yml new file mode 100644 index 000000000..fbb97928b --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/.travis.yml @@ -0,0 +1,25 @@ +language: go +go: + - 1.6 + - 1.7 + - 1.8 + - tip + +sudo: false + +before_install: + - go get github.com/mattn/goveralls + +install: + - go get + - go build + +script: + - make test + - $HOME/gopath/bin/goveralls -service=travis-ci -ignore=vendor/ + +matrix: + allow_failures: + - go: 1.6 + - go: 1.7 + - go: tip diff --git a/vendor/github.com/mozillazg/go-httpheader/CHANGELOG.md b/vendor/github.com/mozillazg/go-httpheader/CHANGELOG.md new file mode 100644 index 000000000..7b643fd64 --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## 0.2.1 (2018-11-03) + +* add go.mod file to identify as a module + + +## 0.2.0 (2017-06-24) + +* support http.Header field. + + +## 0.1.0 (2017-06-10) + +* Initial Release diff --git a/vendor/github.com/mozillazg/go-httpheader/LICENSE b/vendor/github.com/mozillazg/go-httpheader/LICENSE new file mode 100644 index 000000000..8ff7942e2 --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 mozillazg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mozillazg/go-httpheader/Makefile b/vendor/github.com/mozillazg/go-httpheader/Makefile new file mode 100644 index 000000000..aadfe8298 --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/Makefile @@ -0,0 +1,15 @@ +help: + @echo "test run test" + @echo "lint run lint" + +.PHONY: test +test: + go test -v -cover -coverprofile cover.out + go tool cover -html=cover.out -o cover.html + +.PHONY: lint +lint: + gofmt -s -w . + goimports -w . + golint . + go vet diff --git a/vendor/github.com/mozillazg/go-httpheader/README.md b/vendor/github.com/mozillazg/go-httpheader/README.md new file mode 100644 index 000000000..d73daf64c --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/README.md @@ -0,0 +1,63 @@ +# go-httpheader + +go-httpheader is a Go library for encoding structs into Header fields. + +[![Build Status](https://img.shields.io/travis/mozillazg/go-httpheader/master.svg)](https://travis-ci.org/mozillazg/go-httpheader) +[![Coverage Status](https://img.shields.io/coveralls/mozillazg/go-httpheader/master.svg)](https://coveralls.io/r/mozillazg/go-httpheader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/mozillazg/go-httpheader)](https://goreportcard.com/report/github.com/mozillazg/go-httpheader) +[![GoDoc](https://godoc.org/github.com/mozillazg/go-httpheader?status.svg)](https://godoc.org/github.com/mozillazg/go-httpheader) + +## install + +`go get -u github.com/mozillazg/go-httpheader` + + +## usage + +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/mozillazg/go-httpheader" +) + +type Options struct { + hide string + ContentType string `header:"Content-Type"` + Length int + XArray []string `header:"X-Array"` + TestHide string `header:"-"` + IgnoreEmpty string `header:"X-Empty,omitempty"` + IgnoreEmptyN string `header:"X-Empty-N,omitempty"` + CustomHeader http.Header +} + +func main() { + opt := Options{ + hide: "hide", + ContentType: "application/json", + Length: 2, + XArray: []string{"test1", "test2"}, + TestHide: "hide", + IgnoreEmptyN: "n", + CustomHeader: http.Header{ + "X-Test-1": []string{"233"}, + "X-Test-2": []string{"666"}, + }, + } + h, _ := httpheader.Header(opt) + fmt.Printf("%#v", h) + // h: + // http.Header{ + // "X-Test-1": []string{"233"}, + // "X-Test-2": []string{"666"}, + // "Content-Type": []string{"application/json"}, + // "Length": []string{"2"}, + // "X-Array": []string{"test1", "test2"}, + // "X-Empty-N": []string{"n"}, + //} +} +``` diff --git a/vendor/github.com/mozillazg/go-httpheader/encode.go b/vendor/github.com/mozillazg/go-httpheader/encode.go new file mode 100644 index 000000000..c122f2b88 --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/encode.go @@ -0,0 +1,290 @@ +// Package query implements encoding of structs into http.Header fields. +// +// As a simple example: +// +// type Options struct { +// ContentType string `header:"Content-Type"` +// Length int +// } +// +// opt := Options{"application/json", 2} +// h, _ := httpheader.Header(opt) +// fmt.Printf("%#v", h) +// // will output: +// // http.Header{"Content-Type":[]string{"application/json"},"Length":[]string{"2"}} +// +// The exact mapping between Go values and http.Header is described in the +// documentation for the Header() function. +package httpheader + +import ( + "fmt" + "net/http" + "reflect" + "strconv" + "strings" + "time" +) + +const tagName = "header" + +// Version ... +const Version = "0.2.1" + +var timeType = reflect.TypeOf(time.Time{}) +var headerType = reflect.TypeOf(http.Header{}) + +var encoderType = reflect.TypeOf(new(Encoder)).Elem() + +// Encoder is an interface implemented by any type that wishes to encode +// itself into Header fields in a non-standard way. +type Encoder interface { + EncodeHeader(key string, v *http.Header) error +} + +// Header returns the http.Header encoding of v. +// +// Header expects to be passed a struct, and traverses it recursively using the +// following encoding rules. +// +// Each exported struct field is encoded as a Header field unless +// +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option +// +// The empty values are false, 0, any nil pointer or interface value, any array +// slice, map, or string of length zero, and any time.Time that returns true +// for IsZero(). +// +// The Header field name defaults to the struct field name but can be +// specified in the struct field's tag value. The "header" key in the struct +// field's tag value is the key name, followed by an optional comma and +// options. For example: +// +// // Field is ignored by this package. +// Field int `header:"-"` +// +// // Field appears as Header field "X-Name". +// Field int `header:"X-Name"` +// +// // Field appears as Header field "X-Name" and the field is omitted if +// // its value is empty +// Field int `header:"X-Name,omitempty"` +// +// // Field appears as Header field "Field" (the default), but the field +// // is skipped if empty. Note the leading comma. +// Field int `header:",omitempty"` +// +// For encoding individual field values, the following type-dependent rules +// apply: +// +// Boolean values default to encoding as the strings "true" or "false". +// Including the "int" option signals that the field should be encoded as the +// strings "1" or "0". +// +// time.Time values default to encoding as RFC1123("Mon, 02 Jan 2006 15:04:05 GMT") +// timestamps. Including the "unix" option signals that the field should be +// encoded as a Unix time (see time.Unix()) +// +// Slice and Array values default to encoding as multiple Header values of the +// same name. example: +// X-Name: []string{"Tom", "Jim"}, etc. +// +// http.Header values will be used to extend the Header fields. +// +// Anonymous struct fields are usually encoded as if their inner exported +// fields were fields in the outer struct, subject to the standard Go +// visibility rules. An anonymous struct field with a name given in its Header +// tag is treated as having that name, rather than being anonymous. +// +// Non-nil pointer values are encoded as the value pointed to. +// +// All other values are encoded using their default string representation. +// +// Multiple fields that encode to the same Header filed name will be included +// as multiple Header values of the same name. +func Header(v interface{}) (http.Header, error) { + h := make(http.Header) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return h, nil + } + val = val.Elem() + } + + if v == nil { + return h, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("httpheader: Header() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(h, val) + return h, err +} + +// reflectValue populates the header fields from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(header http.Header, val reflect.Value) error { + var embedded []reflect.Value + + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get(tagName) + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if name == "" { + if sf.Anonymous && sv.Kind() == reflect.Struct { + // save embedded struct for later processing + embedded = append(embedded, sv) + continue + } + + name = sf.Name + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(encoderType) { + if !reflect.Indirect(sv).IsValid() { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(Encoder) + if err := m.EncodeHeader(name, &header); err != nil { + return err + } + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + for i := 0; i < sv.Len(); i++ { + k := name + header.Add(k, valueString(sv.Index(i), opts)) + } + continue + } + + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == timeType { + header.Add(name, valueString(sv, opts)) + continue + } + if sv.Type() == headerType { + h := sv.Interface().(http.Header) + for k, vs := range h { + for _, v := range vs { + header.Add(k, v) + } + } + continue + } + + if sv.Kind() == reflect.Struct { + reflectValue(header, sv) + continue + } + + header.Add(name, valueString(sv, opts)) + } + + for _, f := range embedded { + if err := reflectValue(header, f); err != nil { + return err + } + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Kind() == reflect.Bool && opts.Contains("int") { + if v.Bool() { + return "1" + } + return "0" + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if opts.Contains("unix") { + return strconv.FormatInt(t.Unix(), 10) + } + return t.Format(http.TimeFormat) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + + if v.Type() == timeType { + return v.Interface().(time.Time).IsZero() + } + + return false +} + +// tagOptions is the string following a comma in a struct field's "header" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's header tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/vendor/github.com/mozillazg/go-httpheader/go.mod b/vendor/github.com/mozillazg/go-httpheader/go.mod new file mode 100644 index 000000000..27af234eb --- /dev/null +++ b/vendor/github.com/mozillazg/go-httpheader/go.mod @@ -0,0 +1 @@ +module github.com/mozillazg/go-httpheader diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE new file mode 100644 index 000000000..efc75a225 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2017-2018 Tencent Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go new file mode 100644 index 000000000..1927cb568 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go @@ -0,0 +1,261 @@ +package common + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httputil" + "strconv" + "strings" + "time" + + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" +) + +type Client struct { + region string + httpClient *http.Client + httpProfile *profile.HttpProfile + profile *profile.ClientProfile + credential *Credential + signMethod string + unsignedPayload bool + debug bool +} + +func (c *Client) Send(request tchttp.Request, response tchttp.Response) (err error) { + if request.GetDomain() == "" { + domain := c.httpProfile.Endpoint + if domain == "" { + domain = tchttp.GetServiceDomain(request.GetService()) + } + request.SetDomain(domain) + } + + if request.GetHttpMethod() == "" { + request.SetHttpMethod(c.httpProfile.ReqMethod) + } + + tchttp.CompleteCommonParams(request, c.GetRegion()) + + if c.signMethod == "HmacSHA1" || c.signMethod == "HmacSHA256" { + return c.sendWithSignatureV1(request, response) + } else { + return c.sendWithSignatureV3(request, response) + } +} + +func (c *Client) sendWithSignatureV1(request tchttp.Request, response tchttp.Response) (err error) { + // TODO: not an elegant way, it should be done in common params, but finally it need to refactor + request.GetParams()["Language"] = c.profile.Language + err = tchttp.ConstructParams(request) + if err != nil { + return err + } + err = signRequest(request, c.credential, c.signMethod) + if err != nil { + return err + } + httpRequest, err := http.NewRequest(request.GetHttpMethod(), request.GetUrl(), request.GetBodyReader()) + if err != nil { + return err + } + if request.GetHttpMethod() == "POST" { + httpRequest.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"} + } + if c.debug { + outbytes, err := httputil.DumpRequest(httpRequest, true) + if err != nil { + log.Printf("[ERROR] dump request failed because %s", err) + return err + } + log.Printf("[DEBUG] http request = %s", outbytes) + } + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return err + } + err = tchttp.ParseFromHttpResponse(httpResponse, response) + return err +} + +func (c *Client) sendWithSignatureV3(request tchttp.Request, response tchttp.Response) (err error) { + headers := map[string]string{ + "Host": request.GetDomain(), + "X-TC-Action": request.GetAction(), + "X-TC-Version": request.GetVersion(), + "X-TC-Timestamp": request.GetParams()["Timestamp"], + "X-TC-RequestClient": request.GetParams()["RequestClient"], + "X-TC-Language": c.profile.Language, + } + if c.region != "" { + headers["X-TC-Region"] = c.region + } + if c.credential.Token != "" { + headers["X-TC-Token"] = c.credential.Token + } + if request.GetHttpMethod() == "GET" { + headers["Content-Type"] = "application/x-www-form-urlencoded" + } else { + headers["Content-Type"] = "application/json" + } + + // start signature v3 process + + // build canonical request string + httpRequestMethod := request.GetHttpMethod() + canonicalURI := "/" + canonicalQueryString := "" + if httpRequestMethod == "GET" { + err = tchttp.ConstructParams(request) + if err != nil { + return err + } + params := make(map[string]string) + for key, value := range request.GetParams() { + params[key] = value + } + delete(params, "Action") + delete(params, "Version") + delete(params, "Nonce") + delete(params, "Region") + delete(params, "RequestClient") + delete(params, "Timestamp") + canonicalQueryString = tchttp.GetUrlQueriesEncoded(params) + } + canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\n", headers["Content-Type"], headers["Host"]) + signedHeaders := "content-type;host" + requestPayload := "" + if httpRequestMethod == "POST" { + b, err := json.Marshal(request) + if err != nil { + return err + } + requestPayload = string(b) + } + hashedRequestPayload := "" + if c.unsignedPayload { + hashedRequestPayload = sha256hex("UNSIGNED-PAYLOAD") + headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD" + } else { + hashedRequestPayload = sha256hex(requestPayload) + } + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", + httpRequestMethod, + canonicalURI, + canonicalQueryString, + canonicalHeaders, + signedHeaders, + hashedRequestPayload) + //log.Println("canonicalRequest:", canonicalRequest) + + // build string to sign + algorithm := "TC3-HMAC-SHA256" + requestTimestamp := headers["X-TC-Timestamp"] + timestamp, _ := strconv.ParseInt(requestTimestamp, 10, 64) + t := time.Unix(timestamp, 0).UTC() + // must be the format 2006-01-02, ref to package time for more info + date := t.Format("2006-01-02") + credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, request.GetService()) + hashedCanonicalRequest := sha256hex(canonicalRequest) + string2sign := fmt.Sprintf("%s\n%s\n%s\n%s", + algorithm, + requestTimestamp, + credentialScope, + hashedCanonicalRequest) + //log.Println("string2sign", string2sign) + + // sign string + secretDate := hmacsha256(date, "TC3"+c.credential.SecretKey) + secretService := hmacsha256(request.GetService(), secretDate) + secretKey := hmacsha256("tc3_request", secretService) + signature := hex.EncodeToString([]byte(hmacsha256(string2sign, secretKey))) + //log.Println("signature", signature) + + // build authorization + authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", + algorithm, + c.credential.SecretId, + credentialScope, + signedHeaders, + signature) + //log.Println("authorization", authorization) + + headers["Authorization"] = authorization + url := "https://" + request.GetDomain() + request.GetPath() + if canonicalQueryString != "" { + url = url + "?" + canonicalQueryString + } + httpRequest, err := http.NewRequest(httpRequestMethod, url, strings.NewReader(requestPayload)) + if err != nil { + return err + } + for k, v := range headers { + httpRequest.Header[k] = []string{v} + } + if c.debug { + outbytes, err := httputil.DumpRequest(httpRequest, true) + if err != nil { + log.Printf("[ERROR] dump request failed because %s", err) + return err + } + log.Printf("[DEBUG] http request = %s", outbytes) + } + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return err + } + err = tchttp.ParseFromHttpResponse(httpResponse, response) + return err +} + +func (c *Client) GetRegion() string { + return c.region +} + +func (c *Client) Init(region string) *Client { + c.httpClient = &http.Client{} + c.region = region + c.signMethod = "TC3-HMAC-SHA256" + c.debug = false + log.SetFlags(log.LstdFlags | log.Lshortfile) + return c +} + +func (c *Client) WithSecretId(secretId, secretKey string) *Client { + c.credential = NewCredential(secretId, secretKey) + return c +} + +func (c *Client) WithCredential(cred *Credential) *Client { + c.credential = cred + return c +} + +func (c *Client) WithProfile(clientProfile *profile.ClientProfile) *Client { + c.profile = clientProfile + c.signMethod = clientProfile.SignMethod + c.unsignedPayload = clientProfile.UnsignedPayload + c.httpProfile = clientProfile.HttpProfile + c.httpClient.Timeout = time.Duration(c.httpProfile.ReqTimeout) * time.Second + return c +} + +func (c *Client) WithSignatureMethod(method string) *Client { + c.signMethod = method + return c +} + +func (c *Client) WithHttpTransport(transport http.RoundTripper) *Client { + c.httpClient.Transport = transport + return c +} + +func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) { + client = &Client{} + client.Init(region).WithSecretId(secretId, secretKey) + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go new file mode 100644 index 000000000..b734c1373 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go @@ -0,0 +1,58 @@ +package common + +type Credential struct { + SecretId string + SecretKey string + Token string +} + +func NewCredential(secretId, secretKey string) *Credential { + return &Credential{ + SecretId: secretId, + SecretKey: secretKey, + } +} + +func NewTokenCredential(secretId, secretKey, token string) *Credential { + return &Credential{ + SecretId: secretId, + SecretKey: secretKey, + Token: token, + } +} + +func (c *Credential) GetCredentialParams() map[string]string { + p := map[string]string{ + "SecretId": c.SecretId, + } + if c.Token != "" { + p["Token"] = c.Token + } + return p +} + +// Nowhere use them and we haven't well designed these structures and +// underlying method, which leads to the situation that it is hard to +// refactor it to interfaces. +// Hence they are removed and merged into Credential. + +//type TokenCredential struct { +// SecretId string +// SecretKey string +// Token string +//} + +//func NewTokenCredential(secretId, secretKey, token string) *TokenCredential { +// return &TokenCredential{ +// SecretId: secretId, +// SecretKey: secretKey, +// Token: token, +// } +//} + +//func (c *TokenCredential) GetCredentialParams() map[string]string { +// return map[string]string{ +// "SecretId": c.SecretId, +// "Token": c.Token, +// } +//} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go new file mode 100644 index 000000000..27589e59a --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go @@ -0,0 +1,35 @@ +package errors + +import ( + "fmt" +) + +type TencentCloudSDKError struct { + Code string + Message string + RequestId string +} + +func (e *TencentCloudSDKError) Error() string { + return fmt.Sprintf("[TencentCloudSDKError] Code=%s, Message=%s, RequestId=%s", e.Code, e.Message, e.RequestId) +} + +func NewTencentCloudSDKError(code, message, requestId string) error { + return &TencentCloudSDKError{ + Code: code, + Message: message, + RequestId: requestId, + } +} + +func (e *TencentCloudSDKError) GetCode() string { + return e.Code +} + +func (e *TencentCloudSDKError) GetMessage() string { + return e.Message +} + +func (e *TencentCloudSDKError) GetRequestId() string { + return e.RequestId +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go new file mode 100644 index 000000000..c7912ad18 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go @@ -0,0 +1,233 @@ +package common + +import ( + "io" + //"log" + "math/rand" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +const ( + POST = "POST" + GET = "GET" + + RootDomain = "tencentcloudapi.com" + Path = "/" +) + +type Request interface { + GetAction() string + GetBodyReader() io.Reader + GetDomain() string + GetHttpMethod() string + GetParams() map[string]string + GetPath() string + GetService() string + GetUrl() string + GetVersion() string + SetDomain(string) + SetHttpMethod(string) +} + +type BaseRequest struct { + httpMethod string + domain string + path string + params map[string]string + formParams map[string]string + + service string + version string + action string +} + +func (r *BaseRequest) GetAction() string { + return r.action +} + +func (r *BaseRequest) GetHttpMethod() string { + return r.httpMethod +} + +func (r *BaseRequest) GetParams() map[string]string { + return r.params +} + +func (r *BaseRequest) GetPath() string { + return r.path +} + +func (r *BaseRequest) GetDomain() string { + return r.domain +} + +func (r *BaseRequest) SetDomain(domain string) { + r.domain = domain +} + +func (r *BaseRequest) SetHttpMethod(method string) { + switch strings.ToUpper(method) { + case POST: + { + r.httpMethod = POST + } + case GET: + { + r.httpMethod = GET + } + default: + { + r.httpMethod = GET + } + } +} + +func (r *BaseRequest) GetService() string { + return r.service +} + +func (r *BaseRequest) GetUrl() string { + if r.httpMethod == GET { + return "https://" + r.domain + r.path + "?" + GetUrlQueriesEncoded(r.params) + } else if r.httpMethod == POST { + return "https://" + r.domain + r.path + } else { + return "" + } +} + +func (r *BaseRequest) GetVersion() string { + return r.version +} + +func GetUrlQueriesEncoded(params map[string]string) string { + values := url.Values{} + for key, value := range params { + if value != "" { + values.Add(key, value) + } + } + return values.Encode() +} + +func (r *BaseRequest) GetBodyReader() io.Reader { + if r.httpMethod == POST { + s := GetUrlQueriesEncoded(r.params) + return strings.NewReader(s) + } else { + return strings.NewReader("") + } +} + +func (r *BaseRequest) Init() *BaseRequest { + r.domain = "" + r.path = Path + r.params = make(map[string]string) + r.formParams = make(map[string]string) + return r +} + +func (r *BaseRequest) WithApiInfo(service, version, action string) *BaseRequest { + r.service = service + r.version = version + r.action = action + return r +} + +func GetServiceDomain(service string) (domain string) { + domain = service + "." + RootDomain + return +} + +func CompleteCommonParams(request Request, region string) { + params := request.GetParams() + params["Region"] = region + if request.GetVersion() != "" { + params["Version"] = request.GetVersion() + } + params["Action"] = request.GetAction() + params["Timestamp"] = strconv.FormatInt(time.Now().Unix(), 10) + params["Nonce"] = strconv.Itoa(rand.Int()) + params["RequestClient"] = "SDK_GO_3.0.82" +} + +func ConstructParams(req Request) (err error) { + value := reflect.ValueOf(req).Elem() + err = flatStructure(value, req, "") + //log.Printf("[DEBUG] params=%s", req.GetParams()) + return +} + +func flatStructure(value reflect.Value, request Request, prefix string) (err error) { + //log.Printf("[DEBUG] reflect value: %v", value.Type()) + valueType := value.Type() + for i := 0; i < valueType.NumField(); i++ { + tag := valueType.Field(i).Tag + nameTag, hasNameTag := tag.Lookup("name") + if !hasNameTag { + continue + } + field := value.Field(i) + kind := field.Kind() + if kind == reflect.Ptr && field.IsNil() { + continue + } + if kind == reflect.Ptr { + field = field.Elem() + kind = field.Kind() + } + key := prefix + nameTag + if kind == reflect.String { + s := field.String() + if s != "" { + request.GetParams()[key] = s + } + } else if kind == reflect.Bool { + request.GetParams()[key] = strconv.FormatBool(field.Bool()) + } else if kind == reflect.Int || kind == reflect.Int64 { + request.GetParams()[key] = strconv.FormatInt(field.Int(), 10) + } else if kind == reflect.Uint || kind == reflect.Uint64 { + request.GetParams()[key] = strconv.FormatUint(field.Uint(), 10) + } else if kind == reflect.Float64 { + request.GetParams()[key] = strconv.FormatFloat(field.Float(), 'f', -1, 64) + } else if kind == reflect.Slice { + list := value.Field(i) + for j := 0; j < list.Len(); j++ { + vj := list.Index(j) + key := prefix + nameTag + "." + strconv.Itoa(j) + kind = vj.Kind() + if kind == reflect.Ptr && vj.IsNil() { + continue + } + if kind == reflect.Ptr { + vj = vj.Elem() + kind = vj.Kind() + } + if kind == reflect.String { + request.GetParams()[key] = vj.String() + } else if kind == reflect.Bool { + request.GetParams()[key] = strconv.FormatBool(vj.Bool()) + } else if kind == reflect.Int || kind == reflect.Int64 { + request.GetParams()[key] = strconv.FormatInt(vj.Int(), 10) + } else if kind == reflect.Uint || kind == reflect.Uint64 { + request.GetParams()[key] = strconv.FormatUint(vj.Uint(), 10) + } else if kind == reflect.Float64 { + request.GetParams()[key] = strconv.FormatFloat(vj.Float(), 'f', -1, 64) + } else { + if err = flatStructure(vj, request, key+"."); err != nil { + return + } + } + } + } else { + if err = flatStructure(reflect.ValueOf(field.Interface()), request, prefix+nameTag+"."); err != nil { + return + } + } + } + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go new file mode 100644 index 000000000..288f21bdf --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go @@ -0,0 +1,81 @@ +package common + +import ( + "encoding/json" + "fmt" + "io/ioutil" + //"log" + "net/http" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" +) + +type Response interface { + ParseErrorFromHTTPResponse(body []byte) error +} + +type BaseResponse struct { +} + +type ErrorResponse struct { + Response struct { + Error struct { + Code string `json:"Code"` + Message string `json:"Message"` + } `json:"Error" omitempty` + RequestId string `json:"RequestId"` + } `json:"Response"` +} + +type DeprecatedAPIErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` + CodeDesc string `json:"codeDesc"` +} + +func (r *BaseResponse) ParseErrorFromHTTPResponse(body []byte) (err error) { + resp := &ErrorResponse{} + err = json.Unmarshal(body, resp) + if err != nil { + msg := fmt.Sprintf("Fail to parse json content: %s, because: %s", body, err) + return errors.NewTencentCloudSDKError("ClientError.ParseJsonError", msg, "") + } + if resp.Response.Error.Code != "" { + return errors.NewTencentCloudSDKError(resp.Response.Error.Code, resp.Response.Error.Message, resp.Response.RequestId) + } + + deprecated := &DeprecatedAPIErrorResponse{} + err = json.Unmarshal(body, deprecated) + if err != nil { + msg := fmt.Sprintf("Fail to parse json content: %s, because: %s", body, err) + return errors.NewTencentCloudSDKError("ClientError.ParseJsonError", msg, "") + } + if deprecated.Code != 0 { + return errors.NewTencentCloudSDKError(deprecated.CodeDesc, deprecated.Message, "") + } + return nil +} + +func ParseFromHttpResponse(hr *http.Response, response Response) (err error) { + defer hr.Body.Close() + body, err := ioutil.ReadAll(hr.Body) + if err != nil { + msg := fmt.Sprintf("Fail to read response body because %s", err) + return errors.NewTencentCloudSDKError("ClientError.IOError", msg, "") + } + if hr.StatusCode != 200 { + msg := fmt.Sprintf("Request fail with http status code: %s, with body: %s", hr.Status, body) + return errors.NewTencentCloudSDKError("ClientError.HttpStatusCodeError", msg, "") + } + //log.Printf("[DEBUG] Response Body=%s", body) + err = response.ParseErrorFromHTTPResponse(body) + if err != nil { + return + } + err = json.Unmarshal(body, &response) + if err != nil { + msg := fmt.Sprintf("Fail to parse json content: %s, because: %s", body, err) + return errors.NewTencentCloudSDKError("ClientError.ParseJsonError", msg, "") + } + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go new file mode 100644 index 000000000..21069ff99 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go @@ -0,0 +1,21 @@ +package profile + +type ClientProfile struct { + HttpProfile *HttpProfile + // Valid choices: HmacSHA1, HmacSHA256, TC3-HMAC-SHA256. + // Default value is TC3-HMAC-SHA256. + SignMethod string + UnsignedPayload bool + // Valid choices: zh-CN, en-US. + // Default value is zh-CN. + Language string +} + +func NewClientProfile() *ClientProfile { + return &ClientProfile{ + HttpProfile: NewHttpProfile(), + SignMethod: "TC3-HMAC-SHA256", + UnsignedPayload: false, + Language: "zh-CN", + } +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go new file mode 100644 index 000000000..8d4bf8f57 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go @@ -0,0 +1,17 @@ +package profile + +type HttpProfile struct { + ReqMethod string + ReqTimeout int + Endpoint string + Protocol string +} + +func NewHttpProfile() *HttpProfile { + return &HttpProfile{ + ReqMethod: "POST", + ReqTimeout: 60, + Endpoint: "", + Protocol: "HTTPS", + } +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go new file mode 100644 index 000000000..0aa7b7355 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go @@ -0,0 +1,94 @@ +package common + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "sort" + + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" +) + +const ( + SHA256 = "HmacSHA256" + SHA1 = "HmacSHA1" +) + +func Sign(s, secretKey, method string) string { + hashed := hmac.New(sha1.New, []byte(secretKey)) + if method == SHA256 { + hashed = hmac.New(sha256.New, []byte(secretKey)) + } + hashed.Write([]byte(s)) + + return base64.StdEncoding.EncodeToString(hashed.Sum(nil)) +} + +func sha256hex(s string) string { + b := sha256.Sum256([]byte(s)) + return hex.EncodeToString(b[:]) +} + +func hmacsha256(s, key string) string { + hashed := hmac.New(sha256.New, []byte(key)) + hashed.Write([]byte(s)) + return string(hashed.Sum(nil)) +} + +func signRequest(request tchttp.Request, credential *Credential, method string) (err error) { + if method != SHA256 { + method = SHA1 + } + checkAuthParams(request, credential, method) + s := getStringToSign(request) + signature := Sign(s, credential.SecretKey, method) + request.GetParams()["Signature"] = signature + return +} + +func checkAuthParams(request tchttp.Request, credential *Credential, method string) { + params := request.GetParams() + credentialParams := credential.GetCredentialParams() + for key, value := range credentialParams { + params[key] = value + } + params["SignatureMethod"] = method + delete(params, "Signature") +} + +func getStringToSign(request tchttp.Request) string { + method := request.GetHttpMethod() + domain := request.GetDomain() + path := request.GetPath() + + var buf bytes.Buffer + buf.WriteString(method) + buf.WriteString(domain) + buf.WriteString(path) + buf.WriteString("?") + + params := request.GetParams() + // sort params + keys := make([]string, 0, len(params)) + for k, _ := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + for i := range keys { + k := keys[i] + // TODO: check if server side allows empty value in url. + if params[k] == "" { + continue + } + buf.WriteString(k) + buf.WriteString("=") + buf.WriteString(params[k]) + buf.WriteString("&") + } + buf.Truncate(buf.Len() - 1) + return buf.String() +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go new file mode 100644 index 000000000..ec2c786db --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go @@ -0,0 +1,47 @@ +package common + +func IntPtr(v int) *int { + return &v +} + +func Int64Ptr(v int64) *int64 { + return &v +} + +func UintPtr(v uint) *uint { + return &v +} + +func Uint64Ptr(v uint64) *uint64 { + return &v +} + +func Float64Ptr(v float64) *float64 { + return &v +} + +func StringPtr(v string) *string { + return &v +} + +func StringValues(ptrs []*string) []string { + values := make([]string, len(ptrs)) + for i := 0; i < len(ptrs); i++ { + if ptrs[i] != nil { + values[i] = *ptrs[i] + } + } + return values +} + +func StringPtrs(vals []string) []*string { + ptrs := make([]*string, len(vals)) + for i := 0; i < len(vals); i++ { + ptrs[i] = &vals[i] + } + return ptrs +} + +func BoolPtr(v bool) *bool { + return &v +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/client.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/client.go new file mode 100644 index 000000000..4980f915d --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/client.go @@ -0,0 +1,294 @@ +// Copyright (c) 2017-2018 THL A29 Limited, a Tencent company. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v20180813 + +import ( + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" +) + +const APIVersion = "2018-08-13" + +type Client struct { + common.Client +} + +// Deprecated +func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) { + cpf := profile.NewClientProfile() + client = &Client{} + client.Init(region).WithSecretId(secretId, secretKey).WithProfile(cpf) + return +} + +func NewClient(credential *common.Credential, region string, clientProfile *profile.ClientProfile) (client *Client, err error) { + client = &Client{} + client.Init(region). + WithCredential(credential). + WithProfile(clientProfile) + return +} + + +func NewAddResourceTagRequest() (request *AddResourceTagRequest) { + request = &AddResourceTagRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "AddResourceTag") + return +} + +func NewAddResourceTagResponse() (response *AddResourceTagResponse) { + response = &AddResourceTagResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 本接口用于给标签关联资源 +func (c *Client) AddResourceTag(request *AddResourceTagRequest) (response *AddResourceTagResponse, err error) { + if request == nil { + request = NewAddResourceTagRequest() + } + response = NewAddResourceTagResponse() + err = c.Send(request, response) + return +} + +func NewCreateTagRequest() (request *CreateTagRequest) { + request = &CreateTagRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "CreateTag") + return +} + +func NewCreateTagResponse() (response *CreateTagResponse) { + response = &CreateTagResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 本接口用于创建一对标签键和标签值 +func (c *Client) CreateTag(request *CreateTagRequest) (response *CreateTagResponse, err error) { + if request == nil { + request = NewCreateTagRequest() + } + response = NewCreateTagResponse() + err = c.Send(request, response) + return +} + +func NewDeleteResourceTagRequest() (request *DeleteResourceTagRequest) { + request = &DeleteResourceTagRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "DeleteResourceTag") + return +} + +func NewDeleteResourceTagResponse() (response *DeleteResourceTagResponse) { + response = &DeleteResourceTagResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 本接口用于解除标签和资源的关联关系 +func (c *Client) DeleteResourceTag(request *DeleteResourceTagRequest) (response *DeleteResourceTagResponse, err error) { + if request == nil { + request = NewDeleteResourceTagRequest() + } + response = NewDeleteResourceTagResponse() + err = c.Send(request, response) + return +} + +func NewDeleteTagRequest() (request *DeleteTagRequest) { + request = &DeleteTagRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "DeleteTag") + return +} + +func NewDeleteTagResponse() (response *DeleteTagResponse) { + response = &DeleteTagResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 本接口用于删除一对标签键和标签值 +func (c *Client) DeleteTag(request *DeleteTagRequest) (response *DeleteTagResponse, err error) { + if request == nil { + request = NewDeleteTagRequest() + } + response = NewDeleteTagResponse() + err = c.Send(request, response) + return +} + +func NewDescribeResourceTagsByResourceIdsRequest() (request *DescribeResourceTagsByResourceIdsRequest) { + request = &DescribeResourceTagsByResourceIdsRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "DescribeResourceTagsByResourceIds") + return +} + +func NewDescribeResourceTagsByResourceIdsResponse() (response *DescribeResourceTagsByResourceIdsResponse) { + response = &DescribeResourceTagsByResourceIdsResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 用于查询已有资源标签键值对 +func (c *Client) DescribeResourceTagsByResourceIds(request *DescribeResourceTagsByResourceIdsRequest) (response *DescribeResourceTagsByResourceIdsResponse, err error) { + if request == nil { + request = NewDescribeResourceTagsByResourceIdsRequest() + } + response = NewDescribeResourceTagsByResourceIdsResponse() + err = c.Send(request, response) + return +} + +func NewDescribeTagKeysRequest() (request *DescribeTagKeysRequest) { + request = &DescribeTagKeysRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "DescribeTagKeys") + return +} + +func NewDescribeTagKeysResponse() (response *DescribeTagKeysResponse) { + response = &DescribeTagKeysResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 用于查询已建立的标签列表中的标签键。 +func (c *Client) DescribeTagKeys(request *DescribeTagKeysRequest) (response *DescribeTagKeysResponse, err error) { + if request == nil { + request = NewDescribeTagKeysRequest() + } + response = NewDescribeTagKeysResponse() + err = c.Send(request, response) + return +} + +func NewDescribeTagValuesRequest() (request *DescribeTagValuesRequest) { + request = &DescribeTagValuesRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "DescribeTagValues") + return +} + +func NewDescribeTagValuesResponse() (response *DescribeTagValuesResponse) { + response = &DescribeTagValuesResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 用于查询已建立的标签列表中的标签值。 +func (c *Client) DescribeTagValues(request *DescribeTagValuesRequest) (response *DescribeTagValuesResponse, err error) { + if request == nil { + request = NewDescribeTagValuesRequest() + } + response = NewDescribeTagValuesResponse() + err = c.Send(request, response) + return +} + +func NewDescribeTagsRequest() (request *DescribeTagsRequest) { + request = &DescribeTagsRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "DescribeTags") + return +} + +func NewDescribeTagsResponse() (response *DescribeTagsResponse) { + response = &DescribeTagsResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 用于查询已建立的标签列表。 +func (c *Client) DescribeTags(request *DescribeTagsRequest) (response *DescribeTagsResponse, err error) { + if request == nil { + request = NewDescribeTagsRequest() + } + response = NewDescribeTagsResponse() + err = c.Send(request, response) + return +} + +func NewModifyResourceTagsRequest() (request *ModifyResourceTagsRequest) { + request = &ModifyResourceTagsRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "ModifyResourceTags") + return +} + +func NewModifyResourceTagsResponse() (response *ModifyResourceTagsResponse) { + response = &ModifyResourceTagsResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 本接口用于修改资源关联的所有标签 +func (c *Client) ModifyResourceTags(request *ModifyResourceTagsRequest) (response *ModifyResourceTagsResponse, err error) { + if request == nil { + request = NewModifyResourceTagsRequest() + } + response = NewModifyResourceTagsResponse() + err = c.Send(request, response) + return +} + +func NewUpdateResourceTagValueRequest() (request *UpdateResourceTagValueRequest) { + request = &UpdateResourceTagValueRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + request.Init().WithApiInfo("tag", APIVersion, "UpdateResourceTagValue") + return +} + +func NewUpdateResourceTagValueResponse() (response *UpdateResourceTagValueResponse) { + response = &UpdateResourceTagValueResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + return +} + +// 本接口用于修改资源已关联的标签值(标签键不变) +func (c *Client) UpdateResourceTagValue(request *UpdateResourceTagValueRequest) (response *UpdateResourceTagValueResponse, err error) { + if request == nil { + request = NewUpdateResourceTagValueRequest() + } + response = NewUpdateResourceTagValueResponse() + err = c.Send(request, response) + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/models.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/models.go new file mode 100644 index 000000000..b326cddbe --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/models.go @@ -0,0 +1,523 @@ +// Copyright (c) 2017-2018 THL A29 Limited, a Tencent company. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v20180813 + +import ( + "encoding/json" + + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" +) + +type AddResourceTagRequest struct { + *tchttp.BaseRequest + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` + + // 资源六段式描述 + Resource *string `json:"Resource,omitempty" name:"Resource"` +} + +func (r *AddResourceTagRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *AddResourceTagRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type AddResourceTagResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *AddResourceTagResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *AddResourceTagResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type CreateTagRequest struct { + *tchttp.BaseRequest + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` +} + +func (r *CreateTagRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *CreateTagRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type CreateTagResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *CreateTagResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *CreateTagResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DeleteResourceTagRequest struct { + *tchttp.BaseRequest + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 资源六段式描述 + Resource *string `json:"Resource,omitempty" name:"Resource"` +} + +func (r *DeleteResourceTagRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DeleteResourceTagRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DeleteResourceTagResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *DeleteResourceTagResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DeleteResourceTagResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DeleteTagRequest struct { + *tchttp.BaseRequest + + // 需要删除的标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 需要删除的标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` +} + +func (r *DeleteTagRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DeleteTagRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DeleteTagResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *DeleteTagResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DeleteTagResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeResourceTagsByResourceIdsRequest struct { + *tchttp.BaseRequest + + // 业务类型 + ServiceType *string `json:"ServiceType,omitempty" name:"ServiceType"` + + // 资源前缀 + ResourcePrefix *string `json:"ResourcePrefix,omitempty" name:"ResourcePrefix"` + + // 资源唯一标记 + ResourceIds []*string `json:"ResourceIds,omitempty" name:"ResourceIds" list` + + // 资源所在地域 + ResourceRegion *string `json:"ResourceRegion,omitempty" name:"ResourceRegion"` + + // 数据偏移量,默认为 0, 必须为Limit参数的整数倍 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小,默认为 15 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` +} + +func (r *DescribeResourceTagsByResourceIdsRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeResourceTagsByResourceIdsRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeResourceTagsByResourceIdsResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 结果总数 + TotalCount *uint64 `json:"TotalCount,omitempty" name:"TotalCount"` + + // 数据位移偏量 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` + + // 标签列表 + Tags []*TagResource `json:"Tags,omitempty" name:"Tags" list` + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *DescribeResourceTagsByResourceIdsResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeResourceTagsByResourceIdsResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeTagKeysRequest struct { + *tchttp.BaseRequest + + // 创建者用户 Uin,不传或为空只将 Uin 作为条件查询 + CreateUin *uint64 `json:"CreateUin,omitempty" name:"CreateUin"` + + // 数据偏移量,默认为 0, 必须为Limit参数的整数倍 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小,默认为 15 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` +} + +func (r *DescribeTagKeysRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeTagKeysRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeTagKeysResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 结果总数 + TotalCount *uint64 `json:"TotalCount,omitempty" name:"TotalCount"` + + // 数据位移偏量 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` + + // 标签列表 + Tags []*string `json:"Tags,omitempty" name:"Tags" list` + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *DescribeTagKeysResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeTagKeysResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeTagValuesRequest struct { + *tchttp.BaseRequest + + // 标签键列表 + TagKeys []*string `json:"TagKeys,omitempty" name:"TagKeys" list` + + // 创建者用户 Uin,不传或为空只将 Uin 作为条件查询 + CreateUin *uint64 `json:"CreateUin,omitempty" name:"CreateUin"` + + // 数据偏移量,默认为 0, 必须为Limit参数的整数倍 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小,默认为 15 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` +} + +func (r *DescribeTagValuesRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeTagValuesRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeTagValuesResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 结果总数 + TotalCount *uint64 `json:"TotalCount,omitempty" name:"TotalCount"` + + // 数据位移偏量 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` + + // 标签列表 + Tags []*Tag `json:"Tags,omitempty" name:"Tags" list` + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *DescribeTagValuesResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeTagValuesResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeTagsRequest struct { + *tchttp.BaseRequest + + // 标签键,与标签值同时存在或同时不存在,不存在时表示查询该用户所有标签 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 标签值,与标签键同时存在或同时不存在,不存在时表示查询该用户所有标签 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` + + // 数据偏移量,默认为 0, 必须为Limit参数的整数倍 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小,默认为 15 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` + + // 创建者用户 Uin,不传或为空只将 Uin 作为条件查询 + CreateUin *uint64 `json:"CreateUin,omitempty" name:"CreateUin"` +} + +func (r *DescribeTagsRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeTagsRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type DescribeTagsResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 结果总数 + TotalCount *uint64 `json:"TotalCount,omitempty" name:"TotalCount"` + + // 数据位移偏量 + Offset *uint64 `json:"Offset,omitempty" name:"Offset"` + + // 每页大小 + Limit *uint64 `json:"Limit,omitempty" name:"Limit"` + + // 标签列表 + Tags []*TagWithDelete `json:"Tags,omitempty" name:"Tags" list` + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *DescribeTagsResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *DescribeTagsResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type ModifyResourceTagsRequest struct { + *tchttp.BaseRequest + + // 资源的六段式描述 + Resource *string `json:"Resource,omitempty" name:"Resource"` + + // 需要增加或修改的标签集合。如果Resource描述的资源未关联输入的标签键,则增加关联;若已关联,则将该资源关联的键对应的标签值修改为输入值。本接口中ReplaceTags和DeleteTags二者必须存在其一,且二者不能包含相同的标签键 + ReplaceTags []*Tag `json:"ReplaceTags,omitempty" name:"ReplaceTags" list` + + // 需要解关联的标签集合。本接口中ReplaceTags和DeleteTags二者必须存在其一,且二者不能包含相同的标签键 + DeleteTags []*TagKeyObject `json:"DeleteTags,omitempty" name:"DeleteTags" list` +} + +func (r *ModifyResourceTagsRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *ModifyResourceTagsRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type ModifyResourceTagsResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *ModifyResourceTagsResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *ModifyResourceTagsResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type Tag struct { + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` +} + +type TagKeyObject struct { + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` +} + +type TagResource struct { + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` + + // 资源ID + ResourceId *string `json:"ResourceId,omitempty" name:"ResourceId"` + + // 标签键MD5值 + TagKeyMd5 *string `json:"TagKeyMd5,omitempty" name:"TagKeyMd5"` + + // 标签值MD5值 + TagValueMd5 *string `json:"TagValueMd5,omitempty" name:"TagValueMd5"` +} + +type TagWithDelete struct { + + // 标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` + + // 是否可以删除 + CanDelete *uint64 `json:"CanDelete,omitempty" name:"CanDelete"` +} + +type UpdateResourceTagValueRequest struct { + *tchttp.BaseRequest + + // 资源关联的标签键 + TagKey *string `json:"TagKey,omitempty" name:"TagKey"` + + // 修改后的标签值 + TagValue *string `json:"TagValue,omitempty" name:"TagValue"` + + // 资源的六段式描述 + Resource *string `json:"Resource,omitempty" name:"Resource"` +} + +func (r *UpdateResourceTagValueRequest) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *UpdateResourceTagValueRequest) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} + +type UpdateResourceTagValueResponse struct { + *tchttp.BaseResponse + Response *struct { + + // 唯一请求 ID,每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + } `json:"Response"` +} + +func (r *UpdateResourceTagValueResponse) ToJsonString() string { + b, _ := json.Marshal(r) + return string(b) +} + +func (r *UpdateResourceTagValueResponse) FromJsonString(s string) error { + return json.Unmarshal([]byte(s), &r) +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/.bumpversion.cfg b/vendor/github.com/tencentyun/cos-go-sdk-v5/.bumpversion.cfg new file mode 100644 index 000000000..d2bac6df9 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/.bumpversion.cfg @@ -0,0 +1,7 @@ +[bumpversion] +commit = True +tag = True +current_version = 0.7.0 + +[bumpversion:file:cos.go] + diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/.gitignore b/vendor/github.com/tencentyun/cos-go-sdk-v5/.gitignore new file mode 100644 index 000000000..7da68739a --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/.gitignore @@ -0,0 +1,29 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +dist/ +cover.html +cover.out +covprofile +coverage.html \ No newline at end of file diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/.travis.yml b/vendor/github.com/tencentyun/cos-go-sdk-v5/.travis.yml new file mode 100644 index 000000000..e940c8c27 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/.travis.yml @@ -0,0 +1,38 @@ +language: go +go: +- '1.7' +- '1.8' +- '1.9' +- 1.10.x +- 1.11.x +- 1.12.x +- master +sudo: false +before_install: +- go get -u github.com/mattn/goveralls +- go get -u github.com/stretchr/testify +install: +- go get +- go build +- go build github.com/mattn/goveralls +script: +- if [[ ! -n "$COS_SECRETID" ]]; then exit 0 + ; fi +- make test +- make ci-test +- go test -coverprofile=cover.out github.com/toranger/cos-go-sdk-v5 +- "${TRAVIS_HOME}/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out" +matrix: + allow_failures: + - go: 1.7 + - go: master +env: + global: + - secure: XXB/cFVnJcAzhOZ2/zplwjhhhireQQGGRbNscPgQ0kpUQCyPZ6oIHvJMafuP4TVTJHEdMiaDxm0HNvgARuopXVaQNmK2UZj6xw40Ud7OT7ZUnw88xkQkXOI5GwG8oz9LqxIXUSItHegKXRLW0e1PoBdjZNv6lxGFAtuOcl9ekAg/q2lGIIQFefz6NK7gCmGYULKe+4J15VFldoYNM0JesxxxArTvtv8+k+U53oUwy9dex6z5oIA1zGIeKLcOD2xXgbjid/Ett3t0B2w3GfJWoM9rGV0eHgveOAUGe5tQkMKvl5LK1hj+93ZmU0MAG7x7t9jYKrFPqU/eDNJRMb4Ro6L7lIXVEKaBUkLx28PnwFQ5D043GBVtQGqYNcldZXIfbyYEHQZlD/BWFOt5YqTpGg+7Wm4NC3Yffqsurzk54juT7FftzVy0A8MFkqO+c5RHrOSUlm01pWXkGLHgZhUP5gEZEuUaoluSQTZksmAUJZ7F8DxwpE4SYBqfN27PZ87rWDNyOqNv1w1trzwx2IfdHHA+vfCZ7UM5e85gxFWUO2tJCUai2q21v3gBrcAgBOb6BwVzbWAorM2zY20f0l21XxOWMakA+r4JJA3s3EmcczcQeeL6pkFIAh+qKdFEPuyQTjH1mGpPzYFNbWtvPXijQo5PqyGrKL8W1t3ovwXMXoE= + - secure: bep0PPD/oYW5zY0QpeeC+WgFIya5DNRVmR92MO+e5BdFlSJPhstoG8bRh91EeftzC/Hyd3PUEIglPqTgZPxwysqW/81plsU95wV3qJi9gPi7+ZtYXH4xZTnaqgZsTr7jsKSVoKHSu7XqCtbSytW8YMN9wRWzG19/9hX2Z79Q6yNy5l9856Oyj1E2IXDjdZLPsWDhnZ8Vvk1wAVy2fc2esqKzHAZwm8n9vee2yR8vz7GXUszzpKvn4R43eNzdlFEHCmN0ANmxLJZmnYDpZHHfNf4slts+0S6I7awFXppuXUDaJPBRCia4XoFeSw+01IW1Vi0kAwvGLhxjJCWc4M/4ZU0byXDT11tDFvWa19NmnbYiizWiXNVecn1oNWYJqIKe7TTAMAtHSXAPmLX0rXuXKzwM09W6yrLFufCxyix9IOnenEbe9WwSdBbhmeLF3Wu/uVGkDog/FsXJM75sk956vV9UKh9zF4B9/NR8szJMF7shEs0Fbru5UUWheqg4AadPl3dhAWuj2+6NANa1LpH3JVD3II9dlXeMmMvsSwDvrYUaX/S8tf6JwZG0zCJK0TYp05rjxH+NIzWaMUTY7+HwYqqK3pOW3San0SlZiMq8N7GSnKUZ7WRQXYSB4gXHrg+mWyeVC7XnqiRtCwVi+LtPMu+YUbg7dwVi0vtKjYZYIUY= + - secure: Ob28vrOuHMKNKEtChkWbsaVv2SwLhcxXMnvGe4XN+y3mFvdhYnwpt6NdgThF8OCZ0761tvTRmvALfiZnO0uORjTtoHKkVPrnVIxlCcode0NVJZNHGn2fqjemdLKCnSeX7hm+9zeLpCnIvC+Sp3iZ3t2AH4AzgFx6nirWO3HwT5l9rNL9Q1CfwlOpNJJ36r9JTHwQnXmOfOmszUNoZ3rtiFXJ8dCi+BgY0lsiIRSiDkAH7KAPf86REM+ww81AaXG4/RuYx1Vj5zQCtZN7XEOViSXEbqqb8SrIFOccDu5FV12djg+4QS7FSjLVGrdIUcn4oI6pS24Et3oXf8xFx6JLYyGGhgZ2BsyJEx5vLQvkTWnMTrwZVRtCQ+g6lMUQpJhL2rBrmVBUqBFb5IH69O7corQm53n5qLM8IiosAQLfbOtML/1PyEpKCG2aOx1377Fx2yzxXW3ucP1PBqCzli0oCM2T52LfiNvZTzkIU6XJebBnzkZXepzOIFSur86kxgvQFElw9ro2X6XXPKU5S25xVaUSvaN1kmqLSkToJ9S1rmDYXnJR4aH0R2GcLw+EkMHFJJoAjnRHxrB4/1vOJbzmfS+qy6ShRhUMSD8gk4YJ6Y7o9h7oekuWOEn+XGhl29U9T5OApzHfoPEGZwLnpHxAiKJtQtv/TNhBIOFCjigsF7U= +notifications: + email: + recipients: + - wjielai@tencent.com + - fysntian@tencent.com diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/LICENSE b/vendor/github.com/tencentyun/cos-go-sdk-v5/LICENSE new file mode 100644 index 000000000..8ff7942e2 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 mozillazg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/Makefile b/vendor/github.com/tencentyun/cos-go-sdk-v5/Makefile new file mode 100644 index 000000000..e3b7aa319 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/Makefile @@ -0,0 +1,22 @@ +help: + @echo "test run test" + @echo "lint run lint" + @echo "example run examples" + +.PHONY: test +test: + go test -v -cover -coverprofile cover.out + go tool cover -html=cover.out -o cover.html + +.PHONY: lint +lint: + gofmt -s -w . + goimports -w . + golint . + go vet + +.PHONY: example +example: + cd example && sh test.sh +ci-test: + cd costesting && go test -v diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/README.md b/vendor/github.com/tencentyun/cos-go-sdk-v5/README.md new file mode 100644 index 000000000..0316a137b --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/README.md @@ -0,0 +1,95 @@ +# cos-go-sdk-v5 + +腾讯云对象存储服务 COS(Cloud Object Storage) Go SDK(API 版本:V5 版本的 XML API)。 + +## Install + +`go get -u github.com/tencentyun/cos-go-sdk-v5` + + +## Usage + +```go +package main + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "time" + + "github.com/tencentyun/cos-go-sdk-v5" +) + +func main() { + //将修改为真实的信息 + //bucket的命名规则为{name}-{appid} ,此处填写的存储桶名称必须为此格式 + u, _ := url.Parse("https://.cos..myqcloud.com") + b := &cos.BaseURL{BucketURL: u} + c := cos.NewClient(b, &http.Client{ + //设置超时时间 + Timeout: 100 * time.Second, + Transport: &cos.AuthorizationTransport{ + //如实填写账号和密钥,也可以设置为环境变量 + SecretID: os.Getenv("COS_SECRETID"), + SecretKey: os.Getenv("COS_SECRETKEY"), + }, + }) + + name := "test/hello.txt" + resp, err := c.Object.Get(context.Background(), name, nil) + if err != nil { + panic(err) + } + bs, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + fmt.Printf("%s\n", string(bs)) +} +``` + +所有的 API 在 [example](./example/) 目录下都有对应的使用示例。 + +Service API: + +* [x] Get Service(使用示例:[service/get.go](./example/service/get.go)) + +Bucket API: + +* [x] Get Bucket(使用示例:[bucket/get.go](./example/bucket/get.go)) +* [x] Get Bucket ACL(使用示例:[bucket/getACL.go](./example/bucket/getACL.go)) +* [x] Get Bucket CORS(使用示例:[bucket/getCORS.go](./example/bucket/getCORS.go)) +* [x] Get Bucket Location(使用示例:[bucket/getLocation.go](./example/bucket/getLocation.go)) +* [x] Get Buket Lifecycle(使用示例:[bucket/getLifecycle.go](./example/bucket/getLifecycle.go)) +* [x] Get Bucket Tagging(使用示例:[bucket/getTagging.go](./example/bucket/getTagging.go)) +* [x] Put Bucket(使用示例:[bucket/put.go](./example/bucket/put.go)) +* [x] Put Bucket ACL(使用示例:[bucket/putACL.go](./example/bucket/putACL.go)) +* [x] Put Bucket CORS(使用示例:[bucket/putCORS.go](./example/bucket/putCORS.go)) +* [x] Put Bucket Lifecycle(使用示例:[bucket/putLifecycle.go](./example/bucket/putLifecycle.go)) +* [x] Put Bucket Tagging(使用示例:[bucket/putTagging.go](./example/bucket/putTagging.go)) +* [x] Delete Bucket(使用示例:[bucket/delete.go](./example/bucket/delete.go)) +* [x] Delete Bucket CORS(使用示例:[bucket/deleteCORS.go](./example/bucket/deleteCORS.go)) +* [x] Delete Bucket Lifecycle(使用示例:[bucket/deleteLifecycle.go](./example/bucket/deleteLifecycle.go)) +* [x] Delete Bucket Tagging(使用示例:[bucket/deleteTagging.go](./example/bucket/deleteTagging.go)) +* [x] Head Bucket(使用示例:[bucket/head.go](./example/bucket/head.go)) +* [x] List Multipart Uploads(使用示例:[bucket/listMultipartUploads.go](./example/bucket/listMultipartUploads.go)) + +Object API: + +* [x] Get Object(使用示例:[object/get.go](./example/object/get.go)) +* [x] Get Object ACL(使用示例:[object/getACL.go](./example/object/getACL.go)) +* [x] Put Object(使用示例:[object/put.go](./example/object/put.go)) +* [x] Put Object ACL(使用示例:[object/putACL.go](./example/object/putACL.go)) +* [x] Put Object Copy(使用示例:[object/copy.go](./example/object/copy.go)) +* [x] Delete Object(使用示例:[object/delete.go](./example/object/delete.go)) +* [x] Delete Multiple Object(使用示例:[object/deleteMultiple.go](./example/object/deleteMultiple.go)) +* [x] Head Object(使用示例:[object/head.go](./example/object/head.go)) +* [x] Options Object(使用示例:[object/options.go](./example/object/options.go)) +* [x] Initiate Multipart Upload(使用示例:[object/initiateMultipartUpload.go](./example/object/initiateMultipartUpload.go)) +* [x] Upload Part(使用示例:[object/uploadPart.go](./example/object/uploadPart.go)) +* [x] List Parts(使用示例:[object/listParts.go](./example/object/listParts.go)) +* [x] Complete Multipart Upload(使用示例:[object/completeMultipartUpload.go](./example/object/completeMultipartUpload.go)) +* [x] Abort Multipart Upload(使用示例:[object/abortMultipartUpload.go](./example/object/abortMultipartUpload.go)) +* [x] Mutipart Upload(使用示例:[object/MutiUpload.go](./example/object/MutiUpload.go)) diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/auth.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/auth.go new file mode 100644 index 000000000..6b0cc554c --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/auth.go @@ -0,0 +1,305 @@ +package cos + +import ( + "crypto/hmac" + "crypto/sha1" + "fmt" + "hash" + "net/http" + "net/url" + "sort" + "strings" + "sync" + "time" +) + +const sha1SignAlgorithm = "sha1" +const privateHeaderPrefix = "x-cos-" +const defaultAuthExpire = time.Hour + +// 需要校验的 Headers 列表 +var needSignHeaders = map[string]bool{ + "host": true, + "range": true, + "x-cos-acl": true, + "x-cos-grant-read": true, + "x-cos-grant-write": true, + "x-cos-grant-full-control": true, + "response-content-type": true, + "response-content-language": true, + "response-expires": true, + "response-cache-control": true, + "response-content-disposition": true, + "response-content-encoding": true, + "cache-control": true, + "content-disposition": true, + "content-encoding": true, + "content-type": true, + "content-length": true, + "content-md5": true, + "expect": true, + "expires": true, + "x-cos-content-sha1": true, + "x-cos-storage-class": true, + "if-modified-since": true, + "origin": true, + "access-control-request-method": true, + "access-control-request-headers": true, + "x-cos-object-type": true, +} + +func safeURLEncode(s string) string { + s = encodeURIComponent(s) + s = strings.Replace(s, "!", "%21", -1) + s = strings.Replace(s, "'", "%27", -1) + s = strings.Replace(s, "(", "%28", -1) + s = strings.Replace(s, ")", "%29", -1) + s = strings.Replace(s, "*", "%2A", -1) + return s +} + +type valuesSignMap map[string][]string + +func (vs valuesSignMap) Add(key, value string) { + key = strings.ToLower(key) + vs[key] = append(vs[key], value) +} + +func (vs valuesSignMap) Encode() string { + var keys []string + for k := range vs { + keys = append(keys, k) + } + sort.Strings(keys) + + var pairs []string + for _, k := range keys { + items := vs[k] + sort.Strings(items) + for _, val := range items { + pairs = append( + pairs, + fmt.Sprintf("%s=%s", safeURLEncode(k), safeURLEncode(val))) + } + } + return strings.Join(pairs, "&") +} + +// AuthTime 用于生成签名所需的 q-sign-time 和 q-key-time 相关参数 +type AuthTime struct { + SignStartTime time.Time + SignEndTime time.Time + KeyStartTime time.Time + KeyEndTime time.Time +} + +// NewAuthTime 生成 AuthTime 的便捷函数 +// +// expire: 从现在开始多久过期. +func NewAuthTime(expire time.Duration) *AuthTime { + signStartTime := time.Now() + keyStartTime := signStartTime + signEndTime := signStartTime.Add(expire) + keyEndTime := signEndTime + return &AuthTime{ + SignStartTime: signStartTime, + SignEndTime: signEndTime, + KeyStartTime: keyStartTime, + KeyEndTime: keyEndTime, + } +} + +// signString return q-sign-time string +func (a *AuthTime) signString() string { + return fmt.Sprintf("%d;%d", a.SignStartTime.Unix(), a.SignEndTime.Unix()) +} + +// keyString return q-key-time string +func (a *AuthTime) keyString() string { + return fmt.Sprintf("%d;%d", a.KeyStartTime.Unix(), a.KeyEndTime.Unix()) +} + +// newAuthorization 通过一系列步骤生成最终需要的 Authorization 字符串 +func newAuthorization(secretID, secretKey string, req *http.Request, authTime *AuthTime) string { + signTime := authTime.signString() + keyTime := authTime.keyString() + signKey := calSignKey(secretKey, keyTime) + + formatHeaders := *new(string) + signedHeaderList := *new([]string) + formatHeaders, signedHeaderList = genFormatHeaders(req.Header) + formatParameters, signedParameterList := genFormatParameters(req.URL.Query()) + formatString := genFormatString(req.Method, *req.URL, formatParameters, formatHeaders) + + stringToSign := calStringToSign(sha1SignAlgorithm, keyTime, formatString) + signature := calSignature(signKey, stringToSign) + + return genAuthorization( + secretID, signTime, keyTime, signature, signedHeaderList, + signedParameterList, + ) +} + +// AddAuthorizationHeader 给 req 增加签名信息 +func AddAuthorizationHeader(secretID, secretKey string, sessionToken string, req *http.Request, authTime *AuthTime) { + if secretID == "" { + return + } + + auth := newAuthorization(secretID, secretKey, req, + authTime, + ) + if len(sessionToken) > 0 { + req.Header.Set("x-cos-security-token", sessionToken) + } + req.Header.Set("Authorization", auth) +} + +// calSignKey 计算 SignKey +func calSignKey(secretKey, keyTime string) string { + digest := calHMACDigest(secretKey, keyTime, sha1SignAlgorithm) + return fmt.Sprintf("%x", digest) +} + +// calStringToSign 计算 StringToSign +func calStringToSign(signAlgorithm, signTime, formatString string) string { + h := sha1.New() + h.Write([]byte(formatString)) + return fmt.Sprintf("%s\n%s\n%x\n", signAlgorithm, signTime, h.Sum(nil)) +} + +// calSignature 计算 Signature +func calSignature(signKey, stringToSign string) string { + digest := calHMACDigest(signKey, stringToSign, sha1SignAlgorithm) + return fmt.Sprintf("%x", digest) +} + +// genAuthorization 生成 Authorization +func genAuthorization(secretID, signTime, keyTime, signature string, signedHeaderList, signedParameterList []string) string { + return strings.Join([]string{ + "q-sign-algorithm=" + sha1SignAlgorithm, + "q-ak=" + secretID, + "q-sign-time=" + signTime, + "q-key-time=" + keyTime, + "q-header-list=" + strings.Join(signedHeaderList, ";"), + "q-url-param-list=" + strings.Join(signedParameterList, ";"), + "q-signature=" + signature, + }, "&") +} + +// genFormatString 生成 FormatString +func genFormatString(method string, uri url.URL, formatParameters, formatHeaders string) string { + formatMethod := strings.ToLower(method) + formatURI := uri.Path + + return fmt.Sprintf("%s\n%s\n%s\n%s\n", formatMethod, formatURI, + formatParameters, formatHeaders, + ) +} + +// genFormatParameters 生成 FormatParameters 和 SignedParameterList +// instead of the url.Values{} +func genFormatParameters(parameters url.Values) (formatParameters string, signedParameterList []string) { + ps := valuesSignMap{} + for key, values := range parameters { + key = strings.ToLower(key) + for _, value := range values { + ps.Add(key, value) + signedParameterList = append(signedParameterList, key) + } + } + //formatParameters = strings.ToLower(ps.Encode()) + formatParameters = ps.Encode() + sort.Strings(signedParameterList) + return +} + +// genFormatHeaders 生成 FormatHeaders 和 SignedHeaderList +func genFormatHeaders(headers http.Header) (formatHeaders string, signedHeaderList []string) { + hs := valuesSignMap{} + for key, values := range headers { + key = strings.ToLower(key) + for _, value := range values { + if isSignHeader(key) { + hs.Add(key, value) + signedHeaderList = append(signedHeaderList, key) + } + } + } + formatHeaders = hs.Encode() + sort.Strings(signedHeaderList) + return +} + +// HMAC 签名 +func calHMACDigest(key, msg, signMethod string) []byte { + var hashFunc func() hash.Hash + switch signMethod { + case "sha1": + hashFunc = sha1.New + default: + hashFunc = sha1.New + } + h := hmac.New(hashFunc, []byte(key)) + h.Write([]byte(msg)) + return h.Sum(nil) +} + +func isSignHeader(key string) bool { + for k, v := range needSignHeaders { + if key == k && v { + return true + } + } + return strings.HasPrefix(key, privateHeaderPrefix) +} + +// AuthorizationTransport 给请求增加 Authorization header +type AuthorizationTransport struct { + SecretID string + SecretKey string + SessionToken string + rwLocker sync.RWMutex + // 签名多久过期 + Expire time.Duration + Transport http.RoundTripper +} + +// SetCredential update the SecretID(ak), SercretKey(sk), sessiontoken +func (t *AuthorizationTransport) SetCredential(ak, sk, token string) { + t.rwLocker.Lock() + defer t.rwLocker.Unlock() + t.SecretID = ak + t.SecretKey = sk + t.SessionToken = token +} + +// GetCredential get the ak, sk, token +func (t *AuthorizationTransport) GetCredential() (string, string, string) { + t.rwLocker.RLock() + defer t.rwLocker.RUnlock() + return t.SecretID, t.SecretKey, t.SessionToken +} + +// RoundTrip implements the RoundTripper interface. +func (t *AuthorizationTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req = cloneRequest(req) // per RoundTrip contract + if t.Expire == time.Duration(0) { + t.Expire = defaultAuthExpire + } + + ak, sk, token := t.GetCredential() + // 增加 Authorization header + authTime := NewAuthTime(t.Expire) + AddAuthorizationHeader(ak, sk, token, req, authTime) + + resp, err := t.transport().RoundTrip(req) + return resp, err +} + +func (t *AuthorizationTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket.go new file mode 100644 index 000000000..2e3f92c2f --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket.go @@ -0,0 +1,104 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// BucketService 相关 API +type BucketService service + +// BucketGetResult is the result of GetBucket +type BucketGetResult struct { + XMLName xml.Name `xml:"ListBucketResult"` + Name string + Prefix string `xml:"Prefix,omitempty"` + Marker string `xml:"Marker,omitempty"` + NextMarker string `xml:"NextMarker,omitempty"` + Delimiter string `xml:"Delimiter,omitempty"` + MaxKeys int + IsTruncated bool + Contents []Object `xml:"Contents,omitempty"` + CommonPrefixes []string `xml:"CommonPrefixes>Prefix,omitempty"` + EncodingType string `xml:"Encoding-Type,omitempty"` +} + +// BucketGetOptions is the option of GetBucket +type BucketGetOptions struct { + Prefix string `url:"prefix,omitempty"` + Delimiter string `url:"delimiter,omitempty"` + EncodingType string `url:"encoding-type,omitempty"` + Marker string `url:"marker,omitempty"` + MaxKeys int `url:"max-keys,omitempty"` +} + +// Get Bucket请求等同于 List Object请求,可以列出该Bucket下部分或者所有Object,发起该请求需要拥有Read权限。 +// +// https://www.qcloud.com/document/product/436/7734 +func (s *BucketService) Get(ctx context.Context, opt *BucketGetOptions) (*BucketGetResult, *Response, error) { + var res BucketGetResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/", + method: http.MethodGet, + optQuery: opt, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// BucketPutOptions is same to the ACLHeaderOptions +type BucketPutOptions ACLHeaderOptions + +// Put Bucket请求可以在指定账号下创建一个Bucket。 +// +// https://www.qcloud.com/document/product/436/7738 +func (s *BucketService) Put(ctx context.Context, opt *BucketPutOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/", + method: http.MethodPut, + optHeader: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// Delete Bucket请求可以在指定账号下删除Bucket,删除之前要求Bucket为空。 +// +// https://www.qcloud.com/document/product/436/7732 +func (s *BucketService) Delete(ctx context.Context) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/", + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// Head Bucket请求可以确认是否存在该Bucket,是否有权限访问,Head的权限与Read一致。 +// +// 当其存在时,返回 HTTP 状态码200; +// 当无权限时,返回 HTTP 状态码403; +// 当不存在时,返回 HTTP 状态码404。 +// +// https://www.qcloud.com/document/product/436/7735 +func (s *BucketService) Head(ctx context.Context) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/", + method: http.MethodHead, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// Bucket is the meta info of Bucket +type Bucket struct { + Name string + Region string `xml:"Location,omitempty"` + CreationDate string `xml:",omitempty"` +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_acl.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_acl.go new file mode 100644 index 000000000..285b9064b --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_acl.go @@ -0,0 +1,62 @@ +package cos + +import ( + "context" + "net/http" +) + +// BucketGetACLResult is same to the ACLXml +type BucketGetACLResult ACLXml + +// GetACL 使用API读取Bucket的ACL表,只有所有者有权操作。 +// +// https://www.qcloud.com/document/product/436/7733 +func (s *BucketService) GetACL(ctx context.Context) (*BucketGetACLResult, *Response, error) { + var res BucketGetACLResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?acl", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// BucketPutACLOptions is the option of PutBucketACL +type BucketPutACLOptions struct { + Header *ACLHeaderOptions `url:"-" xml:"-"` + Body *ACLXml `url:"-" header:"-"` +} + +// PutACL 使用API写入Bucket的ACL表,您可以通过Header:"x-cos-acl","x-cos-grant-read", +// "x-cos-grant-write","x-cos-grant-full-control"传入ACL信息,也可以通过body以XML格式传入ACL信息, +// +// 但是只能选择Header和Body其中一种,否则返回冲突。 +// +// Put Bucket ACL是一个覆盖操作,传入新的ACL将覆盖原有ACL。只有所有者有权操作。 +// +// "x-cos-acl":枚举值为public-read,private;public-read意味这个Bucket有公有读私有写的权限, +// private意味这个Bucket有私有读写的权限。 +// +// "x-cos-grant-read":意味被赋予权限的用户拥有该Bucket的读权限 +// "x-cos-grant-write":意味被赋予权限的用户拥有该Bucket的写权限 +// "x-cos-grant-full-control":意味被赋予权限的用户拥有该Bucket的读写权限 +// +// https://www.qcloud.com/document/product/436/7737 +func (s *BucketService) PutACL(ctx context.Context, opt *BucketPutACLOptions) (*Response, error) { + header := opt.Header + body := opt.Body + if body != nil { + header = nil + } + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?acl", + method: http.MethodPut, + body: body, + optHeader: header, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_cors.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_cors.go new file mode 100644 index 000000000..1d688c9b2 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_cors.go @@ -0,0 +1,71 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// BucketCORSRule is the rule of BucketCORS +type BucketCORSRule struct { + ID string `xml:"ID,omitempty"` + AllowedMethods []string `xml:"AllowedMethod"` + AllowedOrigins []string `xml:"AllowedOrigin"` + AllowedHeaders []string `xml:"AllowedHeader,omitempty"` + MaxAgeSeconds int `xml:"MaxAgeSeconds,omitempty"` + ExposeHeaders []string `xml:"ExposeHeader,omitempty"` +} + +// BucketGetCORSResult is the result of GetBucketCORS +type BucketGetCORSResult struct { + XMLName xml.Name `xml:"CORSConfiguration"` + Rules []BucketCORSRule `xml:"CORSRule,omitempty"` +} + +// GetCORS 实现 Bucket 跨域访问配置读取。 +// +// https://www.qcloud.com/document/product/436/8274 +func (s *BucketService) GetCORS(ctx context.Context) (*BucketGetCORSResult, *Response, error) { + var res BucketGetCORSResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?cors", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// BucketPutCORSOptions is the option of PutBucketCORS +type BucketPutCORSOptions struct { + XMLName xml.Name `xml:"CORSConfiguration"` + Rules []BucketCORSRule `xml:"CORSRule,omitempty"` +} + +// PutCORS 实现 Bucket 跨域访问设置,您可以通过传入XML格式的配置文件实现配置,文件大小限制为64 KB。 +// +// https://www.qcloud.com/document/product/436/8279 +func (s *BucketService) PutCORS(ctx context.Context, opt *BucketPutCORSOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?cors", + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// DeleteCORS 实现 Bucket 跨域访问配置删除。 +// +// https://www.qcloud.com/document/product/436/8283 +func (s *BucketService) DeleteCORS(ctx context.Context) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?cors", + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_inventory.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_inventory.go new file mode 100644 index 000000000..17ed781e9 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_inventory.go @@ -0,0 +1,134 @@ +package cos + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" +) + +// Notice bucket_inventory only for test. can not use + +// BucketGetInventoryResult same struct to options +type BucketGetInventoryResult BucketPutInventoryOptions + +// BucketListInventoryConfiguartion same struct to options +type BucketListInventoryConfiguartion BucketPutInventoryOptions + +// BucketInventoryFilter ... +type BucketInventoryFilter struct { + Prefix string `xml:"Prefix,omitempty"` +} + +// BucketInventoryOptionalFields ... +type BucketInventoryOptionalFields struct { + XMLName xml.Name `xml:"OptionalFields,omitempty"` + BucketInventoryFields []string `xml:"Field,omitempty"` +} + +// BucketInventorySchedule ... +type BucketInventorySchedule struct { + Frequency string `xml:"Frequency"` +} + +// BucketInventoryEncryption ... +type BucketInventoryEncryption struct { + XMLName xml.Name `xml:"Encryption"` + SSECOS string `xml:"SSE-COS,omitempty"` +} + +// BucketInventoryDestinationContent ... +type BucketInventoryDestinationContent struct { + Bucket string `xml:"Bucket"` + AccountId string `xml:"AccountId,omitempty"` + Prefix string `xml:"Prefix,omitempty"` + Format string `xml:"Format"` + Encryption *BucketInventoryEncryption `xml:"Encryption,omitempty"` +} + +// BucketInventoryDestination ... +type BucketInventoryDestination struct { + XMLName xml.Name `xml:"Destination"` + BucketDestination *BucketInventoryDestinationContent `xml:"COSBucketDestination"` +} + +// BucketPutInventoryOptions ... +type BucketPutInventoryOptions struct { + XMLName xml.Name `xml:"InventoryConfiguration"` + ID string `xml:"Id"` + IsEnabled string `xml:"IsEnabled"` + IncludedObjectVersions string `xml:"IncludedObjectVersions"` + Filter *BucketInventoryFilter `xml:"Filter,omitempty"` + OptionalFields *BucketInventoryOptionalFields `xml:"OptionalFields,omitempty"` + Schedule *BucketInventorySchedule `xml:"Schedule"` + Destination *BucketInventoryDestination `xml:"Destination"` +} + +// ListBucketInventoryConfigResult result of ListBucketInventoryConfiguration +type ListBucketInventoryConfigResult struct { + XMLName xml.Name `xml:"ListInventoryConfigurationResult"` + InventoryConfigurations []BucketListInventoryConfiguartion `xml:"InventoryConfiguration,omitempty"` + IsTruncated bool `xml:"IsTruncated,omitempty"` + ContinuationToken string `xml:"ContinuationToken,omitempty"` + NextContinuationToken string `xml:"NextContinuationToken,omitempty"` +} + +// PutBucketInventory https://cloud.tencent.com/document/product/436/33707 +func (s *BucketService) PutBucketInventoryTest(ctx context.Context, id string, opt *BucketPutInventoryOptions) (*Response, error) { + u := fmt.Sprintf("/?inventory&id=%s", id) + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err + +} + +// GetBucketInventory https://cloud.tencent.com/document/product/436/33705 +func (s *BucketService) GetBucketInventoryTest(ctx context.Context, id string) (*BucketGetInventoryResult, *Response, error) { + u := fmt.Sprintf("/?inventory&id=%s", id) + var res BucketGetInventoryResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// DeleteBucketInventory https://cloud.tencent.com/document/product/436/33704 +func (s *BucketService) DeleteBucketInventoryTest(ctx context.Context, id string) (*Response, error) { + u := fmt.Sprintf("/?inventory&id=%s", id) + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// ListBucketInventoryConfigurations https://cloud.tencent.com/document/product/436/33706 +func (s *BucketService) ListBucketInventoryConfigurationsTest(ctx context.Context, token string) (*ListBucketInventoryConfigResult, *Response, error) { + var res ListBucketInventoryConfigResult + var u string + if token == "" { + u = "/?inventory" + } else { + u = fmt.Sprintf("/?inventory&continuation-token=%s", encodeURIComponent(token)) + } + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err + +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_lifecycle.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_lifecycle.go new file mode 100644 index 000000000..c9ca97f48 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_lifecycle.go @@ -0,0 +1,92 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// BucketLifecycleFilter is the param of BucketLifecycleRule +type BucketLifecycleFilter struct { + Prefix string `xml:"Prefix,omitempty"` +} + +// BucketLifecycleExpiration is the param of BucketLifecycleRule +type BucketLifecycleExpiration struct { + Date string `xml:"Date,omitempty"` + Days int `xml:"Days,omitempty"` +} + +// BucketLifecycleTransition is the param of BucketLifecycleRule +type BucketLifecycleTransition struct { + Date string `xml:"Date,omitempty"` + Days int `xml:"Days,omitempty"` + StorageClass string +} + +// BucketLifecycleAbortIncompleteMultipartUpload is the param of BucketLifecycleRule +type BucketLifecycleAbortIncompleteMultipartUpload struct { + DaysAfterInitiation string `xml:"DaysAfterInititation,omitempty"` +} + +// BucketLifecycleRule is the rule of BucketLifecycle +type BucketLifecycleRule struct { + ID string `xml:"ID,omitempty"` + Status string + Filter *BucketLifecycleFilter `xml:"Filter,omitempty"` + Transition *BucketLifecycleTransition `xml:"Transition,omitempty"` + Expiration *BucketLifecycleExpiration `xml:"Expiration,omitempty"` + AbortIncompleteMultipartUpload *BucketLifecycleAbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"` +} + +// BucketGetLifecycleResult is the result of BucketGetLifecycle +type BucketGetLifecycleResult struct { + XMLName xml.Name `xml:"LifecycleConfiguration"` + Rules []BucketLifecycleRule `xml:"Rule,omitempty"` +} + +// GetLifecycle 请求实现读取生命周期管理的配置。当配置不存在时,返回404 Not Found。 +// https://www.qcloud.com/document/product/436/8278 +func (s *BucketService) GetLifecycle(ctx context.Context) (*BucketGetLifecycleResult, *Response, error) { + var res BucketGetLifecycleResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?lifecycle", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// BucketPutLifecycleOptions is the option of PutBucketLifecycle +type BucketPutLifecycleOptions struct { + XMLName xml.Name `xml:"LifecycleConfiguration"` + Rules []BucketLifecycleRule `xml:"Rule,omitempty"` +} + +// PutLifecycle 请求实现设置生命周期管理的功能。您可以通过该请求实现数据的生命周期管理配置和定期删除。 +// 此请求为覆盖操作,上传新的配置文件将覆盖之前的配置文件。生命周期管理对文件和文件夹同时生效。 +// https://www.qcloud.com/document/product/436/8280 +func (s *BucketService) PutLifecycle(ctx context.Context, opt *BucketPutLifecycleOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?lifecycle", + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// DeleteLifecycle 请求实现删除生命周期管理。 +// https://www.qcloud.com/document/product/436/8284 +func (s *BucketService) DeleteLifecycle(ctx context.Context) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?lifecycle", + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_location.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_location.go new file mode 100644 index 000000000..dd4b5a55d --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_location.go @@ -0,0 +1,28 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// BucketGetLocationResult is the result of BucketGetLocation +type BucketGetLocationResult struct { + XMLName xml.Name `xml:"LocationConstraint"` + Location string `xml:",chardata"` +} + +// GetLocation 接口获取Bucket所在地域信息,只有Bucket所有者有权限读取信息。 +// +// https://www.qcloud.com/document/product/436/8275 +func (s *BucketService) GetLocation(ctx context.Context) (*BucketGetLocationResult, *Response, error) { + var res BucketGetLocationResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?location", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_logging.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_logging.go new file mode 100644 index 000000000..d4ea51300 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_logging.go @@ -0,0 +1,53 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// Notice bucket logging function is testing, can not use. + +// BucketLoggingEnabled main struct of logging +type BucketLoggingEnabled struct { + TargetBucket string `xml:"TargetBucket"` + TargetPrefix string `xml:"TargetPrefix"` +} + +// BucketPutLoggingOptions is the options of PutBucketLogging +type BucketPutLoggingOptions struct { + XMLName xml.Name `xml:"BucketLoggingStatus"` + LoggingEnabled *BucketLoggingEnabled `xml:"LoggingEnabled"` +} + +// BucketGetLoggingResult is the result of GetBucketLogging +type BucketGetLoggingResult struct { + XMLName xml.Name `xml:"BucketLoggingStatus"` + LoggingEnabled *BucketLoggingEnabled `xml:"LoggingEnabled"` +} + +// PutBucketLogging https://cloud.tencent.com/document/product/436/17054 +func (s *BucketService) PutBucketLoggingTest(ctx context.Context, opt *BucketPutLoggingOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?logging", + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// GetBucketLogging https://cloud.tencent.com/document/product/436/17053 +func (s *BucketService) GetBucketLoggingTest(ctx context.Context) (*BucketGetLoggingResult, *Response, error) { + var res BucketGetLoggingResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?logging", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err + +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_part.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_part.go new file mode 100644 index 000000000..e77b4207a --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_part.go @@ -0,0 +1,57 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// ListMultipartUploadsResult is the result of ListMultipartUploads +type ListMultipartUploadsResult struct { + XMLName xml.Name `xml:"ListMultipartUploadsResult"` + Bucket string `xml:"Bucket"` + EncodingType string `xml:"Encoding-Type"` + KeyMarker string + UploadIDMarker string `xml:"UploadIdMarker"` + NextKeyMarker string + NextUploadIDMarker string `xml:"NextUploadIdMarker"` + MaxUploads int + IsTruncated bool + Uploads []struct { + Key string + UploadID string `xml:"UploadId"` + StorageClass string + Initiator *Initiator + Owner *Owner + Initiated string + } `xml:"Upload,omitempty"` + Prefix string + Delimiter string `xml:"delimiter,omitempty"` + CommonPrefixes []string `xml:"CommonPrefixs>Prefix,omitempty"` +} + +// ListMultipartUploadsOptions is the option of ListMultipartUploads +type ListMultipartUploadsOptions struct { + Delimiter string `url:"delimiter,omitempty"` + EncodingType string `url:"encoding-type,omitempty"` + Prefix string `url:"prefix,omitempty"` + MaxUploads int `url:"max-uploads,omitempty"` + KeyMarker string `url:"key-marker,omitempty"` + UploadIDMarker string `url:"upload-id-marker,omitempty"` +} + +// ListMultipartUploads 用来查询正在进行中的分块上传。单次最多列出1000个正在进行中的分块上传。 +// +// https://www.qcloud.com/document/product/436/7736 +func (s *BucketService) ListMultipartUploads(ctx context.Context, opt *ListMultipartUploadsOptions) (*ListMultipartUploadsResult, *Response, error) { + var res ListMultipartUploadsResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?uploads", + method: http.MethodGet, + result: &res, + optQuery: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_replication.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_replication.go new file mode 100644 index 000000000..d0a1a9af9 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_replication.go @@ -0,0 +1,73 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// ReplicationDestination is the sub struct of BucketReplicationRule +type ReplicationDestination struct { + Bucket string `xml:"Bucket"` + StorageClass string `xml:"StorageClass,omitempty"` +} + +// BucketReplicationRule is the main param of replication +type BucketReplicationRule struct { + ID string `xml:"ID,omitempty"` + Status string `xml:"Status"` + Prefix string `xml:"Prefix"` + Destination *ReplicationDestination `xml:"Destination"` +} + +// PutBucketReplicationOptions is the options of PutBucketReplication +type PutBucketReplicationOptions struct { + XMLName xml.Name `xml:"ReplicationConfiguration"` + Role string `xml:"Role"` + Rule []BucketReplicationRule `xml:"Rule"` +} + +// GetBucketReplicationResult is the result of GetBucketReplication +type GetBucketReplicationResult struct { + XMLName xml.Name `xml:"ReplicationConfiguration"` + Role string `xml:"Role"` + Rule []BucketReplicationRule `xml:"Rule"` +} + +// PutBucketReplication https://cloud.tencent.com/document/product/436/19223 +func (s *BucketService) PutBucketReplication(ctx context.Context, opt *PutBucketReplicationOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?replication", + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err + +} + +// GetBucketReplication https://cloud.tencent.com/document/product/436/19222 +func (s *BucketService) GetBucketReplication(ctx context.Context) (*GetBucketReplicationResult, *Response, error) { + var res GetBucketReplicationResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?replication", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err + +} + +// DeleteBucketReplication https://cloud.tencent.com/document/product/436/19221 +func (s *BucketService) DeleteBucketReplication(ctx context.Context) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?replication", + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_tagging.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_tagging.go new file mode 100644 index 000000000..1d8873175 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_tagging.go @@ -0,0 +1,69 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// BucketTaggingTag is the tag of BucketTagging +type BucketTaggingTag struct { + Key string + Value string +} + +// BucketGetTaggingResult is the result of BucketGetTagging +type BucketGetTaggingResult struct { + XMLName xml.Name `xml:"Tagging"` + TagSet []BucketTaggingTag `xml:"TagSet>Tag,omitempty"` +} + +// GetTagging 接口实现获取指定Bucket的标签。 +// +// https://www.qcloud.com/document/product/436/8277 +func (s *BucketService) GetTagging(ctx context.Context) (*BucketGetTaggingResult, *Response, error) { + var res BucketGetTaggingResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?tagging", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// BucketPutTaggingOptions is the option of BucketPutTagging +type BucketPutTaggingOptions struct { + XMLName xml.Name `xml:"Tagging"` + TagSet []BucketTaggingTag `xml:"TagSet>Tag,omitempty"` +} + +// PutTagging 接口实现给用指定Bucket打标签。用来组织和管理相关Bucket。 +// +// 当该请求设置相同Key名称,不同Value时,会返回400。请求成功,则返回204。 +// +// https://www.qcloud.com/document/product/436/8281 +func (s *BucketService) PutTagging(ctx context.Context, opt *BucketPutTaggingOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?tagging", + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// DeleteTagging 接口实现删除指定Bucket的标签。 +// +// https://www.qcloud.com/document/product/436/8286 +func (s *BucketService) DeleteTagging(ctx context.Context) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?tagging", + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_version.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_version.go new file mode 100644 index 000000000..b74527bd8 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/bucket_version.go @@ -0,0 +1,45 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// BucketPutVersionOptions is the options of PutBucketVersioning +type BucketPutVersionOptions struct { + XMLName xml.Name `xml:"VersioningConfiguration"` + Status string `xml:"Status"` +} + +// BucketGetVersionResult is the result of GetBucketVersioning +type BucketGetVersionResult struct { + XMLName xml.Name `xml:"VersioningConfiguration"` + Status string `xml:"Status"` +} + +// PutVersion https://cloud.tencent.com/document/product/436/19889 +// Status has Suspended\Enabled +func (s *BucketService) PutVersioning(ctx context.Context, opt *BucketPutVersionOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?versioning", + method: http.MethodPut, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// GetVersion https://cloud.tencent.com/document/product/436/19888 +func (s *BucketService) GetVersioning(ctx context.Context) (*BucketGetVersionResult, *Response, error) { + var res BucketGetVersionResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?versioning", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/cos.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/cos.go new file mode 100644 index 000000000..adc381faf --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/cos.go @@ -0,0 +1,346 @@ +package cos + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "text/template" + + "strconv" + + "github.com/google/go-querystring/query" + "github.com/mozillazg/go-httpheader" +) + +const ( + // Version current go sdk version + Version = "0.7.3" + userAgent = "cos-go-sdk-v5/" + Version + contentTypeXML = "application/xml" + defaultServiceBaseURL = "http://service.cos.myqcloud.com" +) + +var bucketURLTemplate = template.Must( + template.New("bucketURLFormat").Parse( + "{{.Schema}}://{{.BucketName}}.cos.{{.Region}}.myqcloud.com", + ), +) + +// BaseURL 访问各 API 所需的基础 URL +type BaseURL struct { + // 访问 bucket, object 相关 API 的基础 URL(不包含 path 部分): http://example.com + BucketURL *url.URL + // 访问 service API 的基础 URL(不包含 path 部分): http://example.com + ServiceURL *url.URL +} + +// NewBucketURL 生成 BaseURL 所需的 BucketURL +// +// bucketName: bucket名称, bucket的命名规则为{name}-{appid} ,此处填写的存储桶名称必须为此格式 +// Region: 区域代码: ap-beijing-1,ap-beijing,ap-shanghai,ap-guangzhou... +// secure: 是否使用 https +func NewBucketURL(bucketName, region string, secure bool) *url.URL { + schema := "https" + if !secure { + schema = "http" + } + + w := bytes.NewBuffer(nil) + bucketURLTemplate.Execute(w, struct { + Schema string + BucketName string + Region string + }{ + schema, bucketName, region, + }) + + u, _ := url.Parse(w.String()) + return u +} + +// Client is a client manages communication with the COS API. +type Client struct { + client *http.Client + + UserAgent string + BaseURL *BaseURL + + common service + + Service *ServiceService + Bucket *BucketService + Object *ObjectService +} + +type service struct { + client *Client +} + +// NewClient returns a new COS API client. +func NewClient(uri *BaseURL, httpClient *http.Client) *Client { + if httpClient == nil { + httpClient = &http.Client{} + } + + baseURL := &BaseURL{} + if uri != nil { + baseURL.BucketURL = uri.BucketURL + baseURL.ServiceURL = uri.ServiceURL + } + if baseURL.ServiceURL == nil { + baseURL.ServiceURL, _ = url.Parse(defaultServiceBaseURL) + } + + c := &Client{ + client: httpClient, + UserAgent: userAgent, + BaseURL: baseURL, + } + c.common.client = c + c.Service = (*ServiceService)(&c.common) + c.Bucket = (*BucketService)(&c.common) + c.Object = (*ObjectService)(&c.common) + return c +} + +func (c *Client) newRequest(ctx context.Context, baseURL *url.URL, uri, method string, body interface{}, optQuery interface{}, optHeader interface{}) (req *http.Request, err error) { + uri, err = addURLOptions(uri, optQuery) + if err != nil { + return + } + u, _ := url.Parse(uri) + urlStr := baseURL.ResolveReference(u).String() + + var reader io.Reader + contentType := "" + contentMD5 := "" + if body != nil { + // 上传文件 + if r, ok := body.(io.Reader); ok { + reader = r + } else { + b, err := xml.Marshal(body) + if err != nil { + return nil, err + } + contentType = contentTypeXML + reader = bytes.NewReader(b) + contentMD5 = base64.StdEncoding.EncodeToString(calMD5Digest(b)) + } + } + + req, err = http.NewRequest(method, urlStr, reader) + if err != nil { + return + } + + req.Header, err = addHeaderOptions(req.Header, optHeader) + if err != nil { + return + } + if v := req.Header.Get("Content-Length"); req.ContentLength == 0 && v != "" && v != "0" { + req.ContentLength, _ = strconv.ParseInt(v, 10, 64) + } + + if contentMD5 != "" { + req.Header["Content-MD5"] = []string{contentMD5} + } + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + if req.Header.Get("Content-Type") == "" && contentType != "" { + req.Header.Set("Content-Type", contentType) + } + return +} + +func (c *Client) doAPI(ctx context.Context, req *http.Request, result interface{}, closeBody bool) (*Response, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + return nil, err + } + + defer func() { + if closeBody { + // Close the body to let the Transport reuse the connection + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + } + }() + + response := newResponse(resp) + + err = checkResponse(resp) + if err != nil { + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + return response, err + } + + if result != nil { + if w, ok := result.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + err = xml.NewDecoder(resp.Body).Decode(result) + if err == io.EOF { + err = nil // ignore EOF errors caused by empty response body + } + } + } + + return response, err +} + +type sendOptions struct { + // 基础 URL + baseURL *url.URL + // URL 中除基础 URL 外的剩余部分 + uri string + // 请求方法 + method string + + body interface{} + // url 查询参数 + optQuery interface{} + // http header 参数 + optHeader interface{} + // 用 result 反序列化 resp.Body + result interface{} + // 是否禁用自动调用 resp.Body.Close() + // 自动调用 Close() 是为了能够重用连接 + disableCloseBody bool +} + +func (c *Client) send(ctx context.Context, opt *sendOptions) (resp *Response, err error) { + req, err := c.newRequest(ctx, opt.baseURL, opt.uri, opt.method, opt.body, opt.optQuery, opt.optHeader) + if err != nil { + return + } + + resp, err = c.doAPI(ctx, req, opt.result, !opt.disableCloseBody) + if err != nil { + return + } + return +} + +// addURLOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func addURLOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + // 保留原有的参数,并且放在前面。因为 cos 的 url 路由是以第一个参数作为路由的 + // e.g. /?uploads + q := u.RawQuery + rq := qs.Encode() + if q != "" { + if rq != "" { + u.RawQuery = fmt.Sprintf("%s&%s", q, qs.Encode()) + } + } else { + u.RawQuery = rq + } + return u.String(), nil +} + +// addHeaderOptions adds the parameters in opt as Header fields to req. opt +// must be a struct whose fields may contain "header" tags. +func addHeaderOptions(header http.Header, opt interface{}) (http.Header, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return header, nil + } + + h, err := httpheader.Header(opt) + if err != nil { + return nil, err + } + + for key, values := range h { + for _, value := range values { + header.Add(key, value) + } + } + return header, nil +} + +// Owner defines Bucket/Object's owner +type Owner struct { + UIN string `xml:"uin,omitempty"` + ID string `xml:",omitempty"` + DisplayName string `xml:",omitempty"` +} + +// Initiator same to the Owner struct +type Initiator Owner + +// Response API 响应 +type Response struct { + *http.Response +} + +func newResponse(resp *http.Response) *Response { + return &Response{ + Response: resp, + } +} + +// ACLHeaderOptions is the option of ACLHeader +type ACLHeaderOptions struct { + XCosACL string `header:"x-cos-acl,omitempty" url:"-" xml:"-"` + XCosGrantRead string `header:"x-cos-grant-read,omitempty" url:"-" xml:"-"` + XCosGrantWrite string `header:"x-cos-grant-write,omitempty" url:"-" xml:"-"` + XCosGrantFullControl string `header:"x-cos-grant-full-control,omitempty" url:"-" xml:"-"` +} + +// ACLGrantee is the param of ACLGrant +type ACLGrantee struct { + Type string `xml:"type,attr"` + UIN string `xml:"uin,omitempty"` + URI string `xml:"URI,omitempty"` + ID string `xml:",omitempty"` + DisplayName string `xml:",omitempty"` + SubAccount string `xml:"Subaccount,omitempty"` +} + +// ACLGrant is the param of ACLXml +type ACLGrant struct { + Grantee *ACLGrantee + Permission string +} + +// ACLXml is the ACL body struct +type ACLXml struct { + XMLName xml.Name `xml:"AccessControlPolicy"` + Owner *Owner + AccessControlList []ACLGrant `xml:"AccessControlList>Grant,omitempty"` +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/doc.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/doc.go new file mode 100644 index 000000000..99005ab3a --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/doc.go @@ -0,0 +1,3 @@ +// Package cos is COS(Cloud Object Storage) Go SDK. The V5 version(XML API). +// There are examples of using each API in the project's 'example' directory. +package cos diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/error.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/error.go new file mode 100644 index 000000000..9d06194eb --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/error.go @@ -0,0 +1,49 @@ +package cos + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" +) + +// ErrorResponse 包含 API 返回的错误信息 +// +// https://www.qcloud.com/document/product/436/7730 +type ErrorResponse struct { + XMLName xml.Name `xml:"Error"` + Response *http.Response `xml:"-"` + Code string + Message string + Resource string + RequestID string `header:"x-cos-request-id,omitempty" url:"-" xml:"-"` + TraceID string `xml:"TraceId,omitempty"` +} + +// Error returns the error msg +func (r *ErrorResponse) Error() string { + RequestID := r.RequestID + if RequestID == "" { + RequestID = r.Response.Header.Get("X-Cos-Request-Id") + } + TraceID := r.TraceID + if TraceID == "" { + TraceID = r.Response.Header.Get("X-Cos-Trace-Id") + } + return fmt.Sprintf("%v %v: %d %v(Message: %v, RequestId: %v, TraceId: %v)", + r.Response.Request.Method, r.Response.Request.URL, + r.Response.StatusCode, r.Code, r.Message, RequestID, TraceID) +} + +// 检查 response 是否是出错时的返回的 response +func checkResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + errorResponse := &ErrorResponse{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && data != nil { + xml.Unmarshal(data, errorResponse) + } + return errorResponse +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/go.mod b/vendor/github.com/tencentyun/cos-go-sdk-v5/go.mod new file mode 100644 index 000000000..35afc5cbc --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/go.mod @@ -0,0 +1,10 @@ +module github.com/tencentyun/cos-go-sdk-v5 + +go 1.12 + +require ( + github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409 + github.com/google/go-querystring v1.0.0 + github.com/mozillazg/go-httpheader v0.2.1 + github.com/stretchr/testify v1.3.0 +) diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/go.sum b/vendor/github.com/tencentyun/cos-go-sdk-v5/go.sum new file mode 100644 index 000000000..5ee5a1d23 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/go.sum @@ -0,0 +1,13 @@ +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409 h1:DTQ/38ao/CfXsrK0cSAL+h4R/u0VVvfWLZEOlLwEROI= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/helper.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/helper.go new file mode 100644 index 000000000..08c5a35df --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/helper.go @@ -0,0 +1,85 @@ +package cos + +import ( + "bytes" + "crypto/md5" + "crypto/sha1" + "fmt" + "net/http" +) + +// 计算 md5 或 sha1 时的分块大小 +const calDigestBlockSize = 1024 * 1024 * 10 + +func calMD5Digest(msg []byte) []byte { + // TODO: 分块计算,减少内存消耗 + m := md5.New() + m.Write(msg) + return m.Sum(nil) +} + +func calSHA1Digest(msg []byte) []byte { + // TODO: 分块计算,减少内存消耗 + m := sha1.New() + m.Write(msg) + return m.Sum(nil) +} + +// cloneRequest returns a clone of the provided *http.Request. The clone is a +// shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header, len(r.Header)) + for k, s := range r.Header { + r2.Header[k] = append([]string(nil), s...) + } + return r2 +} + +// encodeURIComponent like same function in javascript +// +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +// +// http://www.ecma-international.org/ecma-262/6.0/#sec-uri-syntax-and-semantics +func encodeURIComponent(s string) string { + var b bytes.Buffer + written := 0 + + for i, n := 0, len(s); i < n; i++ { + c := s[i] + + switch c { + case '-', '_', '.', '!', '~', '*', '\'', '(', ')': + continue + default: + // Unreserved according to RFC 3986 sec 2.3 + if 'a' <= c && c <= 'z' { + + continue + + } + if 'A' <= c && c <= 'Z' { + + continue + + } + if '0' <= c && c <= '9' { + + continue + } + } + + b.WriteString(s[written:i]) + fmt.Fprintf(&b, "%%%02X", c) + written = i + 1 + } + + if written == 0 { + return s + } + b.WriteString(s[written:]) + return b.String() +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/object.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/object.go new file mode 100644 index 000000000..c88dc02d5 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/object.go @@ -0,0 +1,605 @@ +package cos + +import ( + "context" + "encoding/xml" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "sort" + "time" +) + +// ObjectService 相关 API +type ObjectService service + +// ObjectGetOptions is the option of GetObject +type ObjectGetOptions struct { + ResponseContentType string `url:"response-content-type,omitempty" header:"-"` + ResponseContentLanguage string `url:"response-content-language,omitempty" header:"-"` + ResponseExpires string `url:"response-expires,omitempty" header:"-"` + ResponseCacheControl string `url:"response-cache-control,omitempty" header:"-"` + ResponseContentDisposition string `url:"response-content-disposition,omitempty" header:"-"` + ResponseContentEncoding string `url:"response-content-encoding,omitempty" header:"-"` + Range string `url:"-" header:"Range,omitempty"` + IfModifiedSince string `url:"-" header:"If-Modified-Since,omitempty"` +} + +// presignedURLTestingOptions is the opt of presigned url +type presignedURLTestingOptions struct { + authTime *AuthTime +} + +// Get Object 请求可以将一个文件(Object)下载至本地。 +// 该操作需要对目标 Object 具有读权限或目标 Object 对所有人都开放了读权限(公有读)。 +// +// https://www.qcloud.com/document/product/436/7753 +func (s *ObjectService) Get(ctx context.Context, name string, opt *ObjectGetOptions, id ...string) (*Response, error) { + var u string + if len(id) == 1 { + u = fmt.Sprintf("/%s?versionId=%s", encodeURIComponent(name), id[0]) + } else if len(id) == 0 { + u = "/" + encodeURIComponent(name) + } else { + return nil, errors.New("wrong params") + } + + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodGet, + optQuery: opt, + optHeader: opt, + disableCloseBody: true, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// GetToFile download the object to local file +func (s *ObjectService) GetToFile(ctx context.Context, name, localpath string, opt *ObjectGetOptions, id ...string) (*Response, error) { + resp, err := s.Get(ctx, name, opt, id...) + if err != nil { + return resp, err + } + defer resp.Body.Close() + + // If file exist, overwrite it + fd, err := os.OpenFile(localpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660) + if err != nil { + return resp, err + } + + _, err = io.Copy(fd, resp.Body) + fd.Close() + if err != nil { + return resp, err + } + + return resp, nil +} + +// GetPresignedURL get the object presigned to down or upload file by url +func (s *ObjectService) GetPresignedURL(ctx context.Context, httpMethod, name, ak, sk string, expired time.Duration, opt interface{}) (*url.URL, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name), + method: httpMethod, + optQuery: opt, + optHeader: opt, + } + req, err := s.client.newRequest(ctx, sendOpt.baseURL, sendOpt.uri, sendOpt.method, sendOpt.body, sendOpt.optQuery, sendOpt.optHeader) + if err != nil { + return nil, err + } + + var authTime *AuthTime + if opt != nil { + if opt, ok := opt.(*presignedURLTestingOptions); ok { + authTime = opt.authTime + } + } + if authTime == nil { + authTime = NewAuthTime(expired) + } + authorization := newAuthorization(ak, sk, req, authTime) + sign := encodeURIComponent(authorization) + + if req.URL.RawQuery == "" { + req.URL.RawQuery = fmt.Sprintf("sign=%s", sign) + } else { + req.URL.RawQuery = fmt.Sprintf("%s&sign=%s", req.URL.RawQuery, sign) + } + return req.URL, nil + +} + +// ObjectPutHeaderOptions the options of header of the put object +type ObjectPutHeaderOptions struct { + CacheControl string `header:"Cache-Control,omitempty" url:"-"` + ContentDisposition string `header:"Content-Disposition,omitempty" url:"-"` + ContentEncoding string `header:"Content-Encoding,omitempty" url:"-"` + ContentType string `header:"Content-Type,omitempty" url:"-"` + ContentMD5 string `header:"Content-MD5,omitempty" url:"-"` + ContentLength int `header:"Content-Length,omitempty" url:"-"` + Expect string `header:"Expect,omitempty" url:"-"` + Expires string `header:"Expires,omitempty" url:"-"` + XCosContentSHA1 string `header:"x-cos-content-sha1,omitempty" url:"-"` + // 自定义的 x-cos-meta-* header + XCosMetaXXX *http.Header `header:"x-cos-meta-*,omitempty" url:"-"` + XCosStorageClass string `header:"x-cos-storage-class,omitempty" url:"-"` + // 可选值: Normal, Appendable + //XCosObjectType string `header:"x-cos-object-type,omitempty" url:"-"` + // Enable Server Side Encryption, Only supported: AES256 + XCosServerSideEncryption string `header:"x-cos-server-side-encryption,omitempty" url:"-" xml:"-"` +} + +// ObjectPutOptions the options of put object +type ObjectPutOptions struct { + *ACLHeaderOptions `header:",omitempty" url:"-" xml:"-"` + *ObjectPutHeaderOptions `header:",omitempty" url:"-" xml:"-"` +} + +// Put Object请求可以将一个文件(Oject)上传至指定Bucket。 +// +// 当 r 不是 bytes.Buffer/bytes.Reader/strings.Reader 时,必须指定 opt.ObjectPutHeaderOptions.ContentLength +// +// https://www.qcloud.com/document/product/436/7749 +func (s *ObjectService) Put(ctx context.Context, name string, r io.Reader, opt *ObjectPutOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name), + method: http.MethodPut, + body: r, + optHeader: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// PutFromFile put object from local file +// Notice that when use this put large file need set non-body of debug req/resp, otherwise will out of memory +func (s *ObjectService) PutFromFile(ctx context.Context, name string, filePath string, opt *ObjectPutOptions) (*Response, error) { + fd, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer fd.Close() + + return s.Put(ctx, name, fd, opt) +} + +// ObjectCopyHeaderOptions is the head option of the Copy +type ObjectCopyHeaderOptions struct { + // When use replace directive to update meta infos + CacheControl string `header:"Cache-Control,omitempty" url:"-"` + ContentDisposition string `header:"Content-Disposition,omitempty" url:"-"` + ContentEncoding string `header:"Content-Encoding,omitempty" url:"-"` + ContentType string `header:"Content-Type,omitempty" url:"-"` + Expires string `header:"Expires,omitempty" url:"-"` + Expect string `header:"Expect,omitempty" url:"-"` + XCosMetadataDirective string `header:"x-cos-metadata-directive,omitempty" url:"-" xml:"-"` + XCosCopySourceIfModifiedSince string `header:"x-cos-copy-source-If-Modified-Since,omitempty" url:"-" xml:"-"` + XCosCopySourceIfUnmodifiedSince string `header:"x-cos-copy-source-If-Unmodified-Since,omitempty" url:"-" xml:"-"` + XCosCopySourceIfMatch string `header:"x-cos-copy-source-If-Match,omitempty" url:"-" xml:"-"` + XCosCopySourceIfNoneMatch string `header:"x-cos-copy-source-If-None-Match,omitempty" url:"-" xml:"-"` + XCosStorageClass string `header:"x-cos-storage-class,omitempty" url:"-" xml:"-"` + // 自定义的 x-cos-meta-* header + XCosMetaXXX *http.Header `header:"x-cos-meta-*,omitempty" url:"-"` + XCosCopySource string `header:"x-cos-copy-source" url:"-" xml:"-"` + XCosServerSideEncryption string `header:"x-cos-server-side-encryption,omitempty" url:"-" xml:"-"` +} + +// ObjectCopyOptions is the option of Copy, choose header or body +type ObjectCopyOptions struct { + *ObjectCopyHeaderOptions `header:",omitempty" url:"-" xml:"-"` + *ACLHeaderOptions `header:",omitempty" url:"-" xml:"-"` +} + +// ObjectCopyResult is the result of Copy +type ObjectCopyResult struct { + XMLName xml.Name `xml:"CopyObjectResult"` + ETag string `xml:"ETag,omitempty"` + LastModified string `xml:"LastModified,omitempty"` +} + +// Copy 调用 PutObjectCopy 请求实现将一个文件从源路径复制到目标路径。建议文件大小 1M 到 5G, +// 超过 5G 的文件请使用分块上传 Upload - Copy。在拷贝的过程中,文件元属性和 ACL 可以被修改。 +// +// 用户可以通过该接口实现文件移动,文件重命名,修改文件属性和创建副本。 +// +// 注意:在跨帐号复制的时候,需要先设置被复制文件的权限为公有读,或者对目标帐号赋权,同帐号则不需要。 +// +// https://cloud.tencent.com/document/product/436/10881 +func (s *ObjectService) Copy(ctx context.Context, name, sourceURL string, opt *ObjectCopyOptions) (*ObjectCopyResult, *Response, error) { + var res ObjectCopyResult + if opt == nil { + opt = new(ObjectCopyOptions) + } + if opt.ObjectCopyHeaderOptions == nil { + opt.ObjectCopyHeaderOptions = new(ObjectCopyHeaderOptions) + } + opt.XCosCopySource = encodeURIComponent(sourceURL) + + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name), + method: http.MethodPut, + body: nil, + optHeader: opt, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + // If the error occurs during the copy operation, the error response is embedded in the 200 OK response. This means that a 200 OK response can contain either a success or an error. + if err == nil && resp.StatusCode == 200 { + if res.ETag == "" { + return &res, resp, errors.New("response 200 OK, but body contains an error") + } + } + return &res, resp, err +} + +// Delete Object请求可以将一个文件(Object)删除。 +// +// https://www.qcloud.com/document/product/436/7743 +func (s *ObjectService) Delete(ctx context.Context, name string) (*Response, error) { + // When use "" string might call the delete bucket interface + if len(name) == 0 { + return nil, errors.New("empty object name") + } + + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name), + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// ObjectHeadOptions is the option of HeadObject +type ObjectHeadOptions struct { + IfModifiedSince string `url:"-" header:"If-Modified-Since,omitempty"` +} + +// Head Object请求可以取回对应Object的元数据,Head的权限与Get的权限一致 +// +// https://www.qcloud.com/document/product/436/7745 +func (s *ObjectService) Head(ctx context.Context, name string, opt *ObjectHeadOptions, id ...string) (*Response, error) { + var u string + if len(id) == 1 { + u = fmt.Sprintf("/%s?versionId=%s", encodeURIComponent(name), id[0]) + } else if len(id) == 0 { + u = "/" + encodeURIComponent(name) + } else { + return nil, errors.New("wrong params") + } + + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodHead, + optHeader: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + if resp != nil && resp.Header["X-Cos-Object-Type"] != nil && resp.Header["X-Cos-Object-Type"][0] == "appendable" { + resp.Header.Add("x-cos-next-append-position", resp.Header["Content-Length"][0]) + } + + return resp, err +} + +// ObjectOptionsOptions is the option of object options +type ObjectOptionsOptions struct { + Origin string `url:"-" header:"Origin"` + AccessControlRequestMethod string `url:"-" header:"Access-Control-Request-Method"` + AccessControlRequestHeaders string `url:"-" header:"Access-Control-Request-Headers,omitempty"` +} + +// Options Object请求实现跨域访问的预请求。即发出一个 OPTIONS 请求给服务器以确认是否可以进行跨域操作。 +// +// 当CORS配置不存在时,请求返回403 Forbidden。 +// +// https://www.qcloud.com/document/product/436/8288 +func (s *ObjectService) Options(ctx context.Context, name string, opt *ObjectOptionsOptions) (*Response, error) { + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name), + method: http.MethodOptions, + optHeader: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// CASJobParameters support three way: Standard(in 35 hours), Expedited(quick way, in 15 mins), Bulk(in 5-12 hours_ +type CASJobParameters struct { + Tier string `xml:"Tier"` +} + +// ObjectRestoreOptions is the option of object restore +type ObjectRestoreOptions struct { + XMLName xml.Name `xml:"RestoreRequest"` + Days int `xml:"Days"` + Tier *CASJobParameters `xml:"CASJobParameters"` +} + +// PutRestore API can recover an object of type archived by COS archive. +// +// https://cloud.tencent.com/document/product/436/12633 +func (s *ObjectService) PostRestore(ctx context.Context, name string, opt *ObjectRestoreOptions) (*Response, error) { + u := fmt.Sprintf("/%s?restore", encodeURIComponent(name)) + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodPost, + body: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + + return resp, err +} + +// TODO Append 接口在优化未开放使用 +// +// Append请求可以将一个文件(Object)以分块追加的方式上传至 Bucket 中。使用Append Upload的文件必须事前被设定为Appendable。 +// 当Appendable的文件被执行Put Object的操作以后,文件被覆盖,属性改变为Normal。 +// +// 文件属性可以在Head Object操作中被查询到,当您发起Head Object请求时,会返回自定义Header『x-cos-object-type』,该Header只有两个枚举值:Normal或者Appendable。 +// +// 追加上传建议文件大小1M - 5G。如果position的值和当前Object的长度不致,COS会返回409错误。 +// 如果Append一个Normal的Object,COS会返回409 ObjectNotAppendable。 +// +// Appendable的文件不可以被复制,不参与版本管理,不参与生命周期管理,不可跨区域复制。 +// +// 当 r 不是 bytes.Buffer/bytes.Reader/strings.Reader 时,必须指定 opt.ObjectPutHeaderOptions.ContentLength +// +// https://www.qcloud.com/document/product/436/7741 +// func (s *ObjectService) Append(ctx context.Context, name string, position int, r io.Reader, opt *ObjectPutOptions) (*Response, error) { +// u := fmt.Sprintf("/%s?append&position=%d", encodeURIComponent(name), position) +// if position != 0{ +// opt = nil +// } +// sendOpt := sendOptions{ +// baseURL: s.client.BaseURL.BucketURL, +// uri: u, +// method: http.MethodPost, +// optHeader: opt, +// body: r, +// } +// resp, err := s.client.send(ctx, &sendOpt) +// return resp, err +// } + +// ObjectDeleteMultiOptions is the option of DeleteMulti +type ObjectDeleteMultiOptions struct { + XMLName xml.Name `xml:"Delete" header:"-"` + Quiet bool `xml:"Quiet" header:"-"` + Objects []Object `xml:"Object" header:"-"` + //XCosSha1 string `xml:"-" header:"x-cos-sha1"` +} + +// ObjectDeleteMultiResult is the result of DeleteMulti +type ObjectDeleteMultiResult struct { + XMLName xml.Name `xml:"DeleteResult"` + DeletedObjects []Object `xml:"Deleted,omitempty"` + Errors []struct { + Key string + Code string + Message string + } `xml:"Error,omitempty"` +} + +// DeleteMulti 请求实现批量删除文件,最大支持单次删除1000个文件。 +// 对于返回结果,COS提供Verbose和Quiet两种结果模式。Verbose模式将返回每个Object的删除结果; +// Quiet模式只返回报错的Object信息。 +// https://www.qcloud.com/document/product/436/8289 +func (s *ObjectService) DeleteMulti(ctx context.Context, opt *ObjectDeleteMultiOptions) (*ObjectDeleteMultiResult, *Response, error) { + var res ObjectDeleteMultiResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/?delete", + method: http.MethodPost, + body: opt, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// Object is the meta info of the object +type Object struct { + Key string `xml:",omitempty"` + ETag string `xml:",omitempty"` + Size int `xml:",omitempty"` + PartNumber int `xml:",omitempty"` + LastModified string `xml:",omitempty"` + StorageClass string `xml:",omitempty"` + Owner *Owner `xml:",omitempty"` +} + +// MultiUploadOptions is the option of the multiupload, +// ThreadPoolSize default is one +type MultiUploadOptions struct { + OptIni *InitiateMultipartUploadOptions + PartSize int64 + ThreadPoolSize int +} + +type Chunk struct { + Number int + OffSet int64 + Size int64 +} + +// jobs +type Jobs struct { + Name string + UploadId string + FilePath string + RetryTimes int + Chunk Chunk + Data io.Reader + Opt *ObjectUploadPartOptions +} + +type Results struct { + PartNumber int + Resp *Response +} + +func worker(s *ObjectService, jobs <-chan *Jobs, results chan<- *Results) { + for j := range jobs { + fd, err := os.Open(j.FilePath) + var res Results + if err != nil { + res.PartNumber = j.Chunk.Number + res.Resp = nil + results <- &res + } + + fd.Seek(j.Chunk.OffSet, os.SEEK_SET) + // UploadPart do not support the chunk trsf, so need to add the content-length + opt := &ObjectUploadPartOptions{ + ContentLength: int(j.Chunk.Size), + } + + rt := j.RetryTimes + for { + resp, err := s.UploadPart(context.Background(), j.Name, j.UploadId, j.Chunk.Number, + &io.LimitedReader{R: fd, N: j.Chunk.Size}, opt) + res.PartNumber = j.Chunk.Number + res.Resp = resp + if err != nil { + rt-- + if rt == 0 { + fd.Close() + results <- &res + break + } + continue + } + fd.Close() + results <- &res + break + } + } +} + +func SplitFileIntoChunks(filePath string, partSize int64) ([]Chunk, int, error) { + if filePath == "" || partSize <= 0 { + return nil, 0, errors.New("chunkSize invalid") + } + + file, err := os.Open(filePath) + if err != nil { + return nil, 0, err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return nil, 0, err + } + var partNum = stat.Size() / partSize + // 10000 max part size + if partNum >= 10000 { + return nil, 0, errors.New("Too many parts, out of 10000") + } + + var chunks []Chunk + var chunk = Chunk{} + for i := int64(0); i < partNum; i++ { + chunk.Number = int(i + 1) + chunk.OffSet = i * partSize + chunk.Size = partSize + chunks = append(chunks, chunk) + } + + if stat.Size()%partSize > 0 { + chunk.Number = len(chunks) + 1 + chunk.OffSet = int64(len(chunks)) * partSize + chunk.Size = stat.Size() % partSize + chunks = append(chunks, chunk) + partNum++ + } + + return chunks, int(partNum), nil + +} + +// MultiUpload 为高级upload接口,并发分块上传 +// 注意该接口目前只供参考 +// +// 需要指定分块大小 partSize >= 1 ,单位为MB +// 同时请确认分块数量不超过10000 +// + +func (s *ObjectService) MultiUpload(ctx context.Context, name string, filepath string, opt *MultiUploadOptions) (*CompleteMultipartUploadResult, *Response, error) { + // 1.Get the file chunk + bufSize := opt.PartSize * 1024 * 1024 + chunks, partNum, err := SplitFileIntoChunks(filepath, bufSize) + if err != nil { + return nil, nil, err + } + + // 2.Init + optini := opt.OptIni + res, _, err := s.InitiateMultipartUpload(ctx, name, optini) + if err != nil { + return nil, nil, err + } + uploadID := res.UploadID + var poolSize int + if opt.ThreadPoolSize > 0 { + poolSize = opt.ThreadPoolSize + } else { + // Default is one + poolSize = 1 + } + + chjobs := make(chan *Jobs, 100) + chresults := make(chan *Results, 10000) + optcom := &CompleteMultipartUploadOptions{} + + // 3.Start worker + for w := 1; w <= poolSize; w++ { + go worker(s, chjobs, chresults) + } + + // 4.Push jobs + for _, chunk := range chunks { + job := &Jobs{ + Name: name, + RetryTimes: 3, + FilePath: filepath, + UploadId: uploadID, + Chunk: chunk, + } + chjobs <- job + } + close(chjobs) + + // 5.Recv the resp etag to complete + for i := 1; i <= partNum; i++ { + res := <-chresults + // Notice one part fail can not get the etag according. + if res.Resp == nil { + // Some part already fail, can not to get the header inside. + return nil, nil, fmt.Errorf("UploadID %s, part %d failed to get resp content.", uploadID, res.PartNumber) + } + // Notice one part fail can not get the etag according. + etag := res.Resp.Header.Get("ETag") + optcom.Parts = append(optcom.Parts, Object{ + PartNumber: res.PartNumber, ETag: etag}, + ) + } + sort.Sort(ObjectList(optcom.Parts)) + + v, resp, err := s.CompleteMultipartUpload(context.Background(), name, uploadID, optcom) + + return v, resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/object_acl.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/object_acl.go new file mode 100644 index 000000000..2dc5935a4 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/object_acl.go @@ -0,0 +1,63 @@ +package cos + +import ( + "context" + "net/http" +) + +// ObjectGetACLResult is the result of GetObjectACL +type ObjectGetACLResult ACLXml + +// GetACL Get Object ACL接口实现使用API读取Object的ACL表,只有所有者有权操作。 +// +// https://www.qcloud.com/document/product/436/7744 +func (s *ObjectService) GetACL(ctx context.Context, name string) (*ObjectGetACLResult, *Response, error) { + var res ObjectGetACLResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name) + "?acl", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// ObjectPutACLOptions the options of put object acl +type ObjectPutACLOptions struct { + Header *ACLHeaderOptions `url:"-" xml:"-"` + Body *ACLXml `url:"-" header:"-"` +} + +// PutACL 使用API写入Object的ACL表,您可以通过Header:"x-cos-acl", "x-cos-grant-read" , +// "x-cos-grant-write" ,"x-cos-grant-full-control"传入ACL信息, +// 也可以通过body以XML格式传入ACL信息,但是只能选择Header和Body其中一种,否则,返回冲突。 +// +// Put Object ACL是一个覆盖操作,传入新的ACL将覆盖原有ACL。只有所有者有权操作。 +// +// "x-cos-acl":枚举值为public-read,private;public-read意味这个Object有公有读私有写的权限, +// private意味这个Object有私有读写的权限。 +// +// "x-cos-grant-read":意味被赋予权限的用户拥有该Object的读权限 +// +// "x-cos-grant-write":意味被赋予权限的用户拥有该Object的写权限 +// +// "x-cos-grant-full-control":意味被赋予权限的用户拥有该Object的读写权限 +// +// https://www.qcloud.com/document/product/436/7748 +func (s *ObjectService) PutACL(ctx context.Context, name string, opt *ObjectPutACLOptions) (*Response, error) { + header := opt.Header + body := opt.Body + if body != nil { + header = nil + } + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name) + "?acl", + method: http.MethodPut, + optHeader: header, + body: body, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/object_part.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/object_part.go new file mode 100644 index 000000000..6ef577356 --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/object_part.go @@ -0,0 +1,191 @@ +package cos + +import ( + "context" + "encoding/xml" + "errors" + "fmt" + "io" + "net/http" +) + +// InitiateMultipartUploadOptions is the option of InitateMultipartUpload +type InitiateMultipartUploadOptions struct { + *ACLHeaderOptions + *ObjectPutHeaderOptions +} + +// InitiateMultipartUploadResult is the result of InitateMultipartUpload +type InitiateMultipartUploadResult struct { + XMLName xml.Name `xml:"InitiateMultipartUploadResult"` + Bucket string + Key string + UploadID string `xml:"UploadId"` +} + +// InitiateMultipartUpload 请求实现初始化分片上传,成功执行此请求以后会返回Upload ID用于后续的Upload Part请求。 +// +// https://www.qcloud.com/document/product/436/7746 +func (s *ObjectService) InitiateMultipartUpload(ctx context.Context, name string, opt *InitiateMultipartUploadOptions) (*InitiateMultipartUploadResult, *Response, error) { + var res InitiateMultipartUploadResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: "/" + encodeURIComponent(name) + "?uploads", + method: http.MethodPost, + optHeader: opt, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// ObjectUploadPartOptions is the options of upload-part +type ObjectUploadPartOptions struct { + Expect string `header:"Expect,omitempty" url:"-"` + XCosContentSHA1 string `header:"x-cos-content-sha1" url:"-"` + ContentLength int `header:"Content-Length,omitempty" url:"-"` +} + +// UploadPart 请求实现在初始化以后的分块上传,支持的块的数量为1到10000,块的大小为1 MB 到5 GB。 +// 在每次请求Upload Part时候,需要携带partNumber和uploadID,partNumber为块的编号,支持乱序上传。 +// +// 当传入uploadID和partNumber都相同的时候,后传入的块将覆盖之前传入的块。当uploadID不存在时会返回404错误,NoSuchUpload. +// +// 当 r 不是 bytes.Buffer/bytes.Reader/strings.Reader 时,必须指定 opt.ContentLength +// +// https://www.qcloud.com/document/product/436/7750 +func (s *ObjectService) UploadPart(ctx context.Context, name, uploadID string, partNumber int, r io.Reader, opt *ObjectUploadPartOptions) (*Response, error) { + u := fmt.Sprintf("/%s?partNumber=%d&uploadId=%s", encodeURIComponent(name), partNumber, uploadID) + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodPut, + optHeader: opt, + body: r, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} + +// ObjectListPartsOptions is the option of ListParts +type ObjectListPartsOptions struct { + EncodingType string `url:"Encoding-type,omitempty"` + MaxParts string `url:"max-parts,omitempty"` + PartNumberMarker string `url:"part-number-marker,omitempty"` +} + +// ObjectListPartsResult is the result of ListParts +type ObjectListPartsResult struct { + XMLName xml.Name `xml:"ListPartsResult"` + Bucket string + EncodingType string `xml:"Encoding-type,omitempty"` + Key string + UploadID string `xml:"UploadId"` + Initiator *Initiator `xml:"Initiator,omitempty"` + Owner *Owner `xml:"Owner,omitempty"` + StorageClass string + PartNumberMarker string + NextPartNumberMarker string `xml:"NextPartNumberMarker,omitempty"` + MaxParts string + IsTruncated bool + Parts []Object `xml:"Part,omitempty"` +} + +// ListParts 用来查询特定分块上传中的已上传的块。 +// +// https://www.qcloud.com/document/product/436/7747 +func (s *ObjectService) ListParts(ctx context.Context, name, uploadID string, opt *ObjectListPartsOptions) (*ObjectListPartsResult, *Response, error) { + u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) + var res ObjectListPartsResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodGet, + result: &res, + optQuery: opt, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} + +// CompleteMultipartUploadOptions is the option of CompleteMultipartUpload +type CompleteMultipartUploadOptions struct { + XMLName xml.Name `xml:"CompleteMultipartUpload"` + Parts []Object `xml:"Part"` +} + +// CompleteMultipartUploadResult is the result CompleteMultipartUpload +type CompleteMultipartUploadResult struct { + XMLName xml.Name `xml:"CompleteMultipartUploadResult"` + Location string + Bucket string + Key string + ETag string +} + +// ObjectList can used for sort the parts which needs in complete upload part +// sort.Sort(cos.ObjectList(opt.Parts)) +type ObjectList []Object + +func (o ObjectList) Len() int { + return len(o) +} + +func (o ObjectList) Swap(i, j int) { + o[i], o[j] = o[j], o[i] +} + +func (o ObjectList) Less(i, j int) bool { // rewrite the Less method from small to big + return o[i].PartNumber < o[j].PartNumber +} + +// CompleteMultipartUpload 用来实现完成整个分块上传。当您已经使用Upload Parts上传所有块以后,你可以用该API完成上传。 +// 在使用该API时,您必须在Body中给出每一个块的PartNumber和ETag,用来校验块的准确性。 +// +// 由于分块上传的合并需要数分钟时间,因而当合并分块开始的时候,COS就立即返回200的状态码,在合并的过程中, +// COS会周期性的返回空格信息来保持连接活跃,直到合并完成,COS会在Body中返回合并后块的内容。 +// +// 当上传块小于1 MB的时候,在调用该请求时,会返回400 EntityTooSmall; +// 当上传块编号不连续的时候,在调用该请求时,会返回400 InvalidPart; +// 当请求Body中的块信息没有按序号从小到大排列的时候,在调用该请求时,会返回400 InvalidPartOrder; +// 当UploadId不存在的时候,在调用该请求时,会返回404 NoSuchUpload。 +// +// 建议您及时完成分块上传或者舍弃分块上传,因为已上传但是未终止的块会占用存储空间进而产生存储费用。 +// +// https://www.qcloud.com/document/product/436/7742 +func (s *ObjectService) CompleteMultipartUpload(ctx context.Context, name, uploadID string, opt *CompleteMultipartUploadOptions) (*CompleteMultipartUploadResult, *Response, error) { + u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) + var res CompleteMultipartUploadResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodPost, + body: opt, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + // If the error occurs during the copy operation, the error response is embedded in the 200 OK response. This means that a 200 OK response can contain either a success or an error. + if err == nil && resp.StatusCode == 200 { + if res.ETag == "" { + return &res, resp, errors.New("response 200 OK, but body contains an error") + } + } + return &res, resp, err +} + +// AbortMultipartUpload 用来实现舍弃一个分块上传并删除已上传的块。当您调用Abort Multipart Upload时, +// 如果有正在使用这个Upload Parts上传块的请求,则Upload Parts会返回失败。当该UploadID不存在时,会返回404 NoSuchUpload。 +// +// 建议您及时完成分块上传或者舍弃分块上传,因为已上传但是未终止的块会占用存储空间进而产生存储费用。 +// +// https://www.qcloud.com/document/product/436/7740 +func (s *ObjectService) AbortMultipartUpload(ctx context.Context, name, uploadID string) (*Response, error) { + u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.BucketURL, + uri: u, + method: http.MethodDelete, + } + resp, err := s.client.send(ctx, &sendOpt) + return resp, err +} diff --git a/vendor/github.com/tencentyun/cos-go-sdk-v5/service.go b/vendor/github.com/tencentyun/cos-go-sdk-v5/service.go new file mode 100644 index 000000000..defe938ae --- /dev/null +++ b/vendor/github.com/tencentyun/cos-go-sdk-v5/service.go @@ -0,0 +1,35 @@ +package cos + +import ( + "context" + "encoding/xml" + "net/http" +) + +// Service 相关 API +type ServiceService service + +// ServiceGetResult is the result of Get Service +type ServiceGetResult struct { + XMLName xml.Name `xml:"ListAllMyBucketsResult"` + Owner *Owner `xml:"Owner"` + Buckets []Bucket `xml:"Buckets>Bucket,omitempty"` +} + +// Get Service 接口实现获取该用户下所有Bucket列表。 +// +// 该API接口需要使用Authorization签名认证, +// 且只能获取签名中AccessID所属账户的Bucket列表。 +// +// https://www.qcloud.com/document/product/436/8291 +func (s *ServiceService) Get(ctx context.Context) (*ServiceGetResult, *Response, error) { + var res ServiceGetResult + sendOpt := sendOptions{ + baseURL: s.client.BaseURL.ServiceURL, + uri: "/", + method: http.MethodGet, + result: &res, + } + resp, err := s.client.send(ctx, &sendOpt) + return &res, resp, err +} diff --git a/website/docs/backends/types/cos.html.md b/website/docs/backends/types/cos.html.md new file mode 100644 index 000000000..5c4ccc762 --- /dev/null +++ b/website/docs/backends/types/cos.html.md @@ -0,0 +1,61 @@ +--- +layout: "backend-types" +page_title: "Backend Type: cos" +sidebar_current: "docs-backends-types-standard-cos" +description: |- + Terraform can store the state remotely, making it easier to version and work with in a team. +--- + +# COS + +**Kind: Standard (with locking)** + +Stores the state as an object in a configurable prefix in a given bucket on [Tencent Cloud Object Storage](https://intl.cloud.tencent.com/product/cos) (COS). +This backend also supports [state locking](/docs/state/locking.html). + +~> **Warning!** It is highly recommended that you enable [Object Versioning](https://intl.cloud.tencent.com/document/product/436/19883) +on the COS bucket to allow for state recovery in the case of accidental deletions and human error. + +## Example Configuration + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-1258798060" + prefix = "terraform/state" + } +} +``` + +This assumes we have a [COS Bucket](https://www.terraform.io/docs/providers/tencentcloud/r/cos_bucket.html) created named `bucket-for-terraform-state-1258798060`, +Terraform state will be written into the file `terraform/state/terraform.tfstate`. + +## Using the COS remote state + +To make use of the COS remote state we can use the [`terraform_remote_state` data source](/docs/providers/terraform/d/remote_state.html). + +```hcl +data "terraform_remote_state" "foo" { + backend = "cos" + + config = { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-1258798060" + prefix = "terraform/state" + } +} +``` + +## Configuration variables + +The following configuration options or environment variables are supported: + + * `secret_id` - (Optional) Secret id of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_ID`. + * `secret_key` - (Optional) Secret key of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_KEY`. + * `region` - (Optional) The region of the COS bucket. It supports environment variables `TENCENTCLOUD_REGION`. + * `bucket` - (Required) The name of the COS bucket. You shall manually create it first. + * `prefix` - (Optional) The directory for saving the state file in bucket. Default to "env:". + * `key` - (Optional) The path for saving the state file in bucket. Defaults to `terraform.tfstate`. + * `encrypt` - (Optional) Whether to enable server side encryption of the state file. If it is true, COS will use 'AES256' encryption algorithm to encrypt state file. + * `acl` - (Optional) Object ACL to be applied to the state file, allows `private` and `public-read`. Defaults to `private`. diff --git a/website/docs/state/workspaces.html.md b/website/docs/state/workspaces.html.md index 28ea945b7..899d5a5e4 100644 --- a/website/docs/state/workspaces.html.md +++ b/website/docs/state/workspaces.html.md @@ -27,6 +27,7 @@ Multiple workspaces are currently supported by the following backends: * [AzureRM](/docs/backends/types/azurerm.html) * [Consul](/docs/backends/types/consul.html) + * [COS](/docs/backends/types/cos.html) * [GCS](/docs/backends/types/gcs.html) * [Local](/docs/backends/types/local.html) * [Manta](/docs/backends/types/manta.html) diff --git a/website/layouts/backend-types.erb b/website/layouts/backend-types.erb index 315470890..39f1200b6 100644 --- a/website/layouts/backend-types.erb +++ b/website/layouts/backend-types.erb @@ -36,6 +36,9 @@ > consul + > + cos + > etcd