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), ®ion.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), ®ion.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 }