github.com/qiniu/go-sdk/v7@v7.27.0/storage/resume_uploader_v2.go (about)

     1  package storage
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/md5"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"sync"
    14  
    15  	"github.com/qiniu/go-sdk/v7/client"
    16  	"github.com/qiniu/go-sdk/v7/internal/clientv2"
    17  	"github.com/qiniu/go-sdk/v7/storagev2/apis"
    18  	"github.com/qiniu/go-sdk/v7/storagev2/http_client"
    19  	"github.com/qiniu/go-sdk/v7/storagev2/region"
    20  	"github.com/qiniu/go-sdk/v7/storagev2/uplog"
    21  )
    22  
    23  // ResumeUploaderV2 表示一个分片上传 v2 的对象
    24  type ResumeUploaderV2 struct {
    25  	Client  *client.Client
    26  	Cfg     *Config
    27  	storage *apis.Storage
    28  }
    29  
    30  // NewResumeUploaderV2 表示构建一个新的分片上传的对象
    31  func NewResumeUploaderV2(cfg *Config) *ResumeUploaderV2 {
    32  	return NewResumeUploaderV2Ex(cfg, nil)
    33  }
    34  
    35  // NewResumeUploaderV2Ex 表示构建一个新的分片上传 v2 的对象
    36  func NewResumeUploaderV2Ex(cfg *Config, clt *client.Client) *ResumeUploaderV2 {
    37  	if cfg == nil {
    38  		cfg = NewConfig()
    39  	}
    40  
    41  	if clt == nil {
    42  		clt = &client.DefaultClient
    43  	}
    44  
    45  	bucketQuery, _ := region.NewBucketRegionsQuery(getUcEndpoint(cfg.UseHTTPS, nil), &region.BucketRegionsQueryOptions{
    46  		UseInsecureProtocol: !cfg.UseHTTPS,
    47  		Client:              clt.Client,
    48  	})
    49  
    50  	opts := http_client.Options{
    51  		BasicHTTPClient:     clt.Client,
    52  		BucketQuery:         bucketQuery,
    53  		UseInsecureProtocol: !cfg.UseHTTPS,
    54  		AccelerateUploading: cfg.AccelerateUploading,
    55  		HostRetryConfig:     &clientv2.RetryConfig{},
    56  	}
    57  	if region := cfg.GetRegion(); region != nil {
    58  		opts.Regions = region
    59  	}
    60  
    61  	return &ResumeUploaderV2{
    62  		Client:  clt,
    63  		Cfg:     cfg,
    64  		storage: apis.NewStorage(&opts),
    65  	}
    66  }
    67  
    68  // Put 方法用来上传一个文件,支持断点续传和分块上传。
    69  //
    70  // ctx     是请求的上下文。
    71  // ret     是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
    72  // upToken 是由业务服务器颁发的上传凭证。
    73  // key     是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
    74  // f       是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。
    75  // fsize   是要上传的文件大小。
    76  // extra   是上传的一些可选项。详细见 RputV2Extra 结构的描述。
    77  func (p *ResumeUploaderV2) Put(ctx context.Context, ret interface{}, upToken string, key string, f io.ReaderAt, fsize int64, extra *RputV2Extra) error {
    78  	return p.rput(ctx, ret, upToken, key, true, f, fsize, nil, extra)
    79  }
    80  
    81  func (p *ResumeUploaderV2) PutWithoutSize(ctx context.Context, ret interface{}, upToken, key string, r io.Reader, extra *RputV2Extra) error {
    82  	return p.rputWithoutSize(ctx, ret, upToken, key, true, r, extra)
    83  }
    84  
    85  // PutWithoutKey 方法用来上传一个文件,支持断点续传和分块上传。文件命名方式首先看看
    86  // upToken 中是否设置了 saveKey,如果设置了 saveKey,那么按 saveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。
    87  //
    88  // ctx     是请求的上下文。
    89  // ret     是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
    90  // upToken 是由业务服务器颁发的上传凭证。
    91  // f       是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。
    92  // fsize   是要上传的文件大小。
    93  // extra   是上传的一些可选项。详细见 RputV2Extra 结构的描述。
    94  func (p *ResumeUploaderV2) PutWithoutKey(ctx context.Context, ret interface{}, upToken string, f io.ReaderAt, fsize int64, extra *RputV2Extra) error {
    95  	return p.rput(ctx, ret, upToken, "", false, f, fsize, nil, extra)
    96  }
    97  
    98  // PutFile 用来上传一个文件,支持断点续传和分块上传。
    99  // 和 Put 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。
   100  //
   101  // ctx       是请求的上下文。
   102  // ret       是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
   103  // upToken   是由业务服务器颁发的上传凭证。
   104  // key       是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
   105  // localFile 是要上传的文件的本地路径。
   106  // extra     是上传的一些可选项。详细见 RputV2Extra 结构的描述。
   107  func (p *ResumeUploaderV2) PutFile(ctx context.Context, ret interface{}, upToken, key, localFile string, extra *RputV2Extra) error {
   108  	return p.rputFile(ctx, ret, upToken, key, true, localFile, extra)
   109  }
   110  
   111  // PutFileWithoutKey 上传一个文件,支持断点续传和分块上传。文件命名方式首先看看
   112  // upToken 中是否设置了 saveKey,如果设置了 saveKey,那么按 saveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。
   113  // 和 PutWithoutKey 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。
   114  //
   115  // ctx       是请求的上下文。
   116  // ret       是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
   117  // upToken   是由业务服务器颁发的上传凭证。
   118  // localFile 是要上传的文件的本地路径。
   119  // extra     是上传的一些可选项。详细见 RputV2Extra 结构的描述。
   120  func (p *ResumeUploaderV2) PutFileWithoutKey(ctx context.Context, ret interface{}, upToken, localFile string, extra *RputV2Extra) error {
   121  	return p.rputFile(ctx, ret, upToken, "", false, localFile, extra)
   122  }
   123  
   124  func (p *ResumeUploaderV2) rput(ctx context.Context, ret interface{}, upToken string, key string, hasKey bool, f io.ReaderAt, fsize int64, fileDetails *fileDetailsInfo, extra *RputV2Extra) (err error) {
   125  	if extra == nil {
   126  		extra = &RputV2Extra{}
   127  	}
   128  	extra.init()
   129  
   130  	var (
   131  		bucket, recorderKey string
   132  		fileInfo            os.FileInfo = nil
   133  	)
   134  
   135  	if fileDetails != nil {
   136  		fileInfo = fileDetails.fileInfo
   137  	}
   138  
   139  	if _, bucket, err = getAkBucketFromUploadToken(upToken); err != nil {
   140  		return
   141  	}
   142  
   143  	recorderKey = getRecorderKey(extra.Recorder, upToken, key, "v2", extra.PartSize, fileDetails)
   144  
   145  	return uploadByWorkers(
   146  		newResumeUploaderV2Impl(p, bucket, key, hasKey, upToken, makeEndpointsFromUpHost(extra.UpHost), fileInfo, extra, ret, recorderKey),
   147  		ctx, newSizedChunkReader(f, fsize, extra.PartSize))
   148  }
   149  
   150  func (p *ResumeUploaderV2) rputWithoutSize(ctx context.Context, ret interface{}, upToken string, key string, hasKey bool, r io.Reader, extra *RputV2Extra) (err error) {
   151  	if extra == nil {
   152  		extra = &RputV2Extra{}
   153  	}
   154  	extra.init()
   155  
   156  	var bucket string
   157  
   158  	if _, bucket, err = getAkBucketFromUploadToken(upToken); err != nil {
   159  		return
   160  	}
   161  
   162  	return uploadByWorkers(
   163  		newResumeUploaderV2Impl(p, bucket, key, hasKey, upToken, makeEndpointsFromUpHost(extra.UpHost), nil, extra, ret, ""),
   164  		ctx, newUnsizedChunkReader(r, extra.PartSize))
   165  }
   166  
   167  func (p *ResumeUploaderV2) rputFile(ctx context.Context, ret interface{}, upToken string, key string, hasKey bool, localFile string, extra *RputV2Extra) (err error) {
   168  	var (
   169  		file        *os.File
   170  		fileInfo    os.FileInfo
   171  		fileDetails *fileDetailsInfo
   172  	)
   173  
   174  	if file, err = os.Open(localFile); err != nil {
   175  		return
   176  	}
   177  	defer file.Close()
   178  
   179  	if fileInfo, err = file.Stat(); err != nil {
   180  		return
   181  	}
   182  
   183  	if fullPath, absErr := filepath.Abs(file.Name()); absErr == nil {
   184  		fileDetails = &fileDetailsInfo{fileFullPath: fullPath, fileInfo: fileInfo}
   185  	}
   186  
   187  	return p.rput(ctx, ret, upToken, key, hasKey, file, fileInfo.Size(), fileDetails, extra)
   188  }
   189  
   190  // 初始化块请求
   191  func (p *ResumeUploaderV2) InitParts(ctx context.Context, upToken, upHost, bucket, key string, hasKey bool, ret *InitPartsRet) error {
   192  	return p.resumeUploaderAPIs().initParts(ctx, upToken, makeEndpointsFromUpHost(upHost), bucket, key, hasKey, ret)
   193  }
   194  
   195  // 发送块请求
   196  func (p *ResumeUploaderV2) UploadParts(ctx context.Context, upToken, upHost, bucket, key string, hasKey bool, uploadId string, partNumber int64, partMD5 string, ret *UploadPartsRet, body io.Reader, size int) error {
   197  	return p.resumeUploaderAPIs().uploadParts(ctx, upToken, makeEndpointsFromUpHost(upHost), bucket, key, hasKey, uploadId, partNumber, partMD5, ret, body, int64(size))
   198  }
   199  
   200  // 完成块请求
   201  func (p *ResumeUploaderV2) CompleteParts(ctx context.Context, upToken, upHost string, ret interface{}, bucket, key string, hasKey bool, uploadId string, extra *RputV2Extra) (err error) {
   202  	return p.resumeUploaderAPIs().completeParts(ctx, upToken, makeEndpointsFromUpHost(upHost), ret, bucket, key, hasKey, uploadId, extra)
   203  }
   204  
   205  func (p *ResumeUploaderV2) UpHost(ak, bucket string) (upHost string, err error) {
   206  	return p.resumeUploaderAPIs().upHost(ak, bucket)
   207  }
   208  
   209  func (p *ResumeUploaderV2) resumeUploaderAPIs() *resumeUploaderAPIs {
   210  	return &resumeUploaderAPIs{cfg: p.Cfg, storage: p.storage}
   211  }
   212  
   213  type (
   214  	// 用于实现 resumeUploaderBase 的 V2 分片接口
   215  	resumeUploaderV2Impl struct {
   216  		cfg         *Config
   217  		storage     *apis.Storage
   218  		bucket      string
   219  		key         string
   220  		hasKey      bool
   221  		uploadId    string
   222  		expiredAt   int64
   223  		upToken     string
   224  		upEndpoints region.EndpointsProvider
   225  		extra       *RputV2Extra
   226  		fileInfo    os.FileInfo
   227  		recorderKey string
   228  		ret         interface{}
   229  		lock        sync.Mutex
   230  		bufPool     *sync.Pool
   231  	}
   232  
   233  	resumeUploaderV2RecoveryInfoContext struct {
   234  		Offset     int64  `json:"o"`
   235  		Etag       string `json:"e"`
   236  		PartSize   int    `json:"s"`
   237  		PartNumber int64  `json:"p"`
   238  	}
   239  
   240  	resumeUploaderV2RecoveryInfo struct {
   241  		RecorderVersion string                                `json:"v"`
   242  		Region          *Region                               `json:"r"`
   243  		FileSize        int64                                 `json:"s"`
   244  		ModTimeStamp    int64                                 `json:"m"`
   245  		ExpiredAt       int64                                 `json:"e"`
   246  		UploadId        string                                `json:"i"`
   247  		Contexts        []resumeUploaderV2RecoveryInfoContext `json:"c"`
   248  	}
   249  )
   250  
   251  func newResumeUploaderV2Impl(resumeUploader *ResumeUploaderV2, bucket, key string, hasKey bool, upToken string, upEndpoints region.EndpointsProvider, fileInfo os.FileInfo, extra *RputV2Extra, ret interface{}, recorderKey string) *resumeUploaderV2Impl {
   252  	bucketQuery, _ := region.NewBucketRegionsQuery(getUcEndpoint(resumeUploader.Cfg.UseHTTPS, nil), &region.BucketRegionsQueryOptions{
   253  		UseInsecureProtocol: !resumeUploader.Cfg.UseHTTPS,
   254  		Client:              resumeUploader.Client.Client,
   255  	})
   256  	opts := http_client.Options{
   257  		BasicHTTPClient:     resumeUploader.Client.Client,
   258  		BucketQuery:         bucketQuery,
   259  		UseInsecureProtocol: !resumeUploader.Cfg.UseHTTPS,
   260  		AccelerateUploading: resumeUploader.Cfg.AccelerateUploading,
   261  		HostRetryConfig:     &clientv2.RetryConfig{},
   262  	}
   263  	if region := resumeUploader.Cfg.GetRegion(); region != nil {
   264  		opts.Regions = region
   265  	}
   266  	if extra != nil {
   267  		if extra.TryTimes > 0 {
   268  			opts.HostRetryConfig.RetryMax = extra.TryTimes
   269  		}
   270  		if extra.HostFreezeDuration > 0 {
   271  			opts.HostFreezeDuration = extra.HostFreezeDuration
   272  		}
   273  	}
   274  	return &resumeUploaderV2Impl{
   275  		cfg:         resumeUploader.Cfg,
   276  		bucket:      bucket,
   277  		key:         key,
   278  		hasKey:      hasKey,
   279  		upToken:     upToken,
   280  		upEndpoints: upEndpoints,
   281  		fileInfo:    fileInfo,
   282  		recorderKey: recorderKey,
   283  		extra:       extra,
   284  		ret:         ret,
   285  		storage:     apis.NewStorage(&opts),
   286  		bufPool: &sync.Pool{
   287  			New: func() interface{} {
   288  				return bytes.NewBuffer(make([]byte, 0, extra.PartSize))
   289  			},
   290  		},
   291  	}
   292  }
   293  
   294  func (impl *resumeUploaderV2Impl) initUploader(ctx context.Context) (recovered []int64, recoveredSize int64, err error) {
   295  	var ret InitPartsRet
   296  
   297  	if impl.extra.Recorder != nil && len(impl.recorderKey) > 0 {
   298  		if recorderData, err := impl.extra.Recorder.Get(impl.recorderKey); err == nil {
   299  			if recovered, recoveredSize = impl.recover(ctx, recorderData); len(recovered) > 0 {
   300  				return recovered, recoveredSize, nil
   301  			}
   302  			if len(recovered) == 0 {
   303  				impl.deleteUploadRecordIfNeed(nil, true)
   304  			}
   305  		}
   306  	}
   307  
   308  	if err = impl.resumeUploaderAPIs().initParts(ctx, impl.upToken, impl.upEndpoints, impl.bucket, impl.key, impl.hasKey, &ret); err == nil {
   309  		impl.uploadId = ret.UploadID
   310  		impl.expiredAt = ret.ExpireAt
   311  	}
   312  	return nil, 0, err
   313  }
   314  
   315  func (impl *resumeUploaderV2Impl) uploadChunk(ctx context.Context, c chunk) error {
   316  	var (
   317  		apis   = impl.resumeUploaderAPIs()
   318  		ret    UploadPartsRet
   319  		buffer = impl.bufPool.Get().(*bytes.Buffer)
   320  	)
   321  	defer impl.bufPool.Put(buffer)
   322  
   323  	partNumber := c.id + 1
   324  	hasher := md5.New()
   325  	buffer.Reset()
   326  	chunkSize, err := io.Copy(hasher, io.TeeReader(io.NewSectionReader(c.reader, 0, c.size), buffer))
   327  	if err != nil {
   328  		impl.extra.NotifyErr(partNumber, err)
   329  		return err
   330  	} else if chunkSize == 0 {
   331  		return nil
   332  	}
   333  
   334  	md5Value := hex.EncodeToString(hasher.Sum(nil))
   335  	seekableData := bytes.NewReader(buffer.Bytes())
   336  	err = apis.uploadParts(ctx, impl.upToken, impl.upEndpoints, impl.bucket, impl.key, impl.hasKey, impl.uploadId, partNumber, md5Value, &ret, seekableData, chunkSize)
   337  	if err != nil {
   338  		impl.extra.NotifyErr(partNumber, err)
   339  		impl.deleteUploadRecordIfNeed(err, false)
   340  	} else {
   341  		impl.extra.Notify(partNumber, &ret)
   342  
   343  		select {
   344  		case <-ctx.Done():
   345  			return ctx.Err()
   346  		default:
   347  		}
   348  
   349  		func() {
   350  			impl.lock.Lock()
   351  			defer impl.lock.Unlock()
   352  			impl.extra.Progresses = append(impl.extra.Progresses, UploadPartInfo{
   353  				Etag: ret.Etag, PartNumber: partNumber, partSize: int(chunkSize), fileOffset: c.offset,
   354  			})
   355  			_ = impl.save(ctx)
   356  		}()
   357  	}
   358  	return err
   359  }
   360  
   361  func (impl *resumeUploaderV2Impl) final(ctx context.Context) error {
   362  	if impl.extra.Recorder != nil && len(impl.recorderKey) > 0 {
   363  		impl.deleteUploadRecordIfNeed(nil, true)
   364  	}
   365  
   366  	sort.Sort(uploadPartInfos(impl.extra.Progresses))
   367  	err := impl.resumeUploaderAPIs().completeParts(ctx, impl.upToken, impl.upEndpoints, impl.ret, impl.bucket, impl.key, impl.hasKey, impl.uploadId, impl.extra)
   368  	impl.deleteUploadRecordIfNeed(err, false)
   369  	return err
   370  }
   371  
   372  func (impl *resumeUploaderV2Impl) version() uplog.UpApiVersion {
   373  	return uplog.UpApiVersionV2
   374  }
   375  
   376  func (impl *resumeUploaderV2Impl) getUpToken() string {
   377  	return impl.upToken
   378  }
   379  
   380  func (impl *resumeUploaderV2Impl) getBucket() string {
   381  	return impl.bucket
   382  }
   383  
   384  func (impl *resumeUploaderV2Impl) getKey() (string, bool) {
   385  	return impl.key, impl.hasKey
   386  }
   387  
   388  func (impl *resumeUploaderV2Impl) deleteUploadRecordIfNeed(err error, force bool) {
   389  	// 无效删除之前的记录
   390  	if force || (isContextExpiredError(err) && impl.extra.Recorder != nil && len(impl.recorderKey) > 0) {
   391  		_ = impl.extra.Recorder.Delete(impl.recorderKey)
   392  	}
   393  }
   394  
   395  func (impl *resumeUploaderV2Impl) recover(ctx context.Context, recoverData []byte) (recovered []int64, recoveredSize int64) {
   396  	var recoveryInfo resumeUploaderV2RecoveryInfo
   397  	if err := json.Unmarshal(recoverData, &recoveryInfo); err != nil {
   398  		return
   399  	}
   400  	if impl.fileInfo == nil || recoveryInfo.FileSize != impl.fileInfo.Size() ||
   401  		recoveryInfo.RecorderVersion != uploadRecordVersion ||
   402  		recoveryInfo.ModTimeStamp != impl.fileInfo.ModTime().UnixNano() {
   403  		return
   404  	}
   405  
   406  	if isUploadContextExpired(recoveryInfo.ExpiredAt) {
   407  		return
   408  	}
   409  
   410  	impl.uploadId = recoveryInfo.UploadId
   411  	impl.expiredAt = recoveryInfo.ExpiredAt
   412  
   413  	for _, c := range recoveryInfo.Contexts {
   414  		impl.extra.Progresses = append(impl.extra.Progresses, UploadPartInfo{
   415  			Etag: c.Etag, PartNumber: c.PartNumber, fileOffset: c.Offset, partSize: c.PartSize,
   416  		})
   417  		recoveredSize += int64(c.PartSize)
   418  		recovered = append(recovered, int64(c.Offset))
   419  	}
   420  
   421  	return
   422  }
   423  
   424  func (impl *resumeUploaderV2Impl) save(ctx context.Context) error {
   425  	var (
   426  		recoveryInfo  resumeUploaderV2RecoveryInfo
   427  		recoveredData []byte
   428  		err           error
   429  	)
   430  
   431  	if impl.fileInfo == nil || impl.extra.Recorder == nil || len(impl.recorderKey) == 0 {
   432  		return nil
   433  	}
   434  
   435  	recoveryInfo.RecorderVersion = uploadRecordVersion
   436  	recoveryInfo.Region = impl.cfg.Region
   437  	recoveryInfo.FileSize = impl.fileInfo.Size()
   438  	recoveryInfo.ModTimeStamp = impl.fileInfo.ModTime().UnixNano()
   439  	recoveryInfo.UploadId = impl.uploadId
   440  	recoveryInfo.ExpiredAt = impl.expiredAt
   441  	recoveryInfo.Contexts = make([]resumeUploaderV2RecoveryInfoContext, 0, len(impl.extra.Progresses))
   442  
   443  	for _, progress := range impl.extra.Progresses {
   444  		recoveryInfo.Contexts = append(recoveryInfo.Contexts, resumeUploaderV2RecoveryInfoContext{
   445  			Offset: progress.fileOffset, Etag: progress.Etag, PartSize: progress.partSize, PartNumber: progress.PartNumber,
   446  		})
   447  	}
   448  
   449  	if recoveredData, err = json.Marshal(recoveryInfo); err != nil {
   450  		return err
   451  	}
   452  
   453  	return impl.extra.Recorder.Set(impl.recorderKey, recoveredData)
   454  }
   455  
   456  func (impl *resumeUploaderV2Impl) resumeUploaderAPIs() *resumeUploaderAPIs {
   457  	return &resumeUploaderAPIs{cfg: impl.cfg, storage: impl.storage}
   458  }