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  }