github.com/qiniu/go-sdk/v7@v7.27.0/storagev2/downloader/downloaders.go (about) 1 package downloader 2 3 import ( 4 "compress/gzip" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "net/url" 12 "strings" 13 "sync" 14 "time" 15 16 clientv1 "github.com/qiniu/go-sdk/v7/client" 17 "github.com/qiniu/go-sdk/v7/internal/clientv2" 18 "github.com/qiniu/go-sdk/v7/storagev2/backoff" 19 "github.com/qiniu/go-sdk/v7/storagev2/chooser" 20 "github.com/qiniu/go-sdk/v7/storagev2/downloader/destination" 21 resumablerecorder "github.com/qiniu/go-sdk/v7/storagev2/downloader/resumable_recorder" 22 "github.com/qiniu/go-sdk/v7/storagev2/resolver" 23 "github.com/qiniu/go-sdk/v7/storagev2/retrier" 24 "golang.org/x/sync/errgroup" 25 ) 26 27 type ( 28 concurrentDownloader struct { 29 concurrency uint 30 partSize uint64 31 client clientv2.Client 32 resumableRecorder resumablerecorder.ResumableRecorder 33 } 34 35 // 下载器选项 36 DownloaderOptions struct { 37 Client clientv2.Client // HTTP 客户端,如果不配置则使用默认的 HTTP 客户端 38 RetryMax int // 最大重试次数,如果不配置,默认为 9 39 Backoff backoff.Backoff // 重试时间间隔 v2,优先级高于 RetryInterval 40 Resolver resolver.Resolver // 主备域名解析器 41 Chooser chooser.Chooser // IP 选择器 42 43 BeforeResolve func(*http.Request) // 域名解析前回调函数 44 AfterResolve func(*http.Request, []net.IP) // 域名解析后回调函数 45 ResolveError func(*http.Request, error) // 域名解析错误回调函数 46 BeforeBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration) // 退避前回调函数 47 AfterBackoff func(*http.Request, *retrier.RetrierOptions, time.Duration) // 退避后回调函数 48 BeforeRequest func(*http.Request, *retrier.RetrierOptions) // 请求前回调函数 49 AfterResponse func(*http.Response, *retrier.RetrierOptions, error) // 请求后回调函数 50 } 51 52 // 并发下载器选项 53 ConcurrentDownloaderOptions struct { 54 DownloaderOptions 55 Concurrency uint // 并发度 56 PartSize uint64 // 分片大小 57 ResumableRecorder resumablerecorder.ResumableRecorder // 可恢复记录仪 58 } 59 ) 60 61 func (options *DownloaderOptions) toSimpleRetryConfig() clientv2.SimpleRetryConfig { 62 retryMax := options.RetryMax 63 if retryMax <= 0 { 64 retryMax = 9 65 } 66 return clientv2.SimpleRetryConfig{ 67 RetryMax: retryMax, 68 Resolver: options.Resolver, 69 Chooser: options.Chooser, 70 Backoff: options.Backoff, 71 ShouldRetry: func(req *http.Request, resp *http.Response, err error) bool { 72 if err != nil { 73 return retrier.IsErrorRetryable(err) 74 } 75 return resp.StatusCode >= 500 76 }, 77 BeforeResolve: options.BeforeResolve, 78 AfterResolve: options.AfterResolve, 79 ResolveError: options.ResolveError, 80 BeforeBackoff: options.BeforeBackoff, 81 AfterBackoff: options.AfterBackoff, 82 BeforeRequest: options.BeforeRequest, 83 AfterResponse: options.AfterResponse, 84 } 85 } 86 87 // 创建并发下载器 88 func NewConcurrentDownloader(options *ConcurrentDownloaderOptions) DestinationDownloader { 89 if options == nil { 90 options = &ConcurrentDownloaderOptions{} 91 } 92 concurrency := options.Concurrency 93 if concurrency == 0 { 94 concurrency = 4 95 } 96 partSize := options.PartSize 97 if partSize == 0 { 98 partSize = 16 * 1024 * 1024 99 } 100 client := clientv2.NewClient(options.Client, clientv2.NewSimpleRetryInterceptor(options.toSimpleRetryConfig()), retryWhenTokenOutOfDateInterceptor{}) 101 return &concurrentDownloader{concurrency, partSize, client, options.ResumableRecorder} 102 } 103 104 func (downloader concurrentDownloader) Download(ctx context.Context, urlsIter URLsIter, dest destination.Destination, options *DestinationDownloadOptions) (uint64, error) { 105 if options == nil { 106 options = &DestinationDownloadOptions{} 107 } 108 headResponse, err := headRequest(ctx, urlsIter, options.Header, downloader.client) 109 if err != nil { 110 return 0, err 111 } else if headResponse == nil { 112 return 0, errors.New("no url tried") 113 } 114 if onResponseHeader := options.OnResponseHeader; onResponseHeader != nil { 115 onResponseHeader(headResponse.Header) 116 } 117 118 var offset uint64 119 switch headResponse.StatusCode { 120 case http.StatusOK: 121 case http.StatusPartialContent: 122 var unused1, unused2 int64 123 contentRange := headResponse.Header.Get("Content-Range") 124 if _, err = fmt.Sscanf(contentRange, "bytes %d-%d/%d", &offset, &unused1, &unused2); err != nil { 125 return 0, err 126 } 127 default: 128 return 0, clientv1.ResponseError(headResponse) 129 } 130 etag := parseEtag(headResponse.Header.Get("Etag")) 131 if headResponse.ContentLength < 0 || // 无法确定文件实际大小,发出一个请求下载整个文件,不再使用并行下载 132 headResponse.Header.Get("Accept-Ranges") != "bytes" { // 必须返回 Accept-Ranges 头,否则不认为可以分片下载 133 var progress func(uint64) 134 if onDownloadingProgress := options.OnDownloadingProgress; onDownloadingProgress != nil { 135 progress = func(downloaded uint64) { 136 onDownloadingProgress(&DownloadingProgress{Downloaded: downloaded}) 137 } 138 } 139 return downloadToPartReader(ctx, urlsIter, etag, options.Header, downloader.client, dest, progress) 140 } 141 needToDownload := uint64(headResponse.ContentLength) 142 143 var ( 144 readableMedium resumablerecorder.ReadableResumableRecorderMedium 145 writeableMedium resumablerecorder.WriteableResumableRecorderMedium 146 resumableRecorderOpenArgs *resumablerecorder.ResumableRecorderOpenArgs 147 ) 148 if resumableRecorder := downloader.resumableRecorder; resumableRecorder != nil { 149 var destinationID string 150 destinationID, err = dest.DestinationID() 151 if err == nil && destinationID != "" { 152 resumableRecorderOpenArgs = &resumablerecorder.ResumableRecorderOpenArgs{ 153 ETag: etag, 154 DestinationID: destinationID, 155 PartSize: downloader.partSize, 156 TotalSize: needToDownload, 157 Offset: offset, 158 } 159 readableMedium = resumableRecorder.OpenForReading(resumableRecorderOpenArgs) 160 if readableMedium != nil { 161 defer readableMedium.Close() 162 } else if file := dest.GetFile(); file != nil { 163 if err = file.Truncate(0); err != nil { // 无法恢复进度,目标文件清空 164 return 0, err 165 } 166 } 167 } 168 } 169 170 parts, err := dest.Split(needToDownload, downloader.partSize, &destination.SplitOptions{Medium: readableMedium}) 171 if err != nil { 172 return 0, err 173 } 174 if readableMedium != nil { 175 readableMedium.Close() 176 readableMedium = nil 177 } 178 if resumableRecorder := downloader.resumableRecorder; resumableRecorder != nil && resumableRecorderOpenArgs != nil { 179 writeableMedium = resumableRecorder.OpenForAppending(resumableRecorderOpenArgs) 180 if writeableMedium == nil { 181 writeableMedium = resumableRecorder.OpenForCreatingNew(resumableRecorderOpenArgs) 182 } 183 if writeableMedium != nil { 184 defer writeableMedium.Close() 185 } 186 } 187 188 g, ctx := errgroup.WithContext(ctx) 189 g.SetLimit(int(downloader.concurrency)) 190 var ( 191 downloadingProgress = newDownloadingPartsProgress() 192 downloadingProgressMutex sync.Mutex 193 ) 194 for _, part := range parts { 195 p := part 196 urlsIterClone := urlsIter.Clone() 197 g.Go(func() error { 198 n, err := downloader.downloadToPart(ctx, urlsIterClone, etag, offset, options.Header, p, writeableMedium, &downloadingProgressMutex, func(downloaded uint64) { 199 downloadingProgress.setPartDownloadingProgress(p.Offset(), downloaded) 200 if onDownloadingProgress := options.OnDownloadingProgress; onDownloadingProgress != nil { 201 onDownloadingProgress(&DownloadingProgress{Downloaded: downloadingProgress.totalDownloaded(), TotalSize: needToDownload}) 202 } 203 }) 204 if n > 0 { 205 downloadingProgressMutex.Lock() 206 downloadingProgress.partDownloaded(p.Offset(), n) 207 if onDownloadingProgress := options.OnDownloadingProgress; onDownloadingProgress != nil { 208 onDownloadingProgress(&DownloadingProgress{Downloaded: downloadingProgress.totalDownloaded(), TotalSize: needToDownload}) 209 } 210 downloadingProgressMutex.Unlock() 211 } 212 return err 213 }) 214 } 215 err = g.Wait() 216 if writeableMedium != nil { 217 writeableMedium.Close() 218 writeableMedium = nil 219 } 220 if resumableRecorder := downloader.resumableRecorder; resumableRecorder != nil && resumableRecorderOpenArgs != nil && err == nil { 221 resumableRecorder.Delete(resumableRecorderOpenArgs) 222 } 223 return downloadingProgress.totalDownloaded(), err 224 } 225 226 func (downloader concurrentDownloader) downloadToPart( 227 ctx context.Context, urlsIter URLsIter, etag string, originalOffset uint64, headers http.Header, 228 part destination.Part, writeableMedium resumablerecorder.WriteableResumableRecorderMedium, 229 downloadingProgressMutex *sync.Mutex, onDownloadingProgress func(downloaded uint64), 230 ) (uint64, error) { 231 var ( 232 n uint64 233 err error 234 size = part.Size() 235 offset = part.Offset() 236 haveRead = part.HaveDownloaded() 237 downloadingProgressCallback func(uint64) 238 ) 239 if onDownloadingProgress != nil { 240 downloadingProgressCallback = func(downloaded uint64) { 241 if downloadingProgressMutex != nil { 242 downloadingProgressMutex.Lock() 243 defer downloadingProgressMutex.Unlock() 244 } 245 onDownloadingProgress(downloaded) 246 } 247 } 248 for size > haveRead { 249 n, err = downloadToPartReaderWithOffsetAndSize(ctx, urlsIter, etag, originalOffset+offset+haveRead, size-haveRead, 250 headers, downloader.client, part, downloadingProgressCallback) 251 if n > 0 { 252 haveRead += n 253 continue 254 } 255 break 256 } 257 if haveRead > 0 && writeableMedium != nil { 258 writeableMedium.Write(&resumablerecorder.ResumableRecord{ 259 Offset: offset, 260 PartSize: size, 261 PartWritten: haveRead, 262 }) 263 } 264 return haveRead, err 265 } 266 267 func downloadToPartReaderWithOffsetAndSize( 268 ctx context.Context, urlsIter URLsIter, etag string, offset, size uint64, headers http.Header, 269 client clientv2.Client, part destination.PartWriter, onDownloadingProgress func(downloaded uint64), 270 ) (uint64, error) { 271 headers = cloneHeader(headers) 272 setRange(headers, offset, offset+size) 273 return _downloadToPartReader(ctx, urlsIter, headers, etag, client, part, onDownloadingProgress) 274 } 275 276 func downloadToPartReader( 277 ctx context.Context, urlsIter URLsIter, etag string, headers http.Header, 278 client clientv2.Client, part destination.PartWriter, onDownloadingProgress func(downloaded uint64), 279 ) (uint64, error) { 280 if headers.Get("Range") == "" { 281 headers = cloneHeader(headers) 282 setAcceptGzip(headers) 283 } 284 return _downloadToPartReader(ctx, urlsIter, headers, etag, client, part, onDownloadingProgress) 285 } 286 287 func _downloadToPartReader( 288 ctx context.Context, urlsIter URLsIter, headers http.Header, etag string, 289 client clientv2.Client, part destination.PartWriter, onDownloadingProgress func(downloaded uint64), 290 ) (uint64, error) { 291 var ( 292 response *http.Response 293 u url.URL 294 n uint64 295 ok, haveReset bool 296 err, peekErr error 297 ) 298 299 for { 300 if ok, peekErr = urlsIter.Peek(&u); peekErr != nil { 301 return 0, peekErr 302 } else if !ok { 303 if haveReset { 304 break 305 } else { 306 urlsIter.Reset() 307 haveReset = true 308 continue 309 } 310 } 311 req := http.Request{ 312 Method: http.MethodGet, 313 URL: &u, 314 Header: headers, 315 Body: http.NoBody, 316 } 317 ctx = context.WithValue(ctx, urlsIterContextKey{}, urlsIter) 318 if response, err = client.Do(req.WithContext(ctx)); err != nil { 319 if !retrier.IsErrorRetryable(err) { 320 return 0, err 321 } 322 urlsIter.Next() 323 continue 324 } 325 var ( 326 bodyReader io.Reader = response.Body 327 bodyCloser io.Closer = response.Body 328 ) 329 if etag == parseEtag(response.Header.Get("Etag")) { 330 switch response.Header.Get("Content-Encoding") { 331 case "gzip": 332 if bodyReader, err = gzip.NewReader(bodyReader); err != nil { 333 bodyCloser.Close() 334 return 0, err 335 } 336 fallthrough 337 case "": 338 n, err = part.CopyFrom(bodyReader, onDownloadingProgress) 339 bodyCloser.Close() 340 if n > 0 { 341 return n, err 342 } 343 default: 344 bodyCloser.Close() 345 err = errors.New("unrecognized content-encoding") 346 } 347 } else { 348 bodyCloser.Close() 349 err = errors.New("etag dismatch") 350 } 351 urlsIter.Next() 352 } 353 return 0, err 354 } 355 356 func headRequest(ctx context.Context, urlsIter URLsIter, headers http.Header, client clientv2.Client) (response *http.Response, err error) { 357 var ( 358 u url.URL 359 ok, haveReset bool 360 ) 361 if headers.Get("Accept-Encoding") != "" { 362 headers = cloneHeader(headers) 363 headers.Del("Accept-Encoding") 364 } 365 for { 366 if ok, err = urlsIter.Peek(&u); err != nil { 367 return 368 } else if !ok { 369 if haveReset { 370 break 371 } else { 372 urlsIter.Reset() 373 haveReset = true 374 continue 375 } 376 } 377 req := http.Request{ 378 Method: http.MethodHead, 379 URL: &u, 380 Header: headers, 381 Body: http.NoBody, 382 } 383 if response, err = client.Do(req.WithContext(ctx)); err != nil { 384 if !retrier.IsErrorRetryable(err) { 385 return 386 } 387 urlsIter.Next() 388 continue 389 } 390 break 391 } 392 if response != nil && response.Body != nil { 393 response.Body.Close() 394 } 395 return 396 } 397 398 func setAcceptGzip(headers http.Header) { 399 headers.Set("Accept-Encoding", "gzip") 400 } 401 402 func setRange(headers http.Header, from, end uint64) { 403 headers.Set("Range", fmt.Sprintf("bytes=%d-%d", from, end-1)) 404 headers.Del("Accept-Encoding") 405 } 406 407 type downloadingPartsProgress struct { 408 downloaded uint64 409 downloading map[uint64]uint64 410 lock sync.Mutex 411 } 412 413 func newDownloadingPartsProgress() *downloadingPartsProgress { 414 return &downloadingPartsProgress{ 415 downloading: make(map[uint64]uint64), 416 } 417 } 418 419 func (progress *downloadingPartsProgress) setPartDownloadingProgress(offset, downloaded uint64) { 420 progress.lock.Lock() 421 defer progress.lock.Unlock() 422 423 progress.downloading[offset] = downloaded 424 } 425 426 func (progress *downloadingPartsProgress) partDownloaded(offset, partSize uint64) { 427 progress.lock.Lock() 428 defer progress.lock.Unlock() 429 430 delete(progress.downloading, offset) 431 progress.downloaded += partSize 432 } 433 434 func (progress *downloadingPartsProgress) totalDownloaded() uint64 { 435 progress.lock.Lock() 436 defer progress.lock.Unlock() 437 438 downloaded := progress.downloaded 439 for _, b := range progress.downloading { 440 downloaded += b 441 } 442 return downloaded 443 } 444 445 func parseEtag(etag string) string { 446 etag = strings.TrimPrefix(etag, "\"") 447 etag = strings.TrimSuffix(etag, "\"") 448 etag = strings.TrimSuffix(etag, ".gz") 449 return etag 450 } 451 452 func cloneHeader(h http.Header) http.Header { 453 if h == nil { 454 return make(http.Header) 455 } 456 457 // Find total number of values. 458 nv := 0 459 for _, vv := range h { 460 nv += len(vv) 461 } 462 sv := make([]string, nv) // shared backing array for headers' values 463 h2 := make(http.Header, len(h)) 464 for k, vv := range h { 465 n := copy(sv, vv) 466 h2[k] = sv[:n:n] 467 sv = sv[n:] 468 } 469 return h2 470 }