You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1213 lines
38 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. package cos
  2. import (
  3. "context"
  4. "crypto/md5"
  5. "encoding/xml"
  6. "errors"
  7. "fmt"
  8. "hash/crc64"
  9. "io"
  10. "io/ioutil"
  11. "net/http"
  12. "net/url"
  13. "os"
  14. "sort"
  15. "strconv"
  16. "strings"
  17. "time"
  18. )
  19. // ObjectService 相关 API
  20. type ObjectService service
  21. // ObjectGetOptions is the option of GetObject
  22. type ObjectGetOptions struct {
  23. ResponseContentType string `url:"response-content-type,omitempty" header:"-"`
  24. ResponseContentLanguage string `url:"response-content-language,omitempty" header:"-"`
  25. ResponseExpires string `url:"response-expires,omitempty" header:"-"`
  26. ResponseCacheControl string `url:"response-cache-control,omitempty" header:"-"`
  27. ResponseContentDisposition string `url:"response-content-disposition,omitempty" header:"-"`
  28. ResponseContentEncoding string `url:"response-content-encoding,omitempty" header:"-"`
  29. Range string `url:"-" header:"Range,omitempty"`
  30. IfModifiedSince string `url:"-" header:"If-Modified-Since,omitempty"`
  31. // SSE-C
  32. XCosSSECustomerAglo string `header:"x-cos-server-side-encryption-customer-algorithm,omitempty" url:"-" xml:"-"`
  33. XCosSSECustomerKey string `header:"x-cos-server-side-encryption-customer-key,omitempty" url:"-" xml:"-"`
  34. XCosSSECustomerKeyMD5 string `header:"x-cos-server-side-encryption-customer-key-MD5,omitempty" url:"-" xml:"-"`
  35. //兼容其他自定义头部
  36. XOptionHeader *http.Header `header:"-,omitempty" url:"-" xml:"-"`
  37. XCosTrafficLimit int `header:"x-cos-traffic-limit,omitempty" url:"-" xml:"-"`
  38. // 下载进度, ProgressCompleteEvent不能表示对应API调用成功,API是否调用成功的判断标准为返回err==nil
  39. Listener ProgressListener `header:"-" url:"-" xml:"-"`
  40. }
  41. // presignedURLTestingOptions is the opt of presigned url
  42. type presignedURLTestingOptions struct {
  43. authTime *AuthTime
  44. }
  45. // Get Object 请求可以将一个文件(Object)下载至本地。
  46. // 该操作需要对目标 Object 具有读权限或目标 Object 对所有人都开放了读权限(公有读)。
  47. //
  48. // https://www.qcloud.com/document/product/436/7753
  49. func (s *ObjectService) Get(ctx context.Context, name string, opt *ObjectGetOptions, id ...string) (*Response, error) {
  50. var u string
  51. if len(id) == 1 {
  52. u = fmt.Sprintf("/%s?versionId=%s", encodeURIComponent(name), id[0])
  53. } else if len(id) == 0 {
  54. u = "/" + encodeURIComponent(name)
  55. } else {
  56. return nil, errors.New("wrong params")
  57. }
  58. sendOpt := sendOptions{
  59. baseURL: s.client.BaseURL.BucketURL,
  60. uri: u,
  61. method: http.MethodGet,
  62. optQuery: opt,
  63. optHeader: opt,
  64. disableCloseBody: true,
  65. }
  66. resp, err := s.client.send(ctx, &sendOpt)
  67. if opt != nil && opt.Listener != nil {
  68. if err == nil && resp != nil {
  69. if totalBytes, e := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64); e == nil {
  70. resp.Body = TeeReader(resp.Body, nil, totalBytes, opt.Listener)
  71. }
  72. }
  73. }
  74. return resp, err
  75. }
  76. // GetToFile download the object to local file
  77. func (s *ObjectService) GetToFile(ctx context.Context, name, localpath string, opt *ObjectGetOptions, id ...string) (*Response, error) {
  78. resp, err := s.Get(ctx, name, opt, id...)
  79. if err != nil {
  80. return resp, err
  81. }
  82. defer resp.Body.Close()
  83. // If file exist, overwrite it
  84. fd, err := os.OpenFile(localpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
  85. if err != nil {
  86. return resp, err
  87. }
  88. _, err = io.Copy(fd, resp.Body)
  89. fd.Close()
  90. if err != nil {
  91. return resp, err
  92. }
  93. return resp, nil
  94. }
  95. // GetPresignedURL get the object presigned to down or upload file by url
  96. func (s *ObjectService) GetPresignedURL(ctx context.Context, httpMethod, name, ak, sk string, expired time.Duration, opt interface{}) (*url.URL, error) {
  97. sendOpt := sendOptions{
  98. baseURL: s.client.BaseURL.BucketURL,
  99. uri: "/" + encodeURIComponent(name),
  100. method: httpMethod,
  101. optQuery: opt,
  102. optHeader: opt,
  103. }
  104. req, err := s.client.newRequest(ctx, sendOpt.baseURL, sendOpt.uri, sendOpt.method, sendOpt.body, sendOpt.optQuery, sendOpt.optHeader)
  105. if err != nil {
  106. return nil, err
  107. }
  108. var authTime *AuthTime
  109. if opt != nil {
  110. if opt, ok := opt.(*presignedURLTestingOptions); ok {
  111. authTime = opt.authTime
  112. }
  113. }
  114. if authTime == nil {
  115. authTime = NewAuthTime(expired)
  116. }
  117. authorization := newAuthorization(ak, sk, req, authTime)
  118. sign := encodeURIComponent(authorization, []byte{'&', '='})
  119. if req.URL.RawQuery == "" {
  120. req.URL.RawQuery = fmt.Sprintf("%s", sign)
  121. } else {
  122. req.URL.RawQuery = fmt.Sprintf("%s&%s", req.URL.RawQuery, sign)
  123. }
  124. return req.URL, nil
  125. }
  126. // ObjectPutHeaderOptions the options of header of the put object
  127. type ObjectPutHeaderOptions struct {
  128. CacheControl string `header:"Cache-Control,omitempty" url:"-"`
  129. ContentDisposition string `header:"Content-Disposition,omitempty" url:"-"`
  130. ContentEncoding string `header:"Content-Encoding,omitempty" url:"-"`
  131. ContentType string `header:"Content-Type,omitempty" url:"-"`
  132. ContentMD5 string `header:"Content-MD5,omitempty" url:"-"`
  133. ContentLength int64 `header:"Content-Length,omitempty" url:"-"`
  134. ContentLanguage string `header:"Content-Language,omitempty" url:"-"`
  135. Expect string `header:"Expect,omitempty" url:"-"`
  136. Expires string `header:"Expires,omitempty" url:"-"`
  137. XCosContentSHA1 string `header:"x-cos-content-sha1,omitempty" url:"-"`
  138. // 自定义的 x-cos-meta-* header
  139. XCosMetaXXX *http.Header `header:"x-cos-meta-*,omitempty" url:"-"`
  140. XCosStorageClass string `header:"x-cos-storage-class,omitempty" url:"-"`
  141. // 可选值: Normal, Appendable
  142. //XCosObjectType string `header:"x-cos-object-type,omitempty" url:"-"`
  143. // Enable Server Side Encryption, Only supported: AES256
  144. XCosServerSideEncryption string `header:"x-cos-server-side-encryption,omitempty" url:"-" xml:"-"`
  145. // SSE-C
  146. XCosSSECustomerAglo string `header:"x-cos-server-side-encryption-customer-algorithm,omitempty" url:"-" xml:"-"`
  147. XCosSSECustomerKey string `header:"x-cos-server-side-encryption-customer-key,omitempty" url:"-" xml:"-"`
  148. XCosSSECustomerKeyMD5 string `header:"x-cos-server-side-encryption-customer-key-MD5,omitempty" url:"-" xml:"-"`
  149. //兼容其他自定义头部
  150. XOptionHeader *http.Header `header:"-,omitempty" url:"-" xml:"-"`
  151. XCosTrafficLimit int `header:"x-cos-traffic-limit,omitempty" url:"-" xml:"-"`
  152. // 上传进度, ProgressCompleteEvent不能表示对应API调用成功,API是否调用成功的判断标准为返回err==nil
  153. Listener ProgressListener `header:"-" url:"-" xml:"-"`
  154. }
  155. // ObjectPutOptions the options of put object
  156. type ObjectPutOptions struct {
  157. *ACLHeaderOptions `header:",omitempty" url:"-" xml:"-"`
  158. *ObjectPutHeaderOptions `header:",omitempty" url:"-" xml:"-"`
  159. }
  160. // Put Object请求可以将一个文件(Oject)上传至指定Bucket。
  161. //
  162. // https://www.qcloud.com/document/product/436/7749
  163. func (s *ObjectService) Put(ctx context.Context, name string, r io.Reader, uopt *ObjectPutOptions) (*Response, error) {
  164. if r == nil {
  165. return nil, fmt.Errorf("reader is nil")
  166. }
  167. if err := CheckReaderLen(r); err != nil {
  168. return nil, err
  169. }
  170. opt := cloneObjectPutOptions(uopt)
  171. totalBytes, err := GetReaderLen(r)
  172. if err != nil && opt != nil && opt.Listener != nil {
  173. return nil, err
  174. }
  175. if err == nil {
  176. // 与 go http 保持一致, 非bytes.Buffer/bytes.Reader/strings.Reader由用户指定ContentLength, 或使用 Chunk 上传
  177. if opt != nil && opt.ContentLength == 0 && IsLenReader(r) {
  178. opt.ContentLength = totalBytes
  179. }
  180. }
  181. reader := TeeReader(r, nil, totalBytes, nil)
  182. if s.client.Conf.EnableCRC {
  183. reader.writer = crc64.New(crc64.MakeTable(crc64.ECMA))
  184. }
  185. if opt != nil && opt.Listener != nil {
  186. reader.listener = opt.Listener
  187. }
  188. sendOpt := sendOptions{
  189. baseURL: s.client.BaseURL.BucketURL,
  190. uri: "/" + encodeURIComponent(name),
  191. method: http.MethodPut,
  192. body: reader,
  193. optHeader: opt,
  194. }
  195. resp, err := s.client.send(ctx, &sendOpt)
  196. return resp, err
  197. }
  198. // PutFromFile put object from local file
  199. // Notice that when use this put large file need set non-body of debug req/resp, otherwise will out of memory
  200. func (s *ObjectService) PutFromFile(ctx context.Context, name string, filePath string, opt *ObjectPutOptions) (resp *Response, err error) {
  201. nr := 0
  202. for nr < 3 {
  203. fd, e := os.Open(filePath)
  204. if e != nil {
  205. err = e
  206. return
  207. }
  208. resp, err = s.Put(ctx, name, fd, opt)
  209. if err != nil {
  210. nr++
  211. fd.Close()
  212. continue
  213. }
  214. fd.Close()
  215. break
  216. }
  217. return
  218. }
  219. // ObjectCopyHeaderOptions is the head option of the Copy
  220. type ObjectCopyHeaderOptions struct {
  221. // When use replace directive to update meta infos
  222. CacheControl string `header:"Cache-Control,omitempty" url:"-"`
  223. ContentDisposition string `header:"Content-Disposition,omitempty" url:"-"`
  224. ContentEncoding string `header:"Content-Encoding,omitempty" url:"-"`
  225. ContentLanguage string `header:"Content-Language,omitempty" url:"-"`
  226. ContentType string `header:"Content-Type,omitempty" url:"-"`
  227. Expires string `header:"Expires,omitempty" url:"-"`
  228. Expect string `header:"Expect,omitempty" url:"-"`
  229. XCosMetadataDirective string `header:"x-cos-metadata-directive,omitempty" url:"-" xml:"-"`
  230. XCosCopySourceIfModifiedSince string `header:"x-cos-copy-source-If-Modified-Since,omitempty" url:"-" xml:"-"`
  231. XCosCopySourceIfUnmodifiedSince string `header:"x-cos-copy-source-If-Unmodified-Since,omitempty" url:"-" xml:"-"`
  232. XCosCopySourceIfMatch string `header:"x-cos-copy-source-If-Match,omitempty" url:"-" xml:"-"`
  233. XCosCopySourceIfNoneMatch string `header:"x-cos-copy-source-If-None-Match,omitempty" url:"-" xml:"-"`
  234. XCosStorageClass string `header:"x-cos-storage-class,omitempty" url:"-" xml:"-"`
  235. // 自定义的 x-cos-meta-* header
  236. XCosMetaXXX *http.Header `header:"x-cos-meta-*,omitempty" url:"-"`
  237. XCosCopySource string `header:"x-cos-copy-source" url:"-" xml:"-"`
  238. XCosServerSideEncryption string `header:"x-cos-server-side-encryption,omitempty" url:"-" xml:"-"`
  239. // SSE-C
  240. XCosSSECustomerAglo string `header:"x-cos-server-side-encryption-customer-algorithm,omitempty" url:"-" xml:"-"`
  241. XCosSSECustomerKey string `header:"x-cos-server-side-encryption-customer-key,omitempty" url:"-" xml:"-"`
  242. XCosSSECustomerKeyMD5 string `header:"x-cos-server-side-encryption-customer-key-MD5,omitempty" url:"-" xml:"-"`
  243. XCosCopySourceSSECustomerAglo string `header:"x-cos-copy-source-server-side-encryption-customer-algorithm,omitempty" url:"-" xml:"-"`
  244. XCosCopySourceSSECustomerKey string `header:"x-cos-copy-source-server-side-encryption-customer-key,omitempty" url:"-" xml:"-"`
  245. XCosCopySourceSSECustomerKeyMD5 string `header:"x-cos-copy-source-server-side-encryption-customer-key-MD5,omitempty" url:"-" xml:"-"`
  246. //兼容其他自定义头部
  247. XOptionHeader *http.Header `header:"-,omitempty" url:"-" xml:"-"`
  248. }
  249. // ObjectCopyOptions is the option of Copy, choose header or body
  250. type ObjectCopyOptions struct {
  251. *ObjectCopyHeaderOptions `header:",omitempty" url:"-" xml:"-"`
  252. *ACLHeaderOptions `header:",omitempty" url:"-" xml:"-"`
  253. }
  254. // ObjectCopyResult is the result of Copy
  255. type ObjectCopyResult struct {
  256. XMLName xml.Name `xml:"CopyObjectResult"`
  257. ETag string `xml:"ETag,omitempty"`
  258. LastModified string `xml:"LastModified,omitempty"`
  259. CRC64 string `xml:"CRC64,omitempty"`
  260. VersionId string `xml:"VersionId,omitempty"`
  261. }
  262. // Copy 调用 PutObjectCopy 请求实现将一个文件从源路径复制到目标路径。建议文件大小 1M 到 5G,
  263. // 超过 5G 的文件请使用分块上传 Upload - Copy。在拷贝的过程中,文件元属性和 ACL 可以被修改。
  264. //
  265. // 用户可以通过该接口实现文件移动,文件重命名,修改文件属性和创建副本。
  266. //
  267. // 注意:在跨帐号复制的时候,需要先设置被复制文件的权限为公有读,或者对目标帐号赋权,同帐号则不需要。
  268. //
  269. // https://cloud.tencent.com/document/product/436/10881
  270. func (s *ObjectService) Copy(ctx context.Context, name, sourceURL string, opt *ObjectCopyOptions, id ...string) (*ObjectCopyResult, *Response, error) {
  271. surl := strings.SplitN(sourceURL, "/", 2)
  272. if len(surl) < 2 {
  273. return nil, nil, errors.New(fmt.Sprintf("x-cos-copy-source format error: %s", sourceURL))
  274. }
  275. var u string
  276. if len(id) == 1 {
  277. u = fmt.Sprintf("%s/%s?versionId=%s", surl[0], encodeURIComponent(surl[1]), id[0])
  278. } else if len(id) == 0 {
  279. u = fmt.Sprintf("%s/%s", surl[0], encodeURIComponent(surl[1]))
  280. } else {
  281. return nil, nil, errors.New("wrong params")
  282. }
  283. var res ObjectCopyResult
  284. copyOpt := &ObjectCopyOptions{
  285. &ObjectCopyHeaderOptions{},
  286. &ACLHeaderOptions{},
  287. }
  288. if opt != nil {
  289. if opt.ObjectCopyHeaderOptions != nil {
  290. *copyOpt.ObjectCopyHeaderOptions = *opt.ObjectCopyHeaderOptions
  291. }
  292. if opt.ACLHeaderOptions != nil {
  293. *copyOpt.ACLHeaderOptions = *opt.ACLHeaderOptions
  294. }
  295. }
  296. copyOpt.XCosCopySource = u
  297. sendOpt := sendOptions{
  298. baseURL: s.client.BaseURL.BucketURL,
  299. uri: "/" + encodeURIComponent(name),
  300. method: http.MethodPut,
  301. body: nil,
  302. optHeader: copyOpt,
  303. result: &res,
  304. }
  305. resp, err := s.client.send(ctx, &sendOpt)
  306. // If the error occurs during the copy operation, the error response is embedded in the 200 OK response. This means that a 200 OK response can contain either a success or an error.
  307. if err == nil && resp.StatusCode == 200 {
  308. if res.ETag == "" {
  309. return &res, resp, errors.New("response 200 OK, but body contains an error")
  310. }
  311. }
  312. return &res, resp, err
  313. }
  314. type ObjectDeleteOptions struct {
  315. // SSE-C
  316. XCosSSECustomerAglo string `header:"x-cos-server-side-encryption-customer-algorithm,omitempty" url:"-" xml:"-"`
  317. XCosSSECustomerKey string `header:"x-cos-server-side-encryption-customer-key,omitempty" url:"-" xml:"-"`
  318. XCosSSECustomerKeyMD5 string `header:"x-cos-server-side-encryption-customer-key-MD5,omitempty" url:"-" xml:"-"`
  319. //兼容其他自定义头部
  320. XOptionHeader *http.Header `header:"-,omitempty" url:"-" xml:"-"`
  321. VersionId string `header:"-" url:"VersionId,omitempty" xml:"-"`
  322. }
  323. // Delete Object请求可以将一个文件(Object)删除。
  324. //
  325. // https://www.qcloud.com/document/product/436/7743
  326. func (s *ObjectService) Delete(ctx context.Context, name string, opt ...*ObjectDeleteOptions) (*Response, error) {
  327. var optHeader *ObjectDeleteOptions
  328. // When use "" string might call the delete bucket interface
  329. if len(name) == 0 {
  330. return nil, errors.New("empty object name")
  331. }
  332. if len(opt) > 0 {
  333. optHeader = opt[0]
  334. }
  335. sendOpt := sendOptions{
  336. baseURL: s.client.BaseURL.BucketURL,
  337. uri: "/" + encodeURIComponent(name),
  338. method: http.MethodDelete,
  339. optHeader: optHeader,
  340. optQuery: optHeader,
  341. }
  342. resp, err := s.client.send(ctx, &sendOpt)
  343. return resp, err
  344. }
  345. // ObjectHeadOptions is the option of HeadObject
  346. type ObjectHeadOptions struct {
  347. IfModifiedSince string `url:"-" header:"If-Modified-Since,omitempty"`
  348. // SSE-C
  349. XCosSSECustomerAglo string `header:"x-cos-server-side-encryption-customer-algorithm,omitempty" url:"-" xml:"-"`
  350. XCosSSECustomerKey string `header:"x-cos-server-side-encryption-customer-key,omitempty" url:"-" xml:"-"`
  351. XCosSSECustomerKeyMD5 string `header:"x-cos-server-side-encryption-customer-key-MD5,omitempty" url:"-" xml:"-"`
  352. XOptionHeader *http.Header `header:"-,omitempty" url:"-" xml:"-"`
  353. }
  354. // Head Object请求可以取回对应Object的元数据,Head的权限与Get的权限一致
  355. //
  356. // https://www.qcloud.com/document/product/436/7745
  357. func (s *ObjectService) Head(ctx context.Context, name string, opt *ObjectHeadOptions, id ...string) (*Response, error) {
  358. var u string
  359. if len(id) == 1 {
  360. u = fmt.Sprintf("/%s?versionId=%s", encodeURIComponent(name), id[0])
  361. } else if len(id) == 0 {
  362. u = "/" + encodeURIComponent(name)
  363. } else {
  364. return nil, errors.New("wrong params")
  365. }
  366. sendOpt := sendOptions{
  367. baseURL: s.client.BaseURL.BucketURL,
  368. uri: u,
  369. method: http.MethodHead,
  370. optHeader: opt,
  371. }
  372. resp, err := s.client.send(ctx, &sendOpt)
  373. if resp != nil && resp.Header["X-Cos-Object-Type"] != nil && resp.Header["X-Cos-Object-Type"][0] == "appendable" {
  374. resp.Header.Add("x-cos-next-append-position", resp.Header["Content-Length"][0])
  375. }
  376. return resp, err
  377. }
  378. // ObjectOptionsOptions is the option of object options
  379. type ObjectOptionsOptions struct {
  380. Origin string `url:"-" header:"Origin"`
  381. AccessControlRequestMethod string `url:"-" header:"Access-Control-Request-Method"`
  382. AccessControlRequestHeaders string `url:"-" header:"Access-Control-Request-Headers,omitempty"`
  383. }
  384. // Options Object请求实现跨域访问的预请求。即发出一个 OPTIONS 请求给服务器以确认是否可以进行跨域操作。
  385. //
  386. // 当CORS配置不存在时,请求返回403 Forbidden。
  387. //
  388. // https://www.qcloud.com/document/product/436/8288
  389. func (s *ObjectService) Options(ctx context.Context, name string, opt *ObjectOptionsOptions) (*Response, error) {
  390. sendOpt := sendOptions{
  391. baseURL: s.client.BaseURL.BucketURL,
  392. uri: "/" + encodeURIComponent(name),
  393. method: http.MethodOptions,
  394. optHeader: opt,
  395. }
  396. resp, err := s.client.send(ctx, &sendOpt)
  397. return resp, err
  398. }
  399. // CASJobParameters support three way: Standard(in 35 hours), Expedited(quick way, in 15 mins), Bulk(in 5-12 hours_
  400. type CASJobParameters struct {
  401. Tier string `xml:"Tier"`
  402. }
  403. // ObjectRestoreOptions is the option of object restore
  404. type ObjectRestoreOptions struct {
  405. XMLName xml.Name `xml:"RestoreRequest"`
  406. Days int `xml:"Days"`
  407. Tier *CASJobParameters `xml:"CASJobParameters"`
  408. }
  409. // PutRestore API can recover an object of type archived by COS archive.
  410. //
  411. // https://cloud.tencent.com/document/product/436/12633
  412. func (s *ObjectService) PostRestore(ctx context.Context, name string, opt *ObjectRestoreOptions) (*Response, error) {
  413. u := fmt.Sprintf("/%s?restore", encodeURIComponent(name))
  414. sendOpt := sendOptions{
  415. baseURL: s.client.BaseURL.BucketURL,
  416. uri: u,
  417. method: http.MethodPost,
  418. body: opt,
  419. }
  420. resp, err := s.client.send(ctx, &sendOpt)
  421. return resp, err
  422. }
  423. // TODO Append 接口在优化未开放使用
  424. //
  425. // Append请求可以将一个文件(Object)以分块追加的方式上传至 Bucket 中。使用Append Upload的文件必须事前被设定为Appendable。
  426. // 当Appendable的文件被执行Put Object的操作以后,文件被覆盖,属性改变为Normal。
  427. //
  428. // 文件属性可以在Head Object操作中被查询到,当您发起Head Object请求时,会返回自定义Header『x-cos-object-type』,该Header只有两个枚举值:Normal或者Appendable。
  429. //
  430. // 追加上传建议文件大小1M - 5G。如果position的值和当前Object的长度不致,COS会返回409错误。
  431. // 如果Append一个Normal的Object,COS会返回409 ObjectNotAppendable。
  432. //
  433. // Appendable的文件不可以被复制,不参与版本管理,不参与生命周期管理,不可跨区域复制。
  434. //
  435. // 当 r 不是 bytes.Buffer/bytes.Reader/strings.Reader 时,必须指定 opt.ObjectPutHeaderOptions.ContentLength
  436. //
  437. // https://www.qcloud.com/document/product/436/7741
  438. // func (s *ObjectService) Append(ctx context.Context, name string, position int, r io.Reader, opt *ObjectPutOptions) (*Response, error) {
  439. // u := fmt.Sprintf("/%s?append&position=%d", encodeURIComponent(name), position)
  440. // if position != 0{
  441. // opt = nil
  442. // }
  443. // sendOpt := sendOptions{
  444. // baseURL: s.client.BaseURL.BucketURL,
  445. // uri: u,
  446. // method: http.MethodPost,
  447. // optHeader: opt,
  448. // body: r,
  449. // }
  450. // resp, err := s.client.send(ctx, &sendOpt)
  451. // return resp, err
  452. // }
  453. // ObjectDeleteMultiOptions is the option of DeleteMulti
  454. type ObjectDeleteMultiOptions struct {
  455. XMLName xml.Name `xml:"Delete" header:"-"`
  456. Quiet bool `xml:"Quiet" header:"-"`
  457. Objects []Object `xml:"Object" header:"-"`
  458. //XCosSha1 string `xml:"-" header:"x-cos-sha1"`
  459. }
  460. // ObjectDeleteMultiResult is the result of DeleteMulti
  461. type ObjectDeleteMultiResult struct {
  462. XMLName xml.Name `xml:"DeleteResult"`
  463. DeletedObjects []Object `xml:"Deleted,omitempty"`
  464. Errors []struct {
  465. Key string `xml:",omitempty"`
  466. Code string `xml:",omitempty"`
  467. Message string `xml:",omitempty"`
  468. VersionId string `xml:",omitempty"`
  469. } `xml:"Error,omitempty"`
  470. }
  471. // DeleteMulti 请求实现批量删除文件,最大支持单次删除1000个文件。
  472. // 对于返回结果,COS提供Verbose和Quiet两种结果模式。Verbose模式将返回每个Object的删除结果;
  473. // Quiet模式只返回报错的Object信息。
  474. // https://www.qcloud.com/document/product/436/8289
  475. func (s *ObjectService) DeleteMulti(ctx context.Context, opt *ObjectDeleteMultiOptions) (*ObjectDeleteMultiResult, *Response, error) {
  476. var res ObjectDeleteMultiResult
  477. sendOpt := sendOptions{
  478. baseURL: s.client.BaseURL.BucketURL,
  479. uri: "/?delete",
  480. method: http.MethodPost,
  481. body: opt,
  482. result: &res,
  483. }
  484. resp, err := s.client.send(ctx, &sendOpt)
  485. return &res, resp, err
  486. }
  487. // Object is the meta info of the object
  488. type Object struct {
  489. Key string `xml:",omitempty"`
  490. ETag string `xml:",omitempty"`
  491. Size int64 `xml:",omitempty"`
  492. PartNumber int `xml:",omitempty"`
  493. LastModified string `xml:",omitempty"`
  494. StorageClass string `xml:",omitempty"`
  495. Owner *Owner `xml:",omitempty"`
  496. VersionId string `xml:",omitempty"`
  497. }
  498. // MultiUploadOptions is the option of the multiupload,
  499. // ThreadPoolSize default is one
  500. type MultiUploadOptions struct {
  501. OptIni *InitiateMultipartUploadOptions
  502. PartSize int64
  503. ThreadPoolSize int
  504. CheckPoint bool
  505. EnableVerification bool
  506. }
  507. type MultiDownloadOptions struct {
  508. Opt *ObjectGetOptions
  509. PartSize int64
  510. ThreadPoolSize int
  511. }
  512. type Chunk struct {
  513. Number int
  514. OffSet int64
  515. Size int64
  516. Done bool
  517. ETag string
  518. }
  519. // jobs
  520. type Jobs struct {
  521. Name string
  522. UploadId string
  523. FilePath string
  524. RetryTimes int
  525. Chunk Chunk
  526. Data io.Reader
  527. Opt *ObjectUploadPartOptions
  528. DownOpt *ObjectGetOptions
  529. }
  530. type Results struct {
  531. PartNumber int
  532. Resp *Response
  533. err error
  534. }
  535. func LimitReadCloser(r io.Reader, n int64) io.Reader {
  536. var lc LimitedReadCloser
  537. lc.R = r
  538. lc.N = n
  539. return &lc
  540. }
  541. type LimitedReadCloser struct {
  542. io.LimitedReader
  543. }
  544. func (lc *LimitedReadCloser) Close() error {
  545. if r, ok := lc.R.(io.ReadCloser); ok {
  546. return r.Close()
  547. }
  548. return nil
  549. }
  550. func worker(s *ObjectService, jobs <-chan *Jobs, results chan<- *Results) {
  551. for j := range jobs {
  552. j.Opt.ContentLength = j.Chunk.Size
  553. rt := j.RetryTimes
  554. for {
  555. // http.Request.Body can be Closed in request
  556. fd, err := os.Open(j.FilePath)
  557. var res Results
  558. if err != nil {
  559. res.err = err
  560. res.PartNumber = j.Chunk.Number
  561. res.Resp = nil
  562. results <- &res
  563. break
  564. }
  565. fd.Seek(j.Chunk.OffSet, os.SEEK_SET)
  566. resp, err := s.UploadPart(context.Background(), j.Name, j.UploadId, j.Chunk.Number,
  567. LimitReadCloser(fd, j.Chunk.Size), j.Opt)
  568. res.PartNumber = j.Chunk.Number
  569. res.Resp = resp
  570. res.err = err
  571. if err != nil {
  572. rt--
  573. if rt == 0 {
  574. results <- &res
  575. break
  576. }
  577. continue
  578. }
  579. results <- &res
  580. break
  581. }
  582. }
  583. }
  584. func downloadWorker(s *ObjectService, jobs <-chan *Jobs, results chan<- *Results) {
  585. for j := range jobs {
  586. opt := &RangeOptions{
  587. HasStart: true,
  588. HasEnd: true,
  589. Start: j.Chunk.OffSet,
  590. End: j.Chunk.OffSet + j.Chunk.Size - 1,
  591. }
  592. j.DownOpt.Range = FormatRangeOptions(opt)
  593. rt := j.RetryTimes
  594. for {
  595. var res Results
  596. res.PartNumber = j.Chunk.Number
  597. resp, err := s.Get(context.Background(), j.Name, j.DownOpt)
  598. res.err = err
  599. res.Resp = resp
  600. if err != nil {
  601. rt--
  602. if rt == 0 {
  603. results <- &res
  604. break
  605. }
  606. continue
  607. }
  608. defer resp.Body.Close()
  609. fd, err := os.OpenFile(j.FilePath, os.O_WRONLY, 0660)
  610. if err != nil {
  611. res.err = err
  612. results <- &res
  613. break
  614. }
  615. fd.Seek(j.Chunk.OffSet, os.SEEK_SET)
  616. n, err := io.Copy(fd, LimitReadCloser(resp.Body, j.Chunk.Size))
  617. if n != j.Chunk.Size || err != nil {
  618. res.err = fmt.Errorf("io.Copy Failed, read:%v, size:%v, err:%v", n, j.Chunk.Size, err)
  619. }
  620. results <- &res
  621. break
  622. }
  623. }
  624. }
  625. func DividePart(fileSize int64, last int) (int64, int64) {
  626. partSize := int64(last * 1024 * 1024)
  627. partNum := fileSize / partSize
  628. for partNum >= 10000 {
  629. partSize = partSize * 2
  630. partNum = fileSize / partSize
  631. }
  632. return partNum, partSize
  633. }
  634. func SplitFileIntoChunks(filePath string, partSize int64) (int64, []Chunk, int, error) {
  635. if filePath == "" {
  636. return 0, nil, 0, errors.New("filePath invalid")
  637. }
  638. file, err := os.Open(filePath)
  639. if err != nil {
  640. return 0, nil, 0, err
  641. }
  642. defer file.Close()
  643. stat, err := file.Stat()
  644. if err != nil {
  645. return 0, nil, 0, err
  646. }
  647. var partNum int64
  648. if partSize > 0 {
  649. partSize = partSize * 1024 * 1024
  650. partNum = stat.Size() / partSize
  651. if partNum >= 10000 {
  652. return 0, nil, 0, errors.New("Too many parts, out of 10000")
  653. }
  654. } else {
  655. partNum, partSize = DividePart(stat.Size(), 64)
  656. }
  657. var chunks []Chunk
  658. var chunk = Chunk{}
  659. for i := int64(0); i < partNum; i++ {
  660. chunk.Number = int(i + 1)
  661. chunk.OffSet = i * partSize
  662. chunk.Size = partSize
  663. chunks = append(chunks, chunk)
  664. }
  665. if stat.Size()%partSize > 0 {
  666. chunk.Number = len(chunks) + 1
  667. chunk.OffSet = int64(len(chunks)) * partSize
  668. chunk.Size = stat.Size() % partSize
  669. chunks = append(chunks, chunk)
  670. partNum++
  671. }
  672. return int64(stat.Size()), chunks, int(partNum), nil
  673. }
  674. func (s *ObjectService) getResumableUploadID(ctx context.Context, name string) (string, error) {
  675. opt := &ObjectListUploadsOptions{
  676. Prefix: name,
  677. EncodingType: "url",
  678. }
  679. res, _, err := s.ListUploads(ctx, opt)
  680. if err != nil {
  681. return "", err
  682. }
  683. if len(res.Upload) == 0 {
  684. return "", nil
  685. }
  686. last := len(res.Upload) - 1
  687. for last >= 0 {
  688. decodeKey, _ := decodeURIComponent(res.Upload[last].Key)
  689. if decodeKey == name {
  690. return decodeURIComponent(res.Upload[last].UploadID)
  691. }
  692. last = last - 1
  693. }
  694. return "", nil
  695. }
  696. func (s *ObjectService) checkUploadedParts(ctx context.Context, name, UploadID, filepath string, chunks []Chunk, partNum int) error {
  697. var uploadedParts []Object
  698. isTruncated := true
  699. opt := &ObjectListPartsOptions{
  700. EncodingType: "url",
  701. }
  702. for isTruncated {
  703. res, _, err := s.ListParts(ctx, name, UploadID, opt)
  704. if err != nil {
  705. return err
  706. }
  707. if len(res.Parts) > 0 {
  708. uploadedParts = append(uploadedParts, res.Parts...)
  709. }
  710. isTruncated = res.IsTruncated
  711. opt.PartNumberMarker = res.NextPartNumberMarker
  712. }
  713. fd, err := os.Open(filepath)
  714. if err != nil {
  715. return err
  716. }
  717. defer fd.Close()
  718. // 某个分块出错, 重置chunks
  719. ret := func(e error) error {
  720. for i, _ := range chunks {
  721. chunks[i].Done = false
  722. chunks[i].ETag = ""
  723. }
  724. return e
  725. }
  726. for _, part := range uploadedParts {
  727. partNumber := part.PartNumber
  728. if partNumber > partNum {
  729. return ret(errors.New("Part Number is not consistent"))
  730. }
  731. partNumber = partNumber - 1
  732. fd.Seek(chunks[partNumber].OffSet, os.SEEK_SET)
  733. bs, err := ioutil.ReadAll(io.LimitReader(fd, chunks[partNumber].Size))
  734. if err != nil {
  735. return ret(err)
  736. }
  737. localMD5 := fmt.Sprintf("\"%x\"", md5.Sum(bs))
  738. if localMD5 != part.ETag {
  739. return ret(errors.New(fmt.Sprintf("CheckSum Failed in Part[%d]", part.PartNumber)))
  740. }
  741. chunks[partNumber].Done = true
  742. chunks[partNumber].ETag = part.ETag
  743. }
  744. return nil
  745. }
  746. // MultiUpload/Upload 为高级upload接口,并发分块上传
  747. // 注意该接口目前只供参考
  748. //
  749. // 当 partSize > 0 时,由调用者指定分块大小,否则由 SDK 自动切分,单位为MB
  750. // 由调用者指定分块大小时,请确认分块数量不超过10000
  751. //
  752. func (s *ObjectService) MultiUpload(ctx context.Context, name string, filepath string, opt *MultiUploadOptions) (*CompleteMultipartUploadResult, *Response, error) {
  753. return s.Upload(ctx, name, filepath, opt)
  754. }
  755. func (s *ObjectService) Upload(ctx context.Context, name string, filepath string, opt *MultiUploadOptions) (*CompleteMultipartUploadResult, *Response, error) {
  756. if opt == nil {
  757. opt = &MultiUploadOptions{}
  758. }
  759. var localcrc uint64
  760. // 1.Get the file chunk
  761. totalBytes, chunks, partNum, err := SplitFileIntoChunks(filepath, opt.PartSize)
  762. if err != nil {
  763. return nil, nil, err
  764. }
  765. // 校验
  766. if s.client.Conf.EnableCRC {
  767. fd, err := os.Open(filepath)
  768. if err != nil {
  769. return nil, nil, err
  770. }
  771. defer fd.Close()
  772. localcrc, err = calCRC64(fd)
  773. if err != nil {
  774. return nil, nil, err
  775. }
  776. }
  777. // filesize=0 , use simple upload
  778. if partNum == 0 || partNum == 1 {
  779. var opt0 *ObjectPutOptions
  780. if opt.OptIni != nil {
  781. opt0 = &ObjectPutOptions{
  782. opt.OptIni.ACLHeaderOptions,
  783. opt.OptIni.ObjectPutHeaderOptions,
  784. }
  785. }
  786. rsp, err := s.PutFromFile(ctx, name, filepath, opt0)
  787. if err != nil {
  788. return nil, rsp, err
  789. }
  790. result := &CompleteMultipartUploadResult{
  791. Key: name,
  792. ETag: rsp.Header.Get("ETag"),
  793. }
  794. if rsp != nil && s.client.Conf.EnableCRC {
  795. scoscrc := rsp.Header.Get("x-cos-hash-crc64ecma")
  796. icoscrc, _ := strconv.ParseUint(scoscrc, 10, 64)
  797. if icoscrc != localcrc {
  798. return result, rsp, fmt.Errorf("verification failed, want:%v, return:%v", localcrc, icoscrc)
  799. }
  800. }
  801. return result, rsp, nil
  802. }
  803. var uploadID string
  804. resumableFlag := false
  805. if opt.CheckPoint {
  806. var err error
  807. uploadID, err = s.getResumableUploadID(ctx, name)
  808. if err == nil && uploadID != "" {
  809. err = s.checkUploadedParts(ctx, name, uploadID, filepath, chunks, partNum)
  810. resumableFlag = (err == nil)
  811. }
  812. }
  813. // 2.Init
  814. optini := opt.OptIni
  815. if !resumableFlag {
  816. res, _, err := s.InitiateMultipartUpload(ctx, name, optini)
  817. if err != nil {
  818. return nil, nil, err
  819. }
  820. uploadID = res.UploadID
  821. }
  822. var poolSize int
  823. if opt.ThreadPoolSize > 0 {
  824. poolSize = opt.ThreadPoolSize
  825. } else {
  826. // Default is one
  827. poolSize = 1
  828. }
  829. chjobs := make(chan *Jobs, 100)
  830. chresults := make(chan *Results, 10000)
  831. optcom := &CompleteMultipartUploadOptions{}
  832. // 3.Start worker
  833. for w := 1; w <= poolSize; w++ {
  834. go worker(s, chjobs, chresults)
  835. }
  836. // progress started event
  837. var listener ProgressListener
  838. var consumedBytes int64
  839. if opt.OptIni != nil {
  840. listener = opt.OptIni.Listener
  841. }
  842. event := newProgressEvent(ProgressStartedEvent, 0, 0, totalBytes)
  843. progressCallback(listener, event)
  844. // 4.Push jobs
  845. go func() {
  846. for _, chunk := range chunks {
  847. if chunk.Done {
  848. continue
  849. }
  850. partOpt := &ObjectUploadPartOptions{}
  851. if optini != nil && optini.ObjectPutHeaderOptions != nil {
  852. partOpt.XCosSSECustomerAglo = optini.XCosSSECustomerAglo
  853. partOpt.XCosSSECustomerKey = optini.XCosSSECustomerKey
  854. partOpt.XCosSSECustomerKeyMD5 = optini.XCosSSECustomerKeyMD5
  855. partOpt.XCosTrafficLimit = optini.XCosTrafficLimit
  856. }
  857. job := &Jobs{
  858. Name: name,
  859. RetryTimes: 3,
  860. FilePath: filepath,
  861. UploadId: uploadID,
  862. Chunk: chunk,
  863. Opt: partOpt,
  864. }
  865. chjobs <- job
  866. }
  867. close(chjobs)
  868. }()
  869. // 5.Recv the resp etag to complete
  870. err = nil
  871. for i := 0; i < partNum; i++ {
  872. if chunks[i].Done {
  873. optcom.Parts = append(optcom.Parts, Object{
  874. PartNumber: chunks[i].Number, ETag: chunks[i].ETag},
  875. )
  876. if err == nil {
  877. consumedBytes += chunks[i].Size
  878. event = newProgressEvent(ProgressDataEvent, chunks[i].Size, consumedBytes, totalBytes)
  879. progressCallback(listener, event)
  880. }
  881. continue
  882. }
  883. res := <-chresults
  884. // Notice one part fail can not get the etag according.
  885. if res.Resp == nil || res.err != nil {
  886. // Some part already fail, can not to get the header inside.
  887. err = fmt.Errorf("UploadID %s, part %d failed to get resp content. error: %s", uploadID, res.PartNumber, res.err.Error())
  888. continue
  889. }
  890. // Notice one part fail can not get the etag according.
  891. etag := res.Resp.Header.Get("ETag")
  892. optcom.Parts = append(optcom.Parts, Object{
  893. PartNumber: res.PartNumber, ETag: etag},
  894. )
  895. if err == nil {
  896. consumedBytes += chunks[res.PartNumber-1].Size
  897. event = newProgressEvent(ProgressDataEvent, chunks[res.PartNumber-1].Size, consumedBytes, totalBytes)
  898. progressCallback(listener, event)
  899. }
  900. }
  901. close(chresults)
  902. if err != nil {
  903. event = newProgressEvent(ProgressFailedEvent, 0, consumedBytes, totalBytes, err)
  904. progressCallback(listener, event)
  905. return nil, nil, err
  906. }
  907. sort.Sort(ObjectList(optcom.Parts))
  908. event = newProgressEvent(ProgressCompletedEvent, 0, consumedBytes, totalBytes)
  909. progressCallback(listener, event)
  910. v, resp, err := s.CompleteMultipartUpload(context.Background(), name, uploadID, optcom)
  911. if err != nil {
  912. s.AbortMultipartUpload(ctx, name, uploadID)
  913. return v, resp, err
  914. }
  915. if resp != nil && s.client.Conf.EnableCRC {
  916. scoscrc := resp.Header.Get("x-cos-hash-crc64ecma")
  917. icoscrc, _ := strconv.ParseUint(scoscrc, 10, 64)
  918. if icoscrc != localcrc {
  919. return v, resp, fmt.Errorf("verification failed, want:%v, return:%v", localcrc, icoscrc)
  920. }
  921. }
  922. return v, resp, err
  923. }
  924. func SplitSizeIntoChunks(totalBytes int64, partSize int64) ([]Chunk, int, error) {
  925. var partNum int64
  926. if partSize > 0 {
  927. partSize = partSize * 1024 * 1024
  928. partNum = totalBytes / partSize
  929. if partNum >= 10000 {
  930. return nil, 0, errors.New("Too manry parts, out of 10000")
  931. }
  932. } else {
  933. partNum, partSize = DividePart(totalBytes, 64)
  934. }
  935. var chunks []Chunk
  936. var chunk = Chunk{}
  937. for i := int64(0); i < partNum; i++ {
  938. chunk.Number = int(i + 1)
  939. chunk.OffSet = i * partSize
  940. chunk.Size = partSize
  941. chunks = append(chunks, chunk)
  942. }
  943. if totalBytes%partSize > 0 {
  944. chunk.Number = len(chunks) + 1
  945. chunk.OffSet = int64(len(chunks)) * partSize
  946. chunk.Size = totalBytes % partSize
  947. chunks = append(chunks, chunk)
  948. partNum++
  949. }
  950. return chunks, int(partNum), nil
  951. }
  952. func (s *ObjectService) Download(ctx context.Context, name string, filepath string, opt *MultiDownloadOptions) (*Response, error) {
  953. // 参数校验
  954. if opt == nil {
  955. opt = &MultiDownloadOptions{}
  956. }
  957. if opt.Opt != nil && opt.Opt.Range != "" {
  958. return nil, fmt.Errorf("does not supported Range Get")
  959. }
  960. // 获取文件长度和CRC
  961. var coscrc string
  962. resp, err := s.Head(ctx, name, nil)
  963. if err != nil {
  964. return resp, err
  965. }
  966. coscrc = resp.Header.Get("x-cos-hash-crc64ecma")
  967. strTotalBytes := resp.Header.Get("Content-Length")
  968. totalBytes, err := strconv.ParseInt(strTotalBytes, 10, 64)
  969. if err != nil {
  970. return resp, err
  971. }
  972. // 切分
  973. chunks, partNum, err := SplitSizeIntoChunks(totalBytes, opt.PartSize)
  974. if err != nil {
  975. return resp, err
  976. }
  977. // 直接下载到文件
  978. if partNum == 0 || partNum == 1 {
  979. rsp, err := s.GetToFile(ctx, name, filepath, opt.Opt)
  980. if err != nil {
  981. return rsp, err
  982. }
  983. if coscrc != "" && s.client.Conf.EnableCRC {
  984. icoscrc, _ := strconv.ParseUint(coscrc, 10, 64)
  985. fd, err := os.Open(filepath)
  986. if err != nil {
  987. return rsp, err
  988. }
  989. localcrc, err := calCRC64(fd)
  990. if err != nil {
  991. return rsp, err
  992. }
  993. if localcrc != icoscrc {
  994. return rsp, fmt.Errorf("verification failed, want:%v, return:%v", icoscrc, localcrc)
  995. }
  996. }
  997. return rsp, err
  998. }
  999. nfile, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
  1000. if err != nil {
  1001. return resp, err
  1002. }
  1003. nfile.Close()
  1004. var poolSize int
  1005. if opt.ThreadPoolSize > 0 {
  1006. poolSize = opt.ThreadPoolSize
  1007. } else {
  1008. poolSize = 1
  1009. }
  1010. chjobs := make(chan *Jobs, 100)
  1011. chresults := make(chan *Results, 10000)
  1012. for w := 1; w <= poolSize; w++ {
  1013. go downloadWorker(s, chjobs, chresults)
  1014. }
  1015. go func() {
  1016. for _, chunk := range chunks {
  1017. var downOpt ObjectGetOptions
  1018. if opt.Opt != nil {
  1019. downOpt = *opt.Opt
  1020. }
  1021. job := &Jobs{
  1022. Name: name,
  1023. RetryTimes: 3,
  1024. FilePath: filepath,
  1025. Chunk: chunk,
  1026. DownOpt: &downOpt,
  1027. }
  1028. chjobs <- job
  1029. }
  1030. close(chjobs)
  1031. }()
  1032. err = nil
  1033. for i := 0; i < partNum; i++ {
  1034. res := <-chresults
  1035. if res.Resp == nil || res.err != nil {
  1036. err = fmt.Errorf("part %d get resp Content. error: %s", res.PartNumber, res.err.Error())
  1037. continue
  1038. }
  1039. }
  1040. close(chresults)
  1041. if err != nil {
  1042. return nil, err
  1043. }
  1044. if coscrc != "" && s.client.Conf.EnableCRC {
  1045. icoscrc, _ := strconv.ParseUint(coscrc, 10, 64)
  1046. fd, err := os.Open(filepath)
  1047. if err != nil {
  1048. return resp, err
  1049. }
  1050. localcrc, err := calCRC64(fd)
  1051. if err != nil {
  1052. return resp, err
  1053. }
  1054. if localcrc != icoscrc {
  1055. return resp, fmt.Errorf("verification failed, want:%v, return:%v", icoscrc, localcrc)
  1056. }
  1057. }
  1058. return resp, err
  1059. }
  1060. type ObjectPutTaggingOptions struct {
  1061. XMLName xml.Name `xml:"Tagging"`
  1062. TagSet []ObjectTaggingTag `xml:"TagSet>Tag,omitempty"`
  1063. }
  1064. type ObjectTaggingTag BucketTaggingTag
  1065. type ObjectGetTaggingResult ObjectPutTaggingOptions
  1066. func (s *ObjectService) PutTagging(ctx context.Context, name string, opt *ObjectPutTaggingOptions, id ...string) (*Response, error) {
  1067. var u string
  1068. if len(id) == 1 {
  1069. u = fmt.Sprintf("/%s?tagging&versionId=%s", encodeURIComponent(name), id[0])
  1070. } else if len(id) == 0 {
  1071. u = fmt.Sprintf("/%s?tagging", encodeURIComponent(name))
  1072. } else {
  1073. return nil, errors.New("wrong params")
  1074. }
  1075. sendOpt := &sendOptions{
  1076. baseURL: s.client.BaseURL.BucketURL,
  1077. uri: u,
  1078. method: http.MethodPut,
  1079. body: opt,
  1080. }
  1081. resp, err := s.client.send(ctx, sendOpt)
  1082. return resp, err
  1083. }
  1084. func (s *ObjectService) GetTagging(ctx context.Context, name string, id ...string) (*ObjectGetTaggingResult, *Response, error) {
  1085. var u string
  1086. if len(id) == 1 {
  1087. u = fmt.Sprintf("/%s?tagging&versionId=%s", encodeURIComponent(name), id[0])
  1088. } else if len(id) == 0 {
  1089. u = fmt.Sprintf("/%s?tagging", encodeURIComponent(name))
  1090. } else {
  1091. return nil, nil, errors.New("wrong params")
  1092. }
  1093. var res ObjectGetTaggingResult
  1094. sendOpt := &sendOptions{
  1095. baseURL: s.client.BaseURL.BucketURL,
  1096. uri: u,
  1097. method: http.MethodGet,
  1098. result: &res,
  1099. }
  1100. resp, err := s.client.send(ctx, sendOpt)
  1101. return &res, resp, err
  1102. }
  1103. func (s *ObjectService) DeleteTagging(ctx context.Context, name string, id ...string) (*Response, error) {
  1104. var u string
  1105. if len(id) == 1 {
  1106. u = fmt.Sprintf("/%s?tagging&versionId=%s", encodeURIComponent(name), id[0])
  1107. } else if len(id) == 0 {
  1108. u = fmt.Sprintf("/%s?tagging", encodeURIComponent(name))
  1109. } else {
  1110. return nil, errors.New("wrong params")
  1111. }
  1112. sendOpt := &sendOptions{
  1113. baseURL: s.client.BaseURL.BucketURL,
  1114. uri: u,
  1115. method: http.MethodDelete,
  1116. }
  1117. resp, err := s.client.send(ctx, sendOpt)
  1118. return resp, err
  1119. }