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

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"io"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/qiniu/go-sdk/v7/client"
    13  	"github.com/qiniu/go-sdk/v7/storagev2/defaults"
    14  	"github.com/qiniu/go-sdk/v7/storagev2/uplog"
    15  )
    16  
    17  type UploadResumeVersion = int
    18  
    19  const (
    20  	UploadResumeV1 UploadResumeVersion = 1
    21  	UploadResumeV2 UploadResumeVersion = 2
    22  
    23  	uploadMethodForm     = 0
    24  	uploadMethodResumeV1 = 1
    25  	uploadMethodResumeV2 = 2
    26  )
    27  
    28  // UploadConfig 为 UploadManager 提供配置信息
    29  type UploadConfig struct {
    30  	UseHTTPS            bool
    31  	UseCdnDomains       bool
    32  	AccelerateUploading bool
    33  	Regions             *RegionGroup
    34  }
    35  
    36  // NewUploadConfig 创建默认的 UploadConfig 对象
    37  func NewUploadConfig() *UploadConfig {
    38  	var config UploadConfig
    39  	if isDisabled, err := defaults.DisableSecureProtocol(); err == nil {
    40  		config.UseHTTPS = !isDisabled
    41  	}
    42  	return &config
    43  }
    44  
    45  func (config *UploadConfig) init() {
    46  }
    47  
    48  type UploadExtra struct {
    49  	// 可选。
    50  	// 用户自定义参数:key 以"x:"开头,而且 value 不能为空 eg: key为x:qqq
    51  	// 自定义 meta:key 以"x-qn-meta-"开头,而且 value 不能为空 eg: key为x-qn-meta-aaa
    52  	Params map[string]string
    53  
    54  	// 【可选】尝试次数
    55  	TryTimes int
    56  
    57  	// 【可选】主备域名冻结时间(默认:600s),当一个域名请求失败(单个域名会被重试 TryTimes 次),会被冻结一段时间,使用备用域名进行重试,在冻结时间内,域名不能被使用,当一个操作中所有域名竣备冻结操作不在进行重试,返回最后一次操作的错误。
    58  	HostFreezeDuration time.Duration
    59  
    60  	// 【可选】当为 "" 时候,服务端自动判断。
    61  	MimeType string
    62  
    63  	// 【可选】上传事件:进度通知。这个事件的回调函数应该尽可能快地结束。
    64  	OnProgress func(fileSize, uploaded int64)
    65  
    66  	// 【可选】分片上传的上传方式, 默认:UploadResumeV2
    67  	UploadResumeVersion UploadResumeVersion
    68  
    69  	// 【可选】上传阈值,当文件大小大于此阈值时使用分片上传;单位:字节,默认:4 * 1024 * 1024
    70  	UploadThreshold int64
    71  
    72  	// 【可选】分片上传进度记录
    73  	Recorder Recorder
    74  
    75  	// 【可选】分片上传时每次上传的块大小,单位:字节,默认:4 * 1024 * 1024
    76  	PartSize int64
    77  }
    78  
    79  func (extra *UploadExtra) init() {
    80  	if extra.TryTimes == 0 {
    81  		extra.TryTimes = settings.TryTimes
    82  	}
    83  	if extra.HostFreezeDuration <= 0 {
    84  		extra.HostFreezeDuration = 10 * 60 * time.Second
    85  	}
    86  	if extra.UploadResumeVersion != UploadResumeV1 {
    87  		extra.UploadResumeVersion = UploadResumeV2
    88  	}
    89  	if extra.UploadThreshold <= 0 {
    90  		extra.UploadThreshold = 4 * 1024 * 1024
    91  	}
    92  	if extra.PartSize <= 0 {
    93  		extra.PartSize = 4 * 1024 * 1024
    94  	}
    95  
    96  	locker := sync.Mutex{}
    97  	onProgress := extra.OnProgress
    98  	uploadedSize := int64(0)
    99  	extra.OnProgress = func(fileSize, uploaded int64) {
   100  		if onProgress == nil {
   101  			return
   102  		}
   103  
   104  		locker.Lock()
   105  		if uploaded <= uploadedSize {
   106  			locker.Unlock()
   107  			return
   108  		}
   109  		uploadedSize = uploaded
   110  		locker.Unlock()
   111  
   112  		onProgress(fileSize, uploadedSize)
   113  	}
   114  }
   115  
   116  func (extra *UploadExtra) getMetadata() map[string]string {
   117  	if len(extra.Params) == 0 {
   118  		return nil
   119  	}
   120  
   121  	ret := make(map[string]string)
   122  	for key, value := range extra.Params {
   123  		if strings.HasPrefix(key, "x-qn-meta-") && len(value) > 0 {
   124  			ret[key] = value
   125  		}
   126  	}
   127  
   128  	if len(ret) > 0 {
   129  		return ret
   130  	}
   131  
   132  	return nil
   133  }
   134  
   135  func (extra *UploadExtra) getCustomVar() map[string]string {
   136  	if len(extra.Params) == 0 {
   137  		return nil
   138  	}
   139  
   140  	ret := make(map[string]string)
   141  	for key, value := range extra.Params {
   142  		if strings.HasPrefix(key, "x:") && len(value) > 0 {
   143  			ret[key] = value
   144  		}
   145  	}
   146  
   147  	if len(ret) > 0 {
   148  		return ret
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  type UploadRet struct {
   155  	Hash string `json:"hash"`
   156  	Key  string `json:"key"`
   157  }
   158  
   159  type UploadManager struct {
   160  	cfg    *UploadConfig
   161  	client *client.Client
   162  }
   163  
   164  func NewUploadManager(cfg *UploadConfig) *UploadManager {
   165  	return NewUploadManagerEx(cfg, nil)
   166  }
   167  
   168  func NewUploadManagerEx(cfg *UploadConfig, c *client.Client) *UploadManager {
   169  	if cfg == nil {
   170  		cfg = NewUploadConfig()
   171  	}
   172  
   173  	if c == nil {
   174  		c = &client.DefaultClient
   175  	}
   176  
   177  	return &UploadManager{
   178  		cfg:    cfg,
   179  		client: c,
   180  	}
   181  }
   182  
   183  func (manager *UploadManager) Put(ctx context.Context, ret interface{}, upToken string, key *string, source UploadSource, extra *UploadExtra) error {
   184  	if ctx == nil {
   185  		return errors.New("ctx can't be nil")
   186  	}
   187  	if ret == nil {
   188  		return errors.New("ret invalid")
   189  	}
   190  	if len(upToken) == 0 {
   191  		return errors.New("upToken invalid")
   192  	}
   193  	if source == nil {
   194  		return errors.New("source invalid")
   195  	}
   196  
   197  	return manager.putRetryBetweenRegion(ctx, ret, upToken, key, source, extra)
   198  }
   199  
   200  func (manager *UploadManager) putRetryBetweenRegion(ctx context.Context, ret interface{}, upToken string, key *string, source UploadSource, extra *UploadExtra) error {
   201  	if extra == nil {
   202  		extra = &UploadExtra{}
   203  	}
   204  	extra.init()
   205  
   206  	if manager.cfg.Regions == nil {
   207  		regions, err := manager.getRegionGroupWithUploadToken(upToken, extra)
   208  		if err != nil {
   209  			return err
   210  		}
   211  		manager.cfg.Regions = regions
   212  	}
   213  	regions := manager.cfg.Regions.clone()
   214  
   215  	resumeVersion := "v2"
   216  	uploadMethod := uploadMethodResumeV2
   217  	upType := uplog.UpTypeResumableV2
   218  	if source.Size() > 0 && source.Size() < extra.UploadThreshold {
   219  		uploadMethod = uploadMethodForm
   220  		resumeVersion = ""
   221  		upType = uplog.UpTypeForm
   222  	} else if extra.UploadResumeVersion == UploadResumeV1 {
   223  		// 默认使用分片 v2,如果设置了 v1 则使用 v1
   224  		uploadMethod = uploadMethodResumeV1
   225  		resumeVersion = "v1"
   226  		upType = uplog.UpTypeResumableV1
   227  	}
   228  
   229  	if uploadMethod != uploadMethodForm {
   230  		recoverRegion := manager.getRecoverRegion(key, upToken, resumeVersion, source, extra)
   231  		if recoverRegion != nil {
   232  			// 把记录的 Region 插在第一个
   233  			regions.regions = append([]*Region{recoverRegion}, regions.regions...)
   234  		}
   235  	}
   236  
   237  	var (
   238  		targetBucket, targetKey string
   239  		err                     error
   240  	)
   241  	if _, targetBucket, err = getAkBucketFromUploadToken(upToken); err != nil {
   242  		return err
   243  	}
   244  	if key != nil {
   245  		targetKey = *key
   246  	}
   247  
   248  	return uplog.WithQuality(ctx, upType, uint64(source.Size()), targetBucket, targetKey, upToken, func(ctx context.Context, switchRegion func()) (err error) {
   249  		for {
   250  			switchRegion()
   251  			region := regions.GetRegion()
   252  			err = manager.put(ctx, ret, region, uploadMethod, upToken, key, source, extra)
   253  
   254  			// 是否需要重试
   255  			if !shouldUploadAgain(err) {
   256  				break
   257  			}
   258  
   259  			// context 过期不需要切换 region
   260  			// 切换区域是否成功
   261  			if !isContextExpiredError(err) && (!regions.CouldSwitchRegion() || !regions.SwitchRegion()) {
   262  				break
   263  			}
   264  
   265  			// 资源重新加载
   266  			if !source.Rewindable() || source.Rewind() != nil {
   267  				break
   268  			}
   269  		}
   270  		return
   271  	})
   272  }
   273  
   274  func (manager *UploadManager) put(ctx context.Context, ret interface{}, region *Region, uploadMethod int,
   275  	upToken string, key *string, source UploadSource, extra *UploadExtra,
   276  ) error {
   277  	if extra == nil {
   278  		extra = &UploadExtra{}
   279  	}
   280  	extra.init()
   281  
   282  	if uploadMethod == uploadMethodForm {
   283  		return manager.putByForm(ctx, ret, region, upToken, key, source, extra)
   284  	} else if uploadMethod == uploadMethodResumeV1 {
   285  		return manager.putByResumeV1(ctx, ret, region, upToken, key, source, extra)
   286  	} else {
   287  		return manager.putByResumeV2(ctx, ret, region, upToken, key, source, extra)
   288  	}
   289  }
   290  
   291  func (manager *UploadManager) putByForm(ctx context.Context, ret interface{}, region *Region, upToken string, key *string, source UploadSource, extra *UploadExtra) error {
   292  	saveKey, hasKey := uploadKey(key)
   293  	uploadExtra := &PutExtra{
   294  		Params:             extra.Params,
   295  		TryTimes:           extra.TryTimes,
   296  		HostFreezeDuration: extra.HostFreezeDuration,
   297  		MimeType:           extra.MimeType,
   298  		OnProgress:         extra.OnProgress,
   299  	}
   300  	uploader := manager.getFormUploader(region)
   301  
   302  	if reader, ok := source.(*uploadSourceReader); ok {
   303  		return uploader.put(ctx, ret, upToken, saveKey, hasKey, reader.reader, reader.size, uploadExtra, "")
   304  	}
   305  
   306  	if readerAt, ok := source.(*uploadSourceReaderAt); ok {
   307  		reader := io.NewSectionReader(readerAt.reader, 0, readerAt.size)
   308  		return uploader.put(ctx, ret, upToken, saveKey, hasKey, reader, readerAt.size, uploadExtra, "")
   309  	}
   310  
   311  	if reader, ok := source.(*uploadSourceFile); ok {
   312  		return uploader.putFile(ctx, ret, upToken, saveKey, hasKey, reader.filePath, uploadExtra)
   313  	}
   314  
   315  	return errors.New("unknown upload source")
   316  }
   317  
   318  func (manager *UploadManager) putByResumeV1(ctx context.Context, ret interface{}, region *Region, upToken string, key *string, source UploadSource, extra *UploadExtra) error {
   319  	locker := sync.Mutex{}
   320  	uploadedSize := int64(0)
   321  	saveKey, hasKey := uploadKey(key)
   322  	uploadExtra := &RputExtra{
   323  		Recorder:           extra.Recorder,
   324  		Params:             extra.Params,
   325  		MimeType:           extra.MimeType,
   326  		ChunkSize:          int(extra.PartSize),
   327  		TryTimes:           extra.TryTimes,
   328  		HostFreezeDuration: extra.HostFreezeDuration,
   329  		Progresses:         nil,
   330  		Notify: func(blkIdx int, blkSize int, ret *BlkputRet) {
   331  			locker.Lock()
   332  			offset := int64(blkIdx)*blockSize + int64(blkSize) - extra.PartSize
   333  			if offset > uploadedSize {
   334  				uploadedSize = offset
   335  			}
   336  			locker.Unlock()
   337  			extra.OnProgress(source.Size(), uploadedSize)
   338  		},
   339  		NotifyErr: nil,
   340  	}
   341  	uploader := manager.getResumeV1Uploader(region)
   342  
   343  	if reader, ok := source.(*uploadSourceReader); ok {
   344  		return uploader.rputWithoutSize(ctx, ret, upToken, saveKey, hasKey, reader.reader, uploadExtra)
   345  	}
   346  
   347  	if reader, ok := source.(*uploadSourceReaderAt); ok {
   348  		return uploader.rput(ctx, ret, upToken, saveKey, hasKey, reader.reader, reader.size, nil, uploadExtra)
   349  	}
   350  
   351  	if reader, ok := source.(*uploadSourceFile); ok {
   352  		return uploader.rputFile(ctx, ret, upToken, saveKey, hasKey, reader.filePath, uploadExtra)
   353  	}
   354  
   355  	return errors.New("unknown upload source")
   356  }
   357  
   358  func (manager *UploadManager) putByResumeV2(ctx context.Context, ret interface{}, region *Region, upToken string, key *string, source UploadSource, extra *UploadExtra) error {
   359  	locker := sync.Mutex{}
   360  	uploadedSize := int64(0)
   361  	saveKey, hasKey := uploadKey(key)
   362  	uploadExtra := &RputV2Extra{
   363  		Recorder:           extra.Recorder,
   364  		Metadata:           extra.getMetadata(),
   365  		CustomVars:         extra.getCustomVar(),
   366  		MimeType:           extra.MimeType,
   367  		PartSize:           extra.PartSize,
   368  		TryTimes:           extra.TryTimes,
   369  		HostFreezeDuration: extra.HostFreezeDuration,
   370  		Progresses:         nil,
   371  		Notify: func(partNumber int64, ret *UploadPartsRet) {
   372  			if partNumber < 1 {
   373  				return
   374  			}
   375  			locker.Lock()
   376  			offset := (partNumber - 1) * extra.PartSize
   377  			if offset > uploadedSize {
   378  				uploadedSize = offset
   379  			}
   380  			locker.Unlock()
   381  			extra.OnProgress(source.Size(), uploadedSize)
   382  		},
   383  		NotifyErr: nil,
   384  	}
   385  	uploader := manager.getResumeV2Uploader(region)
   386  
   387  	if reader, ok := source.(*uploadSourceReader); ok {
   388  		return uploader.rputWithoutSize(ctx, ret, upToken, saveKey, hasKey, reader.reader, uploadExtra)
   389  	}
   390  
   391  	if reader, ok := source.(*uploadSourceReaderAt); ok {
   392  		return uploader.rput(ctx, ret, upToken, saveKey, hasKey, reader.reader, reader.size, nil, uploadExtra)
   393  	}
   394  
   395  	if reader, ok := source.(*uploadSourceFile); ok {
   396  		return uploader.rputFile(ctx, ret, upToken, saveKey, hasKey, reader.filePath, uploadExtra)
   397  	}
   398  
   399  	return errors.New("unknown upload source")
   400  }
   401  
   402  func (manager *UploadManager) getRecoverRegion(key *string, upToken string, resumeVersion string,
   403  	source UploadSource, extra *UploadExtra,
   404  ) *Region {
   405  	file, ok := source.(*uploadSourceFile)
   406  	if !ok {
   407  		return nil
   408  	}
   409  
   410  	partSize := extra.PartSize
   411  	if resumeVersion == "v1" {
   412  		partSize = blockSize
   413  	}
   414  	saveKey, _ := uploadKey(key)
   415  	recorderKey := getRecorderKey(extra.Recorder, upToken, saveKey, resumeVersion, partSize, &fileDetailsInfo{
   416  		fileFullPath: file.filePath,
   417  		fileInfo:     file.fileInfo,
   418  	})
   419  	if len(recorderKey) == 0 {
   420  		return nil
   421  	}
   422  
   423  	recoverData, gErr := extra.Recorder.Get(recorderKey)
   424  	if gErr != nil {
   425  		return nil
   426  	}
   427  
   428  	var recoveryInfo uploaderRecoveryInfo
   429  	if uErr := json.Unmarshal(recoverData, &recoveryInfo); uErr != nil {
   430  		// 失败则删除,避免脏数据
   431  		_ = extra.Recorder.Delete(recorderKey)
   432  		return nil
   433  	}
   434  
   435  	return recoveryInfo.Region
   436  }
   437  
   438  func (manager *UploadManager) getFormUploader(region *Region) *FormUploader {
   439  	return NewFormUploaderEx(&Config{
   440  		Zone:                region,
   441  		Region:              region,
   442  		UseHTTPS:            manager.cfg.UseHTTPS,
   443  		UseCdnDomains:       manager.cfg.UseCdnDomains,
   444  		AccelerateUploading: manager.cfg.AccelerateUploading,
   445  		CentralRsHost:       "",
   446  	}, manager.client)
   447  }
   448  
   449  func (manager *UploadManager) getResumeV1Uploader(region *Region) *ResumeUploader {
   450  	return NewResumeUploaderEx(&Config{
   451  		Zone:                region,
   452  		Region:              region,
   453  		UseHTTPS:            manager.cfg.UseHTTPS,
   454  		UseCdnDomains:       manager.cfg.UseCdnDomains,
   455  		AccelerateUploading: manager.cfg.AccelerateUploading,
   456  		CentralRsHost:       "",
   457  	}, manager.client)
   458  }
   459  
   460  func (manager *UploadManager) getResumeV2Uploader(region *Region) *ResumeUploaderV2 {
   461  	return NewResumeUploaderV2Ex(&Config{
   462  		Zone:                region,
   463  		Region:              region,
   464  		UseHTTPS:            manager.cfg.UseHTTPS,
   465  		UseCdnDomains:       manager.cfg.UseCdnDomains,
   466  		AccelerateUploading: manager.cfg.AccelerateUploading,
   467  		CentralRsHost:       "",
   468  	}, manager.client)
   469  }
   470  
   471  func (manager *UploadManager) getRegionGroupWithUploadToken(upToken string, extra *UploadExtra) (*RegionGroup, error) {
   472  	ak, bucket, err := getAkBucketFromUploadToken(upToken)
   473  	if err != nil {
   474  		return nil, err
   475  	}
   476  	return getRegionGroupWithOptions(ak, bucket, UCApiOptions{
   477  		UseHttps:            manager.cfg.UseHTTPS,
   478  		AccelerateUploading: manager.cfg.AccelerateUploading,
   479  		RetryMax:            extra.TryTimes,
   480  		HostFreezeDuration:  extra.HostFreezeDuration,
   481  	})
   482  }
   483  
   484  func uploadKey(keyQuote *string) (key string, hashKey bool) {
   485  	if keyQuote == nil {
   486  		return "", false
   487  	} else {
   488  		return *keyQuote, true
   489  	}
   490  }
   491  
   492  type uploaderRecoveryInfo struct {
   493  	Region *Region `json:"r"`
   494  }