github.com/qiniu/go-sdk/v7@v7.27.0/storage/bucket.go (about) 1 package storage 2 3 // TODO: 4 // BucketManager 每个接口的基本逻辑都是设置Mac信息, 获取请求地址, 发送HTTP请求。 5 // 后期可以调整抽象出Request struct, APIOperation struct, 这样不用每个接口都要写 6 // 重复的逻辑 7 8 import ( 9 "context" 10 "encoding/base64" 11 "errors" 12 "fmt" 13 "net" 14 "net/http" 15 "net/url" 16 "strings" 17 "time" 18 19 "github.com/qiniu/go-sdk/v7/internal/clientv2" 20 "github.com/qiniu/go-sdk/v7/storagev2/apis" 21 "github.com/qiniu/go-sdk/v7/storagev2/apis/batch_ops" 22 "github.com/qiniu/go-sdk/v7/storagev2/apis/get_bucket_domains_v3" 23 "github.com/qiniu/go-sdk/v7/storagev2/backoff" 24 "github.com/qiniu/go-sdk/v7/storagev2/chooser" 25 "github.com/qiniu/go-sdk/v7/storagev2/downloader" 26 "github.com/qiniu/go-sdk/v7/storagev2/http_client" 27 "github.com/qiniu/go-sdk/v7/storagev2/region" 28 "github.com/qiniu/go-sdk/v7/storagev2/resolver" 29 "github.com/qiniu/go-sdk/v7/storagev2/retrier" 30 31 "github.com/qiniu/go-sdk/v7/auth" 32 clientv1 "github.com/qiniu/go-sdk/v7/client" 33 ) 34 35 // 资源管理相关的默认域名 36 const ( 37 DefaultRsHost = "rs.qiniu.com" 38 DefaultRsfHost = "rsf.qiniu.com" 39 DefaultAPIHost = "api.qiniu.com" 40 DefaultPubHost = "pu.qbox.me:10200" 41 ) 42 43 // FileInfo 文件基本信息 44 type FileInfo struct { 45 // 资源内容的大小,单位:字节。 46 Fsize int64 `json:"fsize"` 47 48 // 文件的HASH值,使用hash值算法计算。 49 Hash string `json:"hash"` 50 51 // 资源的 MIME 类型。 52 MimeType string `json:"mimeType"` 53 54 /** 55 * 资源的存储类型 56 * 0 表示标准存储 57 * 1 表示低频存储 58 * 2 表示归档存储 59 * 3 表示深度归档存储 60 * 4 表示归档存储直读 61 */ 62 Type int `json:"type"` 63 64 // 上传时间,单位:100纳秒,其值去掉低七位即为Unix时间戳。 65 PutTime int64 `json:"putTime"` 66 67 /** 68 * 归档/深度归档存储文件的解冻状态,归档/深度归档文件冻结时,不返回该字段。 69 * 1 表示解冻中 70 * 2 表示解冻完成 71 */ 72 RestoreStatus int `json:"restoreStatus"` 73 74 /** 75 * 文件的存储状态,即禁用状态和启用状态间的的互相转换,请参考:文件状态。 76 * 0 表示启用 77 * 1 表示禁用 78 */ 79 Status int `json:"status"` 80 81 /** 82 * 文件的 md5 值 83 */ 84 Md5 string `json:"md5"` 85 86 /** 87 * 文件上传时设置的endUser 88 */ 89 EndUser string `json:"endUser"` 90 91 /** 92 * 自定义元数据,含 meta_key 参数 及对应 metaValue 值;仅当自定义元数据后返回该字段。 93 */ 94 MetaData map[string]string `json:"x-qn-meta"` 95 96 /** 97 * 文件过期删除日期,int64 类型,Unix 时间戳格式,具体文件过期日期计算参考 生命周期管理。 98 * 文件在设置过期时间后才会返回该字段(通过生命周期规则设置文件过期时间,仅对该功能发布后满足规则条件新上传文件返回该字段; 99 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件过期删除时间 API 或者 修改文件生命周期 API 指定过期时间;对于已 100 * 经设置过过期时间的历史文件,到期都会正常过期删除,只是服务端没有该字段返回) 101 * 102 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天内删除。 103 */ 104 Expiration int64 `json:"expiration"` 105 106 /** 107 * 文件生命周期中转为低频存储的日期,int64 类型,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 108 * 文件在设置转低频后才会返回该字段(通过生命周期规则设置文件转低频,仅对该功能发布后满足规则条件新上传文件返回该字段; 109 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转低频时间;对于已经设置过转低频时间的历史文 110 * 件,到期都会正常执行,只是服务端没有该字段返回) 111 * 112 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为低频存储类型。 113 */ 114 TransitionToIA int64 `json:"transitionToIA"` 115 116 /** 117 * 文件生命周期中转为归档直读存储的日期,int64 类型,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 118 * 文件在设置转归档直读后才会返回该字段(通过生命周期规则设置文件转归档直读,仅对该功能发布后满足规则条件新上传文件返回该字段; 119 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转归档直读时间;对于已经设置过转归档直读时间的历史文 120 * 件,到期都会正常执行,只是服务端没有该字段返回) 121 * 122 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为归档直读存储类型。 123 */ 124 TransitionToArchiveIR int64 `json:"transitionToArchiveIR"` 125 126 /** 127 * 文件生命周期中转为归档存储的日期,int64 类型,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 128 * 文件在设置转归档后才会返回该字段(通过生命周期规则设置文件转归档,仅对该功能发布后满足规则条件新上传文件返回该字段; 129 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转归档时间;对于已经设置过转归档时间的历史文 130 * 件,到期都会正常执行,只是服务端没有该字段返回) 131 * 132 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为归档存储类型。 133 */ 134 TransitionToArchive int64 `json:"transitionToARCHIVE"` 135 136 /** 137 * 文件生命周期中转为深度归档存储的日期,int64 类型,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 138 * 文件在设置转深度归档后才会返回该字段(通过生命周期规则设置文件转深度归档,仅对该功能发布后满足规则条件新上传文件返回该字段; 139 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转深度归档时间;对于已经设置过转深度归档时间的历史文 140 * 件,到期都会正常执行,只是服务端没有该字段返回) 141 * 142 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为深度归档存储类型。 143 */ 144 TransitionToDeepArchive int64 `json:"transitionToDeepArchive"` 145 146 // 分拣分片信息,可能为空 147 Parts []int64 `json:"parts"` 148 } 149 150 func (f *FileInfo) String() string { 151 str := "" 152 str += fmt.Sprintf("Hash: %s\n", f.Hash) 153 str += fmt.Sprintf("Fsize: %d\n", f.Fsize) 154 str += fmt.Sprintf("PutTime: %d\n", f.PutTime) 155 str += fmt.Sprintf("MimeType: %s\n", f.MimeType) 156 str += fmt.Sprintf("Type: %d\n", f.Type) 157 str += fmt.Sprintf("RestoreStatus: %d\n", f.RestoreStatus) 158 str += fmt.Sprintf("Status: %d\n", f.Status) 159 str += fmt.Sprintf("Md5: %s\n", f.Md5) 160 str += fmt.Sprintf("EndUser: %s\n", f.EndUser) 161 str += fmt.Sprintf("Expiration: %d\n", f.Expiration) 162 str += fmt.Sprintf("TransitionToIA: %d\n", f.TransitionToIA) 163 str += fmt.Sprintf("TransitionToArchiveIR: %d\n", f.TransitionToArchiveIR) 164 str += fmt.Sprintf("TransitionToArchive: %d\n", f.TransitionToArchive) 165 str += fmt.Sprintf("TransitionToDeepArchive: %d\n", f.TransitionToDeepArchive) 166 str += fmt.Sprintf("MetaData: %s\n", f.MetaData) 167 return str 168 } 169 170 // FetchRet 资源抓取的返回值 171 type FetchRet struct { 172 Hash string `json:"hash"` 173 Fsize int64 `json:"fsize"` 174 MimeType string `json:"mimeType"` 175 Key string `json:"key"` 176 } 177 178 func (r *FetchRet) String() string { 179 str := "" 180 str += fmt.Sprintf("Key: %s\n", r.Key) 181 str += fmt.Sprintf("Hash: %s\n", r.Hash) 182 str += fmt.Sprintf("Fsize: %d\n", r.Fsize) 183 str += fmt.Sprintf("MimeType: %s\n", r.MimeType) 184 return str 185 } 186 187 // BatchOpRet 为批量执行操作的返回值 188 // 批量操作支持 stat,copy,delete,move,chgm,chtype,deleteAfterDays几个操作 189 // 其中 stat 为获取文件的基本信息,如果文件存在则返回基本信息,如果文件不存在返回 error 。 190 // 其他的操作,如果成功,则返回 code,不成功会同时返回 error 信息,可以根据 error 信息来判断问题所在。 191 type BatchOpRet struct { 192 Code int `json:"code,omitempty"` 193 Data struct { 194 // 资源内容的大小,单位:字节。 195 Fsize int64 `json:"fsize"` 196 197 // 文件的HASH值,使用hash值算法计算。 198 Hash string `json:"hash"` 199 200 // 资源的 MIME 类型。 201 MimeType string `json:"mimeType"` 202 203 /** 204 * 资源的存储类型 205 * 0 表示标准存储 206 * 1 表示低频存储 207 * 2 表示归档存储 208 * 3 表示深度归档存储 209 * 4 表示归档直读存储 210 */ 211 Type int `json:"type"` 212 213 // 上传时间,单位:100纳秒,其值去掉低七位即为Unix时间戳。 214 PutTime int64 `json:"putTime"` 215 216 /** 217 * 归档/深度归档存储文件的解冻状态,归档/深度归档文件冻结时,不返回该字段。 218 * 1 表示解冻中 219 * 2 表示解冻完成 220 */ 221 RestoreStatus *int `json:"restoreStatus"` 222 223 /** 224 * 文件的存储状态,即禁用状态和启用状态间的的互相转换,请参考:文件状态。 225 * 0 表示启用 226 * 1 表示禁用 227 */ 228 Status *int `json:"status"` 229 230 /** 231 * 文件的 md5 值 232 */ 233 Md5 string `json:"md5"` 234 235 /** 236 * 文件上传时设置的endUser 237 */ 238 EndUser string `json:"endUser"` 239 240 /** 241 * 文件过期删除日期,Unix 时间戳格式,具体文件过期日期计算参考 生命周期管理。 242 * 文件在设置过期时间后才会返回该字段(通过生命周期规则设置文件过期时间,仅对该功能发布后满足规则条件新上传文件返回该字段; 243 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件过期删除时间 API 或者 修改文件生命周期 API 指定过期时间;对于已 244 * 经设置过过期时间的历史文件,到期都会正常过期删除,只是服务端没有该字段返回) 245 * 246 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天内删除。 247 */ 248 Expiration *int64 `json:"expiration"` 249 250 /** 251 * 文件生命周期中转为低频存储的日期,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 252 * 文件在设置转低频后才会返回该字段(通过生命周期规则设置文件转低频,仅对该功能发布后满足规则条件新上传文件返回该字段; 253 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转低频时间;对于已经设置过转低频时间的历史文 254 * 件,到期都会正常执行,只是服务端没有该字段返回) 255 * 256 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为低频存储类型。 257 */ 258 TransitionToIA *int64 `json:"transitionToIA"` 259 260 /** 261 * 文件生命周期中转为归档直读存储的日期,int64 类型,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 262 * 文件在设置转归档直读后才会返回该字段(通过生命周期规则设置文件转归档直读,仅对该功能发布后满足规则条件新上传文件返回该字段; 263 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转归档直读时间;对于已经设置过转归档直读时间的历史文 264 * 件,到期都会正常执行,只是服务端没有该字段返回) 265 * 266 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为归档直读存储类型。 267 */ 268 TransitionToArchiveIR int64 `json:"transitionToArchiveIR"` 269 270 /** 271 * 文件生命周期中转为归档存储的日期,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 272 * 文件在设置转归档后才会返回该字段(通过生命周期规则设置文件转归档,仅对该功能发布后满足规则条件新上传文件返回该字段; 273 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转归档时间;对于已经设置过转归档时间的历史文 274 * 件,到期都会正常执行,只是服务端没有该字段返回) 275 * 276 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为归档存储类型。 277 */ 278 TransitionToArchive *int64 `json:"transitionToARCHIVE"` 279 280 /** 281 * 文件生命周期中转为深度归档存储的日期,Unix 时间戳格式 ,具体日期计算参考 生命周期管理。 282 * 文件在设置转深度归档后才会返回该字段(通过生命周期规则设置文件转深度归档,仅对该功能发布后满足规则条件新上传文件返回该字段; 283 * 历史文件想要返回该字段需要在功能发布后可通过 修改文件生命周期 API 指定转深度归档时间;对于已经设置过转深度归档时间的历史文 284 * 件,到期都会正常执行,只是服务端没有该字段返回) 285 * 286 * 例如:值为1568736000的时间,表示文件会在2019/9/18当天转为深度归档存储类型。 287 */ 288 TransitionToDeepArchive *int64 `json:"transitionToDeepArchive"` 289 290 Error string `json:"error"` 291 } `json:"data,omitempty"` 292 } 293 294 type BucketManagerOptions struct { 295 // 单域名重试次数,当前只有 uc 相关的服务有多域名 296 RetryMax int 297 298 // 主备域名冻结时间(默认:600s),当一个域名请求失败(单个域名会被重试 TryTimes 次),会被冻结一段时间,使用备用域名进行重试,在冻结时间内,域名不能被使用,当一个操作中所有域名竣备冻结操作不在进行重试,返回最后一次操作的错误。 299 HostFreezeDuration time.Duration 300 301 // 域名解析器 302 Resolver resolver.Resolver 303 304 // 域名选择器 305 Chooser chooser.Chooser 306 307 // 退避器 308 Backoff backoff.Backoff 309 310 // 重试器 311 Retrier retrier.Retrier 312 313 // 签名前回调函数 314 BeforeSign func(*http.Request) 315 316 // 签名后回调函数 317 AfterSign func(*http.Request) 318 319 // 签名失败回调函数 320 SignError func(*http.Request, error) 321 322 // 域名解析前回调函数 323 BeforeResolve func(*http.Request) 324 325 // 域名解析后回调函数 326 AfterResolve func(*http.Request, []net.IP) 327 328 // 域名解析错误回调函数 329 ResolveError func(*http.Request, error) 330 331 // 退避前回调函数 332 BeforeBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration) 333 334 // 退避后回调函数 335 AfterBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration) 336 337 // 请求前回调函数 338 BeforeRequest func(*http.Request, *retrier.RetrierOptions) 339 340 // 请求后回调函数 341 AfterResponse func(*http.Response, *retrier.RetrierOptions, error) 342 } 343 344 // BucketManager 提供了对资源进行管理的操作 345 type BucketManager struct { 346 Client *clientv1.Client 347 Mac *auth.Credentials 348 Cfg *Config 349 options BucketManagerOptions 350 apiClient *apis.Storage 351 downloadManager *downloader.DownloadManager 352 } 353 354 // NewBucketManager 用来构建一个新的资源管理对象 355 func NewBucketManager(mac *auth.Credentials, cfg *Config) *BucketManager { 356 return NewBucketManagerEx(mac, cfg, &clientv1.DefaultClient) 357 } 358 359 // NewBucketManagerEx 用来构建一个新的资源管理对象 360 func NewBucketManagerEx(mac *auth.Credentials, cfg *Config, clt *clientv1.Client) *BucketManager { 361 return NewBucketManagerExWithOptions(mac, cfg, clt, BucketManagerOptions{}) 362 } 363 364 func NewBucketManagerExWithOptions(mac *auth.Credentials, cfg *Config, clt *clientv1.Client, options BucketManagerOptions) *BucketManager { 365 if cfg == nil { 366 cfg = NewConfig() 367 } 368 if mac == nil { 369 mac = auth.Default() 370 } 371 372 if clt == nil { 373 clt = &clientv1.DefaultClient 374 } 375 if cfg.CentralRsHost == "" { 376 cfg.CentralRsHost = DefaultRsHost 377 } 378 379 bucketQuery, _ := region.NewBucketRegionsQuery(getUcEndpoint(cfg.UseHTTPS, nil), ®ion.BucketRegionsQueryOptions{ 380 UseInsecureProtocol: !cfg.UseHTTPS, 381 Client: clt.Client, 382 }) 383 384 opts := http_client.Options{ 385 BasicHTTPClient: clt.Client, 386 BucketQuery: bucketQuery, 387 Credentials: mac, 388 Interceptors: []clientv2.Interceptor{}, 389 UseInsecureProtocol: !cfg.UseHTTPS, 390 Resolver: options.Resolver, 391 Chooser: options.Chooser, 392 HostRetryConfig: &clientv2.RetryConfig{RetryMax: options.RetryMax, Retrier: options.Retrier}, 393 HostsRetryConfig: &clientv2.RetryConfig{Retrier: options.Retrier}, 394 HostFreezeDuration: options.HostFreezeDuration, 395 BeforeSign: options.BeforeSign, 396 AfterSign: options.AfterSign, 397 SignError: options.SignError, 398 BeforeResolve: options.BeforeResolve, 399 AfterResolve: options.AfterResolve, 400 ResolveError: options.ResolveError, 401 BeforeBackoff: options.BeforeBackoff, 402 AfterBackoff: options.AfterBackoff, 403 BeforeRequest: options.BeforeRequest, 404 AfterResponse: options.AfterResponse, 405 } 406 if region := cfg.GetRegion(); region != nil { 407 opts.Regions = region 408 } 409 410 return &BucketManager{ 411 Client: clt, 412 Mac: mac, 413 Cfg: cfg, 414 options: options, 415 apiClient: apis.NewStorage(&opts), 416 downloadManager: downloader.NewDownloadManager(&downloader.DownloadManagerOptions{Options: opts}), 417 } 418 } 419 420 // UpdateObjectStatus 用来修改文件状态, 禁用和启用文件的可访问性 421 422 // 请求包: 423 // 424 // POST /chstatus/<EncodedEntry>/status/<status> 425 // status:0表示启用,1表示禁用 426 // 返回包(JSON): 427 // 428 // 200 OK 429 // 当<EncodedEntryURI>解析失败,返回400 Bad Request {"error":"invalid argument"} 430 // 当<EncodedEntryURI>不符合UTF-8编码,返回400 Bad Request {"error":"key must be utf8 encoding"} 431 // 当文件不存在时,返回612 status code 612 {"error":"no such file or directory"} 432 // 当文件当前状态和设置的状态已经一致,返回400 {"error":"already enabled"}或400 {"error":"already disabled"} 433 func (m *BucketManager) UpdateObjectStatus(bucketName string, key string, enable bool) error { 434 var status int64 435 if enable { 436 status = 0 437 } else { 438 status = 1 439 } 440 _, err := m.apiClient.ModifyObjectStatus( 441 context.Background(), 442 &apis.ModifyObjectStatusRequest{ 443 Entry: bucketName + ":" + key, 444 Status: status, 445 }, 446 m.makeRequestOptions(), 447 ) 448 return err 449 } 450 451 // CreateBucket 创建一个七牛存储空间 452 func (m *BucketManager) CreateBucket(bucketName string, regionID RegionID) error { 453 _, err := m.apiClient.CreateBucket( 454 context.Background(), 455 &apis.CreateBucketRequest{ 456 Bucket: bucketName, 457 Region: string(regionID), 458 }, 459 m.makeRequestOptions(), 460 ) 461 return err 462 } 463 464 // Buckets 用来获取空间列表,如果指定了 shared 参数则额外包含仅被授予了读权限的空间,否则额外包含被授予了读写权限的授权空间 465 func (m *BucketManager) Buckets(shared bool) ([]string, error) { 466 var sharedMode string 467 if shared { 468 sharedMode = "rd" 469 } else { 470 sharedMode = "rw" 471 } 472 response, err := m.apiClient.GetBuckets( 473 context.Background(), 474 &apis.GetBucketsRequest{ 475 Shared: sharedMode, 476 }, 477 m.makeRequestOptions(), 478 ) 479 if err != nil { 480 return nil, err 481 } 482 return response.BucketNames, nil 483 } 484 485 // BucketsV4 获取该用户的指定区域内的空间信息,注意该 API 以分页形式返回 Bucket 列表 486 func (m *BucketManager) BucketsV4(input *BucketV4Input) (output BucketsV4Output, err error) { 487 if input == nil { 488 input = &BucketV4Input{} 489 } 490 response, err := m.apiClient.GetBucketsV4( 491 context.Background(), 492 &apis.GetBucketsV4Request{ 493 Region: input.Region, 494 Limit: int64(input.Limit), 495 Marker: input.Marker, 496 }, 497 m.makeRequestOptions(), 498 ) 499 if err != nil { 500 return 501 } 502 output.IsTruncated = response.IsTruncated 503 output.NextMarker = response.NextMarker 504 output.Buckets = make([]BucketV4Output, 0, len(response.Buckets)) 505 for _, bucket := range response.Buckets { 506 ctime, err := time.Parse(time.RFC3339, bucket.CreatedTime) 507 if err != nil { 508 return output, err 509 } 510 output.Buckets = append(output.Buckets, BucketV4Output{ 511 Name: bucket.Name, 512 Region: bucket.Region, 513 Private: bucket.Private, 514 Ctime: ctime, 515 }) 516 } 517 return 518 } 519 520 // DropBucket 删除七牛存储空间 521 func (m *BucketManager) DropBucket(bucketName string) error { 522 _, err := m.apiClient.DeleteBucket( 523 context.Background(), 524 &apis.DeleteBucketRequest{ 525 Bucket: bucketName, 526 }, 527 m.makeRequestOptions(), 528 ) 529 return err 530 } 531 532 // Stat 用来获取一个文件的基本信息 533 func (m *BucketManager) Stat(bucket, key string) (FileInfo, error) { 534 return m.StatWithOpts(bucket, key, nil) 535 } 536 537 type StatOpts struct { 538 NeedParts bool 539 } 540 541 // StatWithParts 用来获取一个文件的基本信息以及分片信息 542 func (m *BucketManager) StatWithOpts(bucket, key string, opt *StatOpts) (FileInfo, error) { 543 if opt == nil { 544 opt = &StatOpts{} 545 } 546 response, err := m.apiClient.StatObject( 547 context.Background(), 548 &apis.StatObjectRequest{ 549 Entry: bucket + ":" + key, 550 NeedParts: opt.NeedParts, 551 }, 552 m.makeRequestOptions(), 553 ) 554 if err != nil { 555 return FileInfo{}, err 556 } 557 return FileInfo{ 558 Fsize: response.Size, 559 Hash: response.Hash, 560 MimeType: response.MimeType, 561 Type: int(response.Type), 562 PutTime: response.PutTime, 563 RestoreStatus: int(response.RestoringStatus), 564 Status: int(response.Status), 565 Md5: response.Md5, 566 EndUser: response.EndUser, 567 MetaData: response.Metadata, 568 Expiration: response.ExpirationTime, 569 TransitionToIA: response.TransitionToIaTime, 570 TransitionToArchive: response.TransitionToArchiveTime, 571 TransitionToDeepArchive: response.TransitionToDeepArchiveTime, 572 Parts: response.Parts, 573 }, nil 574 } 575 576 // Delete 用来删除空间中的一个文件 577 func (m *BucketManager) Delete(bucket, key string) error { 578 _, err := m.apiClient.DeleteObject( 579 context.Background(), 580 &apis.DeleteObjectRequest{ 581 Entry: bucket + ":" + key, 582 }, 583 m.makeRequestOptions(), 584 ) 585 return err 586 } 587 588 // Copy 用来创建已有空间中的文件的一个新的副本 589 func (m *BucketManager) Copy(srcBucket, srcKey, destBucket, destKey string, force bool) error { 590 _, err := m.apiClient.CopyObject( 591 context.Background(), 592 &apis.CopyObjectRequest{ 593 SrcEntry: srcBucket + ":" + srcKey, 594 DestEntry: destBucket + ":" + destKey, 595 IsForce: force, 596 }, 597 m.makeRequestOptions(), 598 ) 599 return err 600 } 601 602 // Move 用来将空间中的一个文件移动到新的空间或者重命名 603 func (m *BucketManager) Move(srcBucket, srcKey, destBucket, destKey string, force bool) error { 604 _, err := m.apiClient.MoveObject( 605 context.Background(), 606 &apis.MoveObjectRequest{ 607 SrcEntry: srcBucket + ":" + srcKey, 608 DestEntry: destBucket + ":" + destKey, 609 IsForce: force, 610 }, 611 m.makeRequestOptions(), 612 ) 613 return err 614 } 615 616 // ChangeMime 用来更新文件的MimeType 617 func (m *BucketManager) ChangeMime(bucket, key, newMime string) error { 618 return m.ChangeMimeAndMeta(bucket, key, newMime, nil) 619 } 620 621 // ChangeMeta 622 // 623 // @Description: 用来修改或者增加 metas,metas 只包含需要修改的,未涉及的保存不变 624 // @receiver m BucketManager 625 // @param bucket 对象所属空间 626 // @param key 对象保存的 key 627 // @param newMime 新的 Mime 628 // @param metas 需要修改的 metas,只包含需要更改的 metas,可增加; 629 // 服务接口中 key 必须包含 x-qn-meta- 前缀,SDK 会对 metas 参数中的 key 进行检测; 630 // - key 如果包含了 x-qn-meta- 前缀,则直接使用 key; 631 // - key 如果不包含了 x-qn-meta- 前缀,则内部会为 key 拼接 x-qn-meta- 前缀。 632 // @return err 错误信息 633 func (m *BucketManager) ChangeMeta(bucket, key string, metas map[string]string) (err error) { 634 return m.ChangeMimeAndMeta(bucket, key, "", metas) 635 } 636 637 // ChangeMimeAndMeta 638 // 639 // @Description: 用用来更新文件的 MimeType 以及修改或者增加 metas,metas 只包含需要修改的 640 // @receiver m BucketManager 641 // @param bucket 对象所属空间 642 // @param key 对象保存的 key 643 // @param newMime 新的 Mime 644 // @param metas 需要修改的 metas,只包含需要更改的 metas,可增加; 645 // 服务接口中 key 必须包含 x-qn-meta- 前缀,SDK 会对 metas 参数中的 key 进行检测; 646 // - key 如果包含了 x-qn-meta- 前缀,则直接使用 key; 647 // - key 如果不包含了 x-qn-meta- 前缀,则内部会为 key 拼接 x-qn-meta- 前缀。 648 // @return err 错误信息 649 func (m *BucketManager) ChangeMimeAndMeta(bucket, key, newMime string, metas map[string]string) error { 650 metaData := make(map[string]string, len(metas)) 651 for k, v := range normalizeMeta(metas) { 652 metaData[k] = v 653 } 654 _, err := m.apiClient.ModifyObjectMetadata( 655 context.Background(), 656 &apis.ModifyObjectMetadataRequest{ 657 Entry: bucket + ":" + key, 658 MimeType: newMime, 659 MetaData: metaData, 660 }, 661 m.makeRequestOptions(), 662 ) 663 return err 664 } 665 666 // ChangeType 用来更新文件的存储类型,0 表示普通存储,1 表示低频存储,2 表示归档存储,3 表示深度归档存储,4 表示归档直读存储 667 func (m *BucketManager) ChangeType(bucket, key string, fileType int) error { 668 _, err := m.apiClient.SetObjectFileType( 669 context.Background(), 670 &apis.SetObjectFileTypeRequest{ 671 Entry: bucket + ":" + key, 672 Type: int64(fileType), 673 }, 674 m.makeRequestOptions(), 675 ) 676 return err 677 } 678 679 // RestoreAr 解冻归档存储类型的文件,可设置解冻有效期1~7天, 完成解冻任务通常需要1~5分钟 680 func (m *BucketManager) RestoreAr(bucket, key string, freezeAfterDays int) error { 681 _, err := m.apiClient.RestoreArchivedObject( 682 context.Background(), 683 &apis.RestoreArchivedObjectRequest{ 684 Entry: bucket + ":" + key, 685 FreezeAfterDays: int64(freezeAfterDays), 686 }, 687 m.makeRequestOptions(), 688 ) 689 return err 690 } 691 692 // DeleteAfterDays 用来更新文件生命周期,如果 days 设置为0,则表示取消文件的定期删除功能,永久存储 693 func (m *BucketManager) DeleteAfterDays(bucket, key string, days int) error { 694 _, err := m.apiClient.DeleteObjectAfterDays( 695 context.Background(), 696 &apis.DeleteObjectAfterDaysRequest{ 697 Entry: bucket + ":" + key, 698 DeleteAfterDays: int64(days), 699 }, 700 m.makeRequestOptions(), 701 ) 702 return err 703 } 704 705 // Batch 接口提供了资源管理的批量操作,支持 stat,copy,move,delete,chgm,chtype,deleteAfterDays几个接口 706 // 没有 bucket 参数,会从 operations 中解析出 bucket 707 // @param operations 操作对象列表,操作对象所属的 bucket 可能会不同,但是必须属于同一个区域 708 func (m *BucketManager) Batch(operations []string) ([]BatchOpRet, error) { 709 if len(operations) == 0 { 710 return nil, errors.New("operations is empty") 711 } 712 713 bucket := "" 714 for _, operation := range operations { 715 paths := strings.Split(operation, "/") 716 if len(paths) < 3 { 717 continue 718 } 719 720 // 按当前模式,第 3 个 entry 是 bucket 和 key 键值对 721 if b, _, err := decodedEntry(paths[2]); err != nil { 722 continue 723 } else { 724 bucket = b 725 break 726 } 727 } 728 if len(bucket) == 0 { 729 return nil, errors.New("can't get one bucket from operations") 730 } 731 732 return m.BatchWithContext(context.Background(), bucket, operations) 733 } 734 735 func convertToBatchOptRet(op batch_ops.OperationResponse) BatchOpRet { 736 var ret BatchOpRet 737 ret.Code = int(op.Code) 738 opData := op.Data 739 ret.Data.Fsize = opData.Size 740 ret.Data.Hash = opData.Hash 741 ret.Data.MimeType = opData.MimeType 742 ret.Data.Type = int(opData.Type) 743 ret.Data.PutTime = opData.PutTime 744 if restoringStatus := int(opData.RestoringStatus); restoringStatus != 0 { 745 ret.Data.RestoreStatus = &restoringStatus 746 } 747 if status := int(opData.Status); status != 0 { 748 ret.Data.Status = &status 749 } 750 ret.Data.Md5 = opData.Md5 751 ret.Data.EndUser = opData.EndUser 752 if expirationTime := opData.ExpirationTime; expirationTime != 0 { 753 ret.Data.Expiration = &expirationTime 754 } 755 if transitionToIA := opData.TransitionToIaTime; transitionToIA != 0 { 756 ret.Data.TransitionToIA = &transitionToIA 757 } 758 if transitionToArchiveTime := opData.TransitionToArchiveTime; transitionToArchiveTime != 0 { 759 ret.Data.TransitionToArchive = &transitionToArchiveTime 760 } 761 if transitionToDeepArchiveTime := opData.TransitionToDeepArchiveTime; transitionToDeepArchiveTime != 0 { 762 ret.Data.TransitionToDeepArchive = &transitionToDeepArchiveTime 763 } 764 ret.Data.Error = opData.Error 765 return ret 766 } 767 768 // BatchWithContext 接口提供了资源管理的批量操作,支持 stat,copy,move,delete,chgm,chtype,deleteAfterDays几个接口 769 // @param ctx context.Context 770 // @param bucket operations 列表中任意一个操作对象所属的 bucket 771 // @param operations 操作对象列表,操作对象所属的 bucket 可能会不同,但是必须属于同一个区域 772 func (m *BucketManager) BatchWithContext(ctx context.Context, bucket string, operations []string) ([]BatchOpRet, error) { 773 if len(operations) > 1000 { 774 return nil, errors.New("batch operation count exceeds the limit of 1000") 775 } 776 if ctx == nil { 777 ctx = context.Background() 778 } 779 780 opts := m.makeRequestOptions() 781 opts.OverwrittenBucketName = bucket 782 response, err := m.apiClient.BatchOps( 783 ctx, 784 &apis.BatchOpsRequest{ 785 Operations: operations, 786 }, 787 opts, 788 ) 789 if err != nil { 790 return nil, err 791 } 792 rets := make([]BatchOpRet, 0, len(response.OperationResponses)) 793 for _, op := range response.OperationResponses { 794 rets = append(rets, convertToBatchOptRet(op)) 795 } 796 return rets, nil 797 } 798 799 // Fetch 根据提供的远程资源链接来抓取一个文件到空间并已指定文件名保存 800 func (m *BucketManager) Fetch(resURL, bucket, key string) (FetchRet, error) { 801 response, err := m.apiClient.FetchObject( 802 context.Background(), 803 &apis.FetchObjectRequest{ 804 FromUrl: resURL, 805 ToEntry: bucket + ":" + key, 806 }, 807 m.makeRequestOptions(), 808 ) 809 if err != nil { 810 return FetchRet{}, err 811 } 812 return FetchRet{ 813 Hash: response.Hash, 814 Fsize: response.Size, 815 MimeType: response.MimeType, 816 Key: response.ObjectName, 817 }, nil 818 } 819 820 func (m *BucketManager) RsReqHost(bucket string) (reqHost string, err error) { 821 var reqErr error 822 823 if m.Cfg.RsHost == "" { 824 reqHost, reqErr = m.RsHost(bucket) 825 if reqErr != nil { 826 err = reqErr 827 return 828 } 829 } else { 830 reqHost = m.Cfg.RsHost 831 } 832 if !strings.HasPrefix(reqHost, "http") { 833 reqHost = endpoint(m.Cfg.UseHTTPS, reqHost) 834 } 835 return 836 } 837 838 func (m *BucketManager) ApiReqHost(bucket string) (reqHost string, err error) { 839 var reqErr error 840 841 if m.Cfg.ApiHost == "" { 842 reqHost, reqErr = m.ApiHost(bucket) 843 if reqErr != nil { 844 err = reqErr 845 return 846 } 847 } else { 848 reqHost = m.Cfg.ApiHost 849 } 850 if !strings.HasPrefix(reqHost, "http") { 851 reqHost = endpoint(m.Cfg.UseHTTPS, reqHost) 852 } 853 return 854 } 855 856 func (m *BucketManager) RsfReqHost(bucket string) (reqHost string, err error) { 857 var reqErr error 858 859 if m.Cfg.RsfHost == "" { 860 reqHost, reqErr = m.RsfHost(bucket) 861 if reqErr != nil { 862 err = reqErr 863 return 864 } 865 } else { 866 reqHost = m.Cfg.RsfHost 867 } 868 if !strings.HasPrefix(reqHost, "http") { 869 reqHost = endpoint(m.Cfg.UseHTTPS, reqHost) 870 } 871 return 872 } 873 874 func (m *BucketManager) IoReqHost(bucket string) (reqHost string, err error) { 875 var reqErr error 876 877 if m.Cfg.IoHost == "" { 878 reqHost, reqErr = m.IovipHost(bucket) 879 if reqErr != nil { 880 err = reqErr 881 return 882 } 883 } else { 884 reqHost = m.Cfg.IoHost 885 } 886 if !strings.HasPrefix(reqHost, "http") { 887 reqHost = endpoint(m.Cfg.UseHTTPS, reqHost) 888 } 889 return 890 } 891 892 // FetchWithoutKey 根据提供的远程资源链接来抓取一个文件到空间并以文件的内容hash作为文件名 893 func (m *BucketManager) FetchWithoutKey(resURL, bucket string) (fetchRet FetchRet, err error) { 894 response, err := m.apiClient.FetchObject( 895 context.Background(), 896 &apis.FetchObjectRequest{ 897 FromUrl: resURL, 898 ToEntry: bucket, 899 }, 900 m.makeRequestOptions(), 901 ) 902 if err != nil { 903 return FetchRet{}, err 904 } 905 return FetchRet{ 906 Hash: response.Hash, 907 Fsize: response.Size, 908 MimeType: response.MimeType, 909 Key: response.ObjectName, 910 }, nil 911 } 912 913 // DomainInfo 是绑定在存储空间上的域名的具体信息 914 type DomainInfo struct { 915 Domain string `json:"domain"` 916 917 // 存储空间名字 918 Tbl string `json:"tbl"` 919 920 // 用户UID 921 Owner int `json:"uid"` 922 Refresh bool `json:"refresh"` 923 Ctime int `json:"ctime"` 924 Utime int `json:"utime"` 925 } 926 927 // ListBucketDomains 返回绑定在存储空间中的域名信息 928 func (m *BucketManager) ListBucketDomains(bucket string) (info []DomainInfo, err error) { 929 toDomainInfo := func(info *get_bucket_domains_v3.DomainInfo) DomainInfo { 930 return DomainInfo{ 931 Domain: info.Domain, 932 Tbl: info.Bucket, 933 Owner: int(info.OwnerId), 934 Refresh: info.AutoRefresh, 935 Ctime: int(info.CreatedTime), 936 Utime: int(info.UpdatedTime), 937 } 938 } 939 response, err := m.apiClient.GetBucketDomainsV3( 940 context.Background(), 941 &apis.GetBucketDomainsV3Request{ 942 BucketName: bucket, 943 }, 944 m.makeRequestOptions(), 945 ) 946 if err != nil { 947 return nil, err 948 } 949 domainInfos := make([]DomainInfo, 0, len(response.DomainInfos)) 950 for _, domainInfo := range response.DomainInfos { 951 domainInfos = append(domainInfos, toDomainInfo(&domainInfo)) 952 } 953 return domainInfos, nil 954 } 955 956 // Prefetch 用来同步镜像空间的资源和镜像源资源内容 957 func (m *BucketManager) Prefetch(bucket, key string) error { 958 _, err := m.apiClient.PrefetchObject( 959 context.Background(), 960 &apis.PrefetchObjectRequest{ 961 Entry: bucket + ":" + key, 962 }, 963 m.makeRequestOptions(), 964 ) 965 return err 966 } 967 968 // SetImage 用来设置空间镜像源 969 func (m *BucketManager) SetImage(siteURL, bucket string) error { 970 _, err := m.apiClient.SetBucketImage( 971 context.Background(), 972 &apis.SetBucketImageRequest{ 973 Bucket: bucket, 974 Url: siteURL, 975 }, 976 m.makeRequestOptions(), 977 ) 978 return err 979 } 980 981 // SetImageWithHost 用来设置空间镜像源,额外添加回源Host头部 982 func (m *BucketManager) SetImageWithHost(siteURL, bucket, host string) error { 983 _, err := m.apiClient.SetBucketImage( 984 context.Background(), 985 &apis.SetBucketImageRequest{ 986 Bucket: bucket, 987 Url: siteURL, 988 Host: host, 989 }, 990 m.makeRequestOptions(), 991 ) 992 return err 993 } 994 995 // UnsetImage 用来取消空间镜像源设置 996 func (m *BucketManager) UnsetImage(bucket string) error { 997 _, err := m.apiClient.UnsetBucketImage( 998 context.Background(), 999 &apis.UnsetBucketImageRequest{ 1000 Bucket: bucket, 1001 }, 1002 m.makeRequestOptions(), 1003 ) 1004 return err 1005 } 1006 1007 type AsyncFetchParam struct { 1008 Url string `json:"url"` 1009 Host string `json:"host,omitempty"` 1010 Bucket string `json:"bucket"` 1011 Key string `json:"key,omitempty"` 1012 Md5 string `json:"md5,omitempty"` 1013 Etag string `json:"etag,omitempty"` 1014 CallbackURL string `json:"callbackurl,omitempty"` 1015 CallbackBody string `json:"callbackbody,omitempty"` 1016 CallbackBodyType string `json:"callbackbodytype,omitempty"` 1017 FileType int `json:"file_type,omitempty"` 1018 } 1019 1020 type AsyncFetchRet struct { 1021 Id string `json:"id"` 1022 Wait int `json:"wait"` 1023 } 1024 1025 func (m *BucketManager) AsyncFetch(param AsyncFetchParam) (ret AsyncFetchRet, err error) { 1026 response, err := m.apiClient.AsyncFetchObject( 1027 context.Background(), 1028 &apis.AsyncFetchObjectRequest{ 1029 Url: param.Url, 1030 Bucket: param.Bucket, 1031 Host: param.Host, 1032 Key: param.Key, 1033 Md5: param.Md5, 1034 Etag: param.Etag, 1035 CallbackUrl: param.CallbackURL, 1036 CallbackBody: param.CallbackBody, 1037 CallbackBodyType: param.CallbackBodyType, 1038 FileType: int64(param.FileType), 1039 }, 1040 m.makeRequestOptions(), 1041 ) 1042 if err != nil { 1043 return AsyncFetchRet{}, err 1044 } 1045 return AsyncFetchRet{ 1046 Id: response.Id, 1047 Wait: int(response.QueuedTasksCount), 1048 }, nil 1049 } 1050 1051 func (m *BucketManager) RsHost(bucket string) (rsHost string, err error) { 1052 zone, err := m.Zone(bucket) 1053 if err != nil { 1054 return 1055 } 1056 1057 if len(zone.RsHost) == 0 { 1058 return "", errors.New("can't get region rs host with bucket:" + bucket) 1059 } 1060 1061 rsHost = zone.GetRsHost(m.Cfg.UseHTTPS) 1062 return 1063 } 1064 1065 func (m *BucketManager) RsfHost(bucket string) (rsfHost string, err error) { 1066 zone, err := m.Zone(bucket) 1067 if err != nil { 1068 return 1069 } 1070 1071 if len(zone.RsfHost) == 0 { 1072 return "", errors.New("can't get region rsf host with bucket:" + bucket) 1073 } 1074 1075 rsfHost = zone.GetRsfHost(m.Cfg.UseHTTPS) 1076 return 1077 } 1078 1079 func (m *BucketManager) IovipHost(bucket string) (iovipHost string, err error) { 1080 zone, err := m.Zone(bucket) 1081 if err != nil { 1082 return 1083 } 1084 1085 if len(zone.IovipHost) == 0 { 1086 return "", errors.New("can't get region io host with bucket:" + bucket) 1087 } 1088 1089 iovipHost = zone.GetIoHost(m.Cfg.UseHTTPS) 1090 return 1091 } 1092 1093 func (m *BucketManager) ApiHost(bucket string) (apiHost string, err error) { 1094 zone, err := m.Zone(bucket) 1095 if err != nil { 1096 return 1097 } 1098 1099 if len(zone.ApiHost) == 0 { 1100 return "", errors.New("can't get region api host with bucket:" + bucket) 1101 } 1102 1103 apiHost = zone.GetApiHost(m.Cfg.UseHTTPS) 1104 return 1105 } 1106 1107 func (m *BucketManager) Zone(bucket string) (z *Zone, err error) { 1108 z = m.Cfg.GetRegion() 1109 if z != nil { 1110 return 1111 } 1112 1113 z, err = GetZone(m.Mac.AccessKey, bucket) 1114 return 1115 } 1116 1117 func (m *BucketManager) makeRequestOptions() *apis.Options { 1118 return &apis.Options{OverwrittenBucketHosts: getUcEndpointProvider(m.Cfg.UseHTTPS, nil)} 1119 } 1120 1121 // 构建op的方法,导出的方法支持在Batch操作中使用 1122 1123 // URIStat 构建 stat 接口的请求命令 1124 func URIStat(bucket, key string) string { 1125 return fmt.Sprintf("/stat/%s", EncodedEntry(bucket, key)) 1126 } 1127 1128 // URIDelete 构建 delete 接口的请求命令 1129 func URIDelete(bucket, key string) string { 1130 return fmt.Sprintf("/delete/%s", EncodedEntry(bucket, key)) 1131 } 1132 1133 // URICopy 构建 copy 接口的请求命令 1134 func URICopy(srcBucket, srcKey, destBucket, destKey string, force bool) string { 1135 return fmt.Sprintf("/copy/%s/%s/force/%v", EncodedEntry(srcBucket, srcKey), 1136 EncodedEntry(destBucket, destKey), force) 1137 } 1138 1139 // URIMove 构建 move 接口的请求命令 1140 func URIMove(srcBucket, srcKey, destBucket, destKey string, force bool) string { 1141 return fmt.Sprintf("/move/%s/%s/force/%v", EncodedEntry(srcBucket, srcKey), 1142 EncodedEntry(destBucket, destKey), force) 1143 } 1144 1145 // URIDeleteAfterDays 构建 deleteAfterDays 接口的请求命令 1146 func URIDeleteAfterDays(bucket, key string, days int) string { 1147 return fmt.Sprintf("/deleteAfterDays/%s/%d", EncodedEntry(bucket, key), days) 1148 } 1149 1150 // URIChangeMime 构建 chgm 接口的请求命令 1151 func URIChangeMime(bucket, key, newMime string) string { 1152 return fmt.Sprintf("/chgm/%s/mime/%s", EncodedEntry(bucket, key), 1153 base64.URLEncoding.EncodeToString([]byte(newMime))) 1154 } 1155 1156 // URIChangeMeta 1157 // 1158 // @Description: 构建 chgm 接口的请求命令,修改 meta 1159 // @param bucket 空间 1160 // @param key 文件保存的 key 1161 // @param metas 需要修改的 metas,只包含需要更改的 metas,可增加 1162 // 服务接口中 key 必须包含 x-qn-meta- 前缀,SDK 会对 metas 中的 key 进行检测 1163 // - key 如果包含了 x-qn-meta- 前缀,则直接使用 key 1164 // - key 如果不包含了 x-qn-meta- 前缀,则内部会为 key 拼接 x-qn-meta- 前缀 1165 // @return string URI 1166 func URIChangeMeta(bucket, key string, changeMetas map[string]string) string { 1167 return URIChangeMimeAndMeta(bucket, key, "", changeMetas) 1168 } 1169 1170 // URIChangeMimeAndMeta 1171 // 1172 // @Description: 构建 chgm 接口的请求命令 1173 // @param bucket 空间 1174 // @param key 文件保存的 key 1175 // @param newMime 新的 mime 1176 // @param metas 需要修改的 metas,只包含需要更改的 metas,可增加 1177 // 服务接口中 key 必须包含 x-qn-meta- 前缀,SDK 会对 metas 中的 key 进行检测 1178 // - key 如果包含了 x-qn-meta- 前缀,则直接使用 key 1179 // - key 如果不包含了 x-qn-meta- 前缀,则内部会为 key 拼接 x-qn-meta- 前缀 1180 // @return string URI 1181 func URIChangeMimeAndMeta(bucket, key, newMime string, metas map[string]string) string { 1182 uri := fmt.Sprintf("/chgm/%s", EncodedEntry(bucket, key)) 1183 if len(newMime) > 0 { 1184 uri = fmt.Sprintf("%s/mime/%s", uri, base64.URLEncoding.EncodeToString([]byte(newMime))) 1185 } 1186 if len(metas) > 0 { 1187 for k, v := range normalizeMeta(metas) { 1188 uri = fmt.Sprintf("%s/%s/%s", uri, k, base64.URLEncoding.EncodeToString([]byte(v))) 1189 } 1190 } 1191 return uri 1192 } 1193 1194 func normalizeMeta(metas map[string]string) map[string]string { 1195 newMetas := make(map[string]string, len(metas)) 1196 for k, v := range metas { 1197 if !strings.HasPrefix(k, "x-qn-meta-") { 1198 k = "x-qn-meta-" + k 1199 } 1200 newMetas[k] = v 1201 } 1202 return newMetas 1203 } 1204 1205 // URIChangeType 构建 chtype 接口的请求命令 1206 func URIChangeType(bucket, key string, fileType int) string { 1207 return fmt.Sprintf("/chtype/%s/type/%d", EncodedEntry(bucket, key), fileType) 1208 } 1209 1210 // URIRestoreAr 构建 restoreAr 接口的请求命令 1211 func URIRestoreAr(bucket, key string, afterDay int) string { 1212 return fmt.Sprintf("/restoreAr/%s/freezeAfterDays/%d", EncodedEntry(bucket, key), afterDay) 1213 } 1214 1215 // EncodedEntry 生成URL Safe Base64编码的 Entry 1216 func EncodedEntry(bucket, key string) string { 1217 entry := fmt.Sprintf("%s:%s", bucket, key) 1218 return base64.URLEncoding.EncodeToString([]byte(entry)) 1219 } 1220 1221 func decodedEntry(entry string) (bucket, key string, err error) { 1222 value, dErr := base64.URLEncoding.DecodeString(entry) 1223 if dErr != nil { 1224 return "", "", dErr 1225 } 1226 bk := strings.Split(string(value), ":") 1227 if len(bk) == 0 { 1228 return "", "", errors.New("entry format error") 1229 } 1230 if len(bk) == 1 { 1231 return bk[0], "", nil 1232 } 1233 return bk[0], bk[1], nil 1234 } 1235 1236 // EncodedEntryWithoutKey 生成 key 为null的情况下 URL Safe Base64编码的Entry 1237 func EncodedEntryWithoutKey(bucket string) string { 1238 return base64.URLEncoding.EncodeToString([]byte(bucket)) 1239 } 1240 1241 // MakePublicURL 用来生成公开空间资源下载链接,注意该方法并不会对 key 进行 escape 1242 func MakePublicURL(domain, key string) (finalUrl string) { 1243 domain = strings.TrimRight(domain, "/") 1244 srcUrl := fmt.Sprintf("%s/%s", domain, key) 1245 srcUri, _ := url.Parse(srcUrl) 1246 finalUrl = srcUri.String() 1247 return 1248 } 1249 1250 // MakePublicURLv2 用来生成公开空间资源下载链接,并且该方法确保 key 将会被 escape 1251 func MakePublicURLv2(domain, key string) string { 1252 return MakePublicURLv2WithQuery(domain, key, nil) 1253 } 1254 1255 // MakePublicURLv2WithQuery 用来生成公开空间资源下载链接,并且该方法确保 key 将会被 escape,并在 URL 后追加经过编码的查询参数 1256 func MakePublicURLv2WithQuery(domain, key string, query url.Values) string { 1257 var rawQuery string 1258 if query != nil { 1259 rawQuery = query.Encode() 1260 } 1261 return makePublicURLv2WithRawQuery(domain, key, rawQuery) 1262 } 1263 1264 // MakePublicURLv2WithQueryString 用来生成公开空间资源下载链接,并且该方法确保 key 将会被 escape,并在 URL 后直接追加查询参数 1265 func MakePublicURLv2WithQueryString(domain, key, query string) string { 1266 return makePublicURLv2WithRawQuery(domain, key, urlEncodeQuery(query)) 1267 } 1268 1269 func makePublicURLv2WithRawQuery(domain, key, rawQuery string) string { 1270 domain = strings.TrimRight(domain, "/") 1271 srcUrl := fmt.Sprintf("%s/%s", domain, urlEncodeQuery(key)) 1272 if rawQuery != "" { 1273 srcUrl += "?" + rawQuery 1274 } 1275 srcUri, _ := url.Parse(srcUrl) 1276 return srcUri.String() 1277 } 1278 1279 // MakePrivateURL 用来生成私有空间资源下载链接,注意该方法并不会对 key 进行 escape 1280 func MakePrivateURL(mac *auth.Credentials, domain, key string, deadline int64) (privateURL string) { 1281 publicURL := MakePublicURL(domain, key) 1282 urlToSign := publicURL 1283 if strings.Contains(publicURL, "?") { 1284 urlToSign = fmt.Sprintf("%s&e=%d", urlToSign, deadline) 1285 } else { 1286 urlToSign = fmt.Sprintf("%s?e=%d", urlToSign, deadline) 1287 } 1288 token := mac.Sign([]byte(urlToSign)) 1289 privateURL = fmt.Sprintf("%s&token=%s", urlToSign, token) 1290 return 1291 } 1292 1293 // MakePrivateURLv2 用来生成私有空间资源下载链接,并且该方法确保 key 将会被 escape 1294 func MakePrivateURLv2(mac *auth.Credentials, domain, key string, deadline int64) (privateURL string) { 1295 return MakePrivateURLv2WithQuery(mac, domain, key, nil, deadline) 1296 } 1297 1298 // MakePrivateURLv2WithQuery 用来生成私有空间资源下载链接,并且该方法确保 key 将会被 escape,并在 URL 后追加经过编码的查询参数 1299 func MakePrivateURLv2WithQuery(mac *auth.Credentials, domain, key string, query url.Values, deadline int64) (privateURL string) { 1300 var rawQuery string 1301 if query != nil { 1302 rawQuery = query.Encode() 1303 } 1304 return makePrivateURLv2WithRawQuery(mac, domain, key, rawQuery, deadline) 1305 } 1306 1307 // MakePrivateURLv2WithQueryString 用来生成私有空间资源下载链接,并且该方法确保 key 将会被 escape,并在 URL 后直接追加查询参数 1308 func MakePrivateURLv2WithQueryString(mac *auth.Credentials, domain, key, query string, deadline int64) (privateURL string) { 1309 return makePrivateURLv2WithRawQuery(mac, domain, key, urlEncodeQuery(query), deadline) 1310 } 1311 1312 func makePrivateURLv2WithRawQuery(mac *auth.Credentials, domain, key, rawQuery string, deadline int64) (privateURL string) { 1313 publicURL := makePublicURLv2WithRawQuery(domain, key, rawQuery) 1314 urlToSign := publicURL 1315 if strings.Contains(publicURL, "?") { 1316 urlToSign = fmt.Sprintf("%s&e=%d", urlToSign, deadline) 1317 } else { 1318 urlToSign = fmt.Sprintf("%s?e=%d", urlToSign, deadline) 1319 } 1320 token := mac.Sign([]byte(urlToSign)) 1321 privateURL = fmt.Sprintf("%s&token=%s", urlToSign, token) 1322 return 1323 } 1324 1325 func urlEncodeQuery(str string) (ret string) { 1326 str = url.QueryEscape(str) 1327 str = strings.Replace(str, "%2F", "/", -1) 1328 str = strings.Replace(str, "%7C", "|", -1) 1329 str = strings.Replace(str, "+", "%20", -1) 1330 return str 1331 }