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 }