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