├── .bumpversion.cfg ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _example ├── README.md ├── bucket │ ├── delete.go │ ├── deleteCORS.go │ ├── deleteLifecycle.go │ ├── deleteTagging.go │ ├── get.go │ ├── getACL.go │ ├── getCORS.go │ ├── getLifecycle.go │ ├── getLocation.go │ ├── getTagging.go │ ├── head.go │ ├── listMultipartUploads.go │ ├── put.go │ ├── putACL.go │ ├── putCORS.go │ ├── putLifecycle.go │ └── putTagging.go ├── object │ ├── abortMultipartUpload.go │ ├── append.go │ ├── completeMultipartUpload.go │ ├── copy.go │ ├── delete.go │ ├── deleteMultiple.go │ ├── get.go │ ├── getACL.go │ ├── getAnonymous.go │ ├── getWithPresignedURL.go │ ├── head.go │ ├── initiateMultipartUpload.go │ ├── listParts.go │ ├── listPartsWithOpt.go │ ├── mock.go │ ├── options.go │ ├── put.go │ ├── putACL.go │ ├── putWithPresignedURL.go │ ├── sessionToken.go │ ├── uploadFile.go │ └── uploadPart.go ├── service │ └── get.go └── test.sh ├── auth.go ├── auth_test.go ├── bucket.go ├── bucket_acl.go ├── bucket_acl_test.go ├── bucket_cors.go ├── bucket_cors_test.go ├── bucket_lifecycle.go ├── bucket_lifecycle_test.go ├── bucket_location.go ├── bucket_location_test.go ├── bucket_part.go ├── bucket_part_test.go ├── bucket_tagging.go ├── bucket_tagging_test.go ├── bucket_test.go ├── codelingo.yaml ├── cos.go ├── cos_test.go ├── debug ├── http.go └── http_test.go ├── doc.go ├── error.go ├── error_test.go ├── go.mod ├── helper.go ├── helper_test.go ├── http.go ├── object.go ├── object_acl.go ├── object_acl_test.go ├── object_part.go ├── object_part_test.go ├── object_test.go ├── service.go └── service_test.go /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | commit = True 3 | tag = True 4 | current_version = 0.13.0 5 | 6 | [bumpversion:file:cos.go] 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | dist/ 26 | cover.html 27 | cover.out 28 | go.sum 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - '1.7.x' 4 | - '1.8.x' 5 | - '1.9.x' 6 | - '1.10.x' 7 | - '1.11.x' 8 | - '1.12.x' 9 | - master 10 | 11 | sudo: false 12 | 13 | before_install: 14 | - go get -t -v ./... 15 | 16 | install: 17 | - go get 18 | - go build 19 | 20 | script: 21 | - make test 22 | - go test -race -coverprofile=coverage.txt -covermode=atomic 23 | 24 | after_success: 25 | - bash <(curl -s https://codecov.io/bash) 26 | 27 | matrix: 28 | allow_failures: 29 | - go: master 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.13.0] (2019-08-18) 4 | 5 | ## 新增 6 | 7 | * `AuthorizationTransport` 增加 `SessionToken` 字段,支持通过临时密钥请求 api,示例: [object/sessionToken.go](./_example/object/sessionToken.go) 。 8 | * 新增 `c.Object.ListPartsWithOpt` 方法,解决 `c.Object.ListParts` 方法忘了支持参数的问题。 9 | * `ListPartsWithOpt(ctx context.Context, name, uploadID string, opt *ObjectListPartsOptions) (*ObjectListPartsResult, *Response, error)` 10 | * `ListParts(ctx context.Context, name, uploadID string) (*ObjectListPartsResult, *Response, error)` 11 | 12 | 13 | ## [0.12.0] (2019-04-27) 14 | 15 | ### 新增 16 | 17 | * 支持使用使用第三方 http client 包或单元测试时 mock 方法调用结果,示例:[object/mock.go](./_example/object/mock.go) 18 | * 新增 `type Sender interface` 19 | * 新增 `type ResponseParser interface` 20 | * 新增 `type DefaultSender struct` 21 | * 新增 `type DefaultResponseParser struct` 22 | 23 | 24 | ## [0.11.1] (2019-04-14) 25 | 26 | ### Bugfix 27 | 28 | * 修复当 url 或 headers 相关参数的值中包含空格或某几个特殊字符串时会出现服务端返回签名不匹配的问题。(via [#13]) 29 | 30 | 31 | ## [0.11.0] (2018-12-08) 32 | 33 | ### 不兼容旧版的变更 34 | 35 | * 根据 COS [官方文档](https://cloud.tencent.com/document/product/436/7751) 更新最新版本下 36 | 各个 API 的参数和响应中包含的字段(新增了部分字段,废弃了一些字段)。 37 | 38 | 影响: 39 | 40 | * 大部分用户无需修改任何代码,部分依赖了字段结构的高级用户需要更新代码。 41 | 42 | ### 新增 43 | 44 | * `Response` 新增几个方法用于快速获取常用的 COS Header 的值: 45 | * `RequestID()` 46 | * `TraceID()` 47 | * `ObjectType()` 48 | * `StorageClass()` 49 | * `VersionID()` 50 | * `ServerSideEncryption()` 51 | * `MetaHeaders()` 52 | * 新增了几个常量用于判断部分固定值 53 | 54 | ### 文档 55 | 56 | * 上传文件操作不再需要在特定情况下强制指定 ContentLength 了(COS 服务端新功能)。 57 | * 强调一下用户可以自己设置超时时间或通过 context 实现终止请求的功能。 58 | 59 | 60 | ## [0.10.0] (2018-11-03) 61 | 62 | ### 变更 63 | 64 | * 当上传文件相关方法的 `r io.Reader` 参数是个 `io.ReadCloser` 时不会再自动调用 `r.Close()` , 65 | 用户需要自行选择合适的时机去调用 `r.Close()` 方法对 r 进行资源回收。(via [#7] Thanks [@jojohappy]) 66 | 67 | 68 | ## [0.9.0] (2018-08-04) 69 | 70 | ### 新增 71 | 72 | * 新增 `c.Object.PresignedURL` 用于获取预签名授权 URL。 73 | 可用于无需知道 SecretID 和 SecretKey 就可以上传和下载文件。 74 | * 上传和下载 Object 的功能支持指定预签名授权 URL。 75 | 76 | 详见 PR 以及使用示例: 77 | 78 | * https://github.com/mozillazg/go-cos/pull/5 79 | * 通过预签名授权 URL 下载文件,示例:[object/getWithPresignedURL.go](./_example/object/getWithPresignedURL.go) 80 | * 通过预签名授权 URL 上传文件,示例:[object/putWithPresignedURL.go](./_example/object/putWithPresignedURL.go) 81 | 82 | 83 | ## [0.8.0] (2018-05-26) 84 | 85 | ### 新增 86 | 87 | * 新增 `func NewBaseURL(bucketURL string) (u *BaseURL, err error)` (via [91f7759]) 88 | 89 | ### 不兼容旧版的变更 90 | 91 | * `NewBucketURL` 函数使用新的 URL 域名规则。(via [7dcd701]) 92 | 93 | 影响: 94 | 95 | * 如果有使用 `NewBucketURL` 函数生成 bucketURL 的话,使用时需要使用新的 Region 名称, 96 | 详见 https://cloud.tencent.com/document/product/436/6224 ,未使用 `NewBucketURL` 函数不受影响 97 | 98 | 99 | ## [0.7.0] (2017-12-23) 100 | 101 | ### 新增 102 | 103 | * 支持新增的 Put Object Copy API 104 | * 新增 `github.com/mozillazg/go-cos/debug`,目前只包含 `DebugRequestTransport` 105 | 106 | 107 | ## [0.6.0] (2017-07-09) 108 | 109 | ### 新增 110 | 111 | * 增加说明在某些情况下 ObjectPutHeaderOptions.ContentLength 必须要指定 112 | * 增加 ObjectUploadPartOptions.ContentLength 113 | 114 | 115 | ## [0.5.0] (2017-06-28) 116 | 117 | ### 修复 118 | 119 | * 修复 ACL 相关 API 突然失效的问题. 120 | (因为 COS ACL 相关 API 的 request 和 response xml body 的结构发生了变化) 121 | 122 | ### 删除 123 | 124 | * 删除调试用的 DebugRequestTransport(把它移动到 examples/ 中) 125 | 126 | 127 | ## [0.4.0] (2017-06-24) 128 | 129 | ### 新增 130 | 131 | * 增加 AuthorizationTransport 辅助添加认证信息 132 | 133 | ### 修改 134 | 135 | * 去掉 API 中的 authTime 参数,默认不再自动添加 Authorization header 136 | 改为通过自定义 client 的方式来添加认证信息 137 | 138 | 139 | ## [0.3.0] (2017-06-23) 140 | 141 | ### 新增 142 | 143 | * 完成剩下的所有 API 144 | 145 | 146 | ## [0.2.0] (2017-06-10) 147 | 148 | ### 不兼容旧版的变更 149 | 150 | * 调用 bucket 相关 API 时不再需要 bucket 参数, 把参数移到 service 中 151 | * 把参数 signStartTime, signEndTime, keyStartTime, keyEndTime 合并为 authTime 152 | 153 | 154 | ## 0.1.0 (2017-06-10) 155 | 156 | ### 新增 157 | 158 | * 完成 Service API 159 | * 完成大部分 Bucket API(还剩一个 Put Bucket Lifecycle) 160 | 161 | 162 | [0.13.0]: https://github.com/mozillazg/go-cos/compare/v0.12.0...v0.13.0 163 | [0.12.0]: https://github.com/mozillazg/go-cos/compare/v0.11.1...v0.12.0 164 | [0.11.1]: https://github.com/mozillazg/go-cos/compare/v0.11.0...v0.11.1 165 | [0.11.0]: https://github.com/mozillazg/go-cos/compare/v0.10.0...v0.11.0 166 | [0.10.0]: https://github.com/mozillazg/go-cos/compare/v0.9.0...v0.10.0 167 | [0.9.0]: https://github.com/mozillazg/go-cos/compare/v0.8.0...v0.9.0 168 | [0.8.0]: https://github.com/mozillazg/go-cos/compare/v0.7.0...v0.8.0 169 | [0.7.0]: https://github.com/mozillazg/go-cos/compare/v0.6.0...v0.7.0 170 | [0.6.0]: https://github.com/mozillazg/go-cos/compare/v0.5.0...v0.6.0 171 | [0.5.0]: https://github.com/mozillazg/go-cos/compare/v0.4.0...v0.5.0 172 | [0.4.0]: https://github.com/mozillazg/go-cos/compare/v0.3.0...v0.4.0 173 | [0.3.0]: https://github.com/mozillazg/go-cos/compare/v0.2.0...v0.3.0 174 | [0.2.0]: https://github.com/mozillazg/go-cos/compare/v0.1.0...v0.2.0 175 | 176 | [91f7759]: https://github.com/mozillazg/go-cos/commit/91f7759958f9631e8997f47d30ae4044455fc971 177 | [7dcd701]: https://github.com/mozillazg/go-cos/commit/7dcd701975f483d57525b292ab31d0f9a6c8866c 178 | [#7]: https://github.com/mozillazg/go-cos/pull/7 179 | [@jojohappy]: https://github.com/jojohappy 180 | [#13]: https://github.com/mozillazg/go-cos/pull/13 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mozillazg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "test run test" 3 | @echo "lint run lint" 4 | @echo "example run examples" 5 | 6 | export GO111MODULE=on 7 | 8 | .PHONY: test 9 | test: 10 | go test -race -v -cover -coverprofile cover.out 11 | go tool cover -html=cover.out -o cover.html 12 | -open cover.html 13 | 14 | .PHONY: lint 15 | lint: 16 | gofmt -s -w . 17 | goimports -w . 18 | golint . 19 | go vet 20 | 21 | .PHONY: example 22 | example: 23 | cd _example && bash test.sh 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-cos 2 | 3 | 腾讯云对象存储服务 COS(Cloud Object Storage) Go SDK(API 版本:V5 版本的 XML API)。 4 | 5 | [![Build Status](https://img.shields.io/travis/mozillazg/go-cos/master.svg)](https://travis-ci.org/mozillazg/go-cos) 6 | [![Coverage Status](https://img.shields.io/codecov/c/github/mozillazg/go-cos/master.svg)](https://codecov.io/gh/mozillazg/go-cos) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/mozillazg/go-cos)](https://goreportcard.com/report/github.com/mozillazg/go-cos) 8 | [![GoDoc](https://godoc.org/github.com/mozillazg/go-cos?status.svg)](https://godoc.org/github.com/mozillazg/go-cos) 9 | 10 | ## Install 11 | 12 | `go get -u github.com/mozillazg/go-cos` 13 | 14 | 推荐使用 [go mod](https://github.com/golang/go/wiki/Modules) 之类的技术指定使用的 go-cos 包版本号。 15 | 16 | 17 | ## Usage 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "io/ioutil" 26 | "net/http" 27 | "net/url" 28 | "os" 29 | 30 | "github.com/mozillazg/go-cos" 31 | ) 32 | 33 | func main() { 34 | b, _ := cos.NewBaseURL("https://-.cos..myqcloud.com") 35 | c := cos.NewClient(b, &http.Client{ 36 | Transport: &cos.AuthorizationTransport{ 37 | SecretID: os.Getenv("COS_SECRETID"), 38 | SecretKey: os.Getenv("COS_SECRETKEY"), 39 | }, 40 | }) 41 | 42 | name := "test/hello.txt" 43 | resp, err := c.Object.Get(context.Background(), name, nil) 44 | if err != nil { 45 | panic(err) 46 | } 47 | defer resp.Body.Close() 48 | bs, _ := ioutil.ReadAll(resp.Body) 49 | fmt.Printf("%s\n", string(bs)) 50 | } 51 | ``` 52 | 53 | 备注: 54 | 55 | * SDK 不会自动设置超时时间,用户根据需要设置合适的超时时间(比如,设置 `http.Client` 的 `Timeout` 字段或者 56 | `Transport` 字段之类的)或在需要时实现所需的超时机制(比如,通过 `context` 包实现)。 57 | * 所有的 API 在 [_example](./_example/) 目录下都有对应的使用示例(示例程序中用到的 `debug` 包只是调试用的不是必需的依赖)。 58 | 59 | ## TODO 60 | 61 | Service API: 62 | 63 | * [x] Get Service(使用示例:[service/get.go](./_example/service/get.go)) 64 | 65 | Bucket API: 66 | 67 | * [x] **Get Bucket**(搜索文件,使用示例:[bucket/get.go](./_example/bucket/get.go)) 68 | * [x] Get Bucket ACL(使用示例:[bucket/getACL.go](./_example/bucket/getACL.go)) 69 | * [x] Get Bucket CORS(使用示例:[bucket/getCORS.go](./_example/bucket/getCORS.go)) 70 | * [x] Get Bucket Location(使用示例:[bucket/getLocation.go](./_example/bucket/getLocation.go)) 71 | * [x] Get Buket Lifecycle(使用示例:[bucket/getLifecycle.go](./_example/bucket/getLifecycle.go)) 72 | * [x] Get Bucket Tagging(使用示例:[bucket/getTagging.go](./_example/bucket/getTagging.go)) 73 | * [ ] Get Bucket policy 74 | * [x] Put Bucket(创建 bucket,使用示例:[bucket/put.go](./_example/bucket/put.go)) 75 | * [x] Put Bucket ACL(使用示例:[bucket/putACL.go](./_example/bucket/putACL.go)) 76 | * [x] Put Bucket CORS(使用示例:[bucket/putCORS.go](./_example/bucket/putCORS.go)) 77 | * [x] Put Bucket Lifecycle(使用示例:[bucket/putLifecycle.go](./_example/bucket/putLifecycle.go)) 78 | * [x] Put Bucket Tagging(使用示例:[bucket/putTagging.go](./_example/bucket/putTagging.go)) 79 | * [ ] Put Bucket policy 80 | * [x] Delete Bucket(删除 bucket,使用示例:[bucket/delete.go](./_example/bucket/delete.go)) 81 | * [x] Delete Bucket CORS(使用示例:[bucket/deleteCORS.go](./_example/bucket/deleteCORS.go)) 82 | * [x] Delete Bucket Lifecycle(使用示例:[bucket/deleteLifecycle.go](./_example/bucket/deleteLifecycle.go)) 83 | * [x] Delete Bucket Tagging(使用示例:[bucket/deleteTagging.go](./_example/bucket/deleteTagging.go)) 84 | * [ ] Delete Bucket policy 85 | * [x] Head Bucket(使用示例:[bucket/head.go](./_example/bucket/head.go)) 86 | * [x] List Multipart Uploads(查询上传的分块,使用示例:[bucket/listMultipartUploads.go](./_example/bucket/listMultipartUploads.go)) 87 | 88 | Object API: 89 | 90 | * [x] **Append Object**(增量更新文件,使用示例:[object/append.go](./_example/object/append.go)) 91 | * [x] **Get Object**(下载文件,使用示例:[object/get.go](./_example/object/get.go)) 92 | * [x] Get Object ACL(使用示例:[object/getACL.go](./_example/object/getACL.go)) 93 | * [x] **Put Object**(上传文件,使用示例:[object/put.go](./_example/object/put.go) or [object/uploadFile.go](./_example/object/uploadFile.go)) 94 | * [x] Put Object ACL(使用示例:[object/putACL.go](./_example/object/putACL.go)) 95 | * [x] Put Object Copy(使用示例:[object/copy.go](./_example/object/copy.go)) 96 | * [x] **Delete Object**(删除文件,使用示例:[object/delete.go](./_example/object/delete.go)) 97 | * [ ] [Post Object](https://cloud.tencent.com/document/product/436/14690) 98 | * [ ] [Post Object restore](https://cloud.tencent.com/document/product/436/12633) 99 | * [x] Delete Multiple Object(使用示例:[object/deleteMultiple.go](./_example/object/deleteMultiple.go)) 100 | * [x] Head Object(使用示例:[object/head.go](./_example/object/head.go)) 101 | * [x] Options Object(使用示例:[object/options.go](./_example/object/options.go)) 102 | * [x] **Initiate Multipart Upload**(初始化分块上传,使用示例:[object/initiateMultipartUpload.go](./_example/object/initiateMultipartUpload.go)) 103 | * [x] **Upload Part**(上传一个分块,使用示例:[object/uploadPart.go](./_example/object/uploadPart.go)) 104 | * [ ] [Upload Part - Copy](https://cloud.tencent.com/document/product/436/8287) 105 | * [x] **List Parts**(列出已上传的分块,使用示例:[object/listParts.go](./_example/object/listParts.go)) 106 | * [x] **Complete Multipart Upload**(合并上传的分块,使用示例:[object/completeMultipartUpload.go](./_example/object/completeMultipartUpload.go)) 107 | * [x] **Abort Multipart Upload**(取消分块上传,使用示例:[object/abortMultipartUpload.go](./_example/object/abortMultipartUpload.go)) 108 | 109 | 其他功能: 110 | 111 | * [x] **生成预签名授权 URL** 112 | * [x] 通过预签名授权 URL 下载文件,示例:[object/getWithPresignedURL.go](./_example/object/getWithPresignedURL.go) 113 | * [x] 通过预签名授权 URL 上传文件,示例:[object/putWithPresignedURL.go](./_example/object/putWithPresignedURL.go) 114 | * [x] 支持临时密钥,示例: [object/sessionToken.go](./_example/object/sessionToken.go) 115 | * [x] 支持使用使用第三方 http client 包或单元测试时 mock 方法调用结果,示例:[object/mock.go](./_example/object/mock.go) 116 | -------------------------------------------------------------------------------- /_example/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ``` 4 | export COS_BUCKET_URL=https://-.cos..myqcloud.com 5 | export COS_SECRETID=xx 6 | export COS_SECRETKEY=xxx 7 | 8 | go run xxx.go 9 | ``` 10 | -------------------------------------------------------------------------------- /_example/bucket/delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse("https://testdelete-1253846586.cn-north.myqcloud.com") 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | _, err := c.Bucket.Delete(context.Background()) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/bucket/deleteCORS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | _, err := c.Bucket.DeleteCORS(context.Background()) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/bucket/deleteLifecycle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse("https://testhuanan-1253846586.cn-south.myqcloud.com") 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | _, err := c.Bucket.DeleteLifecycle(context.Background()) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/bucket/deleteTagging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | _, err := c.Bucket.DeleteTagging(context.Background()) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/bucket/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | b, _ := cos.NewBaseURL(os.Getenv("COS_BUCKET_URL")) 16 | c := cos.NewClient(b, &http.Client{ 17 | Transport: &cos.AuthorizationTransport{ 18 | SecretID: os.Getenv("COS_SECRETID"), 19 | SecretKey: os.Getenv("COS_SECRETKEY"), 20 | Transport: &debug.DebugRequestTransport{ 21 | RequestHeader: true, 22 | RequestBody: true, 23 | ResponseHeader: true, 24 | ResponseBody: true, 25 | }, 26 | }, 27 | }) 28 | 29 | opt := &cos.BucketGetOptions{ 30 | Prefix: "test", 31 | MaxKeys: 3, 32 | } 33 | v, resp, err := c.Bucket.Get(context.Background(), opt) 34 | if err != nil { 35 | panic(err) 36 | } 37 | resp.Body.Close() 38 | 39 | for _, c := range v.Contents { 40 | fmt.Printf("%s, %d\n", c.Key, c.Size) 41 | } 42 | 43 | // 测试特殊字符 44 | opt.Prefix = "test/put_ + !'()* option" 45 | _, resp, err = c.Bucket.Get(context.Background(), opt) 46 | if err != nil { 47 | panic(err) 48 | } 49 | resp.Body.Close() 50 | } 51 | -------------------------------------------------------------------------------- /_example/bucket/getACL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | v, _, err := c.Bucket.GetACL(context.Background()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | for _, a := range v.AccessControlList { 38 | fmt.Printf("%s, %s, %s\n", a.Grantee.Type, a.Grantee.ID, a.Permission) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /_example/bucket/getCORS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | v, _, err := c.Bucket.GetCORS(context.Background()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | for _, r := range v.Rules { 38 | 39 | fmt.Printf("%s, %s\n", r.AllowedOrigins, r.AllowedMethods) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /_example/bucket/getLifecycle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse("https://testhuanan-1253846586.cn-south.myqcloud.com") 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | v, _, err := c.Bucket.GetLifecycle(context.Background()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | for _, r := range v.Rules { 38 | fmt.Printf("%s, %s\n", r.Filter.Prefix, r.Status) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /_example/bucket/getLocation.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | v, _, err := c.Bucket.GetLocation(context.Background()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | fmt.Printf("%s\n", v.Location) 38 | } 39 | -------------------------------------------------------------------------------- /_example/bucket/getTagging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | v, _, err := c.Bucket.GetTagging(context.Background()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | for _, t := range v.TagSet { 38 | fmt.Printf("%s: %s\n", t.Key, t.Value) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /_example/bucket/head.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | resp, err := c.Bucket.Head(context.Background()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | fmt.Println(resp.Status) 38 | } 39 | -------------------------------------------------------------------------------- /_example/bucket/listMultipartUploads.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | opt := &cos.ListMultipartUploadsOptions{ 34 | Prefix: "t", 35 | } 36 | v, _, err := c.Bucket.ListMultipartUploads(context.Background(), opt) 37 | if err != nil { 38 | panic(err) 39 | } 40 | for _, p := range v.Uploads { 41 | fmt.Printf("%s\n", p.Key) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /_example/bucket/put.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse("https://testdelete-1253846586.cn-north.myqcloud.com") 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | //opt := &cos.BucketPutOptions{ 33 | // XCosACL: "public-read", 34 | //} 35 | _, err := c.Bucket.Put(context.Background(), nil) 36 | if err != nil { 37 | panic(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /_example/bucket/putACL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | // with header 33 | opt := &cos.BucketPutACLOptions{ 34 | Header: &cos.ACLHeaderOptions{ 35 | XCosACL: "private", 36 | }, 37 | } 38 | _, err := c.Bucket.PutACL(context.Background(), opt) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | // with body 44 | opt = &cos.BucketPutACLOptions{ 45 | Body: &cos.ACLXml{ 46 | Owner: &cos.Owner{ 47 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 48 | }, 49 | AccessControlList: []cos.ACLGrant{ 50 | { 51 | Grantee: &cos.ACLGrantee{ 52 | Type: "RootAccount", 53 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 54 | }, 55 | 56 | Permission: "FULL_CONTROL", 57 | }, 58 | }, 59 | }, 60 | } 61 | _, err = c.Bucket.PutACL(context.Background(), opt) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /_example/bucket/putCORS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | opt := &cos.BucketPutCORSOptions{ 33 | Rules: []cos.BucketCORSRule{ 34 | { 35 | AllowedOrigins: []string{"http://www.qq.com"}, 36 | AllowedMethods: []string{"PUT", "GET"}, 37 | AllowedHeaders: []string{"x-cos-meta-test", "x-cos-xx"}, 38 | MaxAgeSeconds: 500, 39 | ExposeHeaders: []string{"x-cos-meta-test1"}, 40 | }, 41 | { 42 | ID: "1234", 43 | AllowedOrigins: []string{"http://www.baidu.com", "twitter.com"}, 44 | AllowedMethods: []string{"PUT", "GET"}, 45 | MaxAgeSeconds: 500, 46 | }, 47 | }, 48 | } 49 | _, err := c.Bucket.PutCORS(context.Background(), opt) 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /_example/bucket/putLifecycle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse("https://testhuanan-1253846586.cn-south.myqcloud.com") 16 | b := &cos.BaseURL{ 17 | BucketURL: u, 18 | } 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | lc := &cos.BucketPutLifecycleOptions{ 33 | Rules: []cos.BucketLifecycleRule{ 34 | { 35 | ID: "1234", 36 | Prefix: "test", 37 | Status: "Enabled", 38 | Transition: &cos.BucketLifecycleTransition{ 39 | Days: 10, 40 | StorageClass: "Standard", 41 | }, 42 | }, 43 | { 44 | ID: "123422", 45 | Prefix: "gg", 46 | Status: "Disabled", 47 | Expiration: &cos.BucketLifecycleExpiration{ 48 | Days: 10, 49 | }, 50 | }, 51 | }, 52 | } 53 | _, err := c.Bucket.PutLifecycle(context.Background(), lc) 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /_example/bucket/putTagging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | "time" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{ 18 | BucketURL: u, 19 | } 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | startTime := time.Now() 33 | 34 | tg := &cos.BucketPutTaggingOptions{ 35 | TagSet: []cos.BucketTaggingTag{ 36 | { 37 | Key: "test_k2", 38 | Value: "test_v2", 39 | }, 40 | { 41 | Key: "test_k3", 42 | Value: "test_v3", 43 | }, 44 | { 45 | Key: startTime.Format("02_Jan_06_15_04_MST"), 46 | Value: "test_time", 47 | }, 48 | }, 49 | } 50 | _, err := c.Bucket.PutTagging(context.Background(), tg) 51 | if err != nil { 52 | panic(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /_example/object/abortMultipartUpload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{BucketURL: u} 18 | c := cos.NewClient(b, &http.Client{ 19 | Transport: &cos.AuthorizationTransport{ 20 | SecretID: os.Getenv("COS_SECRETID"), 21 | SecretKey: os.Getenv("COS_SECRETKEY"), 22 | Transport: &debug.DebugRequestTransport{ 23 | RequestHeader: true, 24 | RequestBody: false, 25 | ResponseHeader: true, 26 | ResponseBody: true, 27 | }, 28 | }, 29 | }) 30 | 31 | name := "test_multipart.txt" 32 | v, _, err := c.Object.InitiateMultipartUpload(context.Background(), name, nil) 33 | if err != nil { 34 | panic(err) 35 | } 36 | fmt.Printf("%s\n", v.UploadID) 37 | 38 | resp, err := c.Object.AbortMultipartUpload(context.Background(), name, v.UploadID) 39 | if err != nil { 40 | panic(err) 41 | } 42 | fmt.Printf("%s\n", resp.Status) 43 | } 44 | -------------------------------------------------------------------------------- /_example/object/append.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "math/rand" 8 | "net/url" 9 | "os" 10 | "time" 11 | 12 | "net/http" 13 | 14 | "github.com/mozillazg/go-cos" 15 | "github.com/mozillazg/go-cos/debug" 16 | ) 17 | 18 | func genBigData(blockSize int) []byte { 19 | b := make([]byte, blockSize) 20 | if _, err := rand.Read(b); err != nil { 21 | panic(err) 22 | } 23 | return b 24 | } 25 | 26 | func main() { 27 | // "https://test-1253846586.cn-north.myqcloud.com", 28 | u, _ := url.Parse("https://huadong-1253846586.cn-east.myqcloud.com") 29 | b := &cos.BaseURL{ 30 | BucketURL: u, 31 | } 32 | c := cos.NewClient(b, &http.Client{ 33 | Transport: &cos.AuthorizationTransport{ 34 | SecretID: os.Getenv("COS_SECRETID"), 35 | SecretKey: os.Getenv("COS_SECRETKEY"), 36 | Transport: &debug.DebugRequestTransport{ 37 | RequestHeader: true, 38 | RequestBody: false, 39 | ResponseHeader: true, 40 | ResponseBody: true, 41 | }, 42 | }, 43 | }) 44 | 45 | startTime := time.Now() 46 | 47 | name := fmt.Sprintf("test/test_object_append_%s", startTime.Format(time.RFC3339)) 48 | data := genBigData(1024 * 1024 * 1) 49 | length := len(data) 50 | r := bytes.NewReader(data) 51 | 52 | ctx := context.Background() 53 | 54 | // 第一次就必须 append 55 | resp, err := c.Object.Append(ctx, name, 0, r, nil) 56 | if err != nil { 57 | panic(err) 58 | return 59 | } 60 | fmt.Printf("%s\n", resp.Status) 61 | 62 | // head 63 | if _, err = c.Object.Head(ctx, name, nil); err != nil { 64 | panic(err) 65 | return 66 | } 67 | 68 | // 再次 append 69 | data = genBigData(1024 * 1024 * 5) 70 | r = bytes.NewReader(data) 71 | resp, err = c.Object.Append(context.Background(), name, length, r, nil) 72 | if err != nil { 73 | panic(err) 74 | } 75 | fmt.Printf("%s\n", resp.Status) 76 | } 77 | -------------------------------------------------------------------------------- /_example/object/completeMultipartUpload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/url" 8 | "os" 9 | "strings" 10 | 11 | "net/http" 12 | 13 | "github.com/mozillazg/go-cos" 14 | "github.com/mozillazg/go-cos/debug" 15 | ) 16 | 17 | func initUpload(c *cos.Client, name string) *cos.InitiateMultipartUploadResult { 18 | v, _, err := c.Object.InitiateMultipartUpload(context.Background(), name, nil) 19 | if err != nil { 20 | panic(err) 21 | } 22 | fmt.Printf("%#v\n", v) 23 | return v 24 | } 25 | 26 | func uploadPart(c *cos.Client, name string, uploadID string, blockSize, n int) string { 27 | 28 | b := make([]byte, blockSize) 29 | if _, err := rand.Read(b); err != nil { 30 | panic(err) 31 | } 32 | s := fmt.Sprintf("%X", b) 33 | f := strings.NewReader(s) 34 | 35 | resp, err := c.Object.UploadPart( 36 | context.Background(), name, uploadID, n, f, nil, 37 | ) 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("%s\n", resp.Status) 42 | return resp.Header.Get("Etag") 43 | } 44 | 45 | func main() { 46 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 47 | b := &cos.BaseURL{BucketURL: u} 48 | c := cos.NewClient(b, &http.Client{ 49 | Transport: &cos.AuthorizationTransport{ 50 | SecretID: os.Getenv("COS_SECRETID"), 51 | SecretKey: os.Getenv("COS_SECRETKEY"), 52 | Transport: &debug.DebugRequestTransport{ 53 | RequestHeader: true, 54 | RequestBody: false, 55 | ResponseHeader: true, 56 | ResponseBody: true, 57 | }, 58 | }, 59 | }) 60 | 61 | name := "test/test_complete_upload.go" 62 | up := initUpload(c, name) 63 | uploadID := up.UploadID 64 | blockSize := 1024 * 1024 * 3 65 | 66 | opt := &cos.CompleteMultipartUploadOptions{} 67 | for i := 1; i < 5; i++ { 68 | etag := uploadPart(c, name, uploadID, blockSize, i) 69 | opt.Parts = append(opt.Parts, cos.Object{ 70 | PartNumber: i, ETag: etag}, 71 | ) 72 | } 73 | 74 | c = cos.NewClient(b, &http.Client{ 75 | Transport: &cos.AuthorizationTransport{ 76 | SecretID: os.Getenv("COS_SECRETID"), 77 | SecretKey: os.Getenv("COS_SECRETKEY"), 78 | Transport: &debug.DebugRequestTransport{ 79 | RequestHeader: true, 80 | RequestBody: true, 81 | ResponseHeader: true, 82 | ResponseBody: true, 83 | }, 84 | }, 85 | }) 86 | v, resp, err := c.Object.CompleteMultipartUpload( 87 | context.Background(), name, uploadID, opt, 88 | ) 89 | if err != nil { 90 | panic(err) 91 | } 92 | fmt.Printf("%s\n", resp.Status) 93 | fmt.Printf("%#v\n", v) 94 | fmt.Printf("%s\n", v.Location) 95 | } 96 | -------------------------------------------------------------------------------- /_example/object/copy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | "strings" 8 | 9 | "net/http" 10 | 11 | "fmt" 12 | "io/ioutil" 13 | "time" 14 | 15 | "github.com/mozillazg/go-cos" 16 | "github.com/mozillazg/go-cos/debug" 17 | ) 18 | 19 | func main() { 20 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 21 | b := &cos.BaseURL{BucketURL: u} 22 | c := cos.NewClient(b, &http.Client{ 23 | Transport: &cos.AuthorizationTransport{ 24 | SecretID: os.Getenv("COS_SECRETID"), 25 | SecretKey: os.Getenv("COS_SECRETKEY"), 26 | Transport: &debug.DebugRequestTransport{ 27 | RequestHeader: true, 28 | RequestBody: true, 29 | ResponseHeader: true, 30 | ResponseBody: true, 31 | }, 32 | }, 33 | }) 34 | 35 | source := "test/objectMove1.go" 36 | expected := "test" 37 | f := strings.NewReader(expected) 38 | 39 | _, err := c.Object.Put(context.Background(), source, f, nil) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | soruceURL := fmt.Sprintf("%s/%s", u.Host, source) 45 | dest := fmt.Sprintf("test/objectMove_%d.go", time.Now().Nanosecond()) 46 | //opt := &cos.ObjectCopyOptions{} 47 | res, _, err := c.Object.Copy(context.Background(), dest, soruceURL, nil) 48 | if err != nil { 49 | panic(err) 50 | } 51 | fmt.Printf("%+v\n\n", res) 52 | 53 | resp, err := c.Object.Get(context.Background(), dest, nil) 54 | if err != nil { 55 | panic(err) 56 | } 57 | bs, _ := ioutil.ReadAll(resp.Body) 58 | resp.Body.Close() 59 | result := string(bs) 60 | if result != expected { 61 | panic(fmt.Sprintf("%s != %s", result, expected)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /_example/object/delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{BucketURL: u} 17 | c := cos.NewClient(b, &http.Client{ 18 | Transport: &cos.AuthorizationTransport{ 19 | SecretID: os.Getenv("COS_SECRETID"), 20 | SecretKey: os.Getenv("COS_SECRETKEY"), 21 | Transport: &debug.DebugRequestTransport{ 22 | RequestHeader: true, 23 | RequestBody: true, 24 | ResponseHeader: true, 25 | ResponseBody: true, 26 | }, 27 | }, 28 | }) 29 | 30 | name := "test/objectPut.go" 31 | 32 | _, err := c.Object.Delete(context.Background(), name) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/object/deleteMultiple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "time" 9 | 10 | "bytes" 11 | "io" 12 | 13 | "math/rand" 14 | 15 | "net/http" 16 | 17 | "github.com/mozillazg/go-cos" 18 | "github.com/mozillazg/go-cos/debug" 19 | ) 20 | 21 | func genBigData(blockSize int) []byte { 22 | b := make([]byte, blockSize) 23 | if _, err := rand.Read(b); err != nil { 24 | panic(err) 25 | } 26 | return b 27 | } 28 | 29 | func uploadMulti(c *cos.Client) []string { 30 | names := []string{} 31 | data := genBigData(1024 * 1024 * 1) 32 | ctx := context.Background() 33 | var r io.Reader 34 | var name string 35 | n := 3 36 | 37 | for n > 0 { 38 | name = fmt.Sprintf("test/test_multi_delete_%s", time.Now().Format(time.RFC3339)) 39 | r = bytes.NewReader(data) 40 | 41 | c.Object.Put(ctx, name, r, nil) 42 | names = append(names, name) 43 | n-- 44 | } 45 | return names 46 | } 47 | 48 | func main() { 49 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 50 | b := &cos.BaseURL{BucketURL: u} 51 | c := cos.NewClient(b, &http.Client{ 52 | Transport: &cos.AuthorizationTransport{ 53 | SecretID: os.Getenv("COS_SECRETID"), 54 | SecretKey: os.Getenv("COS_SECRETKEY"), 55 | Transport: &debug.DebugRequestTransport{ 56 | RequestHeader: true, 57 | RequestBody: false, 58 | ResponseHeader: true, 59 | ResponseBody: true, 60 | }, 61 | }, 62 | }) 63 | ctx := context.Background() 64 | 65 | names := uploadMulti(c) 66 | names = append(names, []string{"a", "b", "c", "a+bc/xx&?+# "}...) 67 | obs := []cos.Object{} 68 | for _, v := range names { 69 | obs = append(obs, cos.Object{Key: v}) 70 | } 71 | //sha1 := "" 72 | opt := &cos.ObjectDeleteMultiOptions{ 73 | Objects: obs, 74 | //XCosSha1: sha1, 75 | //Quiet: true, 76 | } 77 | 78 | c = cos.NewClient(b, &http.Client{ 79 | Transport: &cos.AuthorizationTransport{ 80 | SecretID: os.Getenv("COS_SECRETID"), 81 | SecretKey: os.Getenv("COS_SECRETKEY"), 82 | Transport: &debug.DebugRequestTransport{ 83 | RequestHeader: true, 84 | RequestBody: true, 85 | ResponseHeader: true, 86 | ResponseBody: true, 87 | }, 88 | }, 89 | }) 90 | 91 | v, _, err := c.Object.DeleteMulti(ctx, opt) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | for _, x := range v.DeletedObjects { 97 | fmt.Printf("deleted %s\n", x.Key) 98 | } 99 | for _, x := range v.Errors { 100 | fmt.Printf("error %s, %s, %s\n", x.Key, x.Code, x.Message) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /_example/object/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "io/ioutil" 10 | 11 | "net/http" 12 | 13 | "github.com/mozillazg/go-cos" 14 | "github.com/mozillazg/go-cos/debug" 15 | ) 16 | 17 | func main() { 18 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 19 | b := &cos.BaseURL{BucketURL: u} 20 | c := cos.NewClient(b, &http.Client{ 21 | Transport: &cos.AuthorizationTransport{ 22 | SecretID: os.Getenv("COS_SECRETID"), 23 | SecretKey: os.Getenv("COS_SECRETKEY"), 24 | Transport: &debug.DebugRequestTransport{ 25 | RequestHeader: true, 26 | RequestBody: true, 27 | ResponseHeader: true, 28 | ResponseBody: true, 29 | }, 30 | }, 31 | }) 32 | 33 | name := "test/hello.txt" 34 | resp, err := c.Object.Get(context.Background(), name, nil) 35 | if err != nil { 36 | panic(err) 37 | } 38 | bs, _ := ioutil.ReadAll(resp.Body) 39 | resp.Body.Close() 40 | fmt.Printf("%s\n", string(bs)) 41 | 42 | // range 43 | opt := &cos.ObjectGetOptions{ 44 | ResponseContentType: "text/html", 45 | Range: "bytes=0-3", 46 | } 47 | resp, err = c.Object.Get(context.Background(), name, opt) 48 | if err != nil { 49 | panic(err) 50 | } 51 | bs, _ = ioutil.ReadAll(resp.Body) 52 | resp.Body.Close() 53 | fmt.Printf("%s\n", string(bs)) 54 | } 55 | -------------------------------------------------------------------------------- /_example/object/getACL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "net/http" 10 | 11 | "github.com/mozillazg/go-cos" 12 | "github.com/mozillazg/go-cos/debug" 13 | ) 14 | 15 | func main() { 16 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 17 | b := &cos.BaseURL{BucketURL: u} 18 | c := cos.NewClient(b, &http.Client{ 19 | Transport: &cos.AuthorizationTransport{ 20 | SecretID: os.Getenv("COS_SECRETID"), 21 | SecretKey: os.Getenv("COS_SECRETKEY"), 22 | Transport: &debug.DebugRequestTransport{ 23 | RequestHeader: true, 24 | RequestBody: true, 25 | ResponseHeader: true, 26 | ResponseBody: true, 27 | }, 28 | }, 29 | }) 30 | 31 | name := "test/hello.txt" 32 | v, _, err := c.Object.GetACL(context.Background(), name) 33 | if err != nil { 34 | panic(err) 35 | } 36 | for _, a := range v.AccessControlList { 37 | fmt.Printf("%s, %s, %s\n", a.Grantee.Type, a.Grantee.ID, a.Permission) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /_example/object/getAnonymous.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "io/ioutil" 11 | 12 | "github.com/mozillazg/go-cos" 13 | ) 14 | 15 | func upload(c *cos.Client, name string) { 16 | f := strings.NewReader("test") 17 | f = strings.NewReader("test xxx") 18 | opt := &cos.ObjectPutOptions{ 19 | ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ 20 | ContentType: "text/html", 21 | }, 22 | ACLHeaderOptions: &cos.ACLHeaderOptions{ 23 | XCosACL: "public-read", 24 | }, 25 | } 26 | c.Object.Put(context.Background(), name, f, opt) 27 | return 28 | } 29 | 30 | func main() { 31 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 32 | b := &cos.BaseURL{BucketURL: u} 33 | c := cos.NewClient(b, nil) 34 | 35 | name := "test/anonymous_get.go" 36 | upload(c, name) 37 | 38 | resp, err := c.Object.Get(context.Background(), name, nil) 39 | if err != nil { 40 | panic(err) 41 | return 42 | } 43 | bs, _ := ioutil.ReadAll(resp.Body) 44 | defer resp.Body.Close() 45 | fmt.Printf("%s\n", string(bs)) 46 | } 47 | -------------------------------------------------------------------------------- /_example/object/getWithPresignedURL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/mozillazg/go-cos" 13 | "github.com/mozillazg/go-cos/debug" 14 | ) 15 | 16 | func main() { 17 | b, _ := cos.NewBaseURL(os.Getenv("COS_BUCKET_URL")) 18 | auth := cos.Auth{ 19 | SecretID: os.Getenv("COS_SECRETID"), 20 | SecretKey: os.Getenv("COS_SECRETKEY"), 21 | Expire: time.Hour, 22 | } 23 | c := cos.NewClient(b, &http.Client{ 24 | Transport: &cos.AuthorizationTransport{ 25 | SecretID: auth.SecretID, 26 | SecretKey: auth.SecretKey, 27 | Expire: auth.Expire, 28 | Transport: &debug.DebugRequestTransport{ 29 | RequestHeader: true, 30 | RequestBody: true, 31 | ResponseHeader: true, 32 | ResponseBody: true, 33 | }, 34 | }, 35 | }) 36 | 37 | name := "test/hello.txt" 38 | ctx := context.Background() 39 | 40 | // 通过生成签名 header 下载文件 41 | resp, err := c.Object.Get(ctx, name, nil) 42 | if err != nil { 43 | panic(err) 44 | } 45 | bs, _ := ioutil.ReadAll(resp.Body) 46 | resp.Body.Close() 47 | fmt.Printf("%s\n", string(bs)) 48 | 49 | // 获取预签名授权 URL 50 | presignedURL, err := c.Object.PresignedURL(ctx, http.MethodGet, name, auth, nil) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | // 通过预签名授权 URL 下载文件 56 | resp2, err := http.Get(presignedURL.String()) 57 | if err != nil { 58 | panic(err) 59 | } 60 | bs2, _ := ioutil.ReadAll(resp2.Body) 61 | resp2.Body.Close() 62 | fmt.Printf("%s\n", string(bs2)) 63 | 64 | fmt.Printf("%v\n\n", bytes.Compare(bs2, bs) == 0) 65 | 66 | // c.Object.Get 使用 预签名授权 URL 67 | c2 := cos.NewClient(b, &http.Client{ 68 | Transport: &debug.DebugRequestTransport{ 69 | RequestHeader: true, 70 | RequestBody: true, 71 | ResponseHeader: true, 72 | ResponseBody: true, 73 | }, 74 | }) 75 | resp3, err := c2.Object.Get(ctx, name, &cos.ObjectGetOptions{ 76 | PresignedURL: presignedURL, 77 | }) 78 | if err != nil { 79 | panic(err) 80 | } 81 | resp3.Body.Close() 82 | } 83 | -------------------------------------------------------------------------------- /_example/object/head.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | //"net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | //u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | u := cos.NewBucketURL("test", "1253846586", "ap-beijing-1", true) 17 | b := &cos.BaseURL{BucketURL: u} 18 | c := cos.NewClient(b, &http.Client{ 19 | Transport: &cos.AuthorizationTransport{ 20 | SecretID: os.Getenv("COS_SECRETID"), 21 | SecretKey: os.Getenv("COS_SECRETKEY"), 22 | Transport: &debug.DebugRequestTransport{ 23 | RequestHeader: true, 24 | RequestBody: true, 25 | ResponseHeader: true, 26 | ResponseBody: true, 27 | }, 28 | }, 29 | }) 30 | 31 | name := "test/hello.txt" 32 | _, err := c.Object.Head(context.Background(), name, nil) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/object/initiateMultipartUpload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "time" 9 | 10 | "net/http" 11 | 12 | "github.com/mozillazg/go-cos" 13 | "github.com/mozillazg/go-cos/debug" 14 | ) 15 | 16 | func main() { 17 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 18 | b := &cos.BaseURL{BucketURL: u} 19 | c := cos.NewClient(b, &http.Client{ 20 | Transport: &cos.AuthorizationTransport{ 21 | SecretID: os.Getenv("COS_SECRETID"), 22 | SecretKey: os.Getenv("COS_SECRETKEY"), 23 | Transport: &debug.DebugRequestTransport{ 24 | RequestHeader: true, 25 | RequestBody: true, 26 | ResponseHeader: true, 27 | ResponseBody: true, 28 | }, 29 | }, 30 | }) 31 | 32 | name := "test_multipart" + time.Now().Format(time.RFC3339) 33 | v, _, err := c.Object.InitiateMultipartUpload(context.Background(), name, nil) 34 | if err != nil { 35 | panic(err) 36 | } 37 | fmt.Printf("%s\n", v.UploadID) 38 | } 39 | -------------------------------------------------------------------------------- /_example/object/listParts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/url" 8 | "os" 9 | "strings" 10 | 11 | "net/http" 12 | 13 | "github.com/mozillazg/go-cos" 14 | "github.com/mozillazg/go-cos/debug" 15 | ) 16 | 17 | func initUpload(c *cos.Client, name string) *cos.InitiateMultipartUploadResult { 18 | v, _, err := c.Object.InitiateMultipartUpload(context.Background(), name, nil) 19 | if err != nil { 20 | panic(err) 21 | } 22 | fmt.Printf("%#v\n", v) 23 | return v 24 | } 25 | 26 | func uploadPart(c *cos.Client, name string, uploadID string, blockSize, n int) string { 27 | 28 | b := make([]byte, blockSize) 29 | if _, err := rand.Read(b); err != nil { 30 | panic(err) 31 | } 32 | s := fmt.Sprintf("%X", b) 33 | f := strings.NewReader(s) 34 | 35 | resp, err := c.Object.UploadPart( 36 | context.Background(), name, uploadID, n, f, nil, 37 | ) 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("%s\n", resp.Status) 42 | return resp.Header.Get("Etag") 43 | } 44 | 45 | func main() { 46 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 47 | b := &cos.BaseURL{BucketURL: u} 48 | c := cos.NewClient(b, &http.Client{ 49 | Transport: &cos.AuthorizationTransport{ 50 | SecretID: os.Getenv("COS_SECRETID"), 51 | SecretKey: os.Getenv("COS_SECRETKEY"), 52 | Transport: &debug.DebugRequestTransport{ 53 | RequestHeader: true, 54 | RequestBody: false, 55 | ResponseHeader: true, 56 | ResponseBody: true, 57 | }, 58 | }, 59 | }) 60 | 61 | name := "test/test_list_parts.go" 62 | up := initUpload(c, name) 63 | uploadID := up.UploadID 64 | ctx := context.Background() 65 | blockSize := 1024 * 1024 * 3 66 | 67 | for i := 1; i < 5; i++ { 68 | uploadPart(c, name, uploadID, blockSize, i) 69 | } 70 | 71 | v, _, err := c.Object.ListParts(ctx, name, uploadID) 72 | if err != nil { 73 | panic(err) 74 | return 75 | } 76 | for _, p := range v.Parts { 77 | fmt.Printf("%d, %s, %d\n", p.PartNumber, p.ETag, p.Size) 78 | } 79 | fmt.Printf("%s\n", v.Initiator.ID) 80 | fmt.Printf("%s\n", v.Owner.ID) 81 | } 82 | -------------------------------------------------------------------------------- /_example/object/listPartsWithOpt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/url" 8 | "os" 9 | "strings" 10 | 11 | "net/http" 12 | 13 | "github.com/mozillazg/go-cos" 14 | "github.com/mozillazg/go-cos/debug" 15 | ) 16 | 17 | func initUpload(c *cos.Client, name string) *cos.InitiateMultipartUploadResult { 18 | v, _, err := c.Object.InitiateMultipartUpload(context.Background(), name, nil) 19 | if err != nil { 20 | panic(err) 21 | } 22 | fmt.Printf("%#v\n", v) 23 | return v 24 | } 25 | 26 | func uploadPart(c *cos.Client, name string, uploadID string, blockSize, n int) string { 27 | 28 | b := make([]byte, blockSize) 29 | if _, err := rand.Read(b); err != nil { 30 | panic(err) 31 | } 32 | s := fmt.Sprintf("%X", b) 33 | f := strings.NewReader(s) 34 | 35 | resp, err := c.Object.UploadPart( 36 | context.Background(), name, uploadID, n, f, nil, 37 | ) 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("%s\n", resp.Status) 42 | return resp.Header.Get("Etag") 43 | } 44 | 45 | func main() { 46 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 47 | b := &cos.BaseURL{BucketURL: u} 48 | c := cos.NewClient(b, &http.Client{ 49 | Transport: &cos.AuthorizationTransport{ 50 | SecretID: os.Getenv("COS_SECRETID"), 51 | SecretKey: os.Getenv("COS_SECRETKEY"), 52 | Transport: &debug.DebugRequestTransport{ 53 | RequestHeader: true, 54 | RequestBody: false, 55 | ResponseHeader: true, 56 | ResponseBody: true, 57 | }, 58 | }, 59 | }) 60 | 61 | name := "test/test_list_parts.go" 62 | up := initUpload(c, name) 63 | uploadID := up.UploadID 64 | ctx := context.Background() 65 | blockSize := 1024 * 1024 * 3 66 | 67 | for i := 1; i < 5; i++ { 68 | uploadPart(c, name, uploadID, blockSize, i) 69 | } 70 | opt := &cos.ObjectListPartsOptions{ 71 | MaxParts: 2, 72 | } 73 | 74 | v, _, err := c.Object.ListPartsWithOpt(ctx, name, uploadID, opt) 75 | if err != nil { 76 | panic(err) 77 | return 78 | } 79 | for _, p := range v.Parts { 80 | fmt.Printf("%d, %s, %d\n", p.PartNumber, p.ETag, p.Size) 81 | } 82 | fmt.Printf("%s\n", v.Initiator.ID) 83 | fmt.Printf("%s\n", v.Owner.ID) 84 | } 85 | -------------------------------------------------------------------------------- /_example/object/mock.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "reflect" 12 | 13 | "github.com/mozillazg/go-cos" 14 | "github.com/mozillazg/go-cos/debug" 15 | ) 16 | 17 | type MockSender struct{} 18 | 19 | func (s *MockSender) Send(ctx context.Context, caller cos.Caller, req *http.Request) (*http.Response, error) { 20 | // 如果用不到 response 的话,也可以直接 return &http.Response{}, nil 21 | resp, _ := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(`HTTP/1.1 200 OK 22 | Content-Length: 6 23 | Accept-Ranges: bytes 24 | Connection: keep-alive 25 | Content-Type: text/plain; charset=utf-8 26 | Date: Sat, 19 Jan 2019 08:25:27 GMT 27 | Etag: "f572d396fae9206628714fb2ce00f72e94f2258f" 28 | Last-Modified: Mon, 12 Jun 2017 13:36:19 GMT 29 | Server: tencent-cos 30 | X-Cos-Request-Id: NWM0MmRlZjdfMmJhZDM1MGFfNDFkM19hZGI3MQ== 31 | 32 | hello 33 | `))), nil) 34 | return resp, nil 35 | } 36 | 37 | type MockerResponseParser struct { 38 | result *cos.ObjectGetACLResult 39 | } 40 | 41 | func (p *MockerResponseParser) ParseResponse(ctx context.Context, caller cos.Caller, resp *http.Response, result interface{}) (*cos.Response, error) { 42 | b, _ := ioutil.ReadAll(resp.Body) 43 | if string(b) != "hello\n" { 44 | panic(string(b)) 45 | } 46 | 47 | // 插入预设的结果 48 | switch caller.Method { 49 | case cos.MethodObjectGetACL: 50 | v := result.(*cos.ObjectGetACLResult) 51 | *v = *p.result 52 | } 53 | 54 | return &cos.Response{Response: resp}, nil 55 | } 56 | 57 | func main() { 58 | b, _ := cos.NewBaseURL("http://cos.example.com") 59 | c := cos.NewClient(b, &http.Client{ 60 | Transport: &cos.AuthorizationTransport{ 61 | SecretID: os.Getenv("COS_SECRETID"), 62 | SecretKey: os.Getenv("COS_SECRETKEY"), 63 | Transport: &debug.DebugRequestTransport{ 64 | RequestHeader: true, 65 | RequestBody: true, 66 | ResponseHeader: true, 67 | ResponseBody: true, 68 | }, 69 | }, 70 | }) 71 | c.Sender = &MockSender{} 72 | acl := &cos.ObjectGetACLResult{ 73 | Owner: &cos.Owner{ 74 | ID: "test", 75 | }, 76 | AccessControlList: []cos.ACLGrant{ 77 | { 78 | Permission: "READ", 79 | }, 80 | }, 81 | } 82 | c.ResponseParser = &MockerResponseParser{acl} 83 | 84 | result, resp, err := c.Object.GetACL(context.Background(), "test/mock.go") 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | defer resp.Body.Close() 90 | fmt.Printf("%#v\n", result) 91 | if !reflect.DeepEqual(*result, *acl) { 92 | panic(*result) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /_example/object/options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{BucketURL: u} 17 | c := cos.NewClient(b, &http.Client{ 18 | Transport: &cos.AuthorizationTransport{ 19 | SecretID: os.Getenv("COS_SECRETID"), 20 | SecretKey: os.Getenv("COS_SECRETKEY"), 21 | Transport: &debug.DebugRequestTransport{ 22 | RequestHeader: true, 23 | RequestBody: true, 24 | ResponseHeader: true, 25 | ResponseBody: true, 26 | }, 27 | }, 28 | }) 29 | 30 | name := "test/hello.txt" 31 | opt := &cos.ObjectOptionsOptions{ 32 | Origin: "http://www.qq.com", 33 | AccessControlRequestMethod: "PUT", 34 | } 35 | _, err := c.Object.Options(context.Background(), name, opt) 36 | if err != nil { 37 | panic(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /_example/object/put.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | b, _ := cos.NewBaseURL(os.Getenv("COS_BUCKET_URL")) 16 | c := cos.NewClient(b, &http.Client{ 17 | Transport: &cos.AuthorizationTransport{ 18 | SecretID: os.Getenv("COS_SECRETID"), 19 | SecretKey: os.Getenv("COS_SECRETKEY"), 20 | Transport: &debug.DebugRequestTransport{ 21 | RequestHeader: true, 22 | RequestBody: true, 23 | ResponseHeader: true, 24 | ResponseBody: true, 25 | }, 26 | }, 27 | }) 28 | 29 | name := "test/objectPut.go" 30 | f := strings.NewReader("test") 31 | 32 | _, err := c.Object.Put(context.Background(), name, f, nil) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // 测试上传以及特殊字符 38 | name = "test/put_ + !'()* option.go" 39 | contentDisposition := "attachment; filename=Hello - world!(+)'*.go" 40 | f = strings.NewReader("test xxx") 41 | opt := &cos.ObjectPutOptions{ 42 | ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ 43 | ContentType: "text/html", 44 | ContentDisposition: contentDisposition, 45 | }, 46 | ACLHeaderOptions: &cos.ACLHeaderOptions{ 47 | // XCosACL: "public-read", 48 | XCosACL: "private", 49 | }, 50 | } 51 | resp, err := c.Object.Put(context.Background(), name, f, opt) 52 | if err != nil { 53 | panic(err) 54 | } 55 | resp.Body.Close() 56 | 57 | // 测试特殊字符 58 | resp, err = c.Object.Get(context.Background(), name, nil) 59 | if err != nil { 60 | panic(err) 61 | } 62 | resp.Body.Close() 63 | if resp.Header.Get("Content-Disposition") != contentDisposition { 64 | panic(errors.New("wong Content-Disposition")) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /_example/object/putACL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 16 | b := &cos.BaseURL{BucketURL: u} 17 | c := cos.NewClient(b, &http.Client{ 18 | Transport: &cos.AuthorizationTransport{ 19 | SecretID: os.Getenv("COS_SECRETID"), 20 | SecretKey: os.Getenv("COS_SECRETKEY"), 21 | Transport: &debug.DebugRequestTransport{ 22 | RequestHeader: true, 23 | RequestBody: true, 24 | ResponseHeader: true, 25 | ResponseBody: true, 26 | }, 27 | }, 28 | }) 29 | 30 | opt := &cos.ObjectPutACLOptions{ 31 | Header: &cos.ACLHeaderOptions{ 32 | XCosACL: "private", 33 | }, 34 | } 35 | name := "test/hello.txt" 36 | _, err := c.Object.PutACL(context.Background(), name, opt) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | // with body 42 | opt = &cos.ObjectPutACLOptions{ 43 | Body: &cos.ACLXml{ 44 | Owner: &cos.Owner{ 45 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 46 | }, 47 | AccessControlList: []cos.ACLGrant{ 48 | { 49 | Grantee: &cos.ACLGrantee{ 50 | Type: "RootAccount", 51 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 52 | }, 53 | 54 | Permission: "FULL_CONTROL", 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | _, err = c.Object.PutACL(context.Background(), name, opt) 61 | if err != nil { 62 | panic(err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /_example/object/putWithPresignedURL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/mozillazg/go-cos" 13 | "github.com/mozillazg/go-cos/debug" 14 | ) 15 | 16 | func main() { 17 | b, _ := cos.NewBaseURL(os.Getenv("COS_BUCKET_URL")) 18 | auth := cos.Auth{ 19 | SecretID: os.Getenv("COS_SECRETID"), 20 | SecretKey: os.Getenv("COS_SECRETKEY"), 21 | Expire: time.Hour, 22 | } 23 | c := cos.NewClient(b, &http.Client{ 24 | Transport: &cos.AuthorizationTransport{ 25 | SecretID: auth.SecretID, 26 | SecretKey: auth.SecretKey, 27 | Expire: auth.Expire, 28 | Transport: &debug.DebugRequestTransport{ 29 | RequestHeader: true, 30 | RequestBody: true, 31 | ResponseHeader: true, 32 | ResponseBody: true, 33 | }, 34 | }, 35 | }) 36 | 37 | name := "test/objectPut.go" 38 | ctx := context.Background() 39 | f := strings.NewReader("test") 40 | 41 | // 通过生成签名 header 上传文件 42 | _, err := c.Object.Put(ctx, name, f, nil) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // 获取预签名授权 URL 48 | opt := &cos.ObjectPutOptions{ 49 | ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ 50 | // 指定上传内容的 Content-Type,用于指定下载时 response 的 Content-Type 51 | ContentType: "text/html", 52 | }, 53 | } 54 | presignedURL, err := c.Object.PresignedURL(ctx, http.MethodPut, name, auth, opt) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | // 通过预签名授权 URL 上传 60 | data := "test upload with presignedURL" 61 | f = strings.NewReader(data) 62 | req, err := http.NewRequest(http.MethodPut, presignedURL.String(), f) 63 | if err != nil { 64 | panic(err) 65 | } 66 | // 指定上传内容的 Content-Type,用于指定下载时 response 的 Content-Type 67 | req.Header.Set("Content-Type", "text/html") 68 | _, err = http.DefaultClient.Do(req) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | // 验证上传的内容 74 | resp, err := c.Object.Get(ctx, name, nil) 75 | if err != nil { 76 | panic(err) 77 | } 78 | bs, _ := ioutil.ReadAll(resp.Body) 79 | resp.Body.Close() 80 | fmt.Printf("%s\n", string(bs)) 81 | fmt.Printf("Content-Type: %s\n\n", resp.Header.Get("Content-Type")) 82 | fmt.Printf("%v\n\n", strings.Compare(data, string(bs)) == 0) 83 | 84 | // c.Object.Put 使用 预签名授权 URL 85 | c2 := cos.NewClient(b, &http.Client{ 86 | Transport: &debug.DebugRequestTransport{ 87 | RequestHeader: true, 88 | RequestBody: true, 89 | ResponseHeader: true, 90 | ResponseBody: true, 91 | }, 92 | }) 93 | f = strings.NewReader("test c.Object.Put with presignedURL") 94 | opt.PresignedURL = presignedURL 95 | resp2, err := c2.Object.Put(ctx, name, f, opt) 96 | if err != nil { 97 | panic(err) 98 | } 99 | resp2.Body.Close() 100 | } 101 | -------------------------------------------------------------------------------- /_example/object/sessionToken.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 14 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" 15 | sts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813" 16 | 17 | "github.com/mozillazg/go-cos" 18 | "github.com/mozillazg/go-cos/debug" 19 | ) 20 | 21 | type TmpAuth struct { 22 | SecretID string 23 | SecretKey string 24 | SessionToken string 25 | } 26 | 27 | // https://cloud.tencent.com/document/product/598/33416 28 | // https://console.cloud.tencent.com/api/explorer?Product=sts&Version=2018-08-13&Action=GetFederationToken&SignVersion= 29 | // https://cloud.tencent.com/document/product/436/31923 30 | func getTmpAuth() TmpAuth { 31 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 32 | parts := strings.Split(u.Host, ".") 33 | // bucketName := parts[0] 34 | // bucketParts := strings.Split(bucketName, "-") 35 | // appID := bucketParts[len(bucketParts)-1] 36 | region := parts[2] 37 | regex := regexp.MustCompile("-\\d") 38 | region = regex.ReplaceAllString(region, "") 39 | 40 | credential := common.NewCredential( 41 | os.Getenv("COS_SECRETID"), 42 | os.Getenv("COS_SECRETKEY"), 43 | ) 44 | cpf := profile.NewClientProfile() 45 | cpf.HttpProfile.Endpoint = "sts.tencentcloudapi.com" 46 | client, _ := sts.NewClient(credential, region, cpf) 47 | 48 | request := sts.NewGetFederationTokenRequest() 49 | 50 | // 没搞明白怎么为单个文件或目录设置 Policy,按照文档的示例尝试总是不对,所以这里 resource 的值设置为 * 以便可以顺利验证程序功能。 51 | params := "{\"Name\":\"test\",\"Policy\":\"{ \\\"version\\\": \\\"2.0\\\", \\\"statement\\\": [ { \\\"action\\\": [ \\\"name/cos:GetObject\\\" ], \\\"effect\\\": \\\"allow\\\", \\\"resource\\\": [ \\\"*\\\" ] } ] }\"}" 52 | err := request.FromJsonString(params) 53 | if err != nil { 54 | panic(err) 55 | } 56 | response, err := client.GetFederationToken(request) 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | cres := response.Response.Credentials 62 | return TmpAuth{ 63 | SecretID: *cres.TmpSecretId, 64 | SecretKey: *cres.TmpSecretKey, 65 | SessionToken: *cres.Token, 66 | } 67 | } 68 | 69 | func main() { 70 | tmpAuth := getTmpAuth() 71 | fmt.Printf("%#v\n\n", tmpAuth) 72 | 73 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 74 | b := &cos.BaseURL{BucketURL: u} 75 | c := cos.NewClient(b, &http.Client{ 76 | Transport: &cos.AuthorizationTransport{ 77 | // 使用临时密钥 78 | SecretID: tmpAuth.SecretID, 79 | SecretKey: tmpAuth.SecretKey, 80 | SessionToken: tmpAuth.SessionToken, 81 | Transport: &debug.DebugRequestTransport{ 82 | RequestHeader: true, 83 | RequestBody: true, 84 | ResponseHeader: true, 85 | ResponseBody: true, 86 | }, 87 | }, 88 | }) 89 | 90 | name := "test/hello.txt" 91 | resp, err := c.Object.Get(context.Background(), name, nil) 92 | if err != nil { 93 | panic(err) 94 | } 95 | bs, _ := ioutil.ReadAll(resp.Body) 96 | defer resp.Body.Close() 97 | fmt.Printf("%s\n", string(bs)) 98 | } 99 | -------------------------------------------------------------------------------- /_example/object/uploadFile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/mozillazg/go-cos" 10 | "github.com/mozillazg/go-cos/debug" 11 | ) 12 | 13 | func main() { 14 | b, _ := cos.NewBaseURL(os.Getenv("COS_BUCKET_URL")) 15 | c := cos.NewClient(b, &http.Client{ 16 | Transport: &cos.AuthorizationTransport{ 17 | SecretID: os.Getenv("COS_SECRETID"), 18 | SecretKey: os.Getenv("COS_SECRETKEY"), 19 | Transport: &debug.DebugRequestTransport{ 20 | RequestHeader: true, 21 | RequestBody: false, 22 | ResponseHeader: true, 23 | ResponseBody: true, 24 | }, 25 | }, 26 | }) 27 | 28 | name := "test/uploadFile.go" 29 | f, err := os.Open(os.Args[0]) 30 | if err != nil { 31 | panic(err) 32 | } 33 | defer f.Close() 34 | s, err := f.Stat() 35 | if err != nil { 36 | panic(err) 37 | } 38 | fmt.Println(s.Size()) 39 | 40 | _, err = c.Object.Put(context.Background(), name, f, nil) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /_example/object/uploadPart.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "net/url" 9 | "strings" 10 | 11 | "net/http" 12 | 13 | "github.com/mozillazg/go-cos" 14 | "github.com/mozillazg/go-cos/debug" 15 | ) 16 | 17 | func initUpload(c *cos.Client, name string) *cos.InitiateMultipartUploadResult { 18 | v, _, err := c.Object.InitiateMultipartUpload(context.Background(), name, nil) 19 | if err != nil { 20 | panic(err) 21 | } 22 | fmt.Printf("%#v\n", v) 23 | return v 24 | } 25 | 26 | func main() { 27 | u, _ := url.Parse(os.Getenv("COS_BUCKET_URL")) 28 | b := &cos.BaseURL{BucketURL: u} 29 | c := cos.NewClient(b, &http.Client{ 30 | Transport: &cos.AuthorizationTransport{ 31 | SecretID: os.Getenv("COS_SECRETID"), 32 | SecretKey: os.Getenv("COS_SECRETKEY"), 33 | Transport: &debug.DebugRequestTransport{ 34 | RequestHeader: true, 35 | RequestBody: true, 36 | ResponseHeader: true, 37 | ResponseBody: true, 38 | }, 39 | }, 40 | }) 41 | 42 | name := "test/test_multi_upload.go" 43 | up := initUpload(c, name) 44 | uploadID := up.UploadID 45 | 46 | f := strings.NewReader("test heoo") 47 | _, err := c.Object.UploadPart( 48 | context.Background(), name, uploadID, 1, f, nil, 49 | ) 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /_example/service/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "net/http" 9 | 10 | "github.com/mozillazg/go-cos" 11 | "github.com/mozillazg/go-cos/debug" 12 | ) 13 | 14 | func main() { 15 | c := cos.NewClient(nil, &http.Client{ 16 | Transport: &cos.AuthorizationTransport{ 17 | SecretID: os.Getenv("COS_SECRETID"), 18 | SecretKey: os.Getenv("COS_SECRETKEY"), 19 | Transport: &debug.DebugRequestTransport{ 20 | RequestHeader: true, 21 | RequestBody: true, 22 | ResponseHeader: true, 23 | ResponseBody: true, 24 | }, 25 | }, 26 | }) 27 | 28 | s, _, err := c.Service.Get(context.Background()) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | for _, b := range s.Buckets { 34 | fmt.Printf("%#v\n", b) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /_example/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function run() { 4 | go run "$@" 5 | 6 | if [ $? -ne 0 ] 7 | then 8 | exit 3 9 | fi 10 | } 11 | 12 | echo '###### service ####' 13 | run ./service/get.go 14 | 15 | 16 | echo '##### bucket ####' 17 | 18 | # run ./bucket/delete.go 19 | run ./bucket/put.go 20 | run ./bucket/putACL.go 21 | run ./bucket/putCORS.go 22 | run ./bucket/putLifecycle.go 23 | run ./bucket/putTagging.go 24 | run ./bucket/get.go 25 | run ./bucket/getACL.go 26 | run ./bucket/getCORS.go 27 | run ./bucket/getLifecycle.go 28 | run ./bucket/getTagging.go 29 | run ./bucket/getLocation.go 30 | run ./bucket/head.go 31 | run ./bucket/listMultipartUploads.go 32 | run ./bucket/delete.go 33 | run ./bucket/deleteCORS.go 34 | run ./bucket/deleteLifecycle.go 35 | run ./bucket/deleteTagging.go 36 | 37 | 38 | echo '##### object ####' 39 | 40 | run ./bucket/putCORS.go 41 | run ./object/put.go 42 | run ./object/uploadFile.go 43 | run ./object/putACL.go 44 | run ./object/append.go 45 | run ./object/get.go 46 | run ./object/sessionToken.go 47 | run ./object/head.go 48 | run ./object/getAnonymous.go 49 | run ./object/getACL.go 50 | run ./object/listParts.go 51 | run ./object/options.go 52 | run ./object/initiateMultipartUpload.go 53 | run ./object/uploadPart.go 54 | run ./object/completeMultipartUpload.go 55 | run ./object/abortMultipartUpload.go 56 | run ./object/delete.go 57 | run ./object/deleteMultiple.go 58 | run ./object/copy.go 59 | run ./object/getWithPresignedURL.go 60 | run ./object/putWithPresignedURL.go 61 | run ./object/mock.go 62 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "fmt" 7 | "hash" 8 | "net/http" 9 | "net/url" 10 | "sort" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const sha1SignAlgorithm = "sha1" 16 | const privateHeaderPrefix = "x-cos-" 17 | const defaultAuthExpire = time.Hour 18 | 19 | // 需要校验的 Headers 列表 20 | var needSignHeaders = map[string]bool{ 21 | "host": true, 22 | "range": true, 23 | "x-cos-acl": true, 24 | "x-cos-grant-read": true, 25 | "x-cos-grant-write": true, 26 | "x-cos-grant-full-control": true, 27 | "response-content-type": true, 28 | "response-content-language": true, 29 | "response-expires": true, 30 | "response-cache-control": true, 31 | "response-content-disposition": true, 32 | "response-content-encoding": true, 33 | "cache-control": true, 34 | "content-disposition": true, 35 | "content-encoding": true, 36 | // "content-type": true, 37 | "content-length": true, 38 | "content-md5": true, 39 | "expect": true, 40 | "expires": true, 41 | "x-cos-content-sha1": true, 42 | "x-cos-storage-class": true, 43 | "if-modified-since": true, 44 | "origin": true, 45 | "access-control-request-method": true, 46 | "access-control-request-headers": true, 47 | "x-cos-object-type": true, 48 | // "x-cos-security-token": true, 49 | } 50 | 51 | // AuthTime 用于生成签名所需的 q-sign-time 和 q-key-time 相关参数 52 | type AuthTime struct { 53 | SignStartTime time.Time 54 | SignEndTime time.Time 55 | KeyStartTime time.Time 56 | KeyEndTime time.Time 57 | } 58 | 59 | // NewAuthTime 生成 AuthTime 的便捷函数 60 | // 61 | // expire: 从现在开始多久过期. 62 | func NewAuthTime(expire time.Duration) *AuthTime { 63 | if expire == time.Duration(0) { 64 | expire = defaultAuthExpire 65 | } 66 | signStartTime := time.Now() 67 | keyStartTime := signStartTime 68 | signEndTime := signStartTime.Add(expire) 69 | keyEndTime := signEndTime 70 | return &AuthTime{ 71 | SignStartTime: signStartTime, 72 | SignEndTime: signEndTime, 73 | KeyStartTime: keyStartTime, 74 | KeyEndTime: keyEndTime, 75 | } 76 | } 77 | 78 | // signString return q-sign-time string 79 | func (a *AuthTime) signString() string { 80 | return fmt.Sprintf("%d;%d", a.SignStartTime.Unix(), a.SignEndTime.Unix()) 81 | } 82 | 83 | // keyString return q-key-time string 84 | func (a *AuthTime) keyString() string { 85 | return fmt.Sprintf("%d;%d", a.KeyStartTime.Unix(), a.KeyEndTime.Unix()) 86 | } 87 | 88 | // newAuthorization 通过一系列步骤生成最终需要的 Authorization 字符串 89 | // 90 | // https://cloud.tencent.com/document/product/436/7778 91 | func newAuthorization(auth Auth, req *http.Request, authTime AuthTime) string { 92 | secretKey := auth.SecretKey 93 | secretID := auth.SecretID 94 | signTime := authTime.signString() 95 | keyTime := authTime.keyString() 96 | signKey := calSignKey(secretKey, keyTime) 97 | 98 | formatHeaders, signedHeaderList := genFormatHeaders(req.Header) 99 | formatParameters, signedParameterList := genFormatParameters(req.URL.Query()) 100 | formatString := genFormatString(req.Method, *req.URL, formatParameters, formatHeaders) 101 | 102 | stringToSign := calStringToSign(sha1SignAlgorithm, keyTime, formatString) 103 | signature := calSignature(signKey, stringToSign) 104 | 105 | return genAuthorization( 106 | secretID, signTime, keyTime, signature, signedHeaderList, 107 | signedParameterList, 108 | ) 109 | } 110 | 111 | // AddAuthorizationHeader 给 req 增加签名信息 112 | func AddAuthorizationHeader(secretID, secretKey string, req *http.Request, authTime *AuthTime) { 113 | auth := newAuthorization(Auth{ 114 | SecretID: secretID, 115 | SecretKey: secretKey, 116 | }, req, *authTime) 117 | req.Header.Set("Authorization", auth) 118 | } 119 | 120 | // calSignKey 计算 SignKey 121 | func calSignKey(secretKey, keyTime string) string { 122 | digest := calHMACDigest(secretKey, keyTime, sha1SignAlgorithm) 123 | return fmt.Sprintf("%x", digest) 124 | } 125 | 126 | // calStringToSign 计算 StringToSign 127 | func calStringToSign(signAlgorithm, signTime, formatString string) string { 128 | h := sha1.New() 129 | h.Write([]byte(formatString)) 130 | return fmt.Sprintf("%s\n%s\n%x\n", signAlgorithm, signTime, h.Sum(nil)) 131 | } 132 | 133 | // calSignature 计算 Signature 134 | func calSignature(signKey, stringToSign string) string { 135 | digest := calHMACDigest(signKey, stringToSign, sha1SignAlgorithm) 136 | return fmt.Sprintf("%x", digest) 137 | } 138 | 139 | // genAuthorization 生成 Authorization 140 | func genAuthorization(secretID, signTime, keyTime, signature string, signedHeaderList, signedParameterList []string) string { 141 | return strings.Join([]string{ 142 | "q-sign-algorithm=" + sha1SignAlgorithm, 143 | "q-ak=" + secretID, 144 | "q-sign-time=" + signTime, 145 | "q-key-time=" + keyTime, 146 | "q-header-list=" + strings.Join(signedHeaderList, ";"), 147 | "q-url-param-list=" + strings.Join(signedParameterList, ";"), 148 | "q-signature=" + signature, 149 | }, "&") 150 | } 151 | 152 | // genFormatString 生成 FormatString 153 | func genFormatString(method string, uri url.URL, formatParameters, formatHeaders string) string { 154 | formatMethod := strings.ToLower(method) 155 | formatURI := uri.Path 156 | 157 | return fmt.Sprintf("%s\n%s\n%s\n%s\n", formatMethod, formatURI, 158 | formatParameters, formatHeaders, 159 | ) 160 | } 161 | 162 | // https://github.com/tencentyun/cos-nodejs-sdk-v5/blob/a1dad3e9e3776cd24c97975f3aa47631e5001ff0/sdk/util.js#L11 163 | func camSafeURLEncode(s string) string { 164 | s = encodeURIComponent(s) 165 | s = strings.Replace(s, "!", "%21", -1) 166 | s = strings.Replace(s, "'", "%27", -1) 167 | s = strings.Replace(s, "(", "%28", -1) 168 | s = strings.Replace(s, ")", "%29", -1) 169 | s = strings.Replace(s, "*", "%2A", -1) 170 | return s 171 | } 172 | 173 | type valuesForSign map[string][]string 174 | 175 | func (vs valuesForSign) Add(key, value string) { 176 | key = strings.ToLower(key) 177 | vs[key] = append(vs[key], value) 178 | } 179 | 180 | // https://cloud.tencent.com/document/product/436/7778 181 | // https://github.com/tencentyun/cos-nodejs-sdk-v5/blob/a1dad3e9e3776cd24c97975f3aa47631e5001ff0/sdk/util.js#L42-L69 182 | func (vs valuesForSign) Encode() string { 183 | var keys []string 184 | for k := range vs { 185 | keys = append(keys, k) 186 | } 187 | // 字典序排序 188 | sort.Strings(keys) 189 | 190 | var pairs []string 191 | for _, k := range keys { 192 | items := vs[k] 193 | sort.Strings(items) 194 | for _, v := range items { 195 | pairs = append( 196 | pairs, 197 | fmt.Sprintf("%s=%s", camSafeURLEncode(k), camSafeURLEncode(v))) 198 | } 199 | } 200 | // =&= 201 | return strings.Join(pairs, "&") 202 | } 203 | 204 | // genFormatParameters 生成 FormatParameters 和 SignedParameterList 205 | func genFormatParameters(parameters url.Values) (formatParameters string, signedParameterList []string) { 206 | ps := valuesForSign{} 207 | for key, values := range parameters { 208 | key = strings.ToLower(key) 209 | for _, value := range values { 210 | ps.Add(key, value) 211 | signedParameterList = append(signedParameterList, key) 212 | } 213 | } 214 | 215 | formatParameters = ps.Encode() 216 | sort.Strings(signedParameterList) 217 | return 218 | } 219 | 220 | // genFormatHeaders 生成 FormatHeaders 和 SignedHeaderList 221 | func genFormatHeaders(headers http.Header) (formatHeaders string, signedHeaderList []string) { 222 | hs := valuesForSign{} 223 | for key, values := range headers { 224 | key = strings.ToLower(key) 225 | for _, value := range values { 226 | if isSignHeader(key) { 227 | hs.Add(key, value) 228 | signedHeaderList = append(signedHeaderList, key) 229 | } 230 | } 231 | } 232 | 233 | formatHeaders = hs.Encode() 234 | sort.Strings(signedHeaderList) 235 | return 236 | } 237 | 238 | // HMAC 签名 239 | func calHMACDigest(key, msg, signMethod string) []byte { 240 | var hashFunc func() hash.Hash 241 | switch signMethod { 242 | case "sha1": 243 | hashFunc = sha1.New 244 | default: 245 | hashFunc = sha1.New 246 | } 247 | h := hmac.New(hashFunc, []byte(key)) 248 | h.Write([]byte(msg)) 249 | return h.Sum(nil) 250 | } 251 | 252 | func isSignHeader(key string) bool { 253 | for k, v := range needSignHeaders { 254 | if key == k && v { 255 | return true 256 | } 257 | } 258 | return strings.HasPrefix(key, privateHeaderPrefix) 259 | } 260 | 261 | // Auth 签名相关的认证信息 262 | type Auth struct { 263 | SecretID string 264 | SecretKey string 265 | // 签名多久过期,默认是 time.Hour 266 | Expire time.Duration 267 | } 268 | 269 | // AuthorizationTransport 给请求增加 Authorization header 270 | type AuthorizationTransport struct { 271 | SecretID string 272 | SecretKey string 273 | // 临时密钥: https://cloud.tencent.com/document/product/436/14048 274 | SessionToken string 275 | // 签名多久过期,默认是 time.Hour 276 | Expire time.Duration 277 | 278 | // 实际发送 http 请求的 http.RoundTripper,默认使用 http.DefaultTransport 279 | Transport http.RoundTripper 280 | } 281 | 282 | // RoundTrip implements the RoundTripper interface. 283 | func (t *AuthorizationTransport) RoundTrip(req *http.Request) (*http.Response, error) { 284 | // 使用预签名授权 URL 时跳过添加 Authorization header 的步骤 285 | if req.URL.Query().Get("sign") == "" { 286 | req = cloneRequest(req) // per RoundTrip contract 287 | 288 | // 增加 Authorization header 289 | authTime := NewAuthTime(t.Expire) 290 | AddAuthorizationHeader(t.SecretID, t.SecretKey, req, authTime) 291 | if t.SessionToken != "" { 292 | req.Header.Set("x-cos-security-token", t.SessionToken) 293 | } 294 | } 295 | 296 | resp, err := t.transport().RoundTrip(req) 297 | return resp, err 298 | } 299 | 300 | func (t *AuthorizationTransport) transport() http.RoundTripper { 301 | if t.Transport != nil { 302 | return t.Transport 303 | } 304 | return http.DefaultTransport 305 | } 306 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestNewAuthorization(t *testing.T) { 14 | expectAuthorization := `q-sign-algorithm=sha1&q-ak=QmFzZTY0IGlzIGEgZ2VuZXJp&q-sign-time=1480932292;1481012292&q-key-time=1480932292;1481012292&q-header-list=host;x-cos-content-sha1;x-cos-stroage-class&q-url-param-list=&q-signature=91f7814df035319aa08d47e5a7a66ea989d57301` 15 | secretID := "QmFzZTY0IGlzIGEgZ2VuZXJp" 16 | secretKey := "AKIDZfbOA78asKUYBcXFrJD0a1ICvR98JM" 17 | host := "testbucket-125000000.cos.ap-beijing-1.myqcloud.com" 18 | uri := "https://testbucket-125000000.cos.ap-beijing-1.myqcloud.com/testfile2" 19 | startTime := time.Unix(int64(1480932292), 0) 20 | endTime := time.Unix(int64(1481012292), 0) 21 | 22 | req, _ := http.NewRequest("PUT", uri, nil) 23 | req.Header.Add("Host", host) 24 | req.Header.Add("x-cos-content-sha1", "db8ac1c259eb89d4a131b253bacfca5f319d54f2") 25 | req.Header.Add("x-cos-stroage-class", "nearline") 26 | 27 | authTime := &AuthTime{ 28 | SignStartTime: startTime, 29 | SignEndTime: endTime, 30 | KeyStartTime: startTime, 31 | KeyEndTime: endTime, 32 | } 33 | auth := newAuthorization(Auth{ 34 | SecretID: secretID, 35 | SecretKey: secretKey, 36 | }, req, *authTime) 37 | 38 | if auth != expectAuthorization { 39 | t.Errorf("NewAuthorization returned \n%#v, want \n%#v", auth, expectAuthorization) 40 | } 41 | } 42 | 43 | func TestAuthorizationTransport(t *testing.T) { 44 | setup() 45 | defer teardown() 46 | 47 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 48 | auth := r.Header.Get("Authorization") 49 | if auth == "" { 50 | t.Error("AuthorizationTransport didn't add Authorization header") 51 | } 52 | }) 53 | 54 | (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{} 55 | req, _ := http.NewRequest("GET", client.BaseURL.BucketURL.String(), nil) 56 | req.Header.Set("X-Testing", "0") 57 | client.doAPI(context.Background(), Caller{}, req, nil, true) 58 | } 59 | 60 | func TestAuthorizationTransportWithSessionToken(t *testing.T) { 61 | setup() 62 | defer teardown() 63 | 64 | sessionToken := "CxQQbwSzzX5obZm23yEcyQtpROuDB0Q60d322a47737c8241991d12dc4b8387c7J6NL50eH1BYN6VnFYB_Ml6oPZzUxz5wxDGVvvgxZXr1m-4HvmkvmMH4YB02XdVPapKp7oGnrMous2jsSTALo4iU2fuRclbVw-czYwggSxuNxXAwmqcT1HpD3h3zc3e24sryIhJKqzSOczQZjtGrxSSQ4K23o9Mx8VHgrosliU0aIiI2KFhxJhij03SzDDOQcBAwpFZyM0NvpOdN6b14yJbrt9bAzYGNjX-PeU3MXfi0" 65 | 66 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 67 | auth := r.Header.Get("Authorization") 68 | if auth == "" { 69 | t.Error("AuthorizationTransport didn't add Authorization header") 70 | } 71 | token := r.Header.Get("x-cos-security-token") 72 | if token == "" { 73 | t.Error("AuthorizationTransport didn't add x-cos-security-token header") 74 | } 75 | if token != sessionToken { 76 | t.Errorf("AuthorizationTransport didn't add expected x-cos-security-token header, expected: %s, got: %s", sessionToken, token) 77 | } 78 | }) 79 | 80 | (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{ 81 | SecretID: "233", 82 | SecretKey: "666", 83 | SessionToken: sessionToken, 84 | } 85 | req, _ := http.NewRequest("GET", client.BaseURL.BucketURL.String(), nil) 86 | req.Header.Set("X-Testing", "0") 87 | client.doAPI(context.Background(), Caller{}, req, nil, true) 88 | } 89 | 90 | func TestAuthorizationTransport_skip_PresignedURL(t *testing.T) { 91 | setup() 92 | defer teardown() 93 | 94 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 95 | _, exist := r.Header["Authorization"] 96 | if exist { 97 | t.Error("AuthorizationTransport add Authorization header when use PresignedURL") 98 | } 99 | }) 100 | 101 | (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{} 102 | 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" 103 | u := fmt.Sprintf("%s?sign=%s", client.BaseURL.BucketURL.String(), sign) 104 | req, _ := http.NewRequest("GET", u, nil) 105 | client.doAPI(context.Background(), Caller{}, req, nil, true) 106 | } 107 | 108 | func TestAuthorizationTransport_with_another_transport(t *testing.T) { 109 | setup() 110 | defer teardown() 111 | 112 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 113 | auth := r.Header.Get("Authorization") 114 | if auth == "" { 115 | t.Error("AuthorizationTransport didn't add Authorization header") 116 | } 117 | }) 118 | 119 | tr := &testingTransport{} 120 | (client.Sender).(*DefaultSender).Transport = &AuthorizationTransport{ 121 | Transport: tr, 122 | } 123 | req, _ := http.NewRequest("GET", client.BaseURL.BucketURL.String(), nil) 124 | req.Header.Set("X-Testing", "0") 125 | client.doAPI(context.Background(), Caller{}, req, nil, true) 126 | if tr.called != 1 { 127 | t.Error("AuthorizationTransport not call another Transport") 128 | } 129 | } 130 | 131 | type testingTransport struct { 132 | called int 133 | } 134 | 135 | func (t *testingTransport) RoundTrip(req *http.Request) (*http.Response, error) { 136 | t.called++ 137 | return http.DefaultTransport.RoundTrip(req) 138 | } 139 | 140 | func Test_camSafeURLEncode(t *testing.T) { 141 | type args struct { 142 | s string 143 | } 144 | tests := []struct { 145 | name string 146 | args args 147 | want string 148 | }{ 149 | { 150 | name: "no replace", 151 | args: args{"1234 +abc0AB#@"}, 152 | want: "1234%20%2Babc0AB%23%40", 153 | }, 154 | { 155 | name: "replace", 156 | args: args{"1234 +abc0AB#@,!'()*"}, 157 | want: "1234%20%2Babc0AB%23%40%2C%21%27%28%29%2A", 158 | }, 159 | } 160 | for _, tt := range tests { 161 | t.Run(tt.name, func(t *testing.T) { 162 | if got := camSafeURLEncode(tt.args.s); got != tt.want { 163 | t.Errorf("camSafeURLEncode() = %v, want %v", got, tt.want) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | func Test_valuesForSign_Encode(t *testing.T) { 170 | tests := []struct { 171 | name string 172 | vs valuesForSign 173 | want string 174 | }{ 175 | { 176 | name: "test escape", 177 | vs: valuesForSign{ 178 | "test+233": {"value 666"}, 179 | "test+234": {"value 667"}, 180 | }, 181 | want: "test%2B233=value%20666&test%2B234=value%20667", 182 | }, 183 | { 184 | name: "test order", 185 | vs: valuesForSign{ 186 | "test_233": {"value_666"}, 187 | "233": {"value_2"}, 188 | "test_666": {"value_123"}, 189 | }, 190 | want: "233=value_2&test_233=value_666&test_666=value_123", 191 | }, 192 | } 193 | for _, tt := range tests { 194 | t.Run(tt.name, func(t *testing.T) { 195 | if got := tt.vs.Encode(); got != tt.want { 196 | t.Errorf("valuesForSign.Encode() = %v, want %v", got, tt.want) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func Test_valuesForSign_Add(t *testing.T) { 203 | type args struct { 204 | key string 205 | value string 206 | } 207 | tests := []struct { 208 | name string 209 | vs valuesForSign 210 | args args 211 | want valuesForSign 212 | }{ 213 | { 214 | name: "add new key", 215 | vs: valuesForSign{}, 216 | args: args{"test_key", "value_233"}, 217 | want: valuesForSign{"test_key": {"value_233"}}, 218 | }, 219 | { 220 | name: "extend key", 221 | vs: valuesForSign{"test_key": {"value_233"}}, 222 | args: args{"test_key", "value_666"}, 223 | want: valuesForSign{"test_key": {"value_233", "value_666"}}, 224 | }, 225 | { 226 | name: "key to lower(add)", 227 | vs: valuesForSign{}, 228 | args: args{"TEST_KEY", "value_233"}, 229 | want: valuesForSign{"test_key": {"value_233"}}, 230 | }, 231 | { 232 | name: "key to lower(extend)", 233 | vs: valuesForSign{"test_key": {"value_233"}}, 234 | args: args{"TEST_KEY", "value_666"}, 235 | want: valuesForSign{"test_key": {"value_233", "value_666"}}, 236 | }, 237 | } 238 | for _, tt := range tests { 239 | t.Run(tt.name, func(t *testing.T) { 240 | tt.vs.Add(tt.args.key, tt.args.value) 241 | if !reflect.DeepEqual(tt.vs, tt.want) { 242 | t.Errorf("%v, want %v", tt.vs, tt.want) 243 | } 244 | }) 245 | } 246 | } 247 | 248 | func Test_genFormatParameters(t *testing.T) { 249 | type args struct { 250 | parameters url.Values 251 | } 252 | tests := []struct { 253 | name string 254 | args args 255 | wantFormatParameters string 256 | wantSignedParameterList []string 257 | }{ 258 | { 259 | name: "test order", 260 | args: args{url.Values{ 261 | "test_key_233": {"666"}, 262 | "233": {"222"}, 263 | "test_key_2": {"value"}, 264 | }}, 265 | wantFormatParameters: "233=222&test_key_2=value&test_key_233=666", 266 | wantSignedParameterList: []string{"233", "test_key_2", "test_key_233"}, 267 | }, 268 | { 269 | name: "test escape", 270 | args: args{url.Values{ 271 | "Test+key": {"666 value"}, 272 | "233 666": {"22+2"}, 273 | }}, 274 | wantFormatParameters: "233%20666=22%2B2&test%2Bkey=666%20value", 275 | wantSignedParameterList: []string{"233 666", "test+key"}, 276 | }, 277 | } 278 | for _, tt := range tests { 279 | t.Run(tt.name, func(t *testing.T) { 280 | gotFormatParameters, gotSignedParameterList := genFormatParameters(tt.args.parameters) 281 | if gotFormatParameters != tt.wantFormatParameters { 282 | t.Errorf("genFormatParameters() gotFormatParameters = %v, want %v", gotFormatParameters, tt.wantFormatParameters) 283 | } 284 | if !reflect.DeepEqual(gotSignedParameterList, tt.wantSignedParameterList) { 285 | t.Errorf("genFormatParameters() gotSignedParameterList = %v, want %v", gotSignedParameterList, tt.wantSignedParameterList) 286 | } 287 | }) 288 | } 289 | } 290 | 291 | func Test_genFormatHeaders(t *testing.T) { 292 | type args struct { 293 | headers http.Header 294 | } 295 | tests := []struct { 296 | name string 297 | args args 298 | wantFormatHeaders string 299 | wantSignedHeaderList []string 300 | }{ 301 | { 302 | name: "test order", 303 | args: args{http.Header{ 304 | "host": {"example.com"}, 305 | "content-length": {"22"}, 306 | "content-md5": {"xxx222"}, 307 | }}, 308 | wantFormatHeaders: "content-length=22&content-md5=xxx222&host=example.com", 309 | wantSignedHeaderList: []string{"content-length", "content-md5", "host"}, 310 | }, 311 | { 312 | name: "test escape", 313 | args: args{http.Header{ 314 | "host": {"example.com"}, 315 | "content-length": {"22"}, 316 | "Content-Disposition": {"attachment; filename=hello - world!(+).go"}, 317 | }}, 318 | wantFormatHeaders: "content-disposition=attachment%3B%20filename%3Dhello%20-%20world%21%28%2B%29.go&content-length=22&host=example.com", 319 | wantSignedHeaderList: []string{"content-disposition", "content-length", "host"}, 320 | }, 321 | { 322 | name: "test skip key", 323 | args: args{http.Header{ 324 | "Host": {"example.com"}, 325 | "content-length": {"22"}, 326 | "x-cos-xyz": {"lala"}, 327 | "Content-Type": {"text/html"}, 328 | }}, 329 | wantFormatHeaders: "content-length=22&host=example.com&x-cos-xyz=lala", 330 | wantSignedHeaderList: []string{"content-length", "host", "x-cos-xyz"}, 331 | }, 332 | } 333 | for _, tt := range tests { 334 | t.Run(tt.name, func(t *testing.T) { 335 | gotFormatHeaders, gotSignedHeaderList := genFormatHeaders(tt.args.headers) 336 | if gotFormatHeaders != tt.wantFormatHeaders { 337 | t.Errorf("genFormatHeaders() gotFormatHeaders = %v, want %v", gotFormatHeaders, tt.wantFormatHeaders) 338 | } 339 | if !reflect.DeepEqual(gotSignedHeaderList, tt.wantSignedHeaderList) { 340 | t.Errorf("genFormatHeaders() gotSignedHeaderList = %v, want %v", gotSignedHeaderList, tt.wantSignedHeaderList) 341 | } 342 | }) 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /bucket.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // BucketService ... 10 | // 11 | // Bucket 相关 API 12 | type BucketService service 13 | 14 | // BucketGetResult 响应结果 15 | // 16 | // https://cloud.tencent.com/document/product/436/7734 17 | type BucketGetResult struct { 18 | XMLName xml.Name `xml:"ListBucketResult"` 19 | // 说明 Bucket 的信息 20 | Name string 21 | // 前缀匹配,用来规定响应请求返回的文件前缀地址 22 | Prefix string `xml:"Prefix,omitempty"` 23 | // 默认以 UTF-8 二进制顺序列出条目,所有列出条目从 marker 开始 24 | Marker string `xml:"Marker,omitempty"` 25 | // 假如返回条目被截断,则返回 NextMarker 就是下一个条目的起点 26 | NextMarker string `xml:"NextMarker,omitempty"` 27 | // 定界符,见 BucketGetOptions.Delimiter 28 | Delimiter string `xml:"Delimiter,omitempty"` 29 | // 单次响应请求内返回结果的最大的条目数量 30 | MaxKeys int 31 | // 响应请求条目是否被截断,布尔值:true,false 32 | IsTruncated bool 33 | // 元数据信息 34 | Contents []Object `xml:"Contents,omitempty"` 35 | // 将 Prefix 到 delimiter 之间的相同路径归为一类,定义为 Common Prefix 36 | CommonPrefixes []string `xml:"CommonPrefixes>Prefix,omitempty"` 37 | // 编码格式 38 | EncodingType string `xml:"Encoding-Type,omitempty"` 39 | } 40 | 41 | // BucketGetOptions 请求参数 42 | // 43 | // https://cloud.tencent.com/document/product/436/7734 44 | type BucketGetOptions struct { 45 | // 前缀匹配,用来规定返回的文件前缀地址 46 | Prefix string `url:"prefix,omitempty"` 47 | // 定界符为一个符号,如果有 Prefix,则将 Prefix 到 delimiter 之间的相同路径归为一类, 48 | // 定义为 Common Prefix,然后列出所有 Common Prefix。如果没有 Prefix,则从路径起点开始 49 | Delimiter string `url:"delimiter,omitempty"` 50 | // 规定返回值的编码方式,可选值:url 51 | EncodingType string `url:"encoding-type,omitempty"` 52 | // 默认以 UTF-8 二进制顺序列出条目,所有列出条目从 marker 开始 53 | Marker string `url:"marker,omitempty"` 54 | // 单次返回最大的条目数量,默认 1000 55 | MaxKeys int `url:"max-keys,omitempty"` 56 | } 57 | 58 | // MethodBucketGet method name of Bucket.Get 59 | const MethodBucketGet MethodName = "Bucket.Get" 60 | 61 | // Get Bucket 请求等同于 List Object请求,可以列出该 Bucket 下的部分或者全部 Object。 62 | // 此 API 调用者需要对 Bucket 有 Read 权限。 63 | // 64 | // https://cloud.tencent.com/document/product/436/7734 65 | func (s *BucketService) Get(ctx context.Context, opt *BucketGetOptions) (*BucketGetResult, *Response, error) { 66 | var res BucketGetResult 67 | sendOpt := sendOptions{ 68 | baseURL: s.client.BaseURL.BucketURL, 69 | uri: "/", 70 | method: http.MethodGet, 71 | optQuery: opt, 72 | result: &res, 73 | caller: Caller{ 74 | Method: MethodBucketGet, 75 | }, 76 | } 77 | resp, err := s.client.send(ctx, &sendOpt) 78 | return &res, resp, err 79 | } 80 | 81 | // BucketPutOptions ... 82 | type BucketPutOptions ACLHeaderOptions 83 | 84 | // MethodBucketPut method name of Bucket.Put 85 | const MethodBucketPut MethodName = "Bucket.Put" 86 | 87 | // Put Bucket 接口请求可以在指定账号下创建一个 Bucket。该 API 接口不支持匿名请求, 88 | // 您需要使用帯 Authorization 签名认证的请求才能创建新的 Bucket 。 89 | // 创建 Bucket 的用户默认成为 Bucket 的持有者。 90 | // 91 | // 细节分析 92 | // 93 | // 创建 Bucket 时,如果没有指定访问权限,则默认使用私有读写(private)权限。 94 | // 95 | // https://cloud.tencent.com/document/product/436/7738 96 | func (s *BucketService) Put(ctx context.Context, opt *BucketPutOptions) (*Response, error) { 97 | sendOpt := sendOptions{ 98 | baseURL: s.client.BaseURL.BucketURL, 99 | uri: "/", 100 | method: http.MethodPut, 101 | optHeader: opt, 102 | caller: Caller{ 103 | Method: MethodBucketPut, 104 | }, 105 | } 106 | resp, err := s.client.send(ctx, &sendOpt) 107 | return resp, err 108 | } 109 | 110 | // MethodBucketDelete method name of Bucket.Delete 111 | const MethodBucketDelete MethodName = "Bucket.Delete" 112 | 113 | // Delete Bucket 请求可以确认该 Bucket 是否存在,是否有权限访问。HEAD 的权限与 Read 一致。 114 | // 当该 Bucket 存在时,返回 HTTP 状态码 200;当该 Bucket 无访问权限时,返回 HTTP 状态码 403; 115 | // 当该 Bucket 不存在时,返回 HTTP 状态码 404。 116 | // 117 | // 注意: 目前还没有公开获取 Bucket 属性的接口(即可以返回 acl 等信息)。 118 | // 119 | // https://cloud.tencent.com/document/product/436/7735 120 | func (s *BucketService) Delete(ctx context.Context) (*Response, error) { 121 | sendOpt := sendOptions{ 122 | baseURL: s.client.BaseURL.BucketURL, 123 | uri: "/", 124 | method: http.MethodDelete, 125 | caller: Caller{ 126 | Method: MethodBucketDelete, 127 | }, 128 | } 129 | resp, err := s.client.send(ctx, &sendOpt) 130 | return resp, err 131 | } 132 | 133 | // MethodBucketHead method name of Bucket.Head 134 | const MethodBucketHead MethodName = "Bucket.Head" 135 | 136 | // Head Bucket请求可以确认是否存在该Bucket,是否有权限访问,Head的权限与Read一致。 137 | // 138 | // 当其存在时,返回 HTTP 状态码200; 139 | // 当无权限时,返回 HTTP 状态码403; 140 | // 当不存在时,返回 HTTP 状态码404。 141 | // 142 | // https://www.qcloud.com/document/product/436/7735 143 | func (s *BucketService) Head(ctx context.Context) (*Response, error) { 144 | sendOpt := sendOptions{ 145 | baseURL: s.client.BaseURL.BucketURL, 146 | uri: "/", 147 | method: http.MethodHead, 148 | caller: Caller{ 149 | Method: MethodBucketHead, 150 | }, 151 | } 152 | resp, err := s.client.send(ctx, &sendOpt) 153 | return resp, err 154 | } 155 | 156 | // Bucket ... 157 | type Bucket struct { 158 | Name string 159 | AppID string `xml:",omitempty"` 160 | Region string `xml:"Location,omitempty"` 161 | CreateDate string `xml:"CreationDate,omitempty"` 162 | } 163 | -------------------------------------------------------------------------------- /bucket_acl.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // BucketGetACLResult ... 9 | // 10 | // https://cloud.tencent.com/document/product/436/7733 11 | type BucketGetACLResult ACLXml 12 | 13 | // MethodBucketGetACL method name of Bucket.GetACL 14 | const MethodBucketGetACL MethodName = "Bucket.GetACL" 15 | 16 | // GetACL 接口用来获取存储桶的访问权限控制列表。 17 | // 18 | // https://cloud.tencent.com/document/product/436/7733 19 | func (s *BucketService) GetACL(ctx context.Context) (*BucketGetACLResult, *Response, error) { 20 | var res BucketGetACLResult 21 | sendOpt := sendOptions{ 22 | baseURL: s.client.BaseURL.BucketURL, 23 | uri: "/?acl", 24 | method: http.MethodGet, 25 | result: &res, 26 | caller: Caller{ 27 | Method: MethodBucketGetACL, 28 | }, 29 | } 30 | resp, err := s.client.send(ctx, &sendOpt) 31 | return &res, resp, err 32 | } 33 | 34 | // BucketPutACLOptions ... 35 | // Header 和 Body 二选一 36 | type BucketPutACLOptions struct { 37 | Header *ACLHeaderOptions `url:"-" xml:"-"` 38 | Body *ACLXml `url:"-" header:"-"` 39 | } 40 | 41 | // MethodBucketPutACL method name of Bucket.PutACL 42 | const MethodBucketPutACL MethodName = "Bucket.PutACL" 43 | 44 | // PutACL 使用API写入Bucket的ACL表 45 | // 46 | // Put Bucket ACL 是一个覆盖操作,传入新的ACL将覆盖原有ACL。只有所有者有权操作。 47 | // 48 | // 私有 Bucket 可以下可以给某个文件夹设置成公有,那么该文件夹下的文件都是公有; 49 | // 但是把文件夹设置成私有后,在该文件夹中设置的公有属性,不会生效。 50 | // 51 | // https://cloud.tencent.com/document/product/436/7737 52 | func (s *BucketService) PutACL(ctx context.Context, opt *BucketPutACLOptions) (*Response, error) { 53 | header := opt.Header 54 | body := opt.Body 55 | if body != nil { 56 | header = nil 57 | } 58 | sendOpt := sendOptions{ 59 | baseURL: s.client.BaseURL.BucketURL, 60 | uri: "/?acl", 61 | method: http.MethodPut, 62 | body: body, 63 | optHeader: header, 64 | caller: Caller{ 65 | Method: MethodBucketPutACL, 66 | }, 67 | } 68 | resp, err := s.client.send(ctx, &sendOpt) 69 | return resp, err 70 | } 71 | -------------------------------------------------------------------------------- /bucket_acl_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_GetACL(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, "GET") 18 | vs := values{ 19 | "acl": "", 20 | } 21 | testFormValues(t, r, vs) 22 | fmt.Fprint(w, ` 23 | 24 | qcs::cam::uin/100000760461:uin/100000760461 25 | qcs::cam::uin/100000760461:uin/100000760461 26 | 27 | 28 | 29 | 30 | qcs::cam::uin/100000760461:uin/100000760461 31 | qcs::cam::uin/100000760461:uin/100000760461 32 | 33 | FULL_CONTROL 34 | 35 | 36 | 37 | qcs::cam::uin/100000760461:uin/100000760461 38 | qcs::cam::uin/100000760461:uin/100000760461 39 | 40 | READ 41 | 42 | 43 | `) 44 | }) 45 | 46 | ref, _, err := client.Bucket.GetACL(context.Background()) 47 | if err != nil { 48 | t.Fatalf("Bucket.GetACL returned error: %v", err) 49 | } 50 | 51 | want := &BucketGetACLResult{ 52 | XMLName: xml.Name{Local: "AccessControlPolicy"}, 53 | Owner: &Owner{ 54 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 55 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 56 | }, 57 | AccessControlList: []ACLGrant{ 58 | { 59 | Grantee: &ACLGrantee{ 60 | Type: "RootAccount", 61 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 62 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 63 | }, 64 | Permission: "FULL_CONTROL", 65 | }, 66 | { 67 | Grantee: &ACLGrantee{ 68 | Type: "RootAccount", 69 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 70 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 71 | }, 72 | Permission: "READ", 73 | }, 74 | }, 75 | } 76 | 77 | if !reflect.DeepEqual(ref, want) { 78 | t.Errorf("Bucket.GetACL returned %+v, want %+v", ref, want) 79 | } 80 | 81 | } 82 | 83 | func TestBucketService_PutACL_with_header_opt(t *testing.T) { 84 | setup() 85 | defer teardown() 86 | 87 | opt := &BucketPutACLOptions{ 88 | Header: &ACLHeaderOptions{ 89 | XCosACL: "private", 90 | }, 91 | } 92 | 93 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 94 | 95 | testMethod(t, r, http.MethodPut) 96 | vs := values{ 97 | "acl": "", 98 | } 99 | testFormValues(t, r, vs) 100 | testHeader(t, r, "x-cos-acl", "private") 101 | 102 | want := 0 103 | v, _ := r.Body.Read([]byte{}) 104 | if !reflect.DeepEqual(v, want) { 105 | t.Errorf("Bucket.PutACL request body: %#v, want %#v", v, want) 106 | } 107 | }) 108 | 109 | _, err := client.Bucket.PutACL(context.Background(), opt) 110 | if err != nil { 111 | t.Fatalf("Bucket.PutACL returned error: %v", err) 112 | } 113 | 114 | } 115 | 116 | func TestBucketService_PutACL_with_body_opt(t *testing.T) { 117 | setup() 118 | defer teardown() 119 | 120 | opt := &BucketPutACLOptions{ 121 | Body: &ACLXml{ 122 | Owner: &Owner{ 123 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 124 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 125 | }, 126 | AccessControlList: []ACLGrant{ 127 | { 128 | Grantee: &ACLGrantee{ 129 | Type: "RootAccount", 130 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 131 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 132 | }, 133 | 134 | Permission: "FULL_CONTROL", 135 | }, 136 | { 137 | Grantee: &ACLGrantee{ 138 | Type: "RootAccount", 139 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 140 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 141 | }, 142 | Permission: "READ", 143 | }, 144 | }, 145 | }, 146 | } 147 | 148 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 149 | v := new(ACLXml) 150 | xml.NewDecoder(r.Body).Decode(v) 151 | 152 | testMethod(t, r, http.MethodPut) 153 | vs := values{ 154 | "acl": "", 155 | } 156 | testFormValues(t, r, vs) 157 | testHeader(t, r, "x-cos-acl", "") 158 | 159 | want := opt.Body 160 | want.XMLName = xml.Name{Local: "AccessControlPolicy"} 161 | if !reflect.DeepEqual(v, want) { 162 | t.Errorf("Bucket.PutACL request body: %+v, want %+v", v, want) 163 | } 164 | 165 | }) 166 | 167 | _, err := client.Bucket.PutACL(context.Background(), opt) 168 | if err != nil { 169 | t.Fatalf("Bucket.PutACL returned error: %v", err) 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /bucket_cors.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // BucketCORSRule ... 10 | // 11 | // https://cloud.tencent.com/document/product/436/8279 12 | type BucketCORSRule struct { 13 | // 配置规则的 ID 14 | ID string `xml:"ID,omitempty"` 15 | // 允许的 HTTP 操作,枚举值:GET,PUT,HEAD,POST,DELETE 16 | AllowedMethods []string `xml:"AllowedMethod"` 17 | // 允许的访问来源,支持通配符 * 格式为:协议://域名[:端口] 如:http://www.qq.com 18 | AllowedOrigins []string `xml:"AllowedOrigin"` 19 | // 在发送 OPTIONS 请求时告知服务端,接下来的请求可以使用哪些自定义的 HTTP 请求头部,支持通配符 * 20 | AllowedHeaders []string `xml:"AllowedHeader,omitempty"` 21 | // 设置 OPTIONS 请求得到结果的有效期 22 | MaxAgeSeconds int `xml:"MaxAgeSeconds,omitempty"` 23 | // 设置浏览器可以接收到的来自服务器端的自定义头部信息 24 | ExposeHeaders []string `xml:"ExposeHeader,omitempty"` 25 | } 26 | 27 | // BucketGetCORSResult ... 28 | // 29 | // https://cloud.tencent.com/document/product/436/8274 30 | type BucketGetCORSResult struct { 31 | XMLName xml.Name `xml:"CORSConfiguration"` 32 | // 说明跨域资源共享配置的所有信息,最多可以包含100条 CORSRule 33 | Rules []BucketCORSRule `xml:"CORSRule,omitempty"` 34 | } 35 | 36 | // MethodBucketGetCORS method name of Bucket.GetCORS 37 | const MethodBucketGetCORS MethodName = "Bucket.GetCORS" 38 | 39 | // GetCORS ... 40 | // 41 | // Get Bucket CORS 接口实现 Bucket 持有者在 Bucket 上进行跨域资源共享的信息配置。 42 | // (cors 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing))。 43 | // 默认情况下,Bucket 的持有者直接有权限使用该 API 接口,Bucket 持有者也可以将权限授予其他用户。 44 | // 45 | // https://cloud.tencent.com/document/product/436/8274 46 | func (s *BucketService) GetCORS(ctx context.Context) (*BucketGetCORSResult, *Response, error) { 47 | var res BucketGetCORSResult 48 | sendOpt := sendOptions{ 49 | baseURL: s.client.BaseURL.BucketURL, 50 | uri: "/?cors", 51 | method: http.MethodGet, 52 | result: &res, 53 | caller: Caller{ 54 | Method: MethodBucketGetCORS, 55 | }, 56 | } 57 | resp, err := s.client.send(ctx, &sendOpt) 58 | return &res, resp, err 59 | } 60 | 61 | // BucketPutCORSOptions ... 62 | // 63 | // https://cloud.tencent.com/document/product/436/8279 64 | type BucketPutCORSOptions struct { 65 | XMLName xml.Name `xml:"CORSConfiguration"` 66 | // 说明跨域资源共享配置的所有信息,最多可以包含 100 条 CORSRule 67 | Rules []BucketCORSRule `xml:"CORSRule,omitempty"` 68 | } 69 | 70 | // MethodBucketPutCORS method name of Bucket.PutCORS 71 | const MethodBucketPutCORS MethodName = "Bucket.PutCORS" 72 | 73 | // PutCORS ... 74 | // 75 | // Put Bucket CORS 接口用来请求设置 Bucket 的跨域资源共享权限,。 76 | // 默认情况下,Bucket 的持有者直接有权限使用该 API 接口,Bucket 持有者也可以将权限授予其他用户。 77 | // 78 | // https://cloud.tencent.com/document/product/436/8279 79 | func (s *BucketService) PutCORS(ctx context.Context, opt *BucketPutCORSOptions) (*Response, error) { 80 | sendOpt := sendOptions{ 81 | baseURL: s.client.BaseURL.BucketURL, 82 | uri: "/?cors", 83 | method: http.MethodPut, 84 | body: opt, 85 | caller: Caller{ 86 | Method: MethodBucketPutCORS, 87 | }, 88 | } 89 | resp, err := s.client.send(ctx, &sendOpt) 90 | return resp, err 91 | } 92 | 93 | // MethodBucketDeleteCORS method name of Bucket.DeleteCORS 94 | const MethodBucketDeleteCORS MethodName = "Bucket.DeleteCORS" 95 | 96 | // DeleteCORS ... 97 | // 98 | // Delete Bucket CORS 接口请求实现删除跨域访问配置信息。 99 | // 100 | // https://cloud.tencent.com/document/product/436/8283 101 | func (s *BucketService) DeleteCORS(ctx context.Context) (*Response, error) { 102 | sendOpt := sendOptions{ 103 | baseURL: s.client.BaseURL.BucketURL, 104 | uri: "/?cors", 105 | method: http.MethodDelete, 106 | caller: Caller{ 107 | Method: MethodBucketDeleteCORS, 108 | }, 109 | } 110 | resp, err := s.client.send(ctx, &sendOpt) 111 | return resp, err 112 | } 113 | -------------------------------------------------------------------------------- /bucket_cors_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_GetCORS(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, http.MethodGet) 18 | vs := values{ 19 | "cors": "", 20 | } 21 | testFormValues(t, r, vs) 22 | fmt.Fprint(w, ` 23 | 24 | 25 | http://www.qq.com 26 | PUT 27 | GET 28 | x-cos-meta-test 29 | x-cos-xx 30 | x-cos-meta-test1 31 | 500 32 | 33 | 34 | 1234 35 | http://www.baidu.com 36 | twitter.com 37 | PUT 38 | GET 39 | 500 40 | 41 | `) 42 | }) 43 | 44 | ref, _, err := client.Bucket.GetCORS(context.Background()) 45 | if err != nil { 46 | t.Fatalf("Bucket.GetCORS returned error: %v", err) 47 | } 48 | 49 | want := &BucketGetCORSResult{ 50 | XMLName: xml.Name{Local: "CORSConfiguration"}, 51 | Rules: []BucketCORSRule{ 52 | { 53 | AllowedOrigins: []string{"http://www.qq.com"}, 54 | AllowedMethods: []string{"PUT", "GET"}, 55 | AllowedHeaders: []string{"x-cos-meta-test", "x-cos-xx"}, 56 | MaxAgeSeconds: 500, 57 | ExposeHeaders: []string{"x-cos-meta-test1"}, 58 | }, 59 | { 60 | ID: "1234", 61 | AllowedOrigins: []string{"http://www.baidu.com", "twitter.com"}, 62 | AllowedMethods: []string{"PUT", "GET"}, 63 | MaxAgeSeconds: 500, 64 | }, 65 | }, 66 | } 67 | 68 | if !reflect.DeepEqual(ref, want) { 69 | t.Errorf("Bucket.GetLifecycle returned %+v, want %+v", ref, want) 70 | } 71 | } 72 | 73 | func TestBucketService_PutCORS(t *testing.T) { 74 | setup() 75 | defer teardown() 76 | 77 | opt := &BucketPutCORSOptions{ 78 | Rules: []BucketCORSRule{ 79 | { 80 | AllowedOrigins: []string{"http://www.qq.com"}, 81 | AllowedMethods: []string{"PUT", "GET"}, 82 | AllowedHeaders: []string{"x-cos-meta-test", "x-cos-xx"}, 83 | MaxAgeSeconds: 500, 84 | ExposeHeaders: []string{"x-cos-meta-test1"}, 85 | }, 86 | { 87 | ID: "1234", 88 | AllowedOrigins: []string{"http://www.baidu.com", "twitter.com"}, 89 | AllowedMethods: []string{"PUT", "GET"}, 90 | MaxAgeSeconds: 500, 91 | }, 92 | }, 93 | } 94 | 95 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 96 | v := new(BucketPutCORSOptions) 97 | xml.NewDecoder(r.Body).Decode(v) 98 | 99 | testMethod(t, r, http.MethodPut) 100 | vs := values{ 101 | "cors": "", 102 | } 103 | testFormValues(t, r, vs) 104 | 105 | want := opt 106 | want.XMLName = xml.Name{Local: "CORSConfiguration"} 107 | if !reflect.DeepEqual(v, want) { 108 | t.Errorf("Bucket.PutCORS request body: %+v, want %+v", v, want) 109 | } 110 | 111 | }) 112 | 113 | _, err := client.Bucket.PutCORS(context.Background(), opt) 114 | if err != nil { 115 | t.Fatalf("Bucket.PutCORS returned error: %v", err) 116 | } 117 | 118 | } 119 | 120 | func TestBucketService_DeleteCORS(t *testing.T) { 121 | setup() 122 | defer teardown() 123 | 124 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 125 | testMethod(t, r, http.MethodDelete) 126 | vs := values{ 127 | "cors": "", 128 | } 129 | testFormValues(t, r, vs) 130 | w.WriteHeader(http.StatusNoContent) 131 | }) 132 | 133 | _, err := client.Bucket.DeleteCORS(context.Background()) 134 | if err != nil { 135 | t.Fatalf("Bucket.DeleteCORS returned error: %v", err) 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /bucket_lifecycle.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // BucketLifecycleFilter ... 10 | type BucketLifecycleFilter struct { 11 | // 指定规则所适用的前缀。匹配前缀的对象受该规则影响,Prefix 最多只能有一个 12 | Prefix string `xml:"Prefix,omitempty"` 13 | And *BucketLifecycleFilter `xml:"And,omitempty"` 14 | } 15 | 16 | // BucketLifecycleExpiration ... 17 | type BucketLifecycleExpiration struct { 18 | // 指明规则对应的动作在何时操作 19 | Date string `xml:"Date,omitempty"` 20 | // 指明规则对应的动作在对象最后的修改日期过后多少天操作,该字段有效值为正整数 21 | Days int `xml:"Days,omitempty"` 22 | // 删除过期对象删除标记,枚举值 true,false 23 | ExpiredObjectDeleteMarker bool `xml:"ExpiredObjectDeleteMarker,omitempty"` 24 | // 指明规则对应的动作在对象变成非当前版本多少天后执行,该字段有效值是正整数 25 | // 只在作为 NoncurrentVersionExpiration 字段的值时有效 26 | NoncurrentDays int 27 | } 28 | 29 | // BucketLifecycleTransition ... 30 | type BucketLifecycleTransition struct { 31 | // 指明规则对应的动作在何时操作 32 | Date string `xml:"Date,omitempty"` 33 | // 指明规则对应的动作在对象最后的修改日期过后多少天操作,该字段有效值是非负整数 34 | Days int `xml:"Days,omitempty"` 35 | // 指定 Object 转储到的目标存储类型,枚举值: STANDARD_IA, ARCHIVE 36 | StorageClass string 37 | // 指明规则对应的动作在对象变成非当前版本多少天后执行,该字段有效值是非负整数 38 | // 只在作为 NoncurrentVersionTransition 字段的值时有效 39 | NoncurrentDays int 40 | } 41 | 42 | // BucketLifecycleAbortIncompleteMultipartUpload ... 43 | type BucketLifecycleAbortIncompleteMultipartUpload struct { 44 | // 指明分片上传开始后多少天内必须完成上传 45 | DaysAfterInitiation int `xml:"DaysAfterInitiation,omitempty"` 46 | } 47 | 48 | // BucketLifecycleRule ... 49 | // 50 | // https://cloud.tencent.com/document/product/436/8280 51 | type BucketLifecycleRule struct { 52 | // 用于唯一地标识规则,长度不能超过 255 个字符 53 | ID string `xml:"ID,omitempty"` 54 | // Filter 用于描述规则影响的 Object 集合 55 | Filter *BucketLifecycleFilter 56 | // 已废弃,改为使用 Filter 57 | Prefix string 58 | // 指明规则是否启用,枚举值:Enabled,Disabled 59 | Status string 60 | // 规则转换属性,对象何时转换为 Standard_IA 或 Archive 61 | Transition *BucketLifecycleTransition `xml:"Transition,omitempty"` 62 | // 规则过期属性 63 | Expiration *BucketLifecycleExpiration `xml:"Expiration,omitempty"` 64 | // 设置允许分片上传保持运行的最长时间 65 | AbortIncompleteMultipartUpload *BucketLifecycleAbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"` 66 | // 指明非当前版本对象何时过期 67 | NoncurrentVersionExpiration *BucketLifecycleExpiration `xml:"NoncurrentVersionExpiration,omitempty"` 68 | // 指明非当前版本对象何时转换为 STANDARD_IA 或 ARCHIVE 69 | NoncurrentVersionTransition *BucketLifecycleTransition `xml:"NoncurrentVersionTransition,omitempty"` 70 | } 71 | 72 | // BucketGetLifecycleResult ... 73 | // 74 | // https://cloud.tencent.com/document/product/436/8278 75 | type BucketGetLifecycleResult struct { 76 | XMLName xml.Name `xml:"LifecycleConfiguration"` 77 | Rules []BucketLifecycleRule `xml:"Rule,omitempty"` 78 | } 79 | 80 | // MethodBucketGetLifecycle method name of Bucket.GetLifecycle 81 | const MethodBucketGetLifecycle MethodName = "Bucket.GetLifecycle" 82 | 83 | // GetLifecycle ... 84 | // 85 | // Get Bucket Lifecycle 用来查询 Bucket 的生命周期配置。 86 | // 87 | // https://cloud.tencent.com/document/product/436/8278 88 | func (s *BucketService) GetLifecycle(ctx context.Context) (*BucketGetLifecycleResult, *Response, error) { 89 | var res BucketGetLifecycleResult 90 | sendOpt := sendOptions{ 91 | baseURL: s.client.BaseURL.BucketURL, 92 | uri: "/?lifecycle", 93 | method: http.MethodGet, 94 | result: &res, 95 | caller: Caller{ 96 | Method: MethodBucketGetLifecycle, 97 | }, 98 | } 99 | resp, err := s.client.send(ctx, &sendOpt) 100 | return &res, resp, err 101 | } 102 | 103 | // BucketPutLifecycleOptions ... 104 | type BucketPutLifecycleOptions struct { 105 | XMLName xml.Name `xml:"LifecycleConfiguration"` 106 | Rules []BucketLifecycleRule `xml:"Rule,omitempty"` 107 | } 108 | 109 | // MethodBucketPutLifecycle method name of Bucket.PutLifecycle 110 | const MethodBucketPutLifecycle MethodName = "Bucket.PutLifecycle" 111 | 112 | // PutLifecycle ... 113 | // 114 | // COS 支持用户以生命周期配置的方式来管理 Bucket 中 Object 的生命周期。 115 | // 生命周期配置包含一个或多个将应用于一组对象规则的规则集 (其中每个规则为 COS 定义一个操作)。 116 | // 117 | // 这些操作分为以下两种: 118 | // 119 | // 转换操作:定义对象转换为另一个存储类的时间。例如,您可以选择在对象创建 30 天后将其转换为低频存储(STANDARD_IA,适用于不常访问) 120 | // 存储类别。同时也支持将数据沉降到归档存储(Archive,成本更低,目前支持国内园区)。具体参数参见请求示例说明中 Transition 项。 121 | // 过期操作:指定 Object 的过期时间。COS 将会自动为用户删除过期的 Object。 122 | // 123 | // 细节分析 124 | // 125 | // PUT Bucket lifecycle 用于为 Bucket 创建一个新的生命周期配置。如果该 Bucket 已配置生命周期, 126 | // 使用该接口创建新的配置的同时则会覆盖原有的配置。 127 | // 128 | // https://cloud.tencent.com/document/product/436/8280 129 | func (s *BucketService) PutLifecycle(ctx context.Context, opt *BucketPutLifecycleOptions) (*Response, error) { 130 | sendOpt := sendOptions{ 131 | baseURL: s.client.BaseURL.BucketURL, 132 | uri: "/?lifecycle", 133 | method: http.MethodPut, 134 | body: opt, 135 | caller: Caller{ 136 | Method: MethodBucketPutLifecycle, 137 | }, 138 | } 139 | resp, err := s.client.send(ctx, &sendOpt) 140 | return resp, err 141 | } 142 | 143 | // MethodBucketDeleteLifecycle method name of Bucket.DeleteLifecycle 144 | const MethodBucketDeleteLifecycle MethodName = "Bucket.DeleteLifecycle" 145 | 146 | // DeleteLifecycle ... 147 | // 148 | // Delete Bucket Lifecycle 用来删除 Bucket 的生命周期配置。 149 | // 150 | // https://cloud.tencent.com/document/product/436/8284 151 | func (s *BucketService) DeleteLifecycle(ctx context.Context) (*Response, error) { 152 | sendOpt := sendOptions{ 153 | baseURL: s.client.BaseURL.BucketURL, 154 | uri: "/?lifecycle", 155 | method: http.MethodDelete, 156 | caller: Caller{ 157 | Method: MethodBucketDeleteLifecycle, 158 | }, 159 | } 160 | resp, err := s.client.send(ctx, &sendOpt) 161 | return resp, err 162 | } 163 | -------------------------------------------------------------------------------- /bucket_lifecycle_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_GetLifecycle(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, http.MethodGet) 18 | vs := values{ 19 | "lifecycle": "", 20 | } 21 | testFormValues(t, r, vs) 22 | fmt.Fprint(w, ` 23 | 24 | 1234 25 | test 26 | Enabled 27 | 28 | 10 29 | Standard 30 | 31 | 32 | 33 | 123422 34 | gg 35 | Disabled 36 | 37 | 10 38 | 39 | 40 | `) 41 | }) 42 | 43 | ref, _, err := client.Bucket.GetLifecycle(context.Background()) 44 | if err != nil { 45 | t.Fatalf("Bucket.GetLifecycle returned error: %v", err) 46 | } 47 | 48 | want := &BucketGetLifecycleResult{ 49 | XMLName: xml.Name{Local: "LifecycleConfiguration"}, 50 | Rules: []BucketLifecycleRule{ 51 | { 52 | ID: "1234", 53 | Prefix: "test", 54 | Status: "Enabled", 55 | Transition: &BucketLifecycleTransition{Days: 10, StorageClass: "Standard"}, 56 | }, 57 | { 58 | ID: "123422", 59 | Prefix: "gg", 60 | Status: "Disabled", 61 | Expiration: &BucketLifecycleExpiration{Days: 10}, 62 | }, 63 | }, 64 | } 65 | 66 | if !reflect.DeepEqual(ref, want) { 67 | t.Errorf("Bucket.GetLifecycle returned %+v, want %+v", ref, want) 68 | } 69 | } 70 | 71 | func TestBucketService_PutLifecycle(t *testing.T) { 72 | setup() 73 | defer teardown() 74 | 75 | opt := &BucketPutLifecycleOptions{ 76 | Rules: []BucketLifecycleRule{ 77 | { 78 | ID: "1234", 79 | Prefix: "test", 80 | Status: "Enabled", 81 | Transition: &BucketLifecycleTransition{Days: 10, StorageClass: "Standard"}, 82 | }, 83 | { 84 | ID: "123422", 85 | Prefix: "gg", 86 | Status: "Disabled", 87 | Expiration: &BucketLifecycleExpiration{Days: 10}, 88 | }, 89 | }, 90 | } 91 | 92 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 93 | v := new(BucketPutLifecycleOptions) 94 | xml.NewDecoder(r.Body).Decode(v) 95 | 96 | testMethod(t, r, http.MethodPut) 97 | vs := values{ 98 | "lifecycle": "", 99 | } 100 | testFormValues(t, r, vs) 101 | 102 | want := opt 103 | want.XMLName = xml.Name{Local: "LifecycleConfiguration"} 104 | if !reflect.DeepEqual(v, want) { 105 | t.Errorf("Bucket.PutLifecycle request body: %+v, want %+v", v, want) 106 | } 107 | 108 | }) 109 | 110 | _, err := client.Bucket.PutLifecycle(context.Background(), opt) 111 | if err != nil { 112 | t.Fatalf("Bucket.PutLifecycle returned error: %v", err) 113 | } 114 | 115 | } 116 | 117 | func TestBucketService_DeleteLifecycle(t *testing.T) { 118 | setup() 119 | defer teardown() 120 | 121 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 122 | testMethod(t, r, http.MethodDelete) 123 | vs := values{ 124 | "lifecycle": "", 125 | } 126 | testFormValues(t, r, vs) 127 | 128 | w.WriteHeader(http.StatusNoContent) 129 | }) 130 | 131 | _, err := client.Bucket.DeleteLifecycle(context.Background()) 132 | if err != nil { 133 | t.Fatalf("Bucket.DeleteLifecycle returned error: %v", err) 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /bucket_location.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // BucketGetLocationResult ... 10 | type BucketGetLocationResult struct { 11 | XMLName xml.Name `xml:"LocationConstraint"` 12 | // 说明 Bucket 所在地域,枚举值参见 可用地域[1] 文档,如:ap-beijing、ap-hongkong、eu-frankfurt 等 13 | // [1]: https://cloud.tencent.com/document/product/436/6224 14 | Location string `xml:",chardata"` 15 | } 16 | 17 | // MethodBucketGetLocation method name of Bucket.GetLocation 18 | const MethodBucketGetLocation MethodName = "Bucket.GetLocation" 19 | 20 | // GetLocation ... 21 | // 22 | // Get Bucket Location 接口用于获取 Bucket 所在的地域信息,该 GET 操作使用 location 参数返回 Bucket 所在的区域, 23 | // 只有 Bucket 持有者才有该 API 接口的操作权限。 24 | // 25 | // https://cloud.tencent.com/document/product/436/8275 26 | func (s *BucketService) GetLocation(ctx context.Context) (*BucketGetLocationResult, *Response, error) { 27 | var res BucketGetLocationResult 28 | sendOpt := sendOptions{ 29 | baseURL: s.client.BaseURL.BucketURL, 30 | uri: "/?location", 31 | method: http.MethodGet, 32 | result: &res, 33 | caller: Caller{ 34 | Method: MethodBucketGetLocation, 35 | }, 36 | } 37 | resp, err := s.client.send(ctx, &sendOpt) 38 | return &res, resp, err 39 | } 40 | -------------------------------------------------------------------------------- /bucket_location_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_GetLocation(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, "GET") 18 | vs := values{ 19 | "location": "", 20 | } 21 | testFormValues(t, r, vs) 22 | fmt.Fprint(w, ` 23 | cn-north`) 24 | }) 25 | 26 | ref, _, err := client.Bucket.GetLocation(context.Background()) 27 | if err != nil { 28 | t.Fatalf("Bucket.GetLocation returned error: %v", err) 29 | } 30 | 31 | want := &BucketGetLocationResult{ 32 | XMLName: xml.Name{Local: "LocationConstraint"}, 33 | Location: "cn-north", 34 | } 35 | 36 | if !reflect.DeepEqual(ref, want) { 37 | t.Errorf("Bucket.GetLocation returned %+v, want %+v", ref, want) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bucket_part.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // ListMultipartUploadsResult ... 10 | // 11 | // https://cloud.tencent.com/document/product/436/7736 12 | type ListMultipartUploadsResult struct { 13 | XMLName xml.Name `xml:"ListMultipartUploadsResult"` 14 | // 分块上传的目标 Bucket,由用户自定义字符串和系统生成appid数字串由中划线连接而成, 15 | // 如:mybucket-1250000000 16 | Bucket string `xml:"Bucket"` 17 | // 规定返回值的编码格式,合法值:url 18 | EncodingType string `xml:"Encoding-Type"` 19 | // 列出条目从该 key 值开始 20 | KeyMarker string 21 | // 列出条目从该 UploadId 值开始 22 | UploadIDMarker string `xml:"UploadIdMarker"` 23 | // 假如返回条目被截断,则返回 NextKeyMarker 就是下一个条目的起点 24 | NextKeyMarker string 25 | // 假如返回条目被截断,则返回 UploadId 就是下一个条目的起点 26 | NextUploadIDMarker string `xml:"NextUploadIdMarker"` 27 | // 设置最大返回的 multipart 数量,合法取值从 0 到 1000 28 | MaxUploads int 29 | // 返回条目是否被截断 30 | IsTruncated bool 31 | // Upload 列表 32 | Uploads []MultipartUpload `xml:"Upload,omitempty"` 33 | // 限定返回的 Object key 必须以 Prefix 作为前缀。 34 | // 注意使用 prefix 查询时,返回的 key 中仍会包含 Prefix 35 | Prefix string 36 | // 定界符为一个符号,对 object 名字包含指定前缀且第一次出现 delimiter 字符之间的 37 | // object 作为一组元素:common prefix。如果没有 prefix,则从路径起点开始 38 | Delimiter string `xml:"delimiter,omitempty"` 39 | // 将 prefix 到 delimiter 之间的相同路径归为一类,定义为 Common Prefix 40 | CommonPrefixes []string `xml:"CommonPrefixs>Prefix,omitempty"` 41 | } 42 | 43 | // ListMultipartUploadsOptions ... 44 | // 45 | // https://cloud.tencent.com/document/product/436/7736 46 | type ListMultipartUploadsOptions struct { 47 | // https://cloud.tencent.com/document/product/436/7736 48 | Delimiter string `url:"delimiter,omitempty"` 49 | // 规定返回值的编码格式,合法值:url 50 | EncodingType string `url:"encoding-type,omitempty"` 51 | // 限定返回的 Object key 必须以 Prefix 作为前缀。 52 | // 注意使用 prefix 查询时,返回的 key 中仍会包含 Prefix 53 | Prefix string `url:"prefix,omitempty"` 54 | // 设置最大返回的 multipart 数量,合法取值从1到1000,默认1000 55 | MaxUploads int `url:"max-uploads,omitempty"` 56 | // 与 UploadIDMarker 一起使用 57 | // 当 UploadIDMarker 未被指定时,ObjectName 字母顺序大于 KeyMarker 的条目将被列出 58 | // 当 UploadIDMarker 被指定时,ObjectName 字母顺序大于 KeyMarker 的条目被列出, 59 | // ObjectName 字母顺序等于 KeyMarker 同时 UploadID 大于 UploadIDMarker 的条目将被列出。 60 | KeyMarker string `url:"key-marker,omitempty"` 61 | UploadIDMarker string `url:"upload-id-marker,omitempty"` 62 | } 63 | 64 | // MethodBucketListMultipartUploads method name of Bucket.ListMultipartUploads 65 | const MethodBucketListMultipartUploads MethodName = "Bucket.ListMultipartUploads" 66 | 67 | // ListMultipartUploads ... 68 | // 69 | // List Multipart Uploads 用来查询正在进行中的分块上传。单次请求操作最多列出 1000 个正在进行中的分块上传。 70 | // 71 | // 注意:该请求需要有 Bucket 的读权限。 72 | // 73 | // https://cloud.tencent.com/document/product/436/7736 74 | func (s *BucketService) ListMultipartUploads(ctx context.Context, opt *ListMultipartUploadsOptions) (*ListMultipartUploadsResult, *Response, error) { 75 | var res ListMultipartUploadsResult 76 | sendOpt := sendOptions{ 77 | baseURL: s.client.BaseURL.BucketURL, 78 | uri: "/?uploads", 79 | method: http.MethodGet, 80 | result: &res, 81 | optQuery: opt, 82 | caller: Caller{ 83 | Method: MethodBucketListMultipartUploads, 84 | }, 85 | } 86 | resp, err := s.client.send(ctx, &sendOpt) 87 | return &res, resp, err 88 | } 89 | 90 | // MultipartUpload 每个 Multipart Upload 的信息 91 | type MultipartUpload struct { 92 | // Object 的名称 93 | Key string 94 | // 标示本次分块上传的 ID 95 | UploadID string `xml:"UploadID"` 96 | // 用来表示分块的存储级别,枚举值:STANDARD,STANDARD_IA,ARCHIVE 97 | StorageClass string 98 | // 用来表示本次上传发起者的信息 99 | Initiator *Initiator 100 | // 用来表示这些分块所有者的信息 101 | Owner *Owner 102 | // 分块上传的起始时间 103 | Initiated string 104 | } 105 | -------------------------------------------------------------------------------- /bucket_part_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_ListMultipartUploads(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, "GET") 18 | vs := values{ 19 | "uploads": "", 20 | "prefix": "t", 21 | } 22 | testFormValues(t, r, vs) 23 | fmt.Fprint(w, ` 24 | test-1253846586 25 | 26 | 27 | 28 | 1000 29 | t 30 | / 31 | false 32 | 33 | test/ 34 | 35 | 36 | test_multipart.txt 37 | 14972623850a5de3f4f10605ab9f339c8bdf1b77e06f03fb981e7e76c86554b7bdb6072b36 38 | 39 | 100000760461/100000760461 40 | 41 | 42 | 43 | 100000760461/100000760461 44 | 45 | 46 | STANDARD 47 | 2017-06-12T10:13:05.000Z 48 | 49 | 50 | test_multipar2t.txt 51 | 1497515958744e899fc341bfbb995ebd57b395f63930411d855aaac1b5cd7d834a15442831 52 | 53 | qcs::cam::uin/100000760461:uin/100000760461 54 | 100000760461 55 | 56 | 57 | qcs::cam::uin/100000760461:uin/100000760461 58 | 100000760461 59 | 60 | STANDARD 61 | 2017-06-15T08:39:18.000Z 62 | 63 | `) 64 | }) 65 | 66 | opt := &ListMultipartUploadsOptions{ 67 | Prefix: "t", 68 | } 69 | ref, _, err := client.Bucket.ListMultipartUploads(context.Background(), opt) 70 | if err != nil { 71 | t.Fatalf("Bucket.ListMultipartUploads returned error: %v", err) 72 | } 73 | 74 | want := &ListMultipartUploadsResult{ 75 | XMLName: xml.Name{Local: "ListMultipartUploadsResult"}, 76 | Bucket: "test-1253846586", 77 | MaxUploads: 1000, 78 | IsTruncated: false, 79 | Uploads: []MultipartUpload{ 80 | { 81 | Key: "test_multipart.txt", 82 | UploadID: "14972623850a5de3f4f10605ab9f339c8bdf1b77e06f03fb981e7e76c86554b7bdb6072b36", 83 | Initiator: &Initiator{ 84 | ID: "100000760461/100000760461", 85 | }, 86 | Owner: &Owner{ 87 | ID: "100000760461/100000760461", 88 | }, 89 | StorageClass: "STANDARD", 90 | Initiated: "2017-06-12T10:13:05.000Z", 91 | }, 92 | { 93 | Key: "test_multipar2t.txt", 94 | UploadID: "1497515958744e899fc341bfbb995ebd57b395f63930411d855aaac1b5cd7d834a15442831", 95 | Initiator: &Initiator{ 96 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 97 | DisplayName: "100000760461", 98 | }, 99 | Owner: &Owner{ 100 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 101 | DisplayName: "100000760461", 102 | }, 103 | StorageClass: "STANDARD", 104 | Initiated: "2017-06-15T08:39:18.000Z", 105 | }, 106 | }, 107 | Prefix: "t", 108 | CommonPrefixes: []string{"test/"}, 109 | } 110 | 111 | if !reflect.DeepEqual(ref, want) { 112 | t.Errorf("Bucket.ListMultipartUploads returned \n%+v, want \n%+v", ref, want) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /bucket_tagging.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // BucketTaggingTag ... 10 | type BucketTaggingTag struct { 11 | Key string 12 | Value string 13 | } 14 | 15 | // BucketGetTaggingResult ... 16 | type BucketGetTaggingResult struct { 17 | XMLName xml.Name `xml:"Tagging"` 18 | TagSet []BucketTaggingTag `xml:"TagSet>Tag,omitempty"` 19 | } 20 | 21 | // MethodGetTagging method name of Bucket.GetTagging 22 | const MethodGetTagging MethodName = "Bucket.GetTagging" 23 | 24 | // GetTagging ... 25 | // 26 | // Get Bucket Tagging接口实现获取指定Bucket的标签。 27 | // 28 | // https://www.qcloud.com/document/product/436/8277 29 | func (s *BucketService) GetTagging(ctx context.Context) (*BucketGetTaggingResult, *Response, error) { 30 | var res BucketGetTaggingResult 31 | sendOpt := sendOptions{ 32 | baseURL: s.client.BaseURL.BucketURL, 33 | uri: "/?tagging", 34 | method: http.MethodGet, 35 | result: &res, 36 | caller: Caller{ 37 | Method: MethodGetTagging, 38 | }, 39 | } 40 | resp, err := s.client.send(ctx, &sendOpt) 41 | return &res, resp, err 42 | } 43 | 44 | // BucketPutTaggingOptions ... 45 | type BucketPutTaggingOptions struct { 46 | XMLName xml.Name `xml:"Tagging"` 47 | TagSet []BucketTaggingTag `xml:"TagSet>Tag,omitempty"` 48 | } 49 | 50 | // MethodPutTagging method name of Bucket.PutTagging 51 | const MethodPutTagging MethodName = "Bucket.PutTagging" 52 | 53 | // PutTagging ... 54 | // 55 | // Put Bucket Tagging接口实现给用指定Bucket打标签。用来组织和管理相关Bucket。 56 | // 57 | // 当该请求设置相同Key名称,不同Value时,会返回400。请求成功,则返回204。 58 | // 59 | // https://www.qcloud.com/document/product/436/8281 60 | func (s *BucketService) PutTagging(ctx context.Context, opt *BucketPutTaggingOptions) (*Response, error) { 61 | sendOpt := sendOptions{ 62 | baseURL: s.client.BaseURL.BucketURL, 63 | uri: "/?tagging", 64 | method: http.MethodPut, 65 | body: opt, 66 | caller: Caller{ 67 | Method: MethodPutTagging, 68 | }, 69 | } 70 | resp, err := s.client.send(ctx, &sendOpt) 71 | return resp, err 72 | } 73 | 74 | // MethodDeleteTagging method name of Bucket.DeleteTagging 75 | const MethodDeleteTagging MethodName = "Bucket.DeleteTagging" 76 | 77 | // DeleteTagging ... 78 | // 79 | // Delete Bucket Tagging接口实现删除指定Bucket的标签。 80 | // 81 | // https://www.qcloud.com/document/product/436/8286 82 | func (s *BucketService) DeleteTagging(ctx context.Context) (*Response, error) { 83 | sendOpt := sendOptions{ 84 | baseURL: s.client.BaseURL.BucketURL, 85 | uri: "/?tagging", 86 | method: http.MethodDelete, 87 | caller: Caller{ 88 | Method: MethodDeleteTagging, 89 | }, 90 | } 91 | resp, err := s.client.send(ctx, &sendOpt) 92 | return resp, err 93 | } 94 | -------------------------------------------------------------------------------- /bucket_tagging_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_GetTagging(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, "GET") 18 | vs := values{ 19 | "tagging": "", 20 | } 21 | testFormValues(t, r, vs) 22 | fmt.Fprint(w, ` 23 | 24 | 25 | test_k2 26 | test_v2 27 | 28 | 29 | test_k3 30 | test_vv 31 | 32 | 33 | `) 34 | }) 35 | 36 | ref, _, err := client.Bucket.GetTagging(context.Background()) 37 | if err != nil { 38 | t.Fatalf("Bucket.GetTagging returned error: %v", err) 39 | } 40 | 41 | want := &BucketGetTaggingResult{ 42 | XMLName: xml.Name{Local: "Tagging"}, 43 | TagSet: []BucketTaggingTag{ 44 | {"test_k2", "test_v2"}, 45 | {"test_k3", "test_vv"}, 46 | }, 47 | } 48 | 49 | if !reflect.DeepEqual(ref, want) { 50 | t.Errorf("Bucket.GetTagging returned %+v, want %+v", ref, want) 51 | } 52 | } 53 | 54 | func TestBucketService_PutTagging(t *testing.T) { 55 | setup() 56 | defer teardown() 57 | 58 | opt := &BucketPutTaggingOptions{ 59 | TagSet: []BucketTaggingTag{ 60 | { 61 | Key: "test_k2", 62 | Value: "test_v2", 63 | }, 64 | { 65 | Key: "test_k3", 66 | Value: "test_v3", 67 | }, 68 | }, 69 | } 70 | 71 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 72 | v := new(BucketPutTaggingOptions) 73 | xml.NewDecoder(r.Body).Decode(v) 74 | 75 | testMethod(t, r, "PUT") 76 | vs := values{ 77 | "tagging": "", 78 | } 79 | testFormValues(t, r, vs) 80 | 81 | want := opt 82 | want.XMLName = xml.Name{Local: "Tagging"} 83 | if !reflect.DeepEqual(v, want) { 84 | t.Errorf("Bucket.PutTagging request body: %+v, want %+v", v, want) 85 | } 86 | 87 | }) 88 | 89 | _, err := client.Bucket.PutTagging(context.Background(), opt) 90 | if err != nil { 91 | t.Fatalf("Bucket.PutTagging returned error: %v", err) 92 | } 93 | 94 | } 95 | 96 | func TestBucketService_DeleteTagging(t *testing.T) { 97 | setup() 98 | defer teardown() 99 | 100 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 101 | testMethod(t, r, http.MethodDelete) 102 | vs := values{ 103 | "tagging": "", 104 | } 105 | testFormValues(t, r, vs) 106 | 107 | w.WriteHeader(http.StatusNoContent) 108 | }) 109 | 110 | _, err := client.Bucket.DeleteTagging(context.Background()) 111 | if err != nil { 112 | t.Fatalf("Bucket.DeleteTagging returned error: %v", err) 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /bucket_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestBucketService_Get(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | opt := &BucketGetOptions{ 17 | Prefix: "test", 18 | MaxKeys: 2, 19 | } 20 | 21 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 22 | testMethod(t, r, http.MethodGet) 23 | vs := values{ 24 | "prefix": "test", 25 | "max-keys": "2", 26 | } 27 | testFormValues(t, r, vs) 28 | 29 | fmt.Fprint(w, ` 30 | 31 | test-1253846586 32 | test 33 | 34 | 2 35 | true 36 | test/delete.txt 37 | 38 | test/ 39 | 2017-06-09T16:32:25.000Z 40 | "" 41 | 0 42 | 43 | 1253846586 44 | 45 | STANDARD 46 | 47 | 48 | test/anonymous_get.go 49 | 2017-06-17T15:09:26.000Z 50 | "5b7236085f08b3818bfa40b03c946dcc" 51 | 8 52 | 53 | 1253846586 54 | 55 | STANDARD 56 | 57 | `) 58 | }) 59 | 60 | ref, _, err := client.Bucket.Get(context.Background(), opt) 61 | if err != nil { 62 | t.Fatalf("Bucket.Get returned error: %v", err) 63 | } 64 | 65 | want := &BucketGetResult{ 66 | XMLName: xml.Name{Local: "ListBucketResult"}, 67 | Name: "test-1253846586", 68 | Prefix: "test", 69 | MaxKeys: 2, 70 | IsTruncated: true, 71 | NextMarker: "test/delete.txt", 72 | Contents: []Object{ 73 | { 74 | Key: "test/", 75 | LastModified: "2017-06-09T16:32:25.000Z", 76 | ETag: "\"\"", 77 | Size: 0, 78 | Owner: &Owner{ 79 | ID: "1253846586", 80 | }, 81 | StorageClass: "STANDARD", 82 | }, 83 | { 84 | Key: "test/anonymous_get.go", 85 | LastModified: "2017-06-17T15:09:26.000Z", 86 | ETag: "\"5b7236085f08b3818bfa40b03c946dcc\"", 87 | Size: 8, 88 | Owner: &Owner{ 89 | ID: "1253846586", 90 | }, 91 | StorageClass: "STANDARD", 92 | }, 93 | }, 94 | } 95 | 96 | if !reflect.DeepEqual(ref, want) { 97 | t.Errorf("Bucket.Get returned %+v, want %+v", ref, want) 98 | } 99 | } 100 | 101 | func TestBucketService_Put(t *testing.T) { 102 | setup() 103 | defer teardown() 104 | 105 | opt := &BucketPutOptions{ 106 | XCosACL: "public-read", 107 | } 108 | 109 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 110 | v := new(BucketPutTaggingOptions) 111 | xml.NewDecoder(r.Body).Decode(v) 112 | 113 | testMethod(t, r, "PUT") 114 | testHeader(t, r, "x-cos-acl", "public-read") 115 | }) 116 | 117 | _, err := client.Bucket.Put(context.Background(), opt) 118 | if err != nil { 119 | t.Fatalf("Bucket.Put returned error: %v", err) 120 | } 121 | 122 | } 123 | 124 | func TestBucketService_Delete(t *testing.T) { 125 | setup() 126 | defer teardown() 127 | 128 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 129 | testMethod(t, r, http.MethodDelete) 130 | w.WriteHeader(http.StatusNoContent) 131 | }) 132 | 133 | _, err := client.Bucket.Delete(context.Background()) 134 | if err != nil { 135 | t.Fatalf("Bucket.Delete returned error: %v", err) 136 | } 137 | } 138 | 139 | func TestBucketService_Head(t *testing.T) { 140 | setup() 141 | defer teardown() 142 | 143 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 144 | testMethod(t, r, http.MethodHead) 145 | w.WriteHeader(http.StatusOK) 146 | }) 147 | 148 | _, err := client.Bucket.Head(context.Background()) 149 | if err != nil { 150 | t.Fatalf("Bucket.Head returned error: %v", err) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /codelingo.yaml: -------------------------------------------------------------------------------- 1 | tenets: 2 | - import: codelingo/effective-go 3 | - import: codelingo/code-review-comments 4 | -------------------------------------------------------------------------------- /cos.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "encoding/xml" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "reflect" 14 | "strconv" 15 | "strings" 16 | "text/template" 17 | 18 | "github.com/google/go-querystring/query" 19 | "github.com/mozillazg/go-httpheader" 20 | ) 21 | 22 | const ( 23 | // Version ... 24 | Version = "0.13.0" 25 | userAgent = "go-cos/" + Version 26 | contentTypeXML = "application/xml" 27 | defaultServiceBaseURL = "https://service.cos.myqcloud.com" 28 | ) 29 | 30 | var bucketURLTemplate = template.Must( 31 | template.New("bucketURLFormat").Parse( 32 | "{{.Scheme}}://{{.BucketName}}-{{.AppID}}.cos.{{.Region}}.myqcloud.com", 33 | ), 34 | ) 35 | 36 | // BaseURL 访问各 API 所需的基础 URL 37 | type BaseURL struct { 38 | // 访问 bucket, object 相关 API 的基础 URL(不包含 path 部分) 39 | // 比如:https://test-1253846586.cos.ap-beijing.myqcloud.com 40 | // 详见 https://cloud.tencent.com/document/product/436/6224 41 | BucketURL *url.URL 42 | // 访问 service API 的基础 URL(不包含 path 部分) 43 | // 比如:https://service.cos.myqcloud.com 44 | ServiceURL *url.URL 45 | } 46 | 47 | // NewBaseURL 生成 BaseURL 48 | func NewBaseURL(bucketURL string) (u *BaseURL, err error) { 49 | bu, err := url.Parse(bucketURL) 50 | if err != nil { 51 | return 52 | } 53 | su, _ := url.Parse(defaultServiceBaseURL) 54 | u = &BaseURL{ 55 | BucketURL: bu, 56 | ServiceURL: su, 57 | } 58 | return 59 | } 60 | 61 | // NewBucketURL 生成 BaseURL 所需的 BucketURL 62 | // 63 | // bucketName: bucket 名称 64 | // AppID: 应用 ID 65 | // Region: 区域代码,详见 https://cloud.tencent.com/document/product/436/6224 66 | // secure: 是否使用 https 67 | func NewBucketURL(bucketName, appID, region string, secure bool) *url.URL { 68 | scheme := "https" 69 | if !secure { 70 | scheme = "http" 71 | } 72 | 73 | w := bytes.NewBuffer(nil) 74 | bucketURLTemplate.Execute(w, struct { 75 | Scheme string 76 | BucketName string 77 | AppID string 78 | Region string 79 | }{ 80 | scheme, bucketName, appID, region, 81 | }) 82 | 83 | u, _ := url.Parse(w.String()) 84 | return u 85 | } 86 | 87 | // A Client manages communication with the COS API. 88 | type Client struct { 89 | // Sender 用于实际发送 HTTP 请求 90 | Sender Sender 91 | // ResponseParser 用于解析响应 92 | ResponseParser ResponseParser 93 | 94 | UserAgent string 95 | BaseURL *BaseURL 96 | 97 | common service 98 | 99 | // Service 封装了 service 相关的 API 100 | Service *ServiceService 101 | // Bucket 封装了 bucket 相关的 API 102 | Bucket *BucketService 103 | // Object 封装了 object 相关的 API 104 | Object *ObjectService 105 | } 106 | 107 | type service struct { 108 | client *Client 109 | } 110 | 111 | // NewClient returns a new COS API client. 112 | // 使用 DefaultSender 作为 Sender,DefaultResponseParser 作为 ResponseParser 113 | func NewClient(uri *BaseURL, httpClient *http.Client) *Client { 114 | if httpClient == nil { 115 | httpClient = &http.Client{} 116 | } 117 | 118 | baseURL := &BaseURL{} 119 | if uri != nil { 120 | baseURL.BucketURL = uri.BucketURL 121 | baseURL.ServiceURL = uri.ServiceURL 122 | } 123 | if baseURL.ServiceURL == nil { 124 | baseURL.ServiceURL, _ = url.Parse(defaultServiceBaseURL) 125 | } 126 | 127 | c := &Client{ 128 | Sender: &DefaultSender{httpClient}, 129 | ResponseParser: &DefaultResponseParser{}, 130 | UserAgent: userAgent, 131 | BaseURL: baseURL, 132 | } 133 | c.common.client = c 134 | c.Service = (*ServiceService)(&c.common) 135 | c.Bucket = (*BucketService)(&c.common) 136 | c.Object = (*ObjectService)(&c.common) 137 | return c 138 | } 139 | 140 | func (c *Client) newRequest(ctx context.Context, opt *sendOptions) (req *http.Request, err error) { 141 | baseURL := opt.baseURL 142 | uri := opt.uri 143 | method := opt.method 144 | body := opt.body 145 | optQuery := opt.optQuery 146 | optHeader := opt.optHeader 147 | 148 | uri, err = addURLOptions(uri, optQuery) 149 | if err != nil { 150 | return 151 | } 152 | u, _ := url.Parse(uri) 153 | urlStr := baseURL.ResolveReference(u).String() 154 | 155 | var reader io.Reader 156 | contentType := "" 157 | contentMD5 := "" 158 | xsha1 := "" 159 | if body != nil { 160 | // 上传文件 161 | if r, ok := body.(io.Reader); ok { 162 | reader = r 163 | } else { 164 | b, err := xml.Marshal(body) 165 | if err != nil { 166 | return nil, err 167 | } 168 | contentType = contentTypeXML 169 | reader = bytes.NewReader(b) 170 | contentMD5 = base64.StdEncoding.EncodeToString(calMD5Digest(b)) 171 | // xsha1 = base64.StdEncoding.EncodeToString(calSHA1Digest(b)) 172 | } 173 | } else { 174 | contentType = contentTypeXML 175 | } 176 | 177 | req, err = http.NewRequest(method, urlStr, reader) 178 | if err != nil { 179 | return 180 | } 181 | 182 | req.Header, err = addHeaderOptions(req.Header, optHeader) 183 | if err != nil { 184 | return 185 | } 186 | if v := req.Header.Get("Content-Length"); req.ContentLength == 0 && v != "" && v != "0" { 187 | req.ContentLength, _ = strconv.ParseInt(v, 10, 64) 188 | req.Body = ioutil.NopCloser(reader) 189 | } 190 | 191 | if contentMD5 != "" { 192 | req.Header["Content-MD5"] = []string{contentMD5} 193 | } 194 | if xsha1 != "" { 195 | req.Header.Set("x-cos-sha1", xsha1) 196 | } 197 | if c.UserAgent != "" { 198 | req.Header.Set("User-Agent", c.UserAgent) 199 | } 200 | if req.Header.Get("Content-Type") == "" && contentType != "" { 201 | req.Header.Set("Content-Type", contentType) 202 | } 203 | return 204 | } 205 | 206 | func (c *Client) doAPI(ctx context.Context, caller Caller, req *http.Request, result interface{}, closeBody bool) (*Response, error) { 207 | req = req.WithContext(ctx) 208 | resp, err := c.Sender.Send(ctx, caller, req) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | defer func() { 214 | if closeBody { 215 | // Close the body to let the Transport reuse the connection 216 | io.Copy(ioutil.Discard, resp.Body) 217 | resp.Body.Close() 218 | } 219 | }() 220 | 221 | return c.ResponseParser.ParseResponse(ctx, caller, resp, result) 222 | } 223 | 224 | type sendOptions struct { 225 | // 基础 URL 226 | baseURL *url.URL 227 | // URL 中除基础 URL 外的剩余部分 228 | uri string 229 | // 请求方法 230 | method string 231 | 232 | body interface{} 233 | // url 查询参数 234 | optQuery interface{} 235 | // http header 参数 236 | optHeader interface{} 237 | // 用 result 反序列化 resp.Body 238 | result interface{} 239 | // 是否禁用自动调用 resp.Body.Close() 240 | // 自动调用 Close() 是为了能够重用连接 241 | disableCloseBody bool 242 | 243 | caller Caller 244 | } 245 | 246 | func (c *Client) send(ctx context.Context, opt *sendOptions) (resp *Response, err error) { 247 | req, err := c.newRequest(ctx, opt) 248 | if err != nil { 249 | return 250 | } 251 | 252 | resp, err = c.doAPI(ctx, opt.caller, req, opt.result, !opt.disableCloseBody) 253 | if err != nil { 254 | return 255 | } 256 | return 257 | } 258 | 259 | // addURLOptions adds the parameters in opt as URL query parameters to s. opt 260 | // must be a struct whose fields may contain "url" tags. 261 | func addURLOptions(s string, opt interface{}) (string, error) { 262 | v := reflect.ValueOf(opt) 263 | if v.Kind() == reflect.Ptr && v.IsNil() { 264 | return s, nil 265 | } 266 | 267 | u, err := url.Parse(s) 268 | if err != nil { 269 | return s, err 270 | } 271 | 272 | qs, err := query.Values(opt) 273 | if err != nil { 274 | return s, err 275 | } 276 | 277 | // 保留原有的参数,并且放在前面。因为 cos 的 url 路由是以第一个参数作为路由的 278 | // e.g. /?uploads 279 | q := u.RawQuery 280 | rq := qs.Encode() 281 | if q != "" { 282 | if rq != "" { 283 | u.RawQuery = fmt.Sprintf("%s&%s", q, qs.Encode()) 284 | } 285 | } else { 286 | u.RawQuery = rq 287 | } 288 | return u.String(), nil 289 | } 290 | 291 | // addHeaderOptions adds the parameters in opt as Header fields to req. opt 292 | // must be a struct whose fields may contain "header" tags. 293 | func addHeaderOptions(header http.Header, opt interface{}) (http.Header, error) { 294 | v := reflect.ValueOf(opt) 295 | if v.Kind() == reflect.Ptr && v.IsNil() { 296 | return header, nil 297 | } 298 | 299 | h, err := httpheader.Header(opt) 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | for key, values := range h { 305 | for _, value := range values { 306 | header.Add(key, value) 307 | } 308 | } 309 | return header, nil 310 | } 311 | 312 | // Owner ... 313 | type Owner struct { 314 | UIN string `xml:"uin,omitempty"` 315 | ID string `xml:",omitempty"` 316 | DisplayName string `xml:",omitempty"` 317 | } 318 | 319 | // Initiator ... 320 | type Initiator Owner 321 | 322 | // Response API 响应 323 | type Response struct { 324 | *http.Response 325 | } 326 | 327 | func newResponse(resp *http.Response) *Response { 328 | return &Response{ 329 | Response: resp, 330 | } 331 | } 332 | 333 | var ( 334 | xCosRequestID = "x-cos-request-id" 335 | xCosTraceID = "x-cos-trace-id" 336 | xCosObjectType = "x-cos-object-type" 337 | xCosStorageClass = "x-cos-storage-class" 338 | xCosVersionID = "x-cos-version-id" 339 | xCosServerSideEncryption = "x-cos-server-side-encryption" 340 | xCosMetaPrefix = "x-cos-meta-" 341 | ) 342 | 343 | // RequestID 每次请求发送时,服务端将会自动为请求生成一个ID。 344 | func (resp *Response) RequestID() string { 345 | return resp.Header.Get(xCosRequestID) 346 | } 347 | 348 | // TraceID 每次请求出错时,服务端将会自动为这个错误生成一个ID。 349 | func (resp *Response) TraceID() string { 350 | return resp.Header.Get(xCosTraceID) 351 | } 352 | 353 | // ObjectType 用来表示 Object 是否可以被追加上传,枚举值:normal 或者 appendable 354 | func (resp *Response) ObjectType() string { 355 | return resp.Header.Get(xCosObjectType) 356 | } 357 | 358 | // StorageClass Object 的存储级别,枚举值:STANDARD,STANDARD_IA 359 | func (resp *Response) StorageClass() string { 360 | return resp.Header.Get(xCosStorageClass) 361 | } 362 | 363 | // VersionID 如果检索到的对象具有唯一的版本ID,则返回版本ID。 364 | func (resp *Response) VersionID() string { 365 | return resp.Header.Get(xCosVersionID) 366 | } 367 | 368 | // ServerSideEncryption 如果通过 COS 管理的服务端加密来存储对象,响应将包含此头部和所使用的加密算法的值,AES256。 369 | func (resp *Response) ServerSideEncryption() string { 370 | return resp.Header.Get(xCosServerSideEncryption) 371 | } 372 | 373 | // MetaHeaders 用户自定义的元数据 374 | func (resp *Response) MetaHeaders() http.Header { 375 | h := http.Header{} 376 | for k := range resp.Header { 377 | if !strings.HasPrefix(strings.ToLower(k), xCosMetaPrefix) { 378 | continue 379 | } 380 | for _, v := range resp.Header[k] { 381 | h.Add(k, v) 382 | } 383 | } 384 | return h 385 | } 386 | 387 | // ACLHeaderOptions ... 388 | type ACLHeaderOptions struct { 389 | // 定义 Object 的 acl 属性。有效值:private,public-read-write,public-read;默认值:private 390 | XCosACL string `header:"x-cos-acl,omitempty" url:"-" xml:"-"` 391 | // 赋予被授权者读的权限。格式:id="[OwnerUin]" 392 | XCosGrantRead string `header:"x-cos-grant-read,omitempty" url:"-" xml:"-"` 393 | // 赋予被授权者写的权限。格式:id="[OwnerUin]" 394 | XCosGrantWrite string `header:"x-cos-grant-write,omitempty" url:"-" xml:"-"` 395 | // 赋予被授权者所有的权限。格式:id="[OwnerUin]" 396 | XCosGrantFullControl string `header:"x-cos-grant-full-control,omitempty" url:"-" xml:"-"` 397 | } 398 | 399 | // ACLGrantee ... 400 | type ACLGrantee struct { 401 | Type string `xml:"type,attr"` 402 | UIN string `xml:"uin,omitempty"` 403 | ID string `xml:",omitempty"` 404 | DisplayName string `xml:",omitempty"` 405 | SubAccount string `xml:"Subaccount,omitempty"` 406 | } 407 | 408 | // ACLGrant ... 409 | type ACLGrant struct { 410 | Grantee *ACLGrantee 411 | // 指明授予被授权者的权限信息,枚举值:READ,WRITE,FULL_CONTROL 412 | Permission string 413 | } 414 | 415 | // ACLXml ... 416 | // 417 | // https://cloud.tencent.com/document/product/436/7733 418 | type ACLXml struct { 419 | XMLName xml.Name `xml:"AccessControlPolicy"` 420 | Owner *Owner 421 | AccessControlList []ACLGrant `xml:"AccessControlList>Grant,omitempty"` 422 | } 423 | 424 | const ( 425 | // StorageClassStandard Object 的存储级别: STANDARD 426 | StorageClassStandard string = "STANDARD" 427 | // StorageClassStandardTA Object 的存储级别: STANDARD_IA 428 | StorageClassStandardTA string = "STANDARD_IA" 429 | // StorageClassArchive Object 的存储级别: ARCHIVE 430 | StorageClassArchive string = "ARCHIVE" 431 | 432 | // ObjectTypeAppendable : appendable 433 | ObjectTypeAppendable string = "appendable" 434 | // ObjectTypeNormal : normal 435 | ObjectTypeNormal string = "normal" 436 | 437 | // ServerSideEncryptionAES256 服务端加密算法: AES256 438 | ServerSideEncryptionAES256 string = "AES256" 439 | 440 | // PermissionRead 权限值: READ 441 | PermissionRead string = "READ" 442 | // PermissionWrite 权限值: WRITE 443 | PermissionWrite string = "WRITE" 444 | // PermissionFullControl 权限值: FULL_CONTROL 445 | PermissionFullControl string = "FULL_CONTROL" 446 | ) 447 | -------------------------------------------------------------------------------- /cos_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/xml" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/textproto" 13 | "net/url" 14 | "reflect" 15 | "sort" 16 | "strings" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | var ( 22 | // mux is the HTTP request multiplexer used with the test server. 23 | mux *http.ServeMux 24 | 25 | // client is the COS client being tested. 26 | client *Client 27 | 28 | // server is a test HTTP server used to provide mock API responses. 29 | server *httptest.Server 30 | ) 31 | 32 | // setup sets up a test HTTP server along with a cos.Client that is 33 | // configured to talk to that test server. Tests should register handlers on 34 | // mux which provide mock responses for the API method being tested. 35 | func setup() { 36 | // test server 37 | mux = http.NewServeMux() 38 | server = httptest.NewServer(mux) 39 | 40 | u, _ := url.Parse(server.URL) 41 | client = NewClient(&BaseURL{u, u}, nil) 42 | } 43 | 44 | // teardown closes the test HTTP server. 45 | func teardown() { 46 | server.Close() 47 | } 48 | 49 | type values map[string]string 50 | 51 | func testFormValues(t *testing.T, r *http.Request, values values) { 52 | want := url.Values{} 53 | for k, v := range values { 54 | want.Set(k, v) 55 | } 56 | 57 | r.ParseForm() 58 | if got := r.Form; !reflect.DeepEqual(got, want) { 59 | t.Errorf("Request parameters: %v, want %v", got, want) 60 | } 61 | } 62 | 63 | func testMethod(t *testing.T, r *http.Request, want string) { 64 | if got := r.Method; got != want { 65 | t.Errorf("Request method: %v, want %v", got, want) 66 | } 67 | } 68 | 69 | func testHeader(t *testing.T, r *http.Request, header string, want string) { 70 | if got := r.Header.Get(header); got != want { 71 | t.Errorf("Header.Get(%q) returned %q, want %q", header, got, want) 72 | } 73 | } 74 | 75 | func testURLParseError(t *testing.T, err error) { 76 | if err == nil { 77 | t.Errorf("Expected error to be returned") 78 | } 79 | if err, ok := err.(*url.Error); !ok || err.Op != "parse" { 80 | t.Errorf("Expected URL parse error, got %+v", err) 81 | } 82 | } 83 | 84 | func testBody(t *testing.T, r *http.Request, want string) { 85 | b, err := ioutil.ReadAll(r.Body) 86 | if err != nil { 87 | t.Errorf("Error reading request body: %v", err) 88 | } 89 | if got := string(b); got != want { 90 | t.Errorf("request Body is %s, want %s", got, want) 91 | } 92 | } 93 | 94 | // Helper function to test that a value is marshalled to XML as expected. 95 | func testXMLMarshal(t *testing.T, v interface{}, want string) { 96 | j, err := xml.Marshal(v) 97 | if err != nil { 98 | t.Errorf("Unable to marshal JSON for %v", v) 99 | } 100 | 101 | w := new(bytes.Buffer) 102 | err = xml.NewEncoder(w).Encode([]byte(want)) 103 | if err != nil { 104 | t.Errorf("String is not valid json: %s", want) 105 | } 106 | 107 | if w.String() != string(j) { 108 | t.Errorf("xml.Marshal(%q) returned %s, want %s", v, j, w) 109 | } 110 | 111 | // now go the other direction and make sure things unmarshal as expected 112 | u := reflect.ValueOf(v).Interface() 113 | if err := xml.Unmarshal([]byte(want), u); err != nil { 114 | t.Errorf("Unable to unmarshal XML for %v", want) 115 | } 116 | 117 | if !reflect.DeepEqual(v, u) { 118 | t.Errorf("xml.Unmarshal(%q) returned %s, want %s", want, u, v) 119 | } 120 | } 121 | 122 | func TestNewClient(t *testing.T) { 123 | c := NewClient(nil, nil) 124 | 125 | if got, want := c.BaseURL.ServiceURL.String(), defaultServiceBaseURL; got != want { 126 | t.Errorf("NewClient BaseURL is %v, want %v", got, want) 127 | } 128 | if got, want := c.UserAgent, userAgent; got != want { 129 | t.Errorf("NewClient UserAgent is %v, want %v", got, want) 130 | } 131 | } 132 | 133 | func TestNewBucketURL_secure_false(t *testing.T) { 134 | got := NewBucketURL("bname", "idx", "ap-beijing", false).String() 135 | want := "http://bname-idx.cos.ap-beijing.myqcloud.com" 136 | if got != want { 137 | t.Errorf("NewBucketURL is %v, want %v", got, want) 138 | } 139 | } 140 | 141 | func TestNewBucketURL_secure_true(t *testing.T) { 142 | got := NewBucketURL("bname", "idx", "ap-beijing", true).String() 143 | want := "https://bname-idx.cos.ap-beijing.myqcloud.com" 144 | if got != want { 145 | t.Errorf("NewBucketURL is %v, want %v", got, want) 146 | } 147 | } 148 | 149 | func TestNewBaseURL(t *testing.T) { 150 | bu := "https://test-1253846586.cos.ap-beijing.myqcloud.com" 151 | got, _ := NewBaseURL(bu) 152 | if got.BucketURL.String() != bu { 153 | t.Errorf("bucketURL want %s, but got %s", bu, got.BucketURL.String()) 154 | } 155 | if got.ServiceURL.String() != defaultServiceBaseURL { 156 | t.Errorf("serviceURL want %s, but got %s", defaultServiceBaseURL, got.ServiceURL.String()) 157 | } 158 | } 159 | 160 | func TestClient_doAPI(t *testing.T) { 161 | setup() 162 | defer teardown() 163 | 164 | } 165 | 166 | func TestNewAuthTime(t *testing.T) { 167 | a := NewAuthTime(time.Hour) 168 | if a.SignStartTime != a.KeyStartTime || 169 | a.SignEndTime != a.SignEndTime || 170 | a.SignStartTime.Add(time.Hour) != a.SignEndTime { 171 | t.Errorf("NewAuthTime request got %+v is not valid", a) 172 | } 173 | } 174 | 175 | type traceCloser struct { 176 | io.Reader 177 | Called bool 178 | } 179 | 180 | func (t traceCloser) Close() error { 181 | t.Called = true 182 | return nil 183 | } 184 | 185 | func newTraceCloser(r io.Reader) traceCloser { 186 | return traceCloser{r, false} 187 | } 188 | 189 | func Test_doAPI_copy_body(t *testing.T) { 190 | setup() 191 | defer teardown() 192 | 193 | mux.HandleFunc("/test_down", func(w http.ResponseWriter, r *http.Request) { 194 | fmt.Fprint(w, `test`) 195 | }) 196 | 197 | w := bytes.NewBuffer([]byte{}) 198 | resp, err := client.send(context.TODO(), &sendOptions{ 199 | baseURL: client.BaseURL.ServiceURL, 200 | uri: "/test_down", 201 | method: "GET", 202 | result: w, 203 | }) 204 | 205 | if err != nil { 206 | t.Errorf("Expected error == nil, got %+v", err) 207 | } 208 | b, _ := ioutil.ReadAll(resp.Body) 209 | if len(b) != 0 || string(w.Bytes()) != "test" { 210 | t.Errorf( 211 | "Expected body was copy and close, got %+v, %+v", 212 | string(b), string(w.Bytes())) 213 | } 214 | } 215 | 216 | func Test_Response_header_method(t *testing.T) { 217 | setup() 218 | defer teardown() 219 | reqID := "NTk0NTRjZjZfNTViMjM1XzlkMV9hZTZh" 220 | traceID := "OGVmYzZiMmQzYjA2OWNhODk0NTRkMTBiOWVmMDAxODc0OWRkZjk0ZDM1NmI1M2E2MTRlY2MzZDhmNmI5MWI1OTBjYzE2MjAxN2M1MzJiOTdkZjMxMDVlYTZjN2FiMmI0NTk3NWFiNjAyMzdlM2RlMmVmOGNiNWIxYjYwNDFhYmQ=" 221 | objType := "normal" 222 | storageCls := "STANDARD" 223 | versionID := "xxx-v1" // ? 224 | encryption := "AES256" 225 | 226 | mux.HandleFunc("/test_down", func(w http.ResponseWriter, r *http.Request) { 227 | w.Header().Set(xCosRequestID, reqID) 228 | w.Header().Set(xCosTraceID, traceID) 229 | w.Header().Set(xCosObjectType, objType) 230 | w.Header().Set(xCosStorageClass, storageCls) 231 | w.Header().Set(xCosVersionID, versionID) 232 | w.Header().Set(xCosServerSideEncryption, encryption) 233 | w.Header().Add("x-cos-meta-1", "1") 234 | w.Header().Add("x-cos-meta-1", "11") 235 | w.Header().Add("x-cos-meta-2", "2") 236 | w.Header().Add("x-cos-meta-2", "22") 237 | w.Header().Add("x-cos-meta-3", "33") 238 | fmt.Fprint(w, `test`) 239 | }) 240 | 241 | w := bytes.NewBuffer([]byte{}) 242 | resp, err := client.send(context.TODO(), &sendOptions{ 243 | baseURL: client.BaseURL.ServiceURL, 244 | uri: "/test_down", 245 | method: "GET", 246 | result: w, 247 | }) 248 | 249 | if err != nil { 250 | t.Errorf("Expected error == nil, got %+v", err) 251 | } 252 | b, _ := ioutil.ReadAll(resp.Body) 253 | if len(b) != 0 || string(w.Bytes()) != "test" { 254 | t.Errorf( 255 | "Expected body was copy and close, got %+v, %+v", 256 | string(b), string(w.Bytes())) 257 | } 258 | h := resp.MetaHeaders() 259 | keys := []string{} 260 | for k := range h { 261 | keys = append(keys, strings.ToLower(k)) 262 | } 263 | sort.Strings(keys) 264 | if resp.RequestID() != reqID || 265 | resp.TraceID() != traceID || 266 | resp.ObjectType() != objType || 267 | resp.StorageClass() != storageCls || 268 | resp.VersionID() != versionID || 269 | resp.ServerSideEncryption() != encryption || 270 | !reflect.DeepEqual(keys, 271 | []string{"x-cos-meta-1", "x-cos-meta-2", "x-cos-meta-3"}) { 272 | t.Errorf("result of response header method is not expected") 273 | } 274 | v1 := h[textproto.CanonicalMIMEHeaderKey("x-cos-meta-1")] 275 | sort.Strings(v1) 276 | v2 := h[textproto.CanonicalMIMEHeaderKey("x-cos-meta-2")] 277 | sort.Strings(v2) 278 | v3 := h[textproto.CanonicalMIMEHeaderKey("x-cos-meta-3")] 279 | sort.Strings(v3) 280 | if !reflect.DeepEqual(v1, 281 | []string{"1", "11"}) || 282 | !reflect.DeepEqual(v2, 283 | []string{"2", "22"}) || 284 | !reflect.DeepEqual(v3, 285 | []string{"33"}) { 286 | t.Errorf("result of response meta headers is not expected, %s, %s, %s", 287 | v1, v2, v3) 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /debug/http.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/http/httputil" 8 | "os" 9 | ) 10 | 11 | // DebugRequestTransport 会打印请求和响应信息, 方便调试. 12 | type DebugRequestTransport struct { 13 | RequestHeader bool 14 | RequestBody bool // RequestHeader 为 true 时,这个选项才会生效 15 | ResponseHeader bool 16 | ResponseBody bool // ResponseHeader 为 true 时,这个选项才会生效 17 | 18 | // debug 信息输出到 Writer 中, 默认是 os.Stderr 19 | Writer io.Writer 20 | 21 | Transport http.RoundTripper 22 | } 23 | 24 | // RoundTrip implements the RoundTripper interface. 25 | func (t *DebugRequestTransport) RoundTrip(req *http.Request) (*http.Response, error) { 26 | req = cloneRequest(req) // per RoundTrip contract 27 | w := t.Writer 28 | if w == nil { 29 | w = os.Stderr 30 | } 31 | 32 | if t.RequestHeader { 33 | a, _ := httputil.DumpRequestOut(req, t.RequestBody) 34 | fmt.Fprintf(w, "%s\n\n", string(a)) 35 | } 36 | 37 | resp, err := t.transport().RoundTrip(req) 38 | if err != nil { 39 | return resp, err 40 | } 41 | 42 | if t.ResponseHeader { 43 | 44 | b, _ := httputil.DumpResponse(resp, t.ResponseBody) 45 | fmt.Fprintf(w, "%s\n", string(b)) 46 | } 47 | 48 | return resp, err 49 | } 50 | 51 | func (t *DebugRequestTransport) transport() http.RoundTripper { 52 | if t.Transport != nil { 53 | return t.Transport 54 | } 55 | return http.DefaultTransport 56 | } 57 | 58 | // cloneRequest returns a clone of the provided *http.Request. The clone is a 59 | // shallow copy of the struct and its Header map. 60 | func cloneRequest(r *http.Request) *http.Request { 61 | // shallow copy of the struct 62 | r2 := new(http.Request) 63 | *r2 = *r 64 | // deep copy of the Header 65 | r2.Header = make(http.Header, len(r.Header)) 66 | for k, s := range r.Header { 67 | r2.Header[k] = append([]string(nil), s...) 68 | } 69 | return r2 70 | } 71 | -------------------------------------------------------------------------------- /debug/http_test.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | // mux is the HTTP request multiplexer used with the test server. 13 | mux *http.ServeMux 14 | 15 | // server is a test HTTP server used to provide mock API responses. 16 | server *httptest.Server 17 | ) 18 | 19 | // setup sets up a test HTTP server along with a cos.Client that is 20 | // configured to talk to that test server. Tests should register handlers on 21 | // mux which provide mock responses for the API method being tested. 22 | func setup() { 23 | // test server 24 | mux = http.NewServeMux() 25 | server = httptest.NewServer(mux) 26 | } 27 | 28 | // teardown closes the test HTTP server. 29 | func teardown() { 30 | server.Close() 31 | } 32 | 33 | func TestDebugRequestTransport(t *testing.T) { 34 | setup() 35 | defer teardown() 36 | 37 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 38 | w.Header().Add("X-Test-Response", "2333") 39 | w.WriteHeader(http.StatusBadGateway) 40 | w.Write([]byte("test response body")) 41 | }) 42 | 43 | w := bytes.NewBufferString("") 44 | client := http.Client{} 45 | 46 | client.Transport = &DebugRequestTransport{ 47 | RequestHeader: true, 48 | RequestBody: true, 49 | ResponseHeader: true, 50 | ResponseBody: true, 51 | Writer: w, 52 | } 53 | 54 | body := bytes.NewReader([]byte("test_request body")) 55 | req, _ := http.NewRequest("GET", server.URL, body) 56 | req.Header.Add("X-Test-Debug", "123") 57 | client.Do(req) 58 | 59 | b := make([]byte, 800) 60 | w.Read(b) 61 | info := string(b) 62 | if !strings.Contains(info, "GET / HTTP/1.1\r\n") || 63 | !strings.Contains(info, "X-Test-Debug: 123\r\n") { 64 | t.Errorf("DebugRequestTransport debug info %#v don't contains request header", info) 65 | } 66 | if !strings.Contains(info, "\r\n\r\ntest_request body") { 67 | t.Errorf("DebugRequestTransport debug info %#v don't contains request body", info) 68 | } 69 | 70 | if !strings.Contains(info, "HTTP/1.1 502 Bad Gateway\r\n") || 71 | !strings.Contains(info, "X-Test-Response: 2333\r\n") { 72 | t.Errorf("DebugRequestTransport debug info %#v don't contains response header", info) 73 | } 74 | 75 | if !strings.Contains(info, "\r\n\r\ntest response body") { 76 | t.Errorf("DebugRequestTransport debug info %#v don't contains response body", info) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package cos 腾讯云对象存储服务 COS(Cloud Object Storage) Go SDK。 3 | 4 | 5 | COS API Version 6 | 7 | 封装了 V5 版本的 XML API 。 8 | 9 | 10 | Usage 11 | 12 | 在项目的 _example 目录下有各个 API 的使用示例[1] 。 13 | 14 | [1]: 示例文件所对应的 API 说明 https://github.com/mozillazg/go-cos#todo 15 | 16 | 备注 17 | 18 | * SDK 不会自动设置超时时间,用户根据需要设置合适的超时时间(比如,设置 `http.Client` 的 `Timeout` 字段或者 19 | `Transport` 字段之类的)或在需要时实现所需的超时机制(比如,通过 `context` 包实现)。 20 | 21 | * 所有的 API 在 _example 目录下都有对应的使用示例[1](示例程序中用到的 `debug` 包只是调试用的不是必需的依赖)。 22 | 23 | [1]: 示例文件所对应的 API 说明 https://github.com/mozillazg/go-cos#todo 24 | 25 | 26 | Authentication 27 | 28 | 默认所有 API 都是匿名访问. 如果想添加认证信息的话,可以通过自定义一个 http.Client 来添加认证信息. 29 | 30 | 比如, 使用内置的 AuthorizationTransport 来为请求增加 Authorization Header 签名信息: 31 | 32 | client := cos.NewClient(b, &http.Client{ 33 | Transport: &cos.AuthorizationTransport{ 34 | SecretID: "COS_SECRETID", 35 | SecretKey: "COS_SECRETKEY", 36 | }, 37 | }) 38 | 39 | */ 40 | package cos 41 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | // ErrorResponse 包含 COS HTTP API 返回的错误信息 11 | // 12 | // https://cloud.tencent.com/document/product/436/7730 13 | type ErrorResponse struct { 14 | XMLName xml.Name `xml:"Error"` 15 | // TODO: use cos.Response instead 16 | Response *http.Response `xml:"-"` 17 | Code string 18 | Message string 19 | Resource string 20 | RequestID string `xml:"RequestId"` 21 | TraceID string `xml:"TraceId,omitempty"` 22 | } 23 | 24 | // Error ... 25 | func (r *ErrorResponse) Error() string { 26 | return fmt.Sprintf("%v %v: %d %v(Message: %v, RequestId: %v, TraceId: %v)", 27 | r.Response.Request.Method, r.Response.Request.URL, 28 | r.Response.StatusCode, r.Code, r.Message, r.RequestID, r.TraceID) 29 | } 30 | 31 | // 检查 response 是否是出错时的返回的 response 32 | func checkResponse(r *http.Response) error { 33 | if c := r.StatusCode; 200 <= c && c <= 299 { 34 | return nil 35 | } 36 | errorResponse := &ErrorResponse{Response: r} 37 | data, err := ioutil.ReadAll(r.Body) 38 | if err == nil && data != nil { 39 | xml.Unmarshal(data, errorResponse) 40 | } 41 | if errorResponse.RequestID == "" { 42 | errorResponse.RequestID = r.Header.Get(xCosRequestID) 43 | } 44 | if errorResponse.TraceID == "" { 45 | errorResponse.TraceID = r.Header.Get(xCosTraceID) 46 | } 47 | return errorResponse 48 | } 49 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func Test_checkResponse_error(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/test_409", func(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(http.StatusConflict) 18 | fmt.Fprint(w, ` 19 | 20 | BucketAlreadyExists 21 | The requested bucket name is not available. 22 | testdelete-1253846586.cn-north.myqcloud.com 23 | NTk0NTRjZjZfNTViMjM1XzlkMV9hZTZh 24 | OGVmYzZiMmQzYjA2OWNhODk0NTRkMTBiOWVmMDAxODc0OWRkZjk0ZDM1NmI1M2E2MTRlY2MzZDhmNmI5MWI1OTBjYzE2MjAxN2M1MzJiOTdkZjMxMDVlYTZjN2FiMmI0NTk3NWFiNjAyMzdlM2RlMmVmOGNiNWIxYjYwNDFhYmQ= 25 | `) 26 | }) 27 | 28 | _, err := client.send(context.TODO(), &sendOptions{ 29 | baseURL: client.BaseURL.ServiceURL, 30 | uri: "/test_409", 31 | method: "GET", 32 | }) 33 | 34 | if e, ok := err.(*ErrorResponse); ok { 35 | if e.Error() == "" { 36 | t.Errorf("Expected e.Error() not empty, got %+v", e.Error()) 37 | } 38 | if e.Code != "BucketAlreadyExists" { 39 | t.Errorf("Expected BucketAlreadyExists error, got %+v", e.Code) 40 | } 41 | } else { 42 | t.Errorf("Expected ErrorResponse error, got %+v", err) 43 | } 44 | } 45 | 46 | func Test_checkResponse_header(t *testing.T) { 47 | setup() 48 | defer teardown() 49 | reqID := "NTk0NTRjZjZfNTViMjM1XzlkMV9hZTZh" 50 | traceID := "OGVmYzZiMmQzYjA2OWNhODk0NTRkMTBiOWVmMDAxODc0OWRkZjk0ZDM1NmI1M2E2MTRlY2MzZDhmNmI5MWI1OTBjYzE2MjAxN2M1MzJiOTdkZjMxMDVlYTZjN2FiMmI0NTk3NWFiNjAyMzdlM2RlMmVmOGNiNWIxYjYwNDFhYmQ=" 51 | 52 | mux.HandleFunc("/test_409", func(w http.ResponseWriter, r *http.Request) { 53 | w.Header().Set(xCosRequestID, reqID) 54 | w.Header().Set(xCosTraceID, traceID) 55 | w.WriteHeader(http.StatusConflict) 56 | fmt.Fprint(w, ` 57 | 58 | BucketAlreadyExists 59 | The requested bucket name is not available. 60 | testdelete-1253846586.cn-north.myqcloud.com 61 | `) 62 | }) 63 | 64 | _, err := client.send(context.TODO(), &sendOptions{ 65 | baseURL: client.BaseURL.ServiceURL, 66 | uri: "/test_409", 67 | method: "GET", 68 | }) 69 | 70 | if e, ok := err.(*ErrorResponse); ok { 71 | if e.Error() == "" { 72 | t.Errorf("Expected e.Error() not empty, got %+v", e.Error()) 73 | } 74 | if e.Code != "BucketAlreadyExists" { 75 | t.Errorf("Expected BucketAlreadyExists error, got %+v", e.Code) 76 | } 77 | if e.RequestID != reqID { 78 | t.Errorf("Expected use header field when RequestId is missing, got %+v", e.RequestID) 79 | } 80 | if e.TraceID != traceID { 81 | t.Errorf("Expected use header field when TraceId is missing, got %+v", e.TraceID) 82 | } 83 | } else { 84 | t.Errorf("Expected ErrorResponse error, got %+v", err) 85 | } 86 | } 87 | 88 | func Test_checkResponse_no_error(t *testing.T) { 89 | setup() 90 | defer teardown() 91 | 92 | mux.HandleFunc("/test_200", func(w http.ResponseWriter, r *http.Request) { 93 | fmt.Fprint(w, `test`) 94 | }) 95 | 96 | _, err := client.send(context.TODO(), &sendOptions{ 97 | baseURL: client.BaseURL.ServiceURL, 98 | uri: "/test_200", 99 | method: "GET", 100 | }) 101 | if err != nil { 102 | t.Errorf("Expected error == nil, got %+v", err) 103 | } 104 | } 105 | 106 | func Test_error_network_error(t *testing.T) { 107 | setup() 108 | defer teardown() 109 | 110 | _, err := client.send(context.TODO(), &sendOptions{ 111 | baseURL: &url.URL{Scheme: "http", Host: "127.0.0.1:0"}, 112 | uri: "/233", 113 | method: "GET", 114 | }) 115 | if !(strings.Contains(err.Error(), "can't assign requested address") || 116 | strings.Contains(err.Error(), "connection refused")) { 117 | t.Errorf( 118 | `Expected error contains "can't assign requested address" or "connection refused", 119 | got %+v`, err) 120 | } 121 | } 122 | 123 | func Test_error_cancel_error(t *testing.T) { 124 | setup() 125 | defer teardown() 126 | 127 | ctx := context.TODO() 128 | ctx, cancel := context.WithCancel(ctx) 129 | cancel() 130 | _, err := client.send(ctx, &sendOptions{ 131 | baseURL: &url.URL{Scheme: "http", Host: "127.0.0.1:0"}, 132 | uri: "/233", 133 | method: "GET", 134 | }) 135 | if !strings.Contains(err.Error(), "context canceled") { 136 | t.Errorf(`Expected error contains "context canceled", got %+v`, err) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozillazg/go-cos 2 | 3 | require ( 4 | github.com/google/go-querystring v1.0.0 5 | github.com/mozillazg/go-httpheader v0.2.1 6 | ) 7 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | // 计算 md5 或 sha1 时的分块大小 12 | const calDigestBlockSize = 1024 * 1024 * 10 13 | 14 | func calMD5Digest(msg []byte) []byte { 15 | // TODO: 分块计算,减少内存消耗 16 | m := md5.New() 17 | m.Write(msg) 18 | return m.Sum(nil) 19 | } 20 | 21 | func calSHA1Digest(msg []byte) []byte { 22 | // TODO: 分块计算,减少内存消耗 23 | m := sha1.New() 24 | m.Write(msg) 25 | return m.Sum(nil) 26 | } 27 | 28 | // cloneRequest returns a clone of the provided *http.Request. The clone is a 29 | // shallow copy of the struct and its Header map. 30 | func cloneRequest(r *http.Request) *http.Request { 31 | // shallow copy of the struct 32 | r2 := new(http.Request) 33 | *r2 = *r 34 | // deep copy of the Header 35 | r2.Header = make(http.Header, len(r.Header)) 36 | for k, s := range r.Header { 37 | r2.Header[k] = append([]string(nil), s...) 38 | } 39 | return r2 40 | } 41 | 42 | // encodeURIComponent like same function in javascript 43 | // 44 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent 45 | // 46 | // http://www.ecma-international.org/ecma-262/6.0/#sec-uri-syntax-and-semantics 47 | func encodeURIComponent(s string) string { 48 | var b bytes.Buffer 49 | written := 0 50 | 51 | for i, n := 0, len(s); i < n; i++ { 52 | c := s[i] 53 | 54 | switch c { 55 | case '-', '_', '.', '!', '~', '*', '\'', '(', ')': 56 | continue 57 | default: 58 | // Unreserved according to RFC 3986 sec 2.3 59 | if 'a' <= c && c <= 'z' { 60 | 61 | continue 62 | 63 | } 64 | if 'A' <= c && c <= 'Z' { 65 | 66 | continue 67 | 68 | } 69 | if '0' <= c && c <= '9' { 70 | 71 | continue 72 | } 73 | } 74 | 75 | b.WriteString(s[written:i]) 76 | fmt.Fprintf(&b, "%%%02X", c) 77 | written = i + 1 78 | } 79 | 80 | if written == 0 { 81 | return s 82 | } 83 | b.WriteString(s[written:]) 84 | return b.String() 85 | } 86 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_calSHA1Digest(t *testing.T) { 9 | want := "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" 10 | got := fmt.Sprintf("%x", calSHA1Digest([]byte("test"))) 11 | if got != want { 12 | 13 | t.Errorf("calSHA1Digest request sha1: %+v, want %+v", got, want) 14 | } 15 | } 16 | 17 | func Test_calMD5Digest(t *testing.T) { 18 | want := "098f6bcd4621d373cade4e832627b4f6" 19 | got := fmt.Sprintf("%x", calMD5Digest([]byte("test"))) 20 | if got != want { 21 | 22 | t.Errorf("calMD5Digest request md5: %+v, want %+v", got, want) 23 | } 24 | } 25 | 26 | func Test_encodeURIComponent(t *testing.T) { 27 | type args struct { 28 | s string 29 | } 30 | tests := []struct { 31 | name string 32 | args args 33 | want string 34 | }{ 35 | { 36 | name: "empty", 37 | args: args{""}, 38 | want: "", 39 | }, 40 | { 41 | name: "no escape", 42 | args: args{"0123456789abcdefghijkhlmnopqrstuvwxyzABCDEFGHIJKHLMNOPQRSTUVWXYZ-_.!~*'()"}, 43 | want: "0123456789abcdefghijkhlmnopqrstuvwxyzABCDEFGHIJKHLMNOPQRSTUVWXYZ-_.!~*'()", 44 | }, 45 | { 46 | name: "escape", 47 | args: args{"+ $@#/"}, 48 | want: "%2B%20%24%40%23%2F", 49 | }, 50 | { 51 | name: "escape+no", 52 | args: args{"+ $abc@#13/0"}, 53 | want: "%2B%20%24abc%40%2313%2F0", 54 | }, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | if got := encodeURIComponent(tt.args.s); got != tt.want { 59 | t.Errorf("encodeURIComponent() = %v, want %v", got, tt.want) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | // Sender 定义了一个用来发送 http 请求的接口。 11 | // 可以用于替换默认的基于 http.Client 的实现, 12 | // 从而实现使用第三方 http client 或写单元测试时 mock 接口结果的需求。 13 | // 14 | // 实现自定义的 Sender 时可以参考 DefaultSender 的实现。 15 | type Sender interface { 16 | // caller 中包含了从哪个方法触发的 http 请求的信息 17 | // 当 error != nil 时将不会调用 ResponseParser.ParseResponse 解析响应 18 | Send(ctx context.Context, caller Caller, req *http.Request) (*http.Response, error) 19 | } 20 | 21 | // ResponseParser 定义了一个用于解析响应的接口(反序列化 body 或错误检查)。 22 | // 可以用于替换默认的解析响应的实现, 23 | // 从而实现使用自定义的解析方法或写单元测试时 mock 接口结果的需求 24 | // 25 | // 实现自定义的 ResponseParser 时可以参考 DefaultResponseParser 的实现。 26 | type ResponseParser interface { 27 | // caller 中包含了从哪个方法触发的 http 请求的信息 28 | // result: 反序列化后的结果将存储在指针类型的 result 中 29 | ParseResponse(ctx context.Context, caller Caller, resp *http.Response, result interface{}) (*Response, error) 30 | } 31 | 32 | // DefaultSender 是基于 http.Client 的默认 Sender 实现 33 | type DefaultSender struct { 34 | *http.Client 35 | } 36 | 37 | // Send 发送 http 请求 38 | func (s *DefaultSender) Send(ctx context.Context, caller Caller, req *http.Request) (*http.Response, error) { 39 | resp, err := s.Do(req) 40 | if err != nil { 41 | // If we got an error, and the context has been canceled, 42 | // the context's error is probably more useful. 43 | select { 44 | case <-ctx.Done(): 45 | return nil, ctx.Err() 46 | default: 47 | } 48 | return nil, err 49 | } 50 | 51 | return resp, err 52 | } 53 | 54 | // DefaultResponseParser 是默认的 ResponseParser 实现 55 | type DefaultResponseParser struct{} 56 | 57 | // ParseResponse 解析响应内容,反序列化后的结果将存储在指针类型的 result 中 58 | func (p *DefaultResponseParser) ParseResponse(ctx context.Context, caller Caller, resp *http.Response, result interface{}) (*Response, error) { 59 | response := newResponse(resp) 60 | 61 | err := checkResponse(resp) 62 | if err != nil { 63 | // even though there was an error, we still return the response 64 | // in case the caller wants to inspect it further 65 | resp.Body.Close() 66 | return response, err 67 | } 68 | 69 | if result != nil { 70 | if w, ok := result.(io.Writer); ok { 71 | _, err = io.Copy(w, resp.Body) 72 | } else { 73 | err = xml.NewDecoder(resp.Body).Decode(result) 74 | if err == io.EOF { 75 | err = nil // ignore EOF errors caused by empty response body 76 | } 77 | } 78 | } 79 | 80 | return response, err 81 | } 82 | 83 | // MethodName 用于 Caller 中表示调用的是哪个方法 84 | type MethodName string 85 | 86 | // Caller 方法调用信息,用于 Sender 和 ResponseParser 中判断是来自哪个方法的调用 87 | type Caller struct { 88 | // 调用的方法名称 89 | Method MethodName 90 | } 91 | -------------------------------------------------------------------------------- /object_acl.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // ObjectGetACLResult ... 9 | type ObjectGetACLResult ACLXml 10 | 11 | // MethodObjectGetACL method name of Object.GetACL 12 | const MethodObjectGetACL MethodName = "Object.GetACL" 13 | 14 | // GetACL Get Object ACL接口实现使用API读取Object的ACL表,只有所有者有权操作。 15 | // 16 | // 默认情况下,该 GET 操作返回对象的当前版本。您如果需要返回不同的版本,请使用 version Id 子资源。 17 | // 18 | // https://cloud.tencent.com/document/product/436/7744 19 | func (s *ObjectService) GetACL(ctx context.Context, name string) (*ObjectGetACLResult, *Response, error) { 20 | var res ObjectGetACLResult 21 | sendOpt := sendOptions{ 22 | baseURL: s.client.BaseURL.BucketURL, 23 | uri: "/" + encodeURIComponent(name) + "?acl", 24 | method: http.MethodGet, 25 | result: &res, 26 | caller: Caller{ 27 | Method: MethodObjectGetACL, 28 | }, 29 | } 30 | resp, err := s.client.send(ctx, &sendOpt) 31 | return &res, resp, err 32 | } 33 | 34 | // ObjectPutACLOptions ... 35 | type ObjectPutACLOptions struct { 36 | // Header 和 Body 二选一 37 | Header *ACLHeaderOptions `url:"-" xml:"-"` 38 | Body *ACLXml `url:"-" header:"-"` 39 | } 40 | 41 | // MethodObjectPutACL method name of Object.PutACL 42 | const MethodObjectPutACL MethodName = "Object.PutACL" 43 | 44 | // PutACL 使用API写入Object的ACL表。 45 | // 46 | // https://cloud.tencent.com/document/product/436/7748 47 | func (s *ObjectService) PutACL(ctx context.Context, name string, opt *ObjectPutACLOptions) (*Response, error) { 48 | header := opt.Header 49 | body := opt.Body 50 | if body != nil { 51 | header = nil 52 | } 53 | sendOpt := sendOptions{ 54 | baseURL: s.client.BaseURL.BucketURL, 55 | uri: "/" + encodeURIComponent(name) + "?acl", 56 | method: http.MethodPut, 57 | optHeader: header, 58 | body: body, 59 | caller: Caller{ 60 | Method: MethodObjectPutACL, 61 | }, 62 | } 63 | resp, err := s.client.send(ctx, &sendOpt) 64 | return resp, err 65 | } 66 | -------------------------------------------------------------------------------- /object_acl_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestObjectService_GetACL(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | name := "test/hello.txt" 16 | 17 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 18 | testMethod(t, r, "GET") 19 | vs := values{ 20 | "acl": "", 21 | } 22 | testFormValues(t, r, vs) 23 | fmt.Fprint(w, ` 24 | 25 | qcs::cam::uin/100000760461:uin/100000760461 26 | qcs::cam::uin/100000760461:uin/100000760461 27 | 28 | 29 | 30 | 31 | qcs::cam::uin/100000760461:uin/100000760461 32 | qcs::cam::uin/100000760461:uin/100000760461 33 | 34 | FULL_CONTROL 35 | 36 | 37 | 38 | qcs::cam::uin/100000760461:uin/100000760461 39 | qcs::cam::uin/100000760461:uin/100000760461 40 | 41 | READ 42 | 43 | 44 | `) 45 | }) 46 | 47 | ref, _, err := client.Object.GetACL(context.Background(), name) 48 | if err != nil { 49 | t.Fatalf("Object.GetACL returned error: %v", err) 50 | } 51 | 52 | want := &ObjectGetACLResult{ 53 | XMLName: xml.Name{Local: "AccessControlPolicy"}, 54 | Owner: &Owner{ 55 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 56 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 57 | }, 58 | AccessControlList: []ACLGrant{ 59 | { 60 | Grantee: &ACLGrantee{ 61 | Type: "RootAccount", 62 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 63 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 64 | }, 65 | Permission: "FULL_CONTROL", 66 | }, 67 | { 68 | Grantee: &ACLGrantee{ 69 | Type: "RootAccount", 70 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 71 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 72 | }, 73 | Permission: "READ", 74 | }, 75 | }, 76 | } 77 | 78 | if !reflect.DeepEqual(ref, want) { 79 | t.Errorf("Object.GetACL returned %+v, want %+v", ref, want) 80 | } 81 | } 82 | 83 | func TestObjectService_PutACL_with_header_opt(t *testing.T) { 84 | setup() 85 | defer teardown() 86 | 87 | opt := &ObjectPutACLOptions{ 88 | Header: &ACLHeaderOptions{ 89 | XCosACL: "private", 90 | }, 91 | } 92 | name := "test/hello.txt" 93 | 94 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 95 | 96 | testMethod(t, r, http.MethodPut) 97 | vs := values{ 98 | "acl": "", 99 | } 100 | testFormValues(t, r, vs) 101 | testHeader(t, r, "x-cos-acl", "private") 102 | 103 | want := 0 104 | v, _ := r.Body.Read([]byte{}) 105 | if !reflect.DeepEqual(v, want) { 106 | t.Errorf("Object.PutACL request body: %#v, want %#v", v, want) 107 | } 108 | }) 109 | 110 | _, err := client.Object.PutACL(context.Background(), name, opt) 111 | if err != nil { 112 | t.Fatalf("Object.PutACL returned error: %v", err) 113 | } 114 | 115 | } 116 | 117 | func TestObjectService_PutACL_with_body_opt(t *testing.T) { 118 | setup() 119 | defer teardown() 120 | 121 | opt := &ObjectPutACLOptions{ 122 | Body: &ACLXml{ 123 | Owner: &Owner{ 124 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 125 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 126 | }, 127 | AccessControlList: []ACLGrant{ 128 | { 129 | Grantee: &ACLGrantee{ 130 | Type: "RootAccount", 131 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 132 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 133 | }, 134 | 135 | Permission: "FULL_CONTROL", 136 | }, 137 | { 138 | Grantee: &ACLGrantee{ 139 | Type: "RootAccount", 140 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 141 | DisplayName: "qcs::cam::uin/100000760461:uin/100000760461", 142 | }, 143 | Permission: "READ", 144 | }, 145 | }, 146 | }, 147 | } 148 | name := "test/hello.txt" 149 | 150 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 151 | v := new(ACLXml) 152 | xml.NewDecoder(r.Body).Decode(v) 153 | 154 | testMethod(t, r, http.MethodPut) 155 | vs := values{ 156 | "acl": "", 157 | } 158 | testFormValues(t, r, vs) 159 | testHeader(t, r, "x-cos-acl", "") 160 | 161 | want := opt.Body 162 | want.XMLName = xml.Name{Local: "AccessControlPolicy"} 163 | if !reflect.DeepEqual(v, want) { 164 | t.Errorf("Object.PutACL request body: %+v, want %+v", v, want) 165 | } 166 | 167 | }) 168 | 169 | _, err := client.Object.PutACL(context.Background(), name, opt) 170 | if err != nil { 171 | t.Fatalf("Object.PutACL returned error: %v", err) 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /object_part.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | // InitiateMultipartUploadOptions ... 12 | type InitiateMultipartUploadOptions struct { 13 | *ACLHeaderOptions 14 | *ObjectPutHeaderOptions 15 | } 16 | 17 | // InitiateMultipartUploadResult ... 18 | type InitiateMultipartUploadResult struct { 19 | XMLName xml.Name `xml:"InitiateMultipartUploadResult"` 20 | // 分片上传的目标 Bucket,由用户自定义字符串和系统生成appid数字串由中划线连接而成,如:mybucket-1250000000 21 | Bucket string 22 | // Object 的名称 23 | Key string 24 | // 在后续上传中使用的 ID 25 | UploadID string `xml:"UploadId"` 26 | } 27 | 28 | // MethodObjectInitiateMultipartUpload method name of Object.InitiateMultipartUpload 29 | const MethodObjectInitiateMultipartUpload MethodName = "Object.InitiateMultipartUpload" 30 | 31 | // InitiateMultipartUpload ... 32 | // 33 | // Initiate Multipart Upload请求实现初始化分片上传,成功执行此请求以后会返回Upload ID用于后续的Upload Part请求。 34 | // 35 | // https://cloud.tencent.com/document/product/436/7746 36 | func (s *ObjectService) InitiateMultipartUpload(ctx context.Context, name string, opt *InitiateMultipartUploadOptions) (*InitiateMultipartUploadResult, *Response, error) { 37 | var res InitiateMultipartUploadResult 38 | sendOpt := sendOptions{ 39 | baseURL: s.client.BaseURL.BucketURL, 40 | uri: "/" + encodeURIComponent(name) + "?uploads", 41 | method: http.MethodPost, 42 | optHeader: opt, 43 | result: &res, 44 | caller: Caller{ 45 | Method: MethodObjectInitiateMultipartUpload, 46 | }, 47 | } 48 | resp, err := s.client.send(ctx, &sendOpt) 49 | return &res, resp, err 50 | } 51 | 52 | // ObjectUploadPartOptions ... 53 | type ObjectUploadPartOptions struct { 54 | // RFC 2616 中定义的 HTTP 请求内容长度(字节) 55 | Expect string `header:"Expect,omitempty" url:"-"` 56 | XCosContentSHA1 string `header:"x-cos-content-sha1" url:"-"` 57 | // RFC 1864 中定义的经过Base64编码的128-bit 内容 MD5 校验值。此头部用来校验文件内容是否发生变化 58 | ContentMD5 string `header:"Content-MD5" url:"-"` 59 | // RFC 2616 中定义的 HTTP 请求内容长度(字节) 60 | ContentLength int `header:"Content-Length,omitempty" url:"-"` 61 | } 62 | 63 | // MethodObjectUploadPart method name of Object.UploadPart 64 | const MethodObjectUploadPart MethodName = "Object.UploadPart" 65 | 66 | // UploadPart ... 67 | // 68 | // Upload Part 接口请求实现将对象按照分块的方式上传到 COS。 69 | // 最多支持 10000 分块,每个分块大小为 1 MB 到 5 GB ,最后一个分块可以小于 1 MB。 70 | // 71 | // 细节分析 72 | // 73 | // * 分块上传首先需要进行初始化,使用 Initiate Multipart Upload 接口实现,初始化后会得到一个 uploadId ,唯一标识本次上传; 74 | // * 在每次请求 Upload Part 时,需要携带 partNumber 和 uploadId,partNumber 为块的编号,支持乱序上传; 75 | // * 当传入 uploadId 和 partNumber 都相同的时候,后传入的块将覆盖之前传入的块。当 uploadId 不存在时会返回 404 错误,NoSuchUpload。 76 | // 77 | // 当 r 是个 io.ReadCloser 时 UploadPart 方法不会自动调用 r.Close(),用户需要自行选择合适的时机去调用 r.Close() 方法对 r 进行资源回收 78 | // 79 | // https://cloud.tencent.com/document/product/436/7750 80 | func (s *ObjectService) UploadPart(ctx context.Context, name, uploadID string, partNumber int, r io.Reader, opt *ObjectUploadPartOptions) (*Response, error) { 81 | u := fmt.Sprintf("/%s?partNumber=%d&uploadId=%s", encodeURIComponent(name), partNumber, uploadID) 82 | sendOpt := sendOptions{ 83 | baseURL: s.client.BaseURL.BucketURL, 84 | uri: u, 85 | method: http.MethodPut, 86 | optHeader: opt, 87 | body: r, 88 | caller: Caller{ 89 | Method: MethodObjectUploadPart, 90 | }, 91 | } 92 | resp, err := s.client.send(ctx, &sendOpt) 93 | return resp, err 94 | } 95 | 96 | // ObjectListPartsOptions ... 97 | type ObjectListPartsOptions struct { 98 | // 规定返回值的编码方式 99 | EncodingType string `url:"Encoding-type,omitempty"` 100 | // 单次返回最大的条目数量,默认1000 101 | MaxParts int `url:"max-parts,omitempty"` 102 | // 默认以 UTF-8 二进制顺序列出条目,所有列出条目从 marker 开始 103 | PartNumberMarker int `url:"part-number-marker,omitempty"` 104 | } 105 | 106 | // ObjectListPartsResult ... 107 | // 108 | // https://cloud.tencent.com/document/product/436/7747 109 | type ObjectListPartsResult struct { 110 | XMLName xml.Name `xml:"ListPartsResult"` 111 | // 分块上传的目标 Bucket,存储桶的名字,由用户自定义字符串和系统生成 appid 数字串由中划线连接而成, 112 | // 如:mybucket-1250000000 113 | Bucket string 114 | // 编码格式 115 | EncodingType string `xml:"Encoding-type,omitempty"` 116 | // Object 的名字 117 | Key string 118 | // 标识本次分块上传的 ID 119 | UploadID string `xml:"UploadId"` 120 | // 用来表示这些分块所有者的信息 121 | Initiator *Initiator `xml:"Initiator,omitempty"` 122 | // 用来表示这些分块所有者的信息 123 | Owner *Owner `xml:"Owner,omitempty"` 124 | // 用来表示这些分块的存储级别,枚举值:STANDARD,STANDARD_IA,ARCHIVE 125 | StorageClass string 126 | // 默认以 UTF-8 二进制顺序列出条目,所有列出条目从 marker 开始 127 | PartNumberMarker int 128 | // 假如返回条目被截断,则返回 NextMarker 就是下一个条目的起点 129 | NextPartNumberMarker int `xml:"NextPartNumberMarker,omitempty"` 130 | // 单次返回最大的条目数量 131 | MaxParts int 132 | // 响应请求条目是否被截断,布尔值:true,false 133 | IsTruncated bool 134 | // 元数据信息 135 | Parts []Object `xml:"Part,omitempty"` 136 | } 137 | 138 | // MethodObjectListParts method name of Object.ListParts 139 | const MethodObjectListParts MethodName = "Object.ListParts" 140 | 141 | // ListParts ... 142 | // 143 | // List Parts 用来查询特定分块上传中的已上传的块,即罗列出指定 UploadId 所属的所有已上传成功的分块。 144 | // 145 | // https://cloud.tencent.com/document/product/436/7747 146 | func (s *ObjectService) ListParts(ctx context.Context, name, uploadID string) (*ObjectListPartsResult, *Response, error) { 147 | u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) 148 | var res ObjectListPartsResult 149 | sendOpt := sendOptions{ 150 | baseURL: s.client.BaseURL.BucketURL, 151 | uri: u, 152 | method: http.MethodGet, 153 | result: &res, 154 | caller: Caller{ 155 | Method: MethodObjectListParts, 156 | }, 157 | } 158 | resp, err := s.client.send(ctx, &sendOpt) 159 | return &res, resp, err 160 | } 161 | 162 | // MethodObjectListPartsWithOpt method name of Object.ListPartsWithOpt 163 | const MethodObjectListPartsWithOpt MethodName = "Object.ListPartsWithOpt" 164 | 165 | // ListPartsWithOpt ... 166 | // 167 | // ListParts 方法的补充,解决 ListParts 不支持指定参数的问题。 168 | // List Parts 用来查询特定分块上传中的已上传的块,即罗列出指定 UploadId 所属的所有已上传成功的分块。 169 | // 170 | // https://cloud.tencent.com/document/product/436/7747 171 | func (s *ObjectService) ListPartsWithOpt(ctx context.Context, name, uploadID string, opt *ObjectListPartsOptions) (*ObjectListPartsResult, *Response, error) { 172 | u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) 173 | var res ObjectListPartsResult 174 | sendOpt := sendOptions{ 175 | baseURL: s.client.BaseURL.BucketURL, 176 | uri: u, 177 | method: http.MethodGet, 178 | optQuery: opt, 179 | result: &res, 180 | caller: Caller{ 181 | Method: MethodObjectListPartsWithOpt, 182 | }, 183 | } 184 | resp, err := s.client.send(ctx, &sendOpt) 185 | return &res, resp, err 186 | } 187 | 188 | // CompleteMultipartUploadOptions ... 189 | // 190 | // https://cloud.tencent.com/document/product/436/7742 191 | type CompleteMultipartUploadOptions struct { 192 | XMLName xml.Name `xml:"CompleteMultipartUpload"` 193 | Parts []Object `xml:"Part"` 194 | } 195 | 196 | // CompleteMultipartUploadResult ... 197 | // 198 | // https://cloud.tencent.com/document/product/436/7742 199 | type CompleteMultipartUploadResult struct { 200 | XMLName xml.Name `xml:"CompleteMultipartUploadResult"` 201 | // 创建的Object的外网访问域名 202 | Location string 203 | // 分块上传的目标Bucket,由用户自定义字符串和系统生成appid数字串由中划线连接而成, 204 | // 如:mybucket-1250000000 205 | Bucket string 206 | // Object的名称 207 | Key string 208 | // 合并后对象的唯一标签值,该值不是对象内容的 MD5 校验值,仅能用于检查对象唯一性 209 | ETag string 210 | } 211 | 212 | // MethodObjectCompleteMultipartUpload method name of Object.CompleteMultipartUpload 213 | const MethodObjectCompleteMultipartUpload MethodName = "Object.CompleteMultipartUpload" 214 | 215 | // CompleteMultipartUpload ... 216 | // 217 | // Complete Multipart Upload用来实现完成整个分块上传。当您已经使用Upload Parts上传所有块以后,你可以用该API完成上传。 218 | // 在使用该API时,您必须在Body中给出每一个块的PartNumber和ETag,用来校验块的准确性。 219 | // 220 | // 由于分块上传的合并需要数分钟时间,因而当合并分块开始的时候,COS就立即返回200的状态码,在合并的过程中, 221 | // COS会周期性的返回空格信息来保持连接活跃,直到合并完成,COS会在Body中返回合并后块的内容。 222 | // 223 | // 当上传块小于1 MB的时候,在调用该请求时,会返回400 EntityTooSmall; 224 | // 当上传块编号不连续的时候,在调用该请求时,会返回400 InvalidPart; 225 | // 当请求Body中的块信息没有按序号从小到大排列的时候,在调用该请求时,会返回400 InvalidPartOrder; 226 | // 当UploadId不存在的时候,在调用该请求时,会返回404 NoSuchUpload。 227 | // 228 | // 建议您及时完成分块上传或者舍弃分块上传,因为已上传但是未终止的块会占用存储空间进而产生存储费用。 229 | // 230 | // https://cloud.tencent.com/document/product/436/7742 231 | func (s *ObjectService) CompleteMultipartUpload(ctx context.Context, name, uploadID string, opt *CompleteMultipartUploadOptions) (*CompleteMultipartUploadResult, *Response, error) { 232 | u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) 233 | var res CompleteMultipartUploadResult 234 | sendOpt := sendOptions{ 235 | baseURL: s.client.BaseURL.BucketURL, 236 | uri: u, 237 | method: http.MethodPost, 238 | body: opt, 239 | result: &res, 240 | caller: Caller{ 241 | Method: MethodObjectCompleteMultipartUpload, 242 | }, 243 | } 244 | resp, err := s.client.send(ctx, &sendOpt) 245 | return &res, resp, err 246 | } 247 | 248 | // MethodObjectAbortMultipartUpload method name of Object.AbortMultipartUpload 249 | const MethodObjectAbortMultipartUpload MethodName = "Object.AbortMultipartUpload" 250 | 251 | // AbortMultipartUpload ... 252 | // 253 | // Abort Multipart Upload 用来实现舍弃一个分块上传并删除已上传的块。当您调用 Abort Multipart Upload 时, 254 | // 如果有正在使用这个Upload Parts上传块的请求,则Upload Parts会返回失败。当该UploadID不存在时,会返回404 NoSuchUpload。 255 | // 256 | // 建议您及时完成分块上传或者舍弃分块上传,因为已上传但是未终止的块会占用存储空间进而产生存储费用。 257 | // 258 | // https://cloud.tencent.com/document/product/436/7740 259 | func (s *ObjectService) AbortMultipartUpload(ctx context.Context, name, uploadID string) (*Response, error) { 260 | u := fmt.Sprintf("/%s?uploadId=%s", encodeURIComponent(name), uploadID) 261 | sendOpt := sendOptions{ 262 | baseURL: s.client.BaseURL.BucketURL, 263 | uri: u, 264 | method: http.MethodDelete, 265 | caller: Caller{ 266 | Method: MethodObjectAbortMultipartUpload, 267 | }, 268 | } 269 | resp, err := s.client.send(ctx, &sendOpt) 270 | return resp, err 271 | } 272 | -------------------------------------------------------------------------------- /object_part_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/xml" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | func TestObjectService_AbortMultipartUpload(t *testing.T) { 15 | setup() 16 | defer teardown() 17 | name := "test/hello.txt" 18 | uploadID := "xxxxaabcc" 19 | 20 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 21 | testMethod(t, r, http.MethodDelete) 22 | vs := values{ 23 | "uploadId": uploadID, 24 | } 25 | testFormValues(t, r, vs) 26 | 27 | w.WriteHeader(http.StatusNoContent) 28 | }) 29 | 30 | _, err := client.Object.AbortMultipartUpload(context.Background(), 31 | name, uploadID) 32 | if err != nil { 33 | t.Fatalf("Object.AbortMultipartUpload returned error: %v", err) 34 | } 35 | } 36 | 37 | func TestObjectService_InitiateMultipartUpload(t *testing.T) { 38 | setup() 39 | defer teardown() 40 | 41 | opt := &InitiateMultipartUploadOptions{ 42 | ObjectPutHeaderOptions: &ObjectPutHeaderOptions{ 43 | ContentType: "text/html", 44 | }, 45 | ACLHeaderOptions: &ACLHeaderOptions{ 46 | XCosACL: "private", 47 | }, 48 | } 49 | name := "test/hello.txt" 50 | 51 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 52 | v := new(BucketPutTaggingOptions) 53 | xml.NewDecoder(r.Body).Decode(v) 54 | 55 | testMethod(t, r, http.MethodPost) 56 | testHeader(t, r, "x-cos-acl", "private") 57 | testHeader(t, r, "Content-Type", "text/html") 58 | vs := values{ 59 | "uploads": "", 60 | } 61 | testFormValues(t, r, vs) 62 | fmt.Fprint(w, ` 63 | test-1253846586 64 | test/hello.txt 65 | 149795166761115ef06e259b2fceb8ff34bf7dd840883d26a0f90243562dd398efa41718db 66 | `) 67 | }) 68 | 69 | ref, _, err := client.Object.InitiateMultipartUpload(context.Background(), 70 | name, opt) 71 | if err != nil { 72 | t.Fatalf("Object.InitiateMultipartUpload returned error: %v", err) 73 | } 74 | 75 | want := &InitiateMultipartUploadResult{ 76 | XMLName: xml.Name{Local: "InitiateMultipartUploadResult"}, 77 | Bucket: "test-1253846586", 78 | Key: "test/hello.txt", 79 | UploadID: "149795166761115ef06e259b2fceb8ff34bf7dd840883d26a0f90243562dd398efa41718db", 80 | } 81 | 82 | if !reflect.DeepEqual(ref, want) { 83 | t.Errorf("Object.InitiateMultipartUpload returned %+v, want %+v", ref, want) 84 | } 85 | } 86 | 87 | func TestObjectService_UploadPart(t *testing.T) { 88 | setup() 89 | defer teardown() 90 | 91 | opt := &ObjectUploadPartOptions{} 92 | name := "test/hello.txt" 93 | uploadID := "xxxxx" 94 | partNumber := 1 95 | 96 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 97 | testMethod(t, r, http.MethodPut) 98 | vs := values{ 99 | "uploadId": uploadID, 100 | "partNumber": "1", 101 | } 102 | testFormValues(t, r, vs) 103 | 104 | b, _ := ioutil.ReadAll(r.Body) 105 | v := string(b) 106 | want := "hello" 107 | if !reflect.DeepEqual(v, want) { 108 | t.Errorf("Object.UploadPart request body: %#v, want %#v", v, want) 109 | } 110 | }) 111 | 112 | r := bytes.NewReader([]byte("hello")) 113 | _, err := client.Object.UploadPart(context.Background(), 114 | name, uploadID, partNumber, r, opt) 115 | if err != nil { 116 | t.Fatalf("Object.UploadPart returned error: %v", err) 117 | } 118 | 119 | } 120 | 121 | func TestObjectService_ListParts(t *testing.T) { 122 | setup() 123 | defer teardown() 124 | 125 | name := "test/hello.txt" 126 | uploadID := "149795194893578fd83aceef3a88f708f81f00e879fda5ea8a80bf15aba52746d42d512387" 127 | 128 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 129 | v := new(BucketPutTaggingOptions) 130 | xml.NewDecoder(r.Body).Decode(v) 131 | 132 | testMethod(t, r, http.MethodGet) 133 | vs := values{ 134 | "uploadId": uploadID, 135 | } 136 | testFormValues(t, r, vs) 137 | 138 | fmt.Fprint(w, ` 139 | test-1253846586 140 | 141 | test/hello.txt 142 | 149795194893578fd83aceef3a88f708f81f00e879fda5ea8a80bf15aba52746d42d512387 143 | 144 | 1253846586 145 | 1253846586 146 | 147 | 0 148 | 149 | qcs::cam::uin/100000760461:uin/100000760461 150 | 100000760461 151 | 152 | 153 | 1 154 | 2017-06-20T09:45:49.000Z 155 | "fae3dba15f4d9b2d76cbaed5de3a08e3" 156 | 6291456 157 | 158 | 159 | 2 160 | 2017-06-20T09:45:50.000Z 161 | "c81982550f2f965118d486176d9541d4" 162 | 6391456 163 | 164 | Standard 165 | 1000 166 | false 167 | `) 168 | }) 169 | 170 | ref, _, err := client.Object.ListParts(context.Background(), 171 | name, uploadID) 172 | if err != nil { 173 | t.Fatalf("Object.ListParts returned error: %v", err) 174 | } 175 | 176 | want := &ObjectListPartsResult{ 177 | XMLName: xml.Name{Local: "ListPartsResult"}, 178 | Bucket: "test-1253846586", 179 | UploadID: uploadID, 180 | Key: name, 181 | Owner: &Owner{ 182 | ID: "1253846586", 183 | DisplayName: "1253846586", 184 | }, 185 | PartNumberMarker: 0, 186 | Initiator: &Initiator{ 187 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 188 | DisplayName: "100000760461", 189 | }, 190 | Parts: []Object{ 191 | { 192 | PartNumber: 1, 193 | LastModified: "2017-06-20T09:45:49.000Z", 194 | ETag: "\"fae3dba15f4d9b2d76cbaed5de3a08e3\"", 195 | Size: 6291456, 196 | }, 197 | { 198 | PartNumber: 2, 199 | LastModified: "2017-06-20T09:45:50.000Z", 200 | ETag: "\"c81982550f2f965118d486176d9541d4\"", 201 | Size: 6391456, 202 | }, 203 | }, 204 | StorageClass: "Standard", 205 | MaxParts: 1000, 206 | IsTruncated: false, 207 | } 208 | 209 | if !reflect.DeepEqual(ref, want) { 210 | t.Errorf("Object.ListParts returned \n%#v, want \n%#v", ref, want) 211 | } 212 | } 213 | 214 | func TestObjectService_ListPartsWithOpt(t *testing.T) { 215 | setup() 216 | defer teardown() 217 | 218 | name := "test/hello.txt" 219 | uploadID := "149795194893578fd83aceef3a88f708f81f00e879fda5ea8a80bf15aba52746d42d512387" 220 | 221 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 222 | v := new(BucketPutTaggingOptions) 223 | xml.NewDecoder(r.Body).Decode(v) 224 | 225 | testMethod(t, r, http.MethodGet) 226 | vs := values{ 227 | "uploadId": uploadID, 228 | "max-parts": "2", 229 | } 230 | testFormValues(t, r, vs) 231 | 232 | fmt.Fprint(w, ` 233 | test-1253846586 234 | 235 | test/hello.txt 236 | 149795194893578fd83aceef3a88f708f81f00e879fda5ea8a80bf15aba52746d42d512387 237 | 238 | 1253846586 239 | 1253846586 240 | 241 | 0 242 | 243 | qcs::cam::uin/100000760461:uin/100000760461 244 | 100000760461 245 | 246 | 247 | 1 248 | 2017-06-20T09:45:49.000Z 249 | "fae3dba15f4d9b2d76cbaed5de3a08e3" 250 | 6291456 251 | 252 | 253 | 2 254 | 2017-06-20T09:45:50.000Z 255 | "c81982550f2f965118d486176d9541d4" 256 | 6391456 257 | 258 | Standard 259 | 2 260 | 2 261 | true 262 | `) 263 | }) 264 | 265 | opt := &ObjectListPartsOptions{ 266 | MaxParts: 2, 267 | } 268 | ref, _, err := client.Object.ListPartsWithOpt(context.Background(), 269 | name, uploadID, opt) 270 | if err != nil { 271 | t.Fatalf("Object.ListParts returned error: %v", err) 272 | } 273 | 274 | want := &ObjectListPartsResult{ 275 | XMLName: xml.Name{Local: "ListPartsResult"}, 276 | Bucket: "test-1253846586", 277 | UploadID: uploadID, 278 | Key: name, 279 | Owner: &Owner{ 280 | ID: "1253846586", 281 | DisplayName: "1253846586", 282 | }, 283 | NextPartNumberMarker: 2, 284 | PartNumberMarker: 0, 285 | Initiator: &Initiator{ 286 | ID: "qcs::cam::uin/100000760461:uin/100000760461", 287 | DisplayName: "100000760461", 288 | }, 289 | Parts: []Object{ 290 | { 291 | PartNumber: 1, 292 | LastModified: "2017-06-20T09:45:49.000Z", 293 | ETag: "\"fae3dba15f4d9b2d76cbaed5de3a08e3\"", 294 | Size: 6291456, 295 | }, 296 | { 297 | PartNumber: 2, 298 | LastModified: "2017-06-20T09:45:50.000Z", 299 | ETag: "\"c81982550f2f965118d486176d9541d4\"", 300 | Size: 6391456, 301 | }, 302 | }, 303 | StorageClass: "Standard", 304 | MaxParts: 2, 305 | IsTruncated: true, 306 | } 307 | 308 | if !reflect.DeepEqual(ref, want) { 309 | t.Errorf("Object.ListParts returned \n%#v, want \n%#v", ref, want) 310 | } 311 | } 312 | 313 | func TestObjectService_CompleteMultipartUpload(t *testing.T) { 314 | setup() 315 | defer teardown() 316 | name := "test/hello.txt" 317 | uploadID := "149795194893578fd83aceef3a88f708f81f00e879fda5ea8a80bf15aba52746d42d512387" 318 | 319 | opt := &CompleteMultipartUploadOptions{ 320 | Parts: []Object{ 321 | { 322 | PartNumber: 1, 323 | ETag: "\"fae3dba15f4d9b2d76cbaed5de3a08e3\"", 324 | }, 325 | { 326 | PartNumber: 2, 327 | ETag: "\"c81982550f2f965118d486176d9541d4\"", 328 | }, 329 | }, 330 | } 331 | 332 | mux.HandleFunc("/test/hello.txt", func(w http.ResponseWriter, r *http.Request) { 333 | v := new(CompleteMultipartUploadOptions) 334 | xml.NewDecoder(r.Body).Decode(v) 335 | 336 | testMethod(t, r, http.MethodPost) 337 | vs := values{ 338 | "uploadId": uploadID, 339 | } 340 | testFormValues(t, r, vs) 341 | 342 | want := opt 343 | want.XMLName = xml.Name{Local: "CompleteMultipartUpload"} 344 | if !reflect.DeepEqual(v, want) { 345 | t.Errorf("Object.CompleteMultipartUpload request body: %+v, want %+v", v, want) 346 | } 347 | fmt.Fprint(w, ` 348 | test-1253846586.cn-north.myqcloud.com/test/hello.txt 349 | test 350 | test/hello.txt 351 | "594f98b11c6901c0f0683de1085a6d0e-4" 352 | `) 353 | }) 354 | 355 | ref, _, err := client.Object.CompleteMultipartUpload(context.Background(), 356 | name, uploadID, opt) 357 | if err != nil { 358 | t.Fatalf("Object.ListParts returned error: %v", err) 359 | } 360 | 361 | want := &CompleteMultipartUploadResult{ 362 | XMLName: xml.Name{Local: "CompleteMultipartUploadResult"}, 363 | Bucket: "test", 364 | Key: name, 365 | ETag: "\"594f98b11c6901c0f0683de1085a6d0e-4\"", 366 | Location: "test-1253846586.cn-north.myqcloud.com/test/hello.txt", 367 | } 368 | 369 | if !reflect.DeepEqual(ref, want) { 370 | t.Errorf("Object.CompleteMultipartUpload returned \n%#v, want \n%#v", ref, want) 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | ) 8 | 9 | // ServiceService ... 10 | // 11 | // Service 相关 API 12 | type ServiceService service 13 | 14 | // ServiceGetResult ... 15 | type ServiceGetResult struct { 16 | XMLName xml.Name `xml:"ListAllMyBucketsResult"` 17 | Owner *Owner `xml:"Owner"` 18 | Buckets []Bucket `xml:"Buckets>Bucket,omitempty"` 19 | } 20 | 21 | // MethodServiceGet method name of Service.Get 22 | const MethodServiceGet MethodName = "Service.Get" 23 | 24 | // Get Service 接口是用来获取请求者名下的所有存储空间列表(Bucket list)。 25 | // 26 | // https://cloud.tencent.com/document/product/436/8291 27 | func (s *ServiceService) Get(ctx context.Context) (*ServiceGetResult, *Response, error) { 28 | var res ServiceGetResult 29 | sendOpt := sendOptions{ 30 | baseURL: s.client.BaseURL.ServiceURL, 31 | uri: "/", 32 | method: http.MethodGet, 33 | result: &res, 34 | caller: Caller{ 35 | Method: MethodServiceGet, 36 | }, 37 | } 38 | resp, err := s.client.send(ctx, &sendOpt) 39 | return &res, resp, err 40 | } 41 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestServiceService_Get(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | 16 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | testMethod(t, r, "GET") 18 | fmt.Fprint(w, ` 19 | 20 | xbaccxx 21 | 100000760461 22 | 23 | 24 | 25 | huadong-1253846586 26 | cn-east 27 | 2017-06-16T13:08:28Z 28 | 29 | 30 | huanan-1253846586 31 | cn-south 32 | 2017-06-10T09:00:07Z 33 | 34 | 35 | `) 36 | }) 37 | 38 | ref, _, err := client.Service.Get(context.Background()) 39 | if err != nil { 40 | t.Fatalf("Service.Get returned error: %v", err) 41 | } 42 | 43 | want := &ServiceGetResult{ 44 | XMLName: xml.Name{Local: "ListAllMyBucketsResult"}, 45 | Owner: &Owner{ 46 | ID: "xbaccxx", 47 | DisplayName: "100000760461", 48 | }, 49 | Buckets: []Bucket{ 50 | { 51 | Name: "huadong-1253846586", 52 | Region: "cn-east", 53 | CreateDate: "2017-06-16T13:08:28Z", 54 | }, 55 | { 56 | Name: "huanan-1253846586", 57 | Region: "cn-south", 58 | CreateDate: "2017-06-10T09:00:07Z", 59 | }, 60 | }, 61 | } 62 | 63 | if !reflect.DeepEqual(ref, want) { 64 | t.Errorf("Service.Get returned %+v, want %+v", ref, want) 65 | } 66 | } 67 | --------------------------------------------------------------------------------