diff --git a/.travis.yml b/.travis.yml index b0474b7..4583c78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - 1.7 - - 1.8 - - 1.9 + - '1.7.x' + - '1.8.x' + - '1.9.x' - '1.10.x' - '1.11.x' - '1.12.x' diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5a8a3..fa854a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.12.0] + +### 新增 + +* 支持使用使用第三方 http client 包或单元测试时 mock 方法调用结果,示例:[object/mock.go](./_example/object/mock.go) + * 新增 `type Sender interface` + * 新增 `type ResponseParser interface` + * 新增 `type DefaultSender struct` + * 新增 `type DefaultResponseParser struct` + ## [0.11.1] (2019-04-14) diff --git a/README.md b/README.md index c2a7281..5a2c4b7 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,16 @@ func main() { if err != nil { panic(err) } + defer resp.Body.Close() bs, _ := ioutil.ReadAll(resp.Body) - resp.Body.Close() fmt.Printf("%s\n", string(bs)) } ``` 备注: -* SDK 不会自动设置超时时间,用户根据需要设置合适的超时时间(比如,设置 `http.Client` 的 `Timeout` 字段之类的) - 或在需要时实现所需的超时机制(比如,通过 `context` 包实现)。 +* SDK 不会自动设置超时时间,用户根据需要设置合适的超时时间(比如,设置 `http.Client` 的 `Timeout` 字段或者 + `Transport` 字段之类的)或在需要时实现所需的超时机制(比如,通过 `context` 包实现)。 * 所有的 API 在 [_example](./_example/) 目录下都有对应的使用示例(示例程序中用到的 `debug` 包只是调试用的不是必需的依赖)。 ## TODO @@ -112,5 +112,4 @@ Object API: * [x] 通过预签名授权 URL 下载文件,示例:[object/getWithPresignedURL.go](./_example/object/getWithPresignedURL.go) * [x] 通过预签名授权 URL 上传文件,示例:[object/putWithPresignedURL.go](./_example/object/putWithPresignedURL.go) * [ ] 支持临时密钥 -* [ ] 支持使用除 net/http 以外的其他 HTTP Client, - 方便使用第三方 http 包(比如 fasthttp)或单元测试时 mock 调用结果 +* [x] 支持使用使用第三方 http client 包或单元测试时 mock 方法调用结果,示例:[object/mock.go](./_example/object/mock.go) diff --git a/_example/object/mock.go b/_example/object/mock.go new file mode 100644 index 0000000..a14b470 --- /dev/null +++ b/_example/object/mock.go @@ -0,0 +1,94 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "os" + "reflect" + + "github.com/mozillazg/go-cos" + "github.com/mozillazg/go-cos/debug" +) + +type MockSender struct{} + +func (s *MockSender) Send(ctx context.Context, caller cos.Caller, req *http.Request) (*http.Response, error) { + // 如果用不到 response 的话,也可以直接 return &http.Response{}, nil + resp, _ := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(`HTTP/1.1 200 OK +Content-Length: 6 +Accept-Ranges: bytes +Connection: keep-alive +Content-Type: text/plain; charset=utf-8 +Date: Sat, 19 Jan 2019 08:25:27 GMT +Etag: "f572d396fae9206628714fb2ce00f72e94f2258f" +Last-Modified: Mon, 12 Jun 2017 13:36:19 GMT +Server: tencent-cos +X-Cos-Request-Id: NWM0MmRlZjdfMmJhZDM1MGFfNDFkM19hZGI3MQ== + +hello +`))), nil) + return resp, nil +} + +type MockerResponseParser struct { + result *cos.ObjectGetACLResult +} + +func (p *MockerResponseParser) ParseResponse(ctx context.Context, caller cos.Caller, resp *http.Response, result interface{}) (*cos.Response, error) { + b, _ := ioutil.ReadAll(resp.Body) + if string(b) != "hello\n" { + panic(string(b)) + } + + // 插入预设的结果 + switch caller.Method { + case cos.MethodObjectGetACL: + v := result.(*cos.ObjectGetACLResult) + *v = *p.result + } + + return &cos.Response{Response: resp}, nil +} + +func main() { + b, _ := cos.NewBaseURL("http://cos.example.com") + c := cos.NewClient(b, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: os.Getenv("COS_SECRETID"), + SecretKey: os.Getenv("COS_SECRETKEY"), + Transport: &debug.DebugRequestTransport{ + RequestHeader: true, + RequestBody: true, + ResponseHeader: true, + ResponseBody: true, + }, + }, + }) + c.Sender = &MockSender{} + acl := &cos.ObjectGetACLResult{ + Owner: &cos.Owner{ + ID: "test", + }, + AccessControlList: []cos.ACLGrant{ + { + Permission: "READ", + }, + }, + } + c.ResponseParser = &MockerResponseParser{acl} + + result, resp, err := c.Object.GetACL(context.Background(), "test/mock.go") + if err != nil { + panic(err) + } + + defer resp.Body.Close() + fmt.Printf("%#v\n", result) + if !reflect.DeepEqual(*result, *acl) { + panic(*result) + } +} diff --git a/_example/test.sh b/_example/test.sh index d75077b..fe528db 100644 --- a/_example/test.sh +++ b/_example/test.sh @@ -56,4 +56,5 @@ run ./object/delete.go run ./object/deleteMultiple.go run ./object/copy.go run ./object/getWithPresignedURL.go -run ./object/putWithPresignedURL.go \ No newline at end of file +run ./object/putWithPresignedURL.go +run ./object/mock.go diff --git a/auth.go b/auth.go index 7966d61..3d812df 100644 --- a/auth.go +++ b/auth.go @@ -272,6 +272,7 @@ type AuthorizationTransport struct { // 签名多久过期,默认是 time.Hour Expire time.Duration + // 实际发送 http 请求的 http.RoundTripper,默认使用 http.DefaultTransport Transport http.RoundTripper } diff --git a/auth_test.go b/auth_test.go index 311bd68..215597d 100644 --- a/auth_test.go +++ b/auth_test.go @@ -51,10 +51,10 @@ func TestAuthorizationTransport(t *testing.T) { } }) - client.client.Transport = &AuthorizationTransport{} + (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{} req, _ := http.NewRequest("GET", client.BaseURL.BucketURL.String(), nil) req.Header.Set("X-Testing", "0") - client.doAPI(context.Background(), req, nil, true) + client.doAPI(context.Background(), Caller{}, req, nil, true) } func TestAuthorizationTransport_skip_PresignedURL(t *testing.T) { @@ -68,11 +68,11 @@ func TestAuthorizationTransport_skip_PresignedURL(t *testing.T) { } }) - client.client.Transport = &AuthorizationTransport{} + (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{} sign := "q-sign-algorithm=sha1&q-ak=QmFzZTY0IGlzIGEgZ2VuZXJp&q-sign-time=1480932292;1481012292&q-key-time=1480932292;1481012292&q-header-list=&q-url-param-list=&q-signature=a5de76b0734f084a7ea24413f7168b4bdbe5676c" u := fmt.Sprintf("%s?sign=%s", client.BaseURL.BucketURL.String(), sign) req, _ := http.NewRequest("GET", u, nil) - client.doAPI(context.Background(), req, nil, true) + client.doAPI(context.Background(), Caller{}, req, nil, true) } func TestAuthorizationTransport_with_another_transport(t *testing.T) { @@ -87,12 +87,12 @@ func TestAuthorizationTransport_with_another_transport(t *testing.T) { }) tr := &testingTransport{} - client.client.Transport = &AuthorizationTransport{ + (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{ Transport: tr, } req, _ := http.NewRequest("GET", client.BaseURL.BucketURL.String(), nil) req.Header.Set("X-Testing", "0") - client.doAPI(context.Background(), req, nil, true) + client.doAPI(context.Background(), Caller{}, req, nil, true) if tr.called != 1 { t.Error("AuthorizationTransport not call another Transport") } diff --git a/bucket.go b/bucket.go index bfca205..22342d2 100644 --- a/bucket.go +++ b/bucket.go @@ -55,6 +55,9 @@ type BucketGetOptions struct { MaxKeys int `url:"max-keys,omitempty"` } +// MethodBucketGet method name of Bucket.Get +const MethodBucketGet MethodName = "Bucket.Get" + // Get Bucket 请求等同于 List Object请求,可以列出该 Bucket 下的部分或者全部 Object。 // 此 API 调用者需要对 Bucket 有 Read 权限。 // @@ -67,6 +70,9 @@ func (s *BucketService) Get(ctx context.Context, opt *BucketGetOptions) (*Bucket method: http.MethodGet, optQuery: opt, result: &res, + caller: Caller{ + Method: MethodBucketGet, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -75,6 +81,9 @@ func (s *BucketService) Get(ctx context.Context, opt *BucketGetOptions) (*Bucket // BucketPutOptions ... type BucketPutOptions ACLHeaderOptions +// MethodBucketPut method name of Bucket.Put +const MethodBucketPut MethodName = "Bucket.Put" + // Put Bucket 接口请求可以在指定账号下创建一个 Bucket。该 API 接口不支持匿名请求, // 您需要使用帯 Authorization 签名认证的请求才能创建新的 Bucket 。 // 创建 Bucket 的用户默认成为 Bucket 的持有者。 @@ -90,11 +99,17 @@ func (s *BucketService) Put(ctx context.Context, opt *BucketPutOptions) (*Respon uri: "/", method: http.MethodPut, optHeader: opt, + caller: Caller{ + Method: MethodBucketPut, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err } +// MethodBucketDelete method name of Bucket.Delete +const MethodBucketDelete MethodName = "Bucket.Delete" + // Delete Bucket 请求可以确认该 Bucket 是否存在,是否有权限访问。HEAD 的权限与 Read 一致。 // 当该 Bucket 存在时,返回 HTTP 状态码 200;当该 Bucket 无访问权限时,返回 HTTP 状态码 403; // 当该 Bucket 不存在时,返回 HTTP 状态码 404。 @@ -107,11 +122,17 @@ func (s *BucketService) Delete(ctx context.Context) (*Response, error) { baseURL: s.client.BaseURL.BucketURL, uri: "/", method: http.MethodDelete, + caller: Caller{ + Method: MethodBucketDelete, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err } +// MethodBucketHead method name of Bucket.Head +const MethodBucketHead MethodName = "Bucket.Head" + // Head Bucket请求可以确认是否存在该Bucket,是否有权限访问,Head的权限与Read一致。 // // 当其存在时,返回 HTTP 状态码200; @@ -124,6 +145,9 @@ func (s *BucketService) Head(ctx context.Context) (*Response, error) { baseURL: s.client.BaseURL.BucketURL, uri: "/", method: http.MethodHead, + caller: Caller{ + Method: MethodBucketHead, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/bucket_acl.go b/bucket_acl.go index 3a41ed6..8574691 100644 --- a/bucket_acl.go +++ b/bucket_acl.go @@ -10,6 +10,9 @@ import ( // https://cloud.tencent.com/document/product/436/7733 type BucketGetACLResult ACLXml +// MethodBucketGetACL method name of Bucket.GetACL +const MethodBucketGetACL MethodName = "Bucket.GetACL" + // GetACL 接口用来获取存储桶的访问权限控制列表。 // // https://cloud.tencent.com/document/product/436/7733 @@ -20,6 +23,9 @@ func (s *BucketService) GetACL(ctx context.Context) (*BucketGetACLResult, *Respo uri: "/?acl", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodBucketGetACL, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -32,6 +38,9 @@ type BucketPutACLOptions struct { Body *ACLXml `url:"-" header:"-"` } +// MethodBucketPutACL method name of Bucket.PutACL +const MethodBucketPutACL MethodName = "Bucket.PutACL" + // PutACL 使用API写入Bucket的ACL表 // // Put Bucket ACL 是一个覆盖操作,传入新的ACL将覆盖原有ACL。只有所有者有权操作。 @@ -52,6 +61,9 @@ func (s *BucketService) PutACL(ctx context.Context, opt *BucketPutACLOptions) (* method: http.MethodPut, body: body, optHeader: header, + caller: Caller{ + Method: MethodBucketPutACL, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/bucket_cors.go b/bucket_cors.go index 90a7beb..6543c69 100644 --- a/bucket_cors.go +++ b/bucket_cors.go @@ -33,6 +33,9 @@ type BucketGetCORSResult struct { Rules []BucketCORSRule `xml:"CORSRule,omitempty"` } +// MethodBucketGetCORS method name of Bucket.GetCORS +const MethodBucketGetCORS MethodName = "Bucket.GetCORS" + // GetCORS ... // // Get Bucket CORS 接口实现 Bucket 持有者在 Bucket 上进行跨域资源共享的信息配置。 @@ -47,6 +50,9 @@ func (s *BucketService) GetCORS(ctx context.Context) (*BucketGetCORSResult, *Res uri: "/?cors", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodBucketGetCORS, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -61,6 +67,9 @@ type BucketPutCORSOptions struct { Rules []BucketCORSRule `xml:"CORSRule,omitempty"` } +// MethodBucketPutCORS method name of Bucket.PutCORS +const MethodBucketPutCORS MethodName = "Bucket.PutCORS" + // PutCORS ... // // Put Bucket CORS 接口用来请求设置 Bucket 的跨域资源共享权限,。 @@ -73,11 +82,17 @@ func (s *BucketService) PutCORS(ctx context.Context, opt *BucketPutCORSOptions) uri: "/?cors", method: http.MethodPut, body: opt, + caller: Caller{ + Method: MethodBucketPutCORS, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err } +// MethodBucketDeleteCORS method name of Bucket.DeleteCORS +const MethodBucketDeleteCORS MethodName = "Bucket.DeleteCORS" + // DeleteCORS ... // // Delete Bucket CORS 接口请求实现删除跨域访问配置信息。 @@ -88,6 +103,9 @@ func (s *BucketService) DeleteCORS(ctx context.Context) (*Response, error) { baseURL: s.client.BaseURL.BucketURL, uri: "/?cors", method: http.MethodDelete, + caller: Caller{ + Method: MethodBucketDeleteCORS, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/bucket_lifecycle.go b/bucket_lifecycle.go index 73b97ef..bb4e7de 100644 --- a/bucket_lifecycle.go +++ b/bucket_lifecycle.go @@ -77,6 +77,9 @@ type BucketGetLifecycleResult struct { Rules []BucketLifecycleRule `xml:"Rule,omitempty"` } +// MethodBucketGetLifecycle method name of Bucket.GetLifecycle +const MethodBucketGetLifecycle MethodName = "Bucket.GetLifecycle" + // GetLifecycle ... // // Get Bucket Lifecycle 用来查询 Bucket 的生命周期配置。 @@ -89,6 +92,9 @@ func (s *BucketService) GetLifecycle(ctx context.Context) (*BucketGetLifecycleRe uri: "/?lifecycle", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodBucketGetLifecycle, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -100,6 +106,9 @@ type BucketPutLifecycleOptions struct { Rules []BucketLifecycleRule `xml:"Rule,omitempty"` } +// MethodBucketPutLifecycle method name of Bucket.PutLifecycle +const MethodBucketPutLifecycle MethodName = "Bucket.PutLifecycle" + // PutLifecycle ... // // COS 支持用户以生命周期配置的方式来管理 Bucket 中 Object 的生命周期。 @@ -123,11 +132,17 @@ func (s *BucketService) PutLifecycle(ctx context.Context, opt *BucketPutLifecycl uri: "/?lifecycle", method: http.MethodPut, body: opt, + caller: Caller{ + Method: MethodBucketPutLifecycle, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err } +// MethodBucketDeleteLifecycle method name of Bucket.DeleteLifecycle +const MethodBucketDeleteLifecycle MethodName = "Bucket.DeleteLifecycle" + // DeleteLifecycle ... // // Delete Bucket Lifecycle 用来删除 Bucket 的生命周期配置。 @@ -138,6 +153,9 @@ func (s *BucketService) DeleteLifecycle(ctx context.Context) (*Response, error) baseURL: s.client.BaseURL.BucketURL, uri: "/?lifecycle", method: http.MethodDelete, + caller: Caller{ + Method: MethodBucketDeleteLifecycle, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/bucket_location.go b/bucket_location.go index 7890618..800be80 100644 --- a/bucket_location.go +++ b/bucket_location.go @@ -14,6 +14,9 @@ type BucketGetLocationResult struct { Location string `xml:",chardata"` } +// MethodBucketGetLocation method name of Bucket.GetLocation +const MethodBucketGetLocation MethodName = "Bucket.GetLocation" + // GetLocation ... // // Get Bucket Location 接口用于获取 Bucket 所在的地域信息,该 GET 操作使用 location 参数返回 Bucket 所在的区域, @@ -27,6 +30,9 @@ func (s *BucketService) GetLocation(ctx context.Context) (*BucketGetLocationResu uri: "/?location", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodBucketGetLocation, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err diff --git a/bucket_part.go b/bucket_part.go index 7574955..5cc3985 100644 --- a/bucket_part.go +++ b/bucket_part.go @@ -61,6 +61,9 @@ type ListMultipartUploadsOptions struct { UploadIDMarker string `url:"upload-id-marker,omitempty"` } +// MethodBucketListMultipartUploads method name of Bucket.ListMultipartUploads +const MethodBucketListMultipartUploads MethodName = "Bucket.ListMultipartUploads" + // ListMultipartUploads ... // // List Multipart Uploads 用来查询正在进行中的分块上传。单次请求操作最多列出 1000 个正在进行中的分块上传。 @@ -76,6 +79,9 @@ func (s *BucketService) ListMultipartUploads(ctx context.Context, opt *ListMulti method: http.MethodGet, result: &res, optQuery: opt, + caller: Caller{ + Method: MethodBucketListMultipartUploads, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err diff --git a/bucket_tagging.go b/bucket_tagging.go index d0845d3..cddb4fb 100644 --- a/bucket_tagging.go +++ b/bucket_tagging.go @@ -18,6 +18,9 @@ type BucketGetTaggingResult struct { TagSet []BucketTaggingTag `xml:"TagSet>Tag,omitempty"` } +// MethodGetTagging method name of Bucket.GetTagging +const MethodGetTagging MethodName = "Bucket.GetTagging" + // GetTagging ... // // Get Bucket Tagging接口实现获取指定Bucket的标签。 @@ -30,6 +33,9 @@ func (s *BucketService) GetTagging(ctx context.Context) (*BucketGetTaggingResult uri: "/?tagging", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodGetTagging, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -41,6 +47,9 @@ type BucketPutTaggingOptions struct { TagSet []BucketTaggingTag `xml:"TagSet>Tag,omitempty"` } +// MethodPutTagging method name of Bucket.PutTagging +const MethodPutTagging MethodName = "Bucket.PutTagging" + // PutTagging ... // // Put Bucket Tagging接口实现给用指定Bucket打标签。用来组织和管理相关Bucket。 @@ -54,11 +63,17 @@ func (s *BucketService) PutTagging(ctx context.Context, opt *BucketPutTaggingOpt uri: "/?tagging", method: http.MethodPut, body: opt, + caller: Caller{ + Method: MethodPutTagging, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err } +// MethodDeleteTagging method name of Bucket.DeleteTagging +const MethodDeleteTagging MethodName = "Bucket.DeleteTagging" + // DeleteTagging ... // // Delete Bucket Tagging接口实现删除指定Bucket的标签。 @@ -69,6 +84,9 @@ func (s *BucketService) DeleteTagging(ctx context.Context) (*Response, error) { baseURL: s.client.BaseURL.BucketURL, uri: "/?tagging", method: http.MethodDelete, + caller: Caller{ + Method: MethodDeleteTagging, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/cos.go b/cos.go index 7dac403..212f23f 100644 --- a/cos.go +++ b/cos.go @@ -86,16 +86,22 @@ func NewBucketURL(bucketName, appID, region string, secure bool) *url.URL { // A Client manages communication with the COS API. type Client struct { - client *http.Client + // Sender 用于实际发送 HTTP 请求 + Sender Sender + // ResponseParser 用于解析响应 + ResponseParser ResponseParser UserAgent string BaseURL *BaseURL common service + // Service 封装了 service 相关的 API Service *ServiceService - Bucket *BucketService - Object *ObjectService + // Bucket 封装了 bucket 相关的 API + Bucket *BucketService + // Object 封装了 object 相关的 API + Object *ObjectService } type service struct { @@ -103,6 +109,7 @@ type service struct { } // NewClient returns a new COS API client. +// 使用 DefaultSender 作为 Sender,DefaultResponseParser 作为 ResponseParser func NewClient(uri *BaseURL, httpClient *http.Client) *Client { if httpClient == nil { httpClient = &http.Client{} @@ -118,9 +125,10 @@ func NewClient(uri *BaseURL, httpClient *http.Client) *Client { } c := &Client{ - client: httpClient, - UserAgent: userAgent, - BaseURL: baseURL, + Sender: &DefaultSender{httpClient}, + ResponseParser: &DefaultResponseParser{}, + UserAgent: userAgent, + BaseURL: baseURL, } c.common.client = c c.Service = (*ServiceService)(&c.common) @@ -195,18 +203,10 @@ func (c *Client) newRequest(ctx context.Context, opt *sendOptions) (req *http.Re return } -func (c *Client) doAPI(ctx context.Context, req *http.Request, result interface{}, closeBody bool) (*Response, error) { +func (c *Client) doAPI(ctx context.Context, caller Caller, req *http.Request, result interface{}, closeBody bool) (*Response, error) { req = req.WithContext(ctx) - - resp, err := c.client.Do(req) + resp, err := c.Sender.Send(ctx, caller, 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 } @@ -218,28 +218,7 @@ func (c *Client) doAPI(ctx context.Context, req *http.Request, result interface{ } }() - 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 - resp.Body.Close() - 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 + return c.ResponseParser.ParseResponse(ctx, caller, resp, result) } type sendOptions struct { @@ -260,6 +239,8 @@ type sendOptions struct { // 是否禁用自动调用 resp.Body.Close() // 自动调用 Close() 是为了能够重用连接 disableCloseBody bool + + caller Caller } func (c *Client) send(ctx context.Context, opt *sendOptions) (resp *Response, err error) { @@ -268,7 +249,7 @@ func (c *Client) send(ctx context.Context, opt *sendOptions) (resp *Response, er return } - resp, err = c.doAPI(ctx, req, opt.result, !opt.disableCloseBody) + resp, err = c.doAPI(ctx, opt.caller, req, opt.result, !opt.disableCloseBody) if err != nil { return } diff --git a/doc.go b/doc.go index 5854d33..8e06a02 100644 --- a/doc.go +++ b/doc.go @@ -15,7 +15,8 @@ Usage 备注 -* SDK 不会自动设置超时时间,用户根据需要设置合适的超时时间(比如,设置 `http.Client` 的 `Timeout` 字段之类的)或在需要时实现所需的超时机制(比如,通过 `context` 包实现)。 +* SDK 不会自动设置超时时间,用户根据需要设置合适的超时时间(比如,设置 `http.Client` 的 `Timeout` 字段或者 +`Transport` 字段之类的)或在需要时实现所需的超时机制(比如,通过 `context` 包实现)。 * 所有的 API 在 _example 目录下都有对应的使用示例[1](示例程序中用到的 `debug` 包只是调试用的不是必需的依赖)。 diff --git a/error.go b/error.go index 20835e6..e144f02 100644 --- a/error.go +++ b/error.go @@ -11,7 +11,8 @@ import ( // // https://cloud.tencent.com/document/product/436/7730 type ErrorResponse struct { - XMLName xml.Name `xml:"Error"` + XMLName xml.Name `xml:"Error"` + // TODO: use cos.Response instead Response *http.Response `xml:"-"` Code string Message string diff --git a/http.go b/http.go new file mode 100644 index 0000000..4c841a1 --- /dev/null +++ b/http.go @@ -0,0 +1,90 @@ +package cos + +import ( + "context" + "encoding/xml" + "io" + "net/http" +) + +// Sender 定义了一个用来发送 http 请求的接口。 +// 可以用于替换默认的基于 http.Client 的实现, +// 从而实现使用第三方 http client 或写单元测试时 mock 接口结果的需求。 +// +// 实现自定义的 Sender 时可以参考 DefaultSender 的实现。 +type Sender interface { + // caller 中包含了从哪个方法触发的 http 请求的信息 + // 当 error != nil 时将不会调用 ResponseParser.ParseResponse 解析响应 + Send(ctx context.Context, caller Caller, req *http.Request) (*http.Response, error) +} + +// ResponseParser 定义了一个用于解析响应的接口(反序列化 body 或错误检查)。 +// 可以用于替换默认的解析响应的实现, +// 从而实现使用自定义的解析方法或写单元测试时 mock 接口结果的需求 +// +// 实现自定义的 ResponseParser 时可以参考 DefaultResponseParser 的实现。 +type ResponseParser interface { + // caller 中包含了从哪个方法触发的 http 请求的信息 + // result: 反序列化后的结果将存储在指针类型的 result 中 + ParseResponse(ctx context.Context, caller Caller, resp *http.Response, result interface{}) (*Response, error) +} + +// DefaultSender 是基于 http.Client 的默认 Sender 实现 +type DefaultSender struct { + *http.Client +} + +// Send 发送 http 请求 +func (s *DefaultSender) Send(ctx context.Context, caller Caller, req *http.Request) (*http.Response, error) { + resp, err := s.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 + } + + return resp, err +} + +// DefaultResponseParser 是默认的 ResponseParser 实现 +type DefaultResponseParser struct{} + +// ParseResponse 解析响应内容,反序列化后的结果将存储在指针类型的 result 中 +func (p *DefaultResponseParser) ParseResponse(ctx context.Context, caller Caller, resp *http.Response, result interface{}) (*Response, error) { + 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 + resp.Body.Close() + return response, err + } + + if result != nil { + if w, ok := result.(io.Writer); ok { + _, err = 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 +} + +// MethodName 用于 Caller 中表示调用的是哪个方法 +type MethodName string + +// Caller 方法调用信息,用于 Sender 和 ResponseParser 中判断是来自哪个方法的调用 +type Caller struct { + // 调用的方法名称 + Method MethodName +} diff --git a/object.go b/object.go index cae9e88..aacfbff 100644 --- a/object.go +++ b/object.go @@ -45,6 +45,9 @@ type ObjectGetOptions struct { PresignedURL *url.URL `header:"-" url:"-" xml:"-"` } +// MethodObjectGet method name of Object.Get +const MethodObjectGet MethodName = "Object.Get" + // Get Object 请接口请求可以在 COS 的存储桶中将一个文件(对象)下载至本地。 // 该操作需要请求者对目标对象具有读权限或目标对象对所有人都开放了读权限(公有读)。 // @@ -71,6 +74,9 @@ func (s *ObjectService) Get(ctx context.Context, name string, opt *ObjectGetOpti optQuery: opt, optHeader: opt, disableCloseBody: true, + caller: Caller{ + Method: MethodObjectGet, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err @@ -119,6 +125,9 @@ type ObjectPutOptions struct { PresignedURL *url.URL `header:"-" url:"-" xml:"-"` } +// MethodObjectPut method name of Object.Put +const MethodObjectPut MethodName = "Object.Put" + // Put Object请求可以将一个文件(Object)上传至指定Bucket。 // // 版本 @@ -150,6 +159,9 @@ func (s *ObjectService) Put(ctx context.Context, name string, r io.Reader, opt * method: http.MethodPut, body: r, optHeader: opt, + caller: Caller{ + Method: MethodObjectPut, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err @@ -207,6 +219,9 @@ type ObjectCopyResult struct { LastModified string `xml:"LastModified,omitempty"` } +// MethodObjectCopy method name of Object.Copy +const MethodObjectCopy MethodName = "Object.Copy" + // Copy ... // // Put Object Copy 请求实现将一个文件从源路径复制到目标路径。建议文件大小 1M 到 5G, @@ -240,11 +255,17 @@ func (s *ObjectService) Copy(ctx context.Context, name, sourceURL string, opt *O body: nil, optHeader: opt, result: &res, + caller: Caller{ + Method: MethodObjectCopy, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err } +// MethodObjectDelete method name of Object.Delete +const MethodObjectDelete MethodName = "Object.Delete" + // Delete Object 接口请求可以在 COS 的 Bucket 中将一个文件(Object)删除。该操作需要请求者对 Bucket 有 WRITE 权限。 // // 细节分析 @@ -258,6 +279,9 @@ func (s *ObjectService) Delete(ctx context.Context, name string) (*Response, err baseURL: s.client.BaseURL.BucketURL, uri: "/" + encodeURIComponent(name), method: http.MethodDelete, + caller: Caller{ + Method: MethodObjectDelete, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err @@ -269,6 +293,9 @@ type ObjectHeadOptions struct { IfModifiedSince string `url:"-" header:"If-Modified-Since,omitempty"` } +// MethodObjectHead method name of Object.Head +const MethodObjectHead MethodName = "Object.Head" + // Head Object请求可以取回对应Object的元数据,Head的权限与Get的权限一致 // // 默认情况下,HEAD 操作从当前版本的对象中检索元数据。如要从不同版本检索元数据,请使用 versionId 子资源。 @@ -280,6 +307,9 @@ func (s *ObjectService) Head(ctx context.Context, name string, opt *ObjectHeadOp uri: "/" + encodeURIComponent(name), method: http.MethodHead, optHeader: opt, + caller: Caller{ + Method: MethodObjectHead, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err @@ -297,6 +327,9 @@ type ObjectOptionsOptions struct { AccessControlRequestHeaders string `url:"-" header:"Access-Control-Request-Headers,omitempty"` } +// MethodObjectOptions method name of Object.Options +const MethodObjectOptions MethodName = "Object.Options" + // Options Object 接口实现 Object 跨域访问配置的预请求。 // 即在发送跨域请求之前会发送一个 OPTIONS 请求并带上特定的来源域,HTTP 方法和 Header 信息等给 COS, // 以决定是否可以发送真正的跨域请求。当 CORS 配置不存在时,请求返回 403 Forbidden。 @@ -309,11 +342,17 @@ func (s *ObjectService) Options(ctx context.Context, name string, opt *ObjectOpt uri: "/" + encodeURIComponent(name), method: http.MethodOptions, optHeader: opt, + caller: Caller{ + Method: MethodObjectOptions, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err } +// MethodObjectAppend method name of Object.Append +const MethodObjectAppend MethodName = "Object.Append" + // Append ... // // Append请求可以将一个文件(Object)以分块追加的方式上传至 Bucket 中。使用Append Upload的文件必须事前被设定为Appendable。 @@ -338,6 +377,9 @@ func (s *ObjectService) Append(ctx context.Context, name string, position int, r method: http.MethodPost, optHeader: opt, body: r, + caller: Caller{ + Method: MethodObjectAppend, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err @@ -374,6 +416,9 @@ type ObjectDeleteMultiResult struct { } `xml:"Error,omitempty"` } +// MethodObjectDeleteMulti method name of Object.DeleteMulti +const MethodObjectDeleteMulti MethodName = "Object.DeleteMulti" + // DeleteMulti ... // // Delete Multiple Object请求实现批量删除文件,最大支持单次删除1000个文件。 @@ -397,6 +442,9 @@ func (s *ObjectService) DeleteMulti(ctx context.Context, opt *ObjectDeleteMultiO method: http.MethodPost, body: opt, result: &res, + caller: Caller{ + Method: MethodObjectDeleteMulti, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err diff --git a/object_acl.go b/object_acl.go index bacb7cb..802a2ac 100644 --- a/object_acl.go +++ b/object_acl.go @@ -8,6 +8,9 @@ import ( // ObjectGetACLResult ... type ObjectGetACLResult ACLXml +// MethodObjectGetACL method name of Object.GetACL +const MethodObjectGetACL MethodName = "Object.GetACL" + // GetACL Get Object ACL接口实现使用API读取Object的ACL表,只有所有者有权操作。 // // 默认情况下,该 GET 操作返回对象的当前版本。您如果需要返回不同的版本,请使用 version Id 子资源。 @@ -20,6 +23,9 @@ func (s *ObjectService) GetACL(ctx context.Context, name string) (*ObjectGetACLR uri: "/" + encodeURIComponent(name) + "?acl", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodObjectGetACL, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -32,6 +38,9 @@ type ObjectPutACLOptions struct { Body *ACLXml `url:"-" header:"-"` } +// MethodObjectPutACL method name of Object.PutACL +const MethodObjectPutACL MethodName = "Object.PutACL" + // PutACL 使用API写入Object的ACL表。 // // https://cloud.tencent.com/document/product/436/7748 @@ -47,6 +56,9 @@ func (s *ObjectService) PutACL(ctx context.Context, name string, opt *ObjectPutA method: http.MethodPut, optHeader: header, body: body, + caller: Caller{ + Method: MethodObjectPutACL, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/object_part.go b/object_part.go index 5564092..2618b0a 100644 --- a/object_part.go +++ b/object_part.go @@ -25,6 +25,9 @@ type InitiateMultipartUploadResult struct { UploadID string `xml:"UploadId"` } +// MethodObjectInitiateMultipartUpload method name of Object.InitiateMultipartUpload +const MethodObjectInitiateMultipartUpload MethodName = "Object.InitiateMultipartUpload" + // InitiateMultipartUpload ... // // Initiate Multipart Upload请求实现初始化分片上传,成功执行此请求以后会返回Upload ID用于后续的Upload Part请求。 @@ -38,6 +41,9 @@ func (s *ObjectService) InitiateMultipartUpload(ctx context.Context, name string method: http.MethodPost, optHeader: opt, result: &res, + caller: Caller{ + Method: MethodObjectInitiateMultipartUpload, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -54,6 +60,9 @@ type ObjectUploadPartOptions struct { ContentLength int `header:"Content-Length,omitempty" url:"-"` } +// MethodObjectUploadPart method name of Object.UploadPart +const MethodObjectUploadPart MethodName = "Object.UploadPart" + // UploadPart ... // // Upload Part 接口请求实现将对象按照分块的方式上传到 COS。 @@ -76,6 +85,9 @@ func (s *ObjectService) UploadPart(ctx context.Context, name, uploadID string, p method: http.MethodPut, optHeader: opt, body: r, + caller: Caller{ + Method: MethodObjectUploadPart, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err @@ -120,6 +132,9 @@ type ObjectListPartsResult struct { Parts []Object `xml:"Part,omitempty"` } +// MethodObjectListParts method name of Object.ListParts +const MethodObjectListParts MethodName = "Object.ListParts" + // ListParts ... // // List Parts 用来查询特定分块上传中的已上传的块,即罗列出指定 UploadId 所属的所有已上传成功的分块。 @@ -133,6 +148,9 @@ func (s *ObjectService) ListParts(ctx context.Context, name, uploadID string) (* uri: u, method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodObjectListParts, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err @@ -162,6 +180,9 @@ type CompleteMultipartUploadResult struct { ETag string } +// MethodObjectCompleteMultipartUpload method name of Object.CompleteMultipartUpload +const MethodObjectCompleteMultipartUpload MethodName = "Object.CompleteMultipartUpload" + // CompleteMultipartUpload ... // // Complete Multipart Upload用来实现完成整个分块上传。当您已经使用Upload Parts上传所有块以后,你可以用该API完成上传。 @@ -187,11 +208,17 @@ func (s *ObjectService) CompleteMultipartUpload(ctx context.Context, name, uploa method: http.MethodPost, body: opt, result: &res, + caller: Caller{ + Method: MethodObjectCompleteMultipartUpload, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err } +// MethodObjectAbortMultipartUpload method name of Object.AbortMultipartUpload +const MethodObjectAbortMultipartUpload MethodName = "Object.AbortMultipartUpload" + // AbortMultipartUpload ... // // Abort Multipart Upload 用来实现舍弃一个分块上传并删除已上传的块。当您调用 Abort Multipart Upload 时, @@ -206,6 +233,9 @@ func (s *ObjectService) AbortMultipartUpload(ctx context.Context, name, uploadID baseURL: s.client.BaseURL.BucketURL, uri: u, method: http.MethodDelete, + caller: Caller{ + Method: MethodObjectAbortMultipartUpload, + }, } resp, err := s.client.send(ctx, &sendOpt) return resp, err diff --git a/service.go b/service.go index b1d505e..02fc03f 100644 --- a/service.go +++ b/service.go @@ -18,6 +18,9 @@ type ServiceGetResult struct { Buckets []Bucket `xml:"Buckets>Bucket,omitempty"` } +// MethodServiceGet method name of Service.Get +const MethodServiceGet MethodName = "Service.Get" + // Get Service 接口是用来获取请求者名下的所有存储空间列表(Bucket list)。 // // https://cloud.tencent.com/document/product/436/8291 @@ -28,6 +31,9 @@ func (s *ServiceService) Get(ctx context.Context) (*ServiceGetResult, *Response, uri: "/", method: http.MethodGet, result: &res, + caller: Caller{ + Method: MethodServiceGet, + }, } resp, err := s.client.send(ctx, &sendOpt) return &res, resp, err