You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-library/vendor/github.com/tencentyun/cos-go-sdk-v5/cos.go

584 lines
14 KiB

2 years ago
package cos
import (
"bytes"
"context"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
"text/template"
"time"
"regexp"
"strconv"
"github.com/google/go-querystring/query"
"github.com/mozillazg/go-httpheader"
)
const (
// Version current go sdk version
1 year ago
Version = "0.7.41"
UserAgent = "cos-go-sdk-v5/" + Version
2 years ago
contentTypeXML = "application/xml"
defaultServiceBaseURL = "http://service.cos.myqcloud.com"
XOptionalKey = "cos-go-sdk-v5-XOptionalKey"
2 years ago
)
var (
bucketURLTemplate = template.Must(
template.New("bucketURLFormat").Parse(
"{{.Schema}}://{{.BucketName}}.cos.{{.Region}}.myqcloud.com",
),
)
// {<http://>|<https://>}{bucketname-appid}.{cos|cos-internal|cos-website|ci}.{region}.{myqcloud.com/tencentcos.cn}{/}
hostSuffix = regexp.MustCompile(`^.*((cos|cos-internal|cos-website|ci)\.[a-z-1]+|file)\.(myqcloud\.com|tencentcos\.cn).*$`)
hostPrefix = regexp.MustCompile(`^(http://|https://){0,1}([a-z0-9-]+-[0-9]+\.){0,1}((cos|cos-internal|cos-website|ci)\.[a-z-1]+|file)\.(myqcloud\.com|tencentcos\.cn).*$`)
invalidBucketErr = fmt.Errorf("invalid bucket format, please check your cos.BaseURL")
)
// 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
// 访问 job API 的基础 URL (不包含 path 部分): http://example.com
BatchURL *url.URL
// 访问 CI 的基础 URL
CIURL *url.URL
// 访问 Fetch Task 的基础 URL
FetchURL *url.URL
}
// NewBucketURL 生成 BaseURL 所需的 BucketURL
//
// bucketName: bucket名称, bucket的命名规则为{name}-{appid} ,此处填写的存储桶名称必须为此格式
// Region: 区域代码: ap-beijing-1,ap-beijing,ap-shanghai,ap-guangzhou...
// secure: 是否使用 https
2 years ago
func NewBucketURL(bucketName, region string, secure bool) (*url.URL, error) {
schema := "https"
if !secure {
schema = "http"
}
if region == "" {
return nil, fmt.Errorf("region[%v] is invalid", region)
}
if bucketName == "" || !strings.ContainsAny(bucketName, "-") {
return nil, fmt.Errorf("bucketName[%v] is invalid", bucketName)
}
w := bytes.NewBuffer(nil)
bucketURLTemplate.Execute(w, struct {
Schema string
BucketName string
Region string
}{
schema, bucketName, region,
})
u, _ := url.Parse(w.String())
return u, nil
}
type RetryOptions struct {
Count int
Interval time.Duration
StatusCode []int
}
type Config struct {
EnableCRC bool
RequestBodyClose bool
RetryOpt RetryOptions
}
// Client is a client manages communication with the COS API.
type Client struct {
client *http.Client
Host string
UserAgent string
BaseURL *BaseURL
common service
Service *ServiceService
Bucket *BucketService
Object *ObjectService
Batch *BatchService
CI *CIService
Conf *Config
}
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
baseURL.BatchURL = uri.BatchURL
baseURL.CIURL = uri.CIURL
baseURL.FetchURL = uri.FetchURL
}
if baseURL.ServiceURL == nil {
baseURL.ServiceURL, _ = url.Parse(defaultServiceBaseURL)
}
c := &Client{
client: httpClient,
UserAgent: UserAgent,
2 years ago
BaseURL: baseURL,
Conf: &Config{
EnableCRC: true,
RequestBodyClose: false,
RetryOpt: RetryOptions{
Count: 3,
Interval: time.Duration(0),
},
},
}
c.common.client = c
c.Service = (*ServiceService)(&c.common)
c.Bucket = (*BucketService)(&c.common)
c.Object = (*ObjectService)(&c.common)
c.Batch = (*BatchService)(&c.common)
c.CI = (*CIService)(&c.common)
return c
}
type Credential struct {
SecretID string
SecretKey string
SessionToken string
}
func (c *Client) GetCredential() *Credential {
if auth, ok := c.client.Transport.(*AuthorizationTransport); ok {
auth.rwLocker.Lock()
defer auth.rwLocker.Unlock()
return &Credential{
SecretID: auth.SecretID,
SecretKey: auth.SecretKey,
SessionToken: auth.SessionToken,
}
}
if auth, ok := c.client.Transport.(*CVMCredentialTransport); ok {
ak, sk, token, err := auth.GetCredential()
if err != nil {
return nil
}
return &Credential{
SecretID: ak,
SecretKey: sk,
SessionToken: token,
}
}
if auth, ok := c.client.Transport.(*CredentialTransport); ok {
ak, sk, token := auth.Credential.GetSecretId(), auth.Credential.GetSecretKey(), auth.Credential.GetToken()
return &Credential{
SecretID: ak,
SecretKey: sk,
SessionToken: token,
}
}
return nil
}
func (c *Client) newRequest(ctx context.Context, baseURL *url.URL, uri, method string, body interface{}, optQuery interface{}, optHeader interface{}) (req *http.Request, err error) {
if !checkURL(baseURL) {
return nil, invalidBucketErr
}
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(ctx, req.Header, optHeader)
2 years ago
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 v := req.Header.Get("User-Agent"); v == "" || !strings.HasPrefix(v, UserAgent) {
2 years ago
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
}
if req.Header.Get("Content-Type") == "" && contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if c.Host != "" {
req.Host = c.Host
}
if c.Conf.RequestBodyClose {
req.Close = true
}
return
}
func (c *Client) doAPI(ctx context.Context, req *http.Request, result interface{}, closeBody bool) (*Response, error) {
var cancel context.CancelFunc
if closeBody {
ctx, cancel = context.WithCancel(ctx)
defer cancel()
}
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 {
// StatusCode != 2xx when Get Object
if !closeBody {
resp.Body.Close()
}
// even though there was an error, we still return the response
// in case the caller wants to inspect it further
return response, err
}
// need CRC64 verification
if reader, ok := req.Body.(*teeReader); ok {
if c.Conf.EnableCRC && reader.writer != nil && !reader.disableCheckSum {
localcrc := reader.Crc64()
scoscrc := response.Header.Get("x-cos-hash-crc64ecma")
icoscrc, err := strconv.ParseUint(scoscrc, 10, 64)
2 years ago
if icoscrc != localcrc {
return response, fmt.Errorf("verification failed, want:%v, return:%v, x-cos-hash-crc64ecma:%v, err:%v, header:%+v", localcrc, icoscrc, scoscrc, err, response.Header)
2 years ago
}
}
}
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) doRetry(ctx context.Context, opt *sendOptions) (resp *Response, err error) {
if opt.body != nil {
if _, ok := opt.body.(io.Reader); ok {
resp, err = c.send(ctx, opt)
return
}
}
count := 1
if count < c.Conf.RetryOpt.Count {
count = c.Conf.RetryOpt.Count
}
nr := 0
interval := c.Conf.RetryOpt.Interval
for nr < count {
resp, err = c.send(ctx, opt)
if err != nil && err != invalidBucketErr {
if resp != nil && resp.StatusCode <= 499 {
dobreak := true
for _, v := range c.Conf.RetryOpt.StatusCode {
if resp.StatusCode == v {
dobreak = false
break
}
}
if dobreak {
break
}
}
nr++
if interval > 0 && nr < count {
time.Sleep(interval)
}
continue
}
break
}
return
}
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)
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
}
type XOptionalValue struct {
Header *http.Header
}
2 years ago
// addHeaderOptions adds the parameters in opt as Header fields to req. opt
// must be a struct whose fields may contain "header" tags.
func addHeaderOptions(ctx context.Context, header http.Header, opt interface{}) (http.Header, error) {
defer func() {
// 通过context传递
if val := ctx.Value(XOptionalKey); val != nil {
if optVal, ok := val.(*XOptionalValue); ok {
if optVal.Header != nil {
for key, values := range *optVal.Header {
for _, value := range values {
header.Add(key, value)
}
}
}
}
}
}()
2 years ago
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)
}
}
2 years ago
return header, nil
}
func checkURL(baseURL *url.URL) bool {
host := baseURL.String()
if hostSuffix.MatchString(host) && !hostPrefix.MatchString(host) {
return false
}
return true
}
// 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:"-"`
XCosGrantReadACP string `header:"x-cos-grant-read-acp,omitempty" url:"-" xml:"-"`
XCosGrantWriteACP string `header:"x-cos-grant-write-acp,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"`
}
func decodeACL(resp *Response, res *ACLXml) {
ItemMap := map[string]string{
"ACL": "x-cos-acl",
"READ": "x-cos-grant-read",
"WRITE": "x-cos-grant-write",
"READ_ACP": "x-cos-grant-read-acp",
"WRITE_ACP": "x-cos-grant-write-acp",
"FULL_CONTROL": "x-cos-grant-full-control",
}
publicACL := make(map[string]int)
resACL := make(map[string][]string)
for _, item := range res.AccessControlList {
if item.Grantee == nil {
continue
}
if item.Grantee.ID == "qcs::cam::anyone:anyone" || item.Grantee.URI == "http://cam.qcloud.com/groups/global/AllUsers" {
publicACL[item.Permission] = 1
} else if item.Grantee.ID != res.Owner.ID {
resACL[item.Permission] = append(resACL[item.Permission], "id=\""+item.Grantee.ID+"\"")
}
}
if publicACL["FULL_CONTROL"] == 1 || (publicACL["READ"] == 1 && publicACL["WRITE"] == 1) {
resACL["ACL"] = []string{"public-read-write"}
} else if publicACL["READ"] == 1 {
resACL["ACL"] = []string{"public-read"}
} else {
resACL["ACL"] = []string{"private"}
}
for item, header := range ItemMap {
if len(resp.Header.Get(header)) > 0 || len(resACL[item]) == 0 {
continue
}
resp.Header.Set(header, uniqueGrantID(resACL[item]))
}
}
func uniqueGrantID(grantIDs []string) string {
res := []string{}
filter := make(map[string]int)
for _, id := range grantIDs {
if filter[id] != 0 {
continue
}
filter[id] = 1
res = append(res, id)
}
return strings.Join(res, ",")
}