backend/cos: Add TencentCloud backend cos with lock (#22540)
* add TencentCloud COS backend for remote state * add vendor of dependence * fixed error not handle and remove default value for prefix argument * get appid from TF_COS_APPID environment variables
This commit is contained in:
parent
7bd662d587
commit
76e5b446ba
|
@ -5,3 +5,4 @@
|
||||||
/backend/remote-state/azure @hashicorp/terraform-azure
|
/backend/remote-state/azure @hashicorp/terraform-azure
|
||||||
/backend/remote-state/gcs @hashicorp/terraform-google
|
/backend/remote-state/gcs @hashicorp/terraform-google
|
||||||
/backend/remote-state/s3 @hashicorp/terraform-aws
|
/backend/remote-state/s3 @hashicorp/terraform-aws
|
||||||
|
/backend/remote-state/s3 @likexian
|
|
@ -16,6 +16,7 @@ import (
|
||||||
backendArtifactory "github.com/hashicorp/terraform/backend/remote-state/artifactory"
|
backendArtifactory "github.com/hashicorp/terraform/backend/remote-state/artifactory"
|
||||||
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
|
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
|
||||||
backendConsul "github.com/hashicorp/terraform/backend/remote-state/consul"
|
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"
|
backendEtcdv2 "github.com/hashicorp/terraform/backend/remote-state/etcdv2"
|
||||||
backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
|
backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
|
||||||
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
|
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() },
|
"atlas": func() backend.Backend { return backendAtlas.New() },
|
||||||
"azurerm": func() backend.Backend { return backendAzure.New() },
|
"azurerm": func() backend.Backend { return backendAzure.New() },
|
||||||
"consul": func() backend.Backend { return backendConsul.New() },
|
"consul": func() backend.Backend { return backendConsul.New() },
|
||||||
|
"cos": func() backend.Backend { return backendCos.New() },
|
||||||
"etcd": func() backend.Backend { return backendEtcdv2.New() },
|
"etcd": func() backend.Backend { return backendEtcdv2.New() },
|
||||||
"etcdv3": func() backend.Backend { return backendEtcdv3.New() },
|
"etcdv3": func() backend.Backend { return backendEtcdv3.New() },
|
||||||
"gcs": func() backend.Backend { return backendGCS.New() },
|
"gcs": func() backend.Backend { return backendGCS.New() },
|
||||||
|
|
|
@ -18,6 +18,7 @@ func TestInit_backend(t *testing.T) {
|
||||||
{"atlas", "*atlas.Backend"},
|
{"atlas", "*atlas.Backend"},
|
||||||
{"azurerm", "*azure.Backend"},
|
{"azurerm", "*azure.Backend"},
|
||||||
{"consul", "*consul.Backend"},
|
{"consul", "*consul.Backend"},
|
||||||
|
{"cos", "*cos.Backend"},
|
||||||
{"etcdv3", "*etcd.Backend"},
|
{"etcdv3", "*etcd.Backend"},
|
||||||
{"gcs", "*gcs.Backend"},
|
{"gcs", "*gcs.Backend"},
|
||||||
{"inmem", "*inmem.Backend"},
|
{"inmem", "*inmem.Backend"},
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
// <name>.tfstate
|
||||||
|
if !strings.HasSuffix(vv.Key, stateFileSuffix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// default worksapce
|
||||||
|
if path.Join(b.prefix, b.key) == vv.Key {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// <prefix>/<worksapce>/<key>
|
||||||
|
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.
|
||||||
|
`
|
|
@ -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], "")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
3
go.mod
3
go.mod
|
@ -84,6 +84,7 @@ require (
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||||
github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba // indirect
|
github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba // indirect
|
||||||
github.com/lib/pq v1.0.0
|
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/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82
|
||||||
github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b
|
github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b
|
||||||
github.com/mattn/go-colorable v0.1.1
|
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/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
|
||||||
github.com/soheilhy/cmux v0.1.4 // indirect
|
github.com/soheilhy/cmux v0.1.4 // indirect
|
||||||
github.com/spf13/afero v1.2.1
|
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/terraform-providers/terraform-provider-openstack v1.15.0
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
|
||||||
github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5 // indirect
|
github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5 // indirect
|
||||||
|
|
15
go.sum
15
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/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 h1:y8Gs8CzNfDF5AZvjr+5UyGQvQEBL7pwo+v+wX6q9JI8=
|
||||||
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
|
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 h1:tuQ7w+my8a8mkwN7x2TSd7OzTjkZ7rAeSyH4xncuAMI=
|
||||||
github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no=
|
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=
|
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/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 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
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 h1:wnfcqULT+N2seWf6y4yHzmi7GD2kNx4Ute0qArktD48=
|
||||||
github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84=
|
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=
|
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/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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
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/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 h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
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/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 h1:Z4EH+5EffvBEhh37F0C0DnpklTMh00JOkjW5zK3ofBI=
|
||||||
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw=
|
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 h1:adpjqej+F8BAX9dHmuPF47sUIkgifeqBu6p7iCsyj0Y=
|
||||||
github.com/terraform-providers/terraform-provider-openstack v1.15.0/go.mod h1:2aQ6n/BtChAl1y2S60vebhyJyZXBsuAI5G4+lHrT1Ew=
|
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=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
|
||||||
|
|
|
@ -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/
|
|
@ -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/)
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
[bumpversion]
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
current_version = 0.2.1
|
||||||
|
|
||||||
|
[bumpversion:file:encode.go]
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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"},
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
module github.com/mozillazg/go-httpheader
|
|
@ -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.
|
261
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go
generated
vendored
Normal file
261
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
58
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go
generated
vendored
Normal file
58
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go
generated
vendored
Normal file
|
@ -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,
|
||||||
|
// }
|
||||||
|
//}
|
35
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go
generated
vendored
Normal file
35
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
233
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go
generated
vendored
Normal file
233
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
81
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go
generated
vendored
Normal file
81
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
21
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go
generated
vendored
Normal file
21
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go
generated
vendored
Normal file
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
17
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go
generated
vendored
Normal file
17
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go
generated
vendored
Normal file
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
94
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go
generated
vendored
Normal file
94
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go
generated
vendored
Normal file
|
@ -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()
|
||||||
|
}
|
47
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go
generated
vendored
Normal file
47
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
294
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/client.go
generated
vendored
Normal file
294
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/client.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
523
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/models.go
generated
vendored
Normal file
523
vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813/models.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
[bumpversion]
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
current_version = 0.7.0
|
||||||
|
|
||||||
|
[bumpversion:file:cos.go]
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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>和<region>修改为真实的信息
|
||||||
|
//bucket的命名规则为{name}-{appid} ,此处填写的存储桶名称必须为此格式
|
||||||
|
u, _ := url.Parse("https://<bucket>.cos.<region>.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))
|
|
@ -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
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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`.
|
|
@ -27,6 +27,7 @@ Multiple workspaces are currently supported by the following backends:
|
||||||
|
|
||||||
* [AzureRM](/docs/backends/types/azurerm.html)
|
* [AzureRM](/docs/backends/types/azurerm.html)
|
||||||
* [Consul](/docs/backends/types/consul.html)
|
* [Consul](/docs/backends/types/consul.html)
|
||||||
|
* [COS](/docs/backends/types/cos.html)
|
||||||
* [GCS](/docs/backends/types/gcs.html)
|
* [GCS](/docs/backends/types/gcs.html)
|
||||||
* [Local](/docs/backends/types/local.html)
|
* [Local](/docs/backends/types/local.html)
|
||||||
* [Manta](/docs/backends/types/manta.html)
|
* [Manta](/docs/backends/types/manta.html)
|
||||||
|
|
|
@ -36,6 +36,9 @@
|
||||||
<li<%= sidebar_current("docs-backends-types-standard-consul") %>>
|
<li<%= sidebar_current("docs-backends-types-standard-consul") %>>
|
||||||
<a href="/docs/backends/types/consul.html">consul</a>
|
<a href="/docs/backends/types/consul.html">consul</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-backends-types-standard-cos") %>>
|
||||||
|
<a href="/docs/backends/types/cos.html">cos</a>
|
||||||
|
</li>
|
||||||
<li<%= sidebar_current("docs-backends-types-standard-etcdv2") %>>
|
<li<%= sidebar_current("docs-backends-types-standard-etcdv2") %>>
|
||||||
<a href="/docs/backends/types/etcd.html">etcd</a>
|
<a href="/docs/backends/types/etcd.html">etcd</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue