package storage import ( "bytes" "context" "crypto/md5" "encoding/hex" "encoding/json" "github.com/qiniu/go-sdk/v7/client" "github.com/qiniu/go-sdk/v7/internal/hostprovider" "io" "os" "path/filepath" "sort" "sync" ) // ResumeUploaderV2 表示一个分片上传 v2 的对象 type ResumeUploaderV2 struct { Client *client.Client Cfg *Config } // NewResumeUploaderV2 表示构建一个新的分片上传的对象 func NewResumeUploaderV2(cfg *Config) *ResumeUploaderV2 { return NewResumeUploaderV2Ex(cfg, nil) } // NewResumeUploaderV2Ex 表示构建一个新的分片上传 v2 的对象 func NewResumeUploaderV2Ex(cfg *Config, clt *client.Client) *ResumeUploaderV2 { if cfg == nil { cfg = &Config{} } if clt == nil { clt = &client.DefaultClient } return &ResumeUploaderV2{ Client: clt, Cfg: cfg, } } // Put 方法用来上传一个文件,支持断点续传和分块上传。 // // ctx 是请求的上下文。 // ret 是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。 // upToken 是由业务服务器颁发的上传凭证。 // key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。 // f 是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。 // fsize 是要上传的文件大小。 // extra 是上传的一些可选项。详细见 RputV2Extra 结构的描述。 func (p *ResumeUploaderV2) Put(ctx context.Context, ret interface{}, upToken string, key string, f io.ReaderAt, fsize int64, extra *RputV2Extra) error { return p.rput(ctx, ret, upToken, key, true, f, fsize, nil, extra) } func (p *ResumeUploaderV2) PutWithoutSize(ctx context.Context, ret interface{}, upToken, key string, r io.Reader, extra *RputV2Extra) error { return p.rputWithoutSize(ctx, ret, upToken, key, true, r, extra) } // PutWithoutKey 方法用来上传一个文件,支持断点续传和分块上传。文件命名方式首先看看 // upToken 中是否设置了 saveKey,如果设置了 saveKey,那么按 saveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。 // // ctx 是请求的上下文。 // ret 是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。 // upToken 是由业务服务器颁发的上传凭证。 // f 是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。 // fsize 是要上传的文件大小。 // extra 是上传的一些可选项。详细见 RputV2Extra 结构的描述。 func (p *ResumeUploaderV2) PutWithoutKey(ctx context.Context, ret interface{}, upToken string, f io.ReaderAt, fsize int64, extra *RputV2Extra) error { return p.rput(ctx, ret, upToken, "", false, f, fsize, nil, extra) } // PutFile 用来上传一个文件,支持断点续传和分块上传。 // 和 Put 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。 // // ctx 是请求的上下文。 // ret 是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。 // upToken 是由业务服务器颁发的上传凭证。 // key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。 // localFile 是要上传的文件的本地路径。 // extra 是上传的一些可选项。详细见 RputV2Extra 结构的描述。 func (p *ResumeUploaderV2) PutFile(ctx context.Context, ret interface{}, upToken, key, localFile string, extra *RputV2Extra) error { return p.rputFile(ctx, ret, upToken, key, true, localFile, extra) } // PutFileWithoutKey 上传一个文件,支持断点续传和分块上传。文件命名方式首先看看 // upToken 中是否设置了 saveKey,如果设置了 saveKey,那么按 saveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。 // 和 PutWithoutKey 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。 // // ctx 是请求的上下文。 // ret 是上传成功后返回的数据。如果 upToken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。 // upToken 是由业务服务器颁发的上传凭证。 // localFile 是要上传的文件的本地路径。 // extra 是上传的一些可选项。详细见 RputV2Extra 结构的描述。 func (p *ResumeUploaderV2) PutFileWithoutKey(ctx context.Context, ret interface{}, upToken, localFile string, extra *RputV2Extra) error { return p.rputFile(ctx, ret, upToken, "", false, localFile, extra) } 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) { if extra == nil { extra = &RputV2Extra{} } extra.init() var ( accessKey, bucket, recorderKey string fileInfo os.FileInfo = nil hostProvider hostprovider.HostProvider = nil ) if fileDetails != nil { fileInfo = fileDetails.fileInfo } if accessKey, bucket, err = getAkBucketFromUploadToken(upToken); err != nil { return } if extra.UpHost != "" { hostProvider = hostprovider.NewWithHosts([]string{extra.getUpHost(p.Cfg.UseHTTPS)}) } else { hostProvider, err = p.resumeUploaderAPIs().upHostProvider(accessKey, bucket, extra.TryTimes, extra.HostFreezeDuration) if err != nil { return } } recorderKey = getRecorderKey(extra.Recorder, upToken, key, "v2", extra.PartSize, fileDetails) return uploadByWorkers( newResumeUploaderV2Impl(p, bucket, key, hasKey, upToken, hostProvider, fileInfo, extra, ret, recorderKey), ctx, newSizedChunkReader(f, fsize, extra.PartSize)) } func (p *ResumeUploaderV2) rputWithoutSize(ctx context.Context, ret interface{}, upToken string, key string, hasKey bool, r io.Reader, extra *RputV2Extra) (err error) { if extra == nil { extra = &RputV2Extra{} } extra.init() var ( accessKey, bucket string hostProvider hostprovider.HostProvider = nil ) if accessKey, bucket, err = getAkBucketFromUploadToken(upToken); err != nil { return } if extra.UpHost != "" { hostProvider = hostprovider.NewWithHosts([]string{extra.getUpHost(p.Cfg.UseHTTPS)}) } else { hostProvider, err = p.resumeUploaderAPIs().upHostProvider(accessKey, bucket, extra.TryTimes, extra.HostFreezeDuration) if err != nil { return } } return uploadByWorkers( newResumeUploaderV2Impl(p, bucket, key, hasKey, upToken, hostProvider, nil, extra, ret, ""), ctx, newUnsizedChunkReader(r, extra.PartSize)) } func (p *ResumeUploaderV2) rputFile(ctx context.Context, ret interface{}, upToken string, key string, hasKey bool, localFile string, extra *RputV2Extra) (err error) { var ( file *os.File fileInfo os.FileInfo fileDetails *fileDetailsInfo ) if file, err = os.Open(localFile); err != nil { return } defer file.Close() if fileInfo, err = file.Stat(); err != nil { return } if fullPath, absErr := filepath.Abs(file.Name()); absErr == nil { fileDetails = &fileDetailsInfo{fileFullPath: fullPath, fileInfo: fileInfo} } return p.rput(ctx, ret, upToken, key, hasKey, file, fileInfo.Size(), fileDetails, extra) } // 初始化块请求 func (p *ResumeUploaderV2) InitParts(ctx context.Context, upToken, upHost, bucket, key string, hasKey bool, ret *InitPartsRet) error { return p.resumeUploaderAPIs().initParts(ctx, upToken, upHost, bucket, key, hasKey, ret) } // 发送块请求 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 { return p.resumeUploaderAPIs().uploadParts(ctx, upToken, upHost, bucket, key, hasKey, uploadId, partNumber, partMD5, ret, body, int64(size)) } // 完成块请求 func (p *ResumeUploaderV2) CompleteParts(ctx context.Context, upToken, upHost string, ret interface{}, bucket, key string, hasKey bool, uploadId string, extra *RputV2Extra) (err error) { return p.resumeUploaderAPIs().completeParts(ctx, upToken, upHost, ret, bucket, key, hasKey, uploadId, extra) } func (p *ResumeUploaderV2) UpHost(ak, bucket string) (upHost string, err error) { return p.resumeUploaderAPIs().upHost(ak, bucket) } func (p *ResumeUploaderV2) resumeUploaderAPIs() *resumeUploaderAPIs { return &resumeUploaderAPIs{Client: p.Client, Cfg: p.Cfg} } type ( // 用于实现 resumeUploaderBase 的 V2 分片接口 resumeUploaderV2Impl struct { client *client.Client cfg *Config bucket string key string hasKey bool uploadId string expiredAt int64 upToken string upHostProvider hostprovider.HostProvider extra *RputV2Extra fileInfo os.FileInfo recorderKey string ret interface{} lock sync.Mutex bufPool *sync.Pool } resumeUploaderV2RecoveryInfoContext struct { Offset int64 `json:"o"` Etag string `json:"e"` PartSize int `json:"s"` PartNumber int64 `json:"p"` } resumeUploaderV2RecoveryInfo struct { RecorderVersion string `json:"v"` Region *Region `json:"r"` FileSize int64 `json:"s"` ModTimeStamp int64 `json:"m"` ExpiredAt int64 `json:"e"` UploadId string `json:"i"` Contexts []resumeUploaderV2RecoveryInfoContext `json:"c"` } ) func newResumeUploaderV2Impl(resumeUploader *ResumeUploaderV2, bucket, key string, hasKey bool, upToken string, upHostProvider hostprovider.HostProvider, fileInfo os.FileInfo, extra *RputV2Extra, ret interface{}, recorderKey string) *resumeUploaderV2Impl { return &resumeUploaderV2Impl{ client: resumeUploader.Client, cfg: resumeUploader.Cfg, bucket: bucket, key: key, hasKey: hasKey, upToken: upToken, upHostProvider: upHostProvider, fileInfo: fileInfo, recorderKey: recorderKey, extra: extra, ret: ret, bufPool: &sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, 0, extra.PartSize)) }, }, } } func (impl *resumeUploaderV2Impl) initUploader(ctx context.Context) ([]int64, error) { var ( recovered []int64 ret InitPartsRet ) if impl.extra.Recorder != nil && len(impl.recorderKey) > 0 { if recorderData, err := impl.extra.Recorder.Get(impl.recorderKey); err == nil { if recovered = impl.recover(ctx, recorderData); len(recovered) > 0 { return recovered, nil } if len(recovered) == 0 { impl.deleteUploadRecordIfNeed(nil, true) } } } err := doUploadAction(impl.upHostProvider, impl.extra.TryTimes, impl.extra.HostFreezeDuration, func(host string) error { return impl.resumeUploaderAPIs().initParts(ctx, impl.upToken, host, impl.bucket, impl.key, impl.hasKey, &ret) }) if err == nil { impl.uploadId = ret.UploadID impl.expiredAt = ret.ExpireAt } return nil, err } func (impl *resumeUploaderV2Impl) uploadChunk(ctx context.Context, c chunk) error { var ( apis = impl.resumeUploaderAPIs() ret UploadPartsRet chunkSize int64 buffer = impl.bufPool.Get().(*bytes.Buffer) err error ) defer impl.bufPool.Put(buffer) partNumber := c.id + 1 hasher := md5.New() buffer.Reset() chunkSize, err = io.Copy(hasher, io.TeeReader(io.NewSectionReader(c.reader, 0, c.size), buffer)) if err != nil { impl.extra.NotifyErr(partNumber, err) return err } else if chunkSize == 0 { return nil } md5Value := hex.EncodeToString(hasher.Sum(nil)) seekableData := bytes.NewReader(buffer.Bytes()) err = doUploadAction(impl.upHostProvider, impl.extra.TryTimes, impl.extra.HostFreezeDuration, func(host string) error { if _, sErr := seekableData.Seek(0, io.SeekStart); sErr != nil { return sErr } return apis.uploadParts(ctx, impl.upToken, host, impl.bucket, impl.key, impl.hasKey, impl.uploadId, partNumber, md5Value, &ret, seekableData, chunkSize) }) if err != nil { impl.extra.NotifyErr(partNumber, err) impl.deleteUploadRecordIfNeed(err, false) } else { impl.extra.Notify(partNumber, &ret) select { case <-ctx.Done(): return ctx.Err() default: } func() { impl.lock.Lock() defer impl.lock.Unlock() impl.extra.Progresses = append(impl.extra.Progresses, UploadPartInfo{ Etag: ret.Etag, PartNumber: partNumber, partSize: int(chunkSize), fileOffset: c.offset, }) impl.save(ctx) }() } return err } func (impl *resumeUploaderV2Impl) final(ctx context.Context) error { if impl.extra.Recorder != nil && len(impl.recorderKey) > 0 { impl.deleteUploadRecordIfNeed(nil, true) } sort.Sort(uploadPartInfos(impl.extra.Progresses)) err := doUploadAction(impl.upHostProvider, impl.extra.TryTimes, impl.extra.HostFreezeDuration, func(host string) error { return impl.resumeUploaderAPIs().completeParts(ctx, impl.upToken, host, impl.ret, impl.bucket, impl.key, impl.hasKey, impl.uploadId, impl.extra) }) impl.deleteUploadRecordIfNeed(err, false) return err } func (impl *resumeUploaderV2Impl) deleteUploadRecordIfNeed(err error, force bool) { // 无效删除之前的记录 if force || (isContextExpiredError(err) && impl.extra.Recorder != nil && len(impl.recorderKey) > 0) { _ = impl.extra.Recorder.Delete(impl.recorderKey) } } func (impl *resumeUploaderV2Impl) recover(ctx context.Context, recoverData []byte) (recovered []int64) { var recoveryInfo resumeUploaderV2RecoveryInfo if err := json.Unmarshal(recoverData, &recoveryInfo); err != nil { return } if impl.fileInfo == nil || recoveryInfo.FileSize != impl.fileInfo.Size() || recoveryInfo.RecorderVersion != uploadRecordVersion || recoveryInfo.ModTimeStamp != impl.fileInfo.ModTime().UnixNano() { return } if isUploadContextExpired(recoveryInfo.ExpiredAt) { return } impl.uploadId = recoveryInfo.UploadId impl.expiredAt = recoveryInfo.ExpiredAt for _, c := range recoveryInfo.Contexts { impl.extra.Progresses = append(impl.extra.Progresses, UploadPartInfo{ Etag: c.Etag, PartNumber: c.PartNumber, fileOffset: c.Offset, partSize: c.PartSize, }) recovered = append(recovered, int64(c.Offset)) } return } func (impl *resumeUploaderV2Impl) save(ctx context.Context) { var ( recoveryInfo resumeUploaderV2RecoveryInfo recoveredData []byte err error ) if impl.fileInfo == nil || impl.extra.Recorder == nil || len(impl.recorderKey) == 0 { return } recoveryInfo.RecorderVersion = uploadRecordVersion recoveryInfo.Region = impl.cfg.Region recoveryInfo.FileSize = impl.fileInfo.Size() recoveryInfo.ModTimeStamp = impl.fileInfo.ModTime().UnixNano() recoveryInfo.UploadId = impl.uploadId recoveryInfo.ExpiredAt = impl.expiredAt recoveryInfo.Contexts = make([]resumeUploaderV2RecoveryInfoContext, 0, len(impl.extra.Progresses)) for _, progress := range impl.extra.Progresses { recoveryInfo.Contexts = append(recoveryInfo.Contexts, resumeUploaderV2RecoveryInfoContext{ Offset: progress.fileOffset, Etag: progress.Etag, PartSize: progress.partSize, PartNumber: progress.PartNumber, }) } if recoveredData, err = json.Marshal(recoveryInfo); err != nil { return } impl.extra.Recorder.Set(impl.recorderKey, recoveredData) } func (impl *resumeUploaderV2Impl) resumeUploaderAPIs() *resumeUploaderAPIs { return &resumeUploaderAPIs{Client: impl.client, Cfg: impl.cfg} }