github.com/qiniu/go-sdk/v7@v7.27.0/storagev2/downloader/downloaders_test.go (about)

     1  package downloader_test
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"context"
     7  	"crypto/md5"
     8  	"fmt"
     9  	"io"
    10  	"math/rand"
    11  	"net/http"
    12  	"net/http/httptest"
    13  	"net/url"
    14  	"os"
    15  	"path/filepath"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"sync/atomic"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/qiniu/go-sdk/v7/storagev2/backoff"
    24  	"github.com/qiniu/go-sdk/v7/storagev2/chooser"
    25  	"github.com/qiniu/go-sdk/v7/storagev2/downloader"
    26  	"github.com/qiniu/go-sdk/v7/storagev2/downloader/destination"
    27  	resumablerecorder "github.com/qiniu/go-sdk/v7/storagev2/downloader/resumable_recorder"
    28  	"github.com/qiniu/go-sdk/v7/storagev2/resolver"
    29  )
    30  
    31  func TestConcurrentDownloaderWithSinglePart(t *testing.T) {
    32  	var (
    33  		counts [3]uint64
    34  		hasher = md5.New()
    35  	)
    36  	handler := func(id int, w http.ResponseWriter, r *http.Request) {
    37  		counts[id-1] += 1
    38  		switch id {
    39  		case 1:
    40  			w.WriteHeader(http.StatusGatewayTimeout)
    41  		case 2:
    42  			w.WriteHeader(http.StatusServiceUnavailable)
    43  		case 3:
    44  			w.Header().Set("Accept-Ranges", "bytes")
    45  			switch r.Method {
    46  			case http.MethodHead:
    47  				w.Header().Set("Etag", "testetag1")
    48  				w.Header().Set("Content-Length", strconv.Itoa(1024*1024))
    49  			case http.MethodGet:
    50  				w.Header().Set("Etag", "testetag1")
    51  				w.Header().Set("Content-Length", strconv.Itoa(1024*1024))
    52  				_, err := io.Copy(w, io.TeeReader(io.LimitReader(rand.New(rand.NewSource(time.Now().UnixNano())), 1024*1024), hasher))
    53  				if err != nil {
    54  					t.Fatal(err)
    55  				}
    56  			default:
    57  				w.WriteHeader(http.StatusMethodNotAllowed)
    58  			}
    59  		default:
    60  			w.WriteHeader(http.StatusPaymentRequired)
    61  		}
    62  	}
    63  	server1 := newTestServer(1, handler)
    64  	defer server1.Close()
    65  	url1, err := url.Parse(server1.URL)
    66  	if err != nil {
    67  		t.Fatal(err)
    68  	}
    69  	url1.Path = "/testfile"
    70  
    71  	server2 := newTestServer(2, handler)
    72  	defer server2.Close()
    73  	url2, err := url.Parse(server2.URL)
    74  	if err != nil {
    75  		t.Fatal(err)
    76  	}
    77  	url2.Path = "/testfile"
    78  
    79  	server3 := newTestServer(3, handler)
    80  	defer server3.Close()
    81  	url3, err := url.Parse(server3.URL)
    82  	if err != nil {
    83  		t.Fatal(err)
    84  	}
    85  	url3.Path = "/testfile"
    86  
    87  	d := downloader.NewConcurrentDownloader(&downloader.ConcurrentDownloaderOptions{
    88  		Concurrency: 1,
    89  		PartSize:    4 * 1024 * 1024,
    90  		DownloaderOptions: downloader.DownloaderOptions{
    91  			Backoff:  backoff.NewFixedBackoff(0),
    92  			Resolver: resolver.NewDefaultResolver(),
    93  			Chooser:  chooser.NewDirectChooser(),
    94  		},
    95  	})
    96  	var (
    97  		buf            closableBuffer
    98  		lastDownloaded uint64
    99  	)
   100  	n, err := d.Download(
   101  		context.Background(),
   102  		downloader.NewURLsIter([]*url.URL{url1, url2, url3}),
   103  		destination.NewWriteCloserDestination(&buf, ""),
   104  		&downloader.DestinationDownloadOptions{
   105  			OnDownloadingProgress: func(progress *downloader.DownloadingProgress) {
   106  				if progress.Downloaded < lastDownloaded {
   107  					t.Fatalf("unexpected downloaded progress")
   108  				}
   109  				lastDownloaded = progress.Downloaded
   110  				if progress.TotalSize != 1024*1024 {
   111  					t.Fatalf("unexpected downloaded progress")
   112  				}
   113  			},
   114  		})
   115  	if err != nil {
   116  		t.Fatal(err)
   117  	}
   118  	if n != 1024*1024 {
   119  		t.Fatalf("unexpected downloaded size")
   120  	}
   121  	if lastDownloaded != 1024*1024 {
   122  		t.Fatalf("unexpected downloaded progress")
   123  	}
   124  	if counts[0] != 10 {
   125  		t.Fatalf("unexpected called count")
   126  	}
   127  	if counts[1] != 10 {
   128  		t.Fatalf("unexpected called count")
   129  	}
   130  	if counts[2] != 2 {
   131  		t.Fatalf("unexpected called count")
   132  	}
   133  	serverMD5 := hasher.Sum(nil)
   134  	clientMD5 := md5.Sum(buf.Bytes())
   135  	if !bytes.Equal(serverMD5, clientMD5[:]) {
   136  		t.Fatalf("unexpected hash")
   137  	}
   138  }
   139  
   140  func TestConcurrentDownloaderWithCompression(t *testing.T) {
   141  	var counts uint64
   142  	hasher := md5.New()
   143  	handler := func(id int, w http.ResponseWriter, r *http.Request) {
   144  		counts += 1
   145  		switch id {
   146  		case 1:
   147  			w.Header().Set("Accept-Ranges", "bytes")
   148  			switch r.Method {
   149  			case http.MethodHead:
   150  				if r.Header.Get("Accept-Encoding") != "" {
   151  					t.Fatalf("unexpected accept-encoding")
   152  				}
   153  				w.Header().Set("Etag", "\"testetag1\"")
   154  			case http.MethodGet:
   155  				if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
   156  					t.Fatalf("unexpected accept-encoding")
   157  				}
   158  				w.Header().Set("Etag", "\"testetag1.gz\"")
   159  				w.Header().Set("Content-Encoding", "gzip")
   160  				var (
   161  					r   = io.TeeReader(io.LimitReader(rand.New(rand.NewSource(time.Now().UnixNano())), 1024*1024), hasher)
   162  					err error
   163  				)
   164  				gw := gzip.NewWriter(w)
   165  				if _, err = io.Copy(gw, r); err != nil {
   166  					t.Fatal(err)
   167  				}
   168  				if err = gw.Close(); err != nil {
   169  					t.Fatal(err)
   170  				}
   171  			default:
   172  				w.WriteHeader(http.StatusMethodNotAllowed)
   173  			}
   174  		default:
   175  			w.WriteHeader(http.StatusPaymentRequired)
   176  		}
   177  	}
   178  	server1 := newTestServer(1, handler)
   179  	defer server1.Close()
   180  	url1, err := url.Parse(server1.URL)
   181  	if err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	url1.Path = "/testfile"
   185  	d := downloader.NewConcurrentDownloader(&downloader.ConcurrentDownloaderOptions{
   186  		Concurrency: 1,
   187  		PartSize:    4 * 1024 * 1024,
   188  		DownloaderOptions: downloader.DownloaderOptions{
   189  			Backoff:  backoff.NewFixedBackoff(0),
   190  			Resolver: resolver.NewDefaultResolver(),
   191  			Chooser:  chooser.NewDirectChooser(),
   192  		},
   193  	})
   194  	var (
   195  		buf            closableBuffer
   196  		lastDownloaded uint64
   197  	)
   198  	n, err := d.Download(
   199  		context.Background(),
   200  		downloader.NewURLsIter([]*url.URL{url1}),
   201  		destination.NewWriteCloserDestination(&buf, ""), &downloader.DestinationDownloadOptions{
   202  			OnDownloadingProgress: func(progress *downloader.DownloadingProgress) {
   203  				if progress.Downloaded < lastDownloaded {
   204  					t.Fatalf("unexpected downloaded progress")
   205  				}
   206  				lastDownloaded = progress.Downloaded
   207  				if progress.TotalSize != 0 {
   208  					t.Fatalf("unexpected downloaded progress")
   209  				}
   210  			},
   211  		})
   212  	if err != nil {
   213  		t.Fatal(err)
   214  	}
   215  	if n != 1024*1024 {
   216  		t.Fatalf("unexpected downloaded size")
   217  	}
   218  	if lastDownloaded != 1024*1024 {
   219  		t.Fatalf("unexpected downloaded progress")
   220  	}
   221  	if counts != 2 {
   222  		t.Fatalf("unexpected called count")
   223  	}
   224  	serverMD5 := hasher.Sum(nil)
   225  	clientMD5 := md5.Sum(buf.Bytes())
   226  	if !bytes.Equal(serverMD5, clientMD5[:]) {
   227  		t.Fatalf("unexpected hash")
   228  	}
   229  }
   230  
   231  func TestConcurrentDownloaderWithMultipleParts(t *testing.T) {
   232  	tmpDir, err := os.MkdirTemp("", "")
   233  	if err != nil {
   234  		t.Fatal(err)
   235  	}
   236  	defer os.RemoveAll(tmpDir)
   237  
   238  	srcFile, err := os.CreateTemp(tmpDir, "testfile")
   239  	if err != nil {
   240  		t.Fatal(err)
   241  	}
   242  	defer srcFile.Close()
   243  
   244  	dstFile, err := os.CreateTemp(tmpDir, "testfile2")
   245  	if err != nil {
   246  		t.Fatal(err)
   247  	}
   248  	defer dstFile.Close()
   249  
   250  	const SIZE = 1024 * 1024 * 127
   251  	if _, err = io.CopyN(srcFile, rand.New(rand.NewSource(time.Now().UnixNano())), SIZE); err != nil {
   252  		t.Fatal(err)
   253  	}
   254  
   255  	mux := http.NewServeMux()
   256  	handler := http.FileServer(http.Dir(tmpDir))
   257  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   258  		w.Header().Set("Etag", "testetag1")
   259  		handler.ServeHTTP(w, r)
   260  	})
   261  	server1 := httptest.NewServer(mux)
   262  	defer server1.Close()
   263  	url1, err := url.Parse(server1.URL)
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  	url1.Path = "/" + filepath.Base(srcFile.Name())
   268  	d := downloader.NewConcurrentDownloader(&downloader.ConcurrentDownloaderOptions{
   269  		Concurrency: 16,
   270  		PartSize:    4 * 1024 * 1024,
   271  		DownloaderOptions: downloader.DownloaderOptions{
   272  			Backoff:  backoff.NewFixedBackoff(0),
   273  			Resolver: resolver.NewDefaultResolver(),
   274  			Chooser:  chooser.NewDirectChooser(),
   275  		},
   276  	})
   277  	dest, err := destination.NewFileDestination(dstFile.Name())
   278  	if err != nil {
   279  		t.Fatal(err)
   280  	}
   281  	var lastDownloaded uint64
   282  	n, err := d.Download(
   283  		context.Background(),
   284  		downloader.NewURLsIter([]*url.URL{url1}),
   285  		dest,
   286  		&downloader.DestinationDownloadOptions{
   287  			OnDownloadingProgress: func(progress *downloader.DownloadingProgress) {
   288  				if progress.Downloaded < atomic.LoadUint64(&lastDownloaded) {
   289  					t.Fatalf("unexpected downloaded progress")
   290  				}
   291  				atomic.StoreUint64(&lastDownloaded, progress.Downloaded)
   292  				if progress.TotalSize != SIZE {
   293  					t.Fatalf("unexpected downloaded progress")
   294  				}
   295  			},
   296  		})
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	if n != SIZE {
   301  		t.Fatalf("unexpected downloaded size")
   302  	}
   303  	if lastDownloaded != SIZE {
   304  		t.Fatalf("unexpected downloaded progress")
   305  	}
   306  	hasher := md5.New()
   307  	if _, err = srcFile.Seek(0, io.SeekStart); err != nil {
   308  		t.Fatal(err)
   309  	}
   310  	if _, err = io.Copy(hasher, srcFile); err != nil {
   311  		t.Fatal(err)
   312  	}
   313  	serverMD5 := hasher.Sum(nil)
   314  	hasher.Reset()
   315  	if _, err = dstFile.Seek(0, io.SeekStart); err != nil {
   316  		t.Fatal(err)
   317  	}
   318  	if _, err = io.Copy(hasher, dstFile); err != nil {
   319  		t.Fatal(err)
   320  	}
   321  	clientMD5 := hasher.Sum(nil)
   322  	if !bytes.Equal(serverMD5, clientMD5) {
   323  		t.Fatalf("unexpected hash")
   324  	}
   325  }
   326  
   327  func TestConcurrentDownloaderWithMultiplePartsAndRange(t *testing.T) {
   328  	tmpDir, err := os.MkdirTemp("", "")
   329  	if err != nil {
   330  		t.Fatal(err)
   331  	}
   332  	defer os.RemoveAll(tmpDir)
   333  
   334  	srcFile, err := os.CreateTemp(tmpDir, "testfile")
   335  	if err != nil {
   336  		t.Fatal(err)
   337  	}
   338  	defer srcFile.Close()
   339  
   340  	dstFile, err := os.CreateTemp(tmpDir, "testfile2")
   341  	if err != nil {
   342  		t.Fatal(err)
   343  	}
   344  	defer dstFile.Close()
   345  
   346  	const SIZE = 1024 * 1024 * 127
   347  	if _, err = io.CopyN(srcFile, rand.New(rand.NewSource(time.Now().UnixNano())), SIZE); err != nil {
   348  		t.Fatal(err)
   349  	}
   350  	const REQUEST_SIZE = 1024 * 1024 * 126
   351  
   352  	mux := http.NewServeMux()
   353  	handler := http.FileServer(http.Dir(tmpDir))
   354  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   355  		w.Header().Set("Etag", "testetag1")
   356  		handler.ServeHTTP(w, r)
   357  	})
   358  	server1 := httptest.NewServer(mux)
   359  	defer server1.Close()
   360  	url1, err := url.Parse(server1.URL)
   361  	if err != nil {
   362  		t.Fatal(err)
   363  	}
   364  	url1.Path = "/" + filepath.Base(srcFile.Name())
   365  	d := downloader.NewConcurrentDownloader(&downloader.ConcurrentDownloaderOptions{
   366  		Concurrency: 16,
   367  		PartSize:    4 * 1024 * 1024,
   368  		DownloaderOptions: downloader.DownloaderOptions{
   369  			Backoff:  backoff.NewFixedBackoff(0),
   370  			Resolver: resolver.NewDefaultResolver(),
   371  			Chooser:  chooser.NewDirectChooser(),
   372  		},
   373  	})
   374  	dest, err := destination.NewFileDestination(dstFile.Name())
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  	var lastDownloaded uint64
   379  	n, err := d.Download(
   380  		context.Background(),
   381  		downloader.NewURLsIter([]*url.URL{url1}),
   382  		dest,
   383  		&downloader.DestinationDownloadOptions{
   384  			Header: http.Header{"Range": []string{fmt.Sprintf("bytes=%d-", SIZE-REQUEST_SIZE)}},
   385  			OnDownloadingProgress: func(progress *downloader.DownloadingProgress) {
   386  				if progress.Downloaded < atomic.LoadUint64(&lastDownloaded) {
   387  					t.Fatalf("unexpected downloaded progress")
   388  				}
   389  				atomic.StoreUint64(&lastDownloaded, progress.Downloaded)
   390  				if progress.TotalSize != REQUEST_SIZE {
   391  					t.Fatalf("unexpected downloaded progress")
   392  				}
   393  			},
   394  		})
   395  	if err != nil {
   396  		t.Fatal(err)
   397  	}
   398  	if n != REQUEST_SIZE {
   399  		t.Fatalf("unexpected downloaded size")
   400  	}
   401  	if lastDownloaded != REQUEST_SIZE {
   402  		t.Fatalf("unexpected downloaded progress")
   403  	}
   404  	hasher := md5.New()
   405  	if _, err = srcFile.Seek(SIZE-REQUEST_SIZE, io.SeekStart); err != nil {
   406  		t.Fatal(err)
   407  	}
   408  	if _, err = io.Copy(hasher, srcFile); err != nil {
   409  		t.Fatal(err)
   410  	}
   411  	serverMD5 := hasher.Sum(nil)
   412  	hasher.Reset()
   413  	if _, err = dstFile.Seek(0, io.SeekStart); err != nil {
   414  		t.Fatal(err)
   415  	}
   416  	if _, err = io.Copy(hasher, dstFile); err != nil {
   417  		t.Fatal(err)
   418  	}
   419  	clientMD5 := hasher.Sum(nil)
   420  	if !bytes.Equal(serverMD5, clientMD5) {
   421  		t.Fatalf("unexpected hash")
   422  	}
   423  }
   424  
   425  func TestConcurrentDownloaderWithResumableRecorder(t *testing.T) {
   426  	tmpDir, err := os.MkdirTemp("", "")
   427  	if err != nil {
   428  		t.Fatal(err)
   429  	}
   430  	defer os.RemoveAll(tmpDir)
   431  
   432  	srcFile, err := os.CreateTemp(tmpDir, "srcFile")
   433  	if err != nil {
   434  		t.Fatal(err)
   435  	}
   436  	defer srcFile.Close()
   437  
   438  	dstFile, err := os.CreateTemp(tmpDir, "dstFile")
   439  	if err != nil {
   440  		t.Fatal(err)
   441  	}
   442  	defer dstFile.Close()
   443  
   444  	if _, err = io.CopyN(srcFile, rand.New(rand.NewSource(time.Now().UnixNano())), 10*1024*1024); err != nil {
   445  		t.Fatal(err)
   446  	}
   447  
   448  	ranges := make(map[uint64]uint64)
   449  	var rangesMutex sync.Mutex
   450  	handler := func(id int, w http.ResponseWriter, r *http.Request) {
   451  		switch id {
   452  		case 1:
   453  			w.Header().Set("Accept-Ranges", "bytes")
   454  			switch r.Method {
   455  			case http.MethodHead:
   456  				w.Header().Set("Etag", "testetag1")
   457  				w.Header().Set("Content-Length", strconv.Itoa(10*1024*1024))
   458  			case http.MethodGet:
   459  				w.Header().Set("Etag", "testetag1")
   460  
   461  				var fromOffset, toOffset uint64
   462  				if _, err = fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &fromOffset, &toOffset); err != nil {
   463  					t.Fatal(err)
   464  				}
   465  				rangesMutex.Lock()
   466  				ranges[fromOffset] = toOffset - fromOffset + 1
   467  				rangesMutex.Unlock()
   468  
   469  				w.Header().Set("Content-Length", strconv.FormatUint(toOffset-fromOffset+1, 10))
   470  				w.WriteHeader(http.StatusPartialContent)
   471  				if _, err = io.Copy(w, io.NewSectionReader(srcFile, int64(fromOffset), int64(toOffset-fromOffset+1))); err != nil {
   472  					t.Fatal(err)
   473  				}
   474  			}
   475  		default:
   476  			w.WriteHeader(http.StatusPaymentRequired)
   477  		}
   478  	}
   479  	server1 := newTestServer(1, handler)
   480  	defer server1.Close()
   481  	url1, err := url.Parse(server1.URL)
   482  	if err != nil {
   483  		t.Fatal(err)
   484  	}
   485  	url1.Path = "/testfile"
   486  
   487  	resumableRecorder := resumablerecorder.NewJsonFileSystemResumableRecorder(tmpDir)
   488  	options := resumablerecorder.ResumableRecorderOpenArgs{
   489  		ETag:          "testetag1",
   490  		DestinationID: dstFile.Name(),
   491  		PartSize:      1024 * 1024,
   492  		TotalSize:     10 * 1024 * 1024,
   493  	}
   494  	writableMedium := resumableRecorder.OpenForCreatingNew(&options)
   495  	defer writableMedium.Close()
   496  
   497  	if err = writableMedium.Write(&resumablerecorder.ResumableRecord{
   498  		Offset:      0,
   499  		PartSize:    1024 * 1024,
   500  		PartWritten: 1024,
   501  	}); err != nil {
   502  		t.Fatal(err)
   503  	}
   504  	if err = writableMedium.Write(&resumablerecorder.ResumableRecord{
   505  		Offset:      1024 * 1024,
   506  		PartSize:    1024 * 1024,
   507  		PartWritten: 1024 * 1024,
   508  	}); err != nil {
   509  		t.Fatal(err)
   510  	}
   511  	if err = writableMedium.Close(); err != nil {
   512  		t.Fatal(err)
   513  	}
   514  
   515  	d := downloader.NewConcurrentDownloader(&downloader.ConcurrentDownloaderOptions{
   516  		Concurrency:       10,
   517  		PartSize:          1024 * 1024,
   518  		ResumableRecorder: resumableRecorder,
   519  		DownloaderOptions: downloader.DownloaderOptions{
   520  			Backoff:  backoff.NewFixedBackoff(0),
   521  			Resolver: resolver.NewDefaultResolver(),
   522  			Chooser:  chooser.NewDirectChooser(),
   523  		},
   524  	})
   525  	dest, err := destination.NewFileDestination(dstFile.Name())
   526  	if err != nil {
   527  		t.Fatal(err)
   528  	}
   529  	var lastDownloaded uint64
   530  	n, err := d.Download(
   531  		context.Background(),
   532  		downloader.NewURLsIter([]*url.URL{url1}),
   533  		dest,
   534  		&downloader.DestinationDownloadOptions{
   535  			OnDownloadingProgress: func(progress *downloader.DownloadingProgress) {
   536  				if progress.Downloaded < lastDownloaded {
   537  					t.Fatalf("unexpected downloaded progress")
   538  				}
   539  				lastDownloaded = progress.Downloaded
   540  				if progress.TotalSize != 10*1024*1024 {
   541  					t.Fatalf("unexpected downloaded progress")
   542  				}
   543  			},
   544  		})
   545  	if err != nil {
   546  		t.Fatal(err)
   547  	}
   548  	if n != 10*1024*1024 {
   549  		t.Fatal(err)
   550  	}
   551  	if lastDownloaded != 10*1024*1024 {
   552  		t.Fatalf("unexpected downloaded progress")
   553  	}
   554  	if len(ranges) != 9 {
   555  		t.Fatalf("unexpected ranges")
   556  	}
   557  	if ranges[1024] != 1024*1023 {
   558  		t.Fatalf("unexpected ranges")
   559  	}
   560  	if ranges[1024*1024] != 0 {
   561  		t.Fatalf("unexpected ranges")
   562  	}
   563  
   564  	readableMedium := resumableRecorder.OpenForReading(&options)
   565  	if readableMedium != nil {
   566  		t.Fatalf("medium is not expected to be found")
   567  	}
   568  }
   569  
   570  func newTestServer(id int, handler func(int, http.ResponseWriter, *http.Request)) *httptest.Server {
   571  	mux := http.NewServeMux()
   572  	mux.HandleFunc("/testfile", func(w http.ResponseWriter, r *http.Request) {
   573  		handler(id, w, r)
   574  	})
   575  	return httptest.NewServer(mux)
   576  }
   577  
   578  type closableBuffer struct {
   579  	bytes.Buffer
   580  }
   581  
   582  func (w closableBuffer) Close() error {
   583  	return nil
   584  }