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), &region.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  }