github.com/qiniu/go-sdk/v7@v7.27.0/storagev2/uploader/multi_parts_uploader_test.go (about)

     1  //go:build unit
     2  // +build unit
     3  
     4  package uploader
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"fmt"
    12  	"hash/crc32"
    13  	"io"
    14  	"math/rand"
    15  	"net/http"
    16  	"net/http/httptest"
    17  	"os"
    18  	"strconv"
    19  	"strings"
    20  	"sync/atomic"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/gorilla/mux"
    25  	internal_io "github.com/qiniu/go-sdk/v7/internal/io"
    26  	"github.com/qiniu/go-sdk/v7/storagev2/apis"
    27  	"github.com/qiniu/go-sdk/v7/storagev2/credentials"
    28  	"github.com/qiniu/go-sdk/v7/storagev2/http_client"
    29  	"github.com/qiniu/go-sdk/v7/storagev2/region"
    30  	resumablerecorder "github.com/qiniu/go-sdk/v7/storagev2/uploader/resumable_recorder"
    31  )
    32  
    33  func TestMultiPartsUploader(t *testing.T) {
    34  	tmpFile, err := os.CreateTemp("", "multi-parts-uploader-test-*")
    35  	if err != nil {
    36  		t.Fatal(err)
    37  	}
    38  	defer os.Remove(tmpFile.Name())
    39  	defer tmpFile.Close()
    40  
    41  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
    42  	if _, err = io.CopyN(tmpFile, r, 5*1024*1024); err != nil {
    43  		t.Fatal(err)
    44  	}
    45  	if _, err = tmpFile.Seek(0, io.SeekStart); err != nil {
    46  		t.Fatal(err)
    47  	}
    48  
    49  	var server *httptest.Server
    50  	serveMux := mux.NewRouter()
    51  	serveMux.HandleFunc("/mkblk/4194304", func(w http.ResponseWriter, r *http.Request) {
    52  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
    53  			t.Fatalf("unexpected authorization")
    54  		}
    55  		actualBody, err := internal_io.ReadAll(r.Body)
    56  		if err != nil {
    57  			t.Fatal(err)
    58  		}
    59  		expectedBody, err := internal_io.ReadAll(io.NewSectionReader(tmpFile, 0, 4*1024*1024))
    60  		if err != nil {
    61  			t.Fatal(err)
    62  		}
    63  		if !bytes.Equal(actualBody, expectedBody) {
    64  			t.Fatalf("unexpected body")
    65  		}
    66  		jsonBody, err := json.Marshal(&apis.ResumableUploadV1MakeBlockResponse{
    67  			Ctx:       "testctx1",
    68  			Checksum:  "testchecksum1",
    69  			Crc32:     int64(crc32.ChecksumIEEE(actualBody)),
    70  			Host:      server.URL,
    71  			ExpiredAt: time.Now().Add(1 * time.Hour).Unix(),
    72  		})
    73  		if err != nil {
    74  			t.Fatal(err)
    75  		}
    76  		w.Header().Add("X-ReqId", "fakereqid")
    77  		w.Write(jsonBody)
    78  	}).Methods(http.MethodPost)
    79  	serveMux.HandleFunc("/mkblk/1048576", func(w http.ResponseWriter, r *http.Request) {
    80  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
    81  			t.Fatalf("unexpected authorization")
    82  		}
    83  		actualBody, err := internal_io.ReadAll(r.Body)
    84  		if err != nil {
    85  			t.Fatal(err)
    86  		}
    87  		expectedBody, err := internal_io.ReadAll(io.NewSectionReader(tmpFile, 4*1024*1024, 1*1024*1024))
    88  		if err != nil {
    89  			t.Fatal(err)
    90  		}
    91  		if !bytes.Equal(actualBody, expectedBody) {
    92  			t.Fatalf("unexpected body")
    93  		}
    94  		jsonBody, err := json.Marshal(&apis.ResumableUploadV1MakeBlockResponse{
    95  			Ctx:       "testctx2",
    96  			Checksum:  "testchecksum2",
    97  			Crc32:     int64(crc32.ChecksumIEEE(actualBody)),
    98  			Host:      server.URL,
    99  			ExpiredAt: time.Now().Add(1 * time.Hour).Unix(),
   100  		})
   101  		if err != nil {
   102  			t.Fatal(err)
   103  		}
   104  		w.Header().Add("X-ReqId", "fakereqid")
   105  		w.Write(jsonBody)
   106  	}).Methods(http.MethodPost)
   107  	serveMux.PathPrefix("/mkfile/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   108  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
   109  			t.Fatalf("unexpected authorization")
   110  		}
   111  		components := strings.Split(strings.TrimPrefix(r.URL.Path, "/mkfile/"), "/")
   112  		if components[0] != strconv.FormatInt(5*1024*1024, 10) {
   113  			t.Fatalf("unexpected fileSize")
   114  		}
   115  		components = components[1:]
   116  		for len(components) > 0 {
   117  			switch components[0] {
   118  			case "key":
   119  				value := components[1]
   120  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   121  				if err != nil {
   122  					t.Fatal(err)
   123  				}
   124  				if string(valueBytes) != "testkey" {
   125  					t.Fatalf("unexpected key")
   126  				}
   127  			case "fname":
   128  				value := components[1]
   129  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   130  				if err != nil {
   131  					t.Fatal(err)
   132  				}
   133  				if string(valueBytes) != "testfilename" {
   134  					t.Fatalf("unexpected fname")
   135  				}
   136  			case "mimeType":
   137  				value := components[1]
   138  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   139  				if err != nil {
   140  					t.Fatal(err)
   141  				}
   142  				if string(valueBytes) != "application/json" {
   143  					t.Fatalf("unexpected mimeType")
   144  				}
   145  			case "x-qn-meta-a":
   146  				value := components[1]
   147  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   148  				if err != nil {
   149  					t.Fatal(err)
   150  				}
   151  				if string(valueBytes) != "b" {
   152  					t.Fatalf("unexpected x-qn-meta-a")
   153  				}
   154  			case "x-qn-meta-c":
   155  				value := components[1]
   156  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   157  				if err != nil {
   158  					t.Fatal(err)
   159  				}
   160  				if string(valueBytes) != "d" {
   161  					t.Fatalf("unexpected x-qn-meta-c")
   162  				}
   163  			case "x:a":
   164  				value := components[1]
   165  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   166  				if err != nil {
   167  					t.Fatal(err)
   168  				}
   169  				if string(valueBytes) != "b" {
   170  					t.Fatalf("unexpected x:a")
   171  				}
   172  			case "x:c":
   173  				value := components[1]
   174  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   175  				if err != nil {
   176  					t.Fatal(err)
   177  				}
   178  				if string(valueBytes) != "d" {
   179  					t.Fatalf("unexpected x:c")
   180  				}
   181  			default:
   182  				t.Fatalf("unexpected component key: %s", components[0])
   183  			}
   184  			components = components[2:]
   185  		}
   186  		actualBody, err := internal_io.ReadAll(r.Body)
   187  		if err != nil {
   188  			t.Fatal(err)
   189  		}
   190  		if string(actualBody) != "testctx1,testctx2" {
   191  			t.Fatalf("unexpected body")
   192  		}
   193  		w.Header().Add("X-ReqId", "fakereqid")
   194  		w.Write([]byte(`{"ok":true}`))
   195  	}).Methods(http.MethodPost)
   196  	server = httptest.NewServer(serveMux)
   197  	defer server.Close()
   198  
   199  	multiPartsUploader := newMultiPartsUploader(newConcurrentMultiPartsUploaderScheduler(
   200  		NewMultiPartsUploaderV1(&MultiPartsUploaderOptions{
   201  			Options: http_client.Options{
   202  				Regions:     &region.Region{Up: region.Endpoints{Preferred: []string{server.URL}}},
   203  				Credentials: credentials.NewCredentials("testak", "testsk"),
   204  			},
   205  		}), &concurrentMultiPartsUploaderSchedulerOptions{PartSize: 1 << 22, Concurrency: 2},
   206  	))
   207  
   208  	var (
   209  		key         = "testkey"
   210  		returnValue struct {
   211  			Ok bool `json:"ok"`
   212  		}
   213  		lastUploaded uint64
   214  	)
   215  	if err = multiPartsUploader.UploadFile(context.Background(), tmpFile.Name(), &ObjectOptions{
   216  		BucketName:  "testbucket",
   217  		ObjectName:  &key,
   218  		FileName:    "testfilename",
   219  		ContentType: "application/json",
   220  		Metadata:    map[string]string{"a": "b", "c": "d"},
   221  		CustomVars:  map[string]string{"a": "b", "c": "d"},
   222  		OnUploadingProgress: func(progress *UploadingProgress) {
   223  			if progress.TotalSize != 5*1024*1024 {
   224  				t.Fatalf("unexpected file size")
   225  			} else if progress.Uploaded > progress.TotalSize {
   226  				t.Fatalf("unexpected uploaded")
   227  			} else if lu := atomic.SwapUint64(&lastUploaded, progress.Uploaded); lu > progress.Uploaded || lu > progress.TotalSize {
   228  				t.Fatalf("unexpected uploaded")
   229  			}
   230  		},
   231  	}, &returnValue); err != nil {
   232  		t.Fatal(err)
   233  	} else if !returnValue.Ok {
   234  		t.Fatalf("unexpected response body")
   235  	}
   236  }
   237  
   238  func TestMultiPartsUploaderResuming(t *testing.T) {
   239  	tmpDir, err := os.MkdirTemp("", "multi-parts-uploader-test-*")
   240  	if err != nil {
   241  		t.Fatal(err)
   242  	}
   243  	defer os.RemoveAll(tmpDir)
   244  
   245  	tmpFile, err := os.CreateTemp("", "multi-parts-uploader-test-*")
   246  	if err != nil {
   247  		t.Fatal(err)
   248  	}
   249  	defer os.Remove(tmpFile.Name())
   250  	defer tmpFile.Close()
   251  
   252  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   253  	if _, err = io.CopyN(tmpFile, r, 5*1024*1024); err != nil {
   254  		t.Fatal(err)
   255  	}
   256  	if _, err = tmpFile.Seek(0, io.SeekStart); err != nil {
   257  		t.Fatal(err)
   258  	}
   259  
   260  	var server *httptest.Server
   261  	serveMux := mux.NewRouter()
   262  	serveMux.PathPrefix("/mkfile/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   263  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
   264  			t.Fatalf("unexpected authorization")
   265  		}
   266  		components := strings.Split(strings.TrimPrefix(r.URL.Path, "/mkfile/"), "/")
   267  		if components[0] != strconv.FormatInt(5*1024*1024, 10) {
   268  			t.Fatalf("unexpected fileSize")
   269  		}
   270  		components = components[1:]
   271  		for len(components) > 0 {
   272  			switch components[0] {
   273  			case "key":
   274  				value := components[1]
   275  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   276  				if err != nil {
   277  					t.Fatal(err)
   278  				}
   279  				if string(valueBytes) != "testkey" {
   280  					t.Fatalf("unexpected key")
   281  				}
   282  			case "fname":
   283  				value := components[1]
   284  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   285  				if err != nil {
   286  					t.Fatal(err)
   287  				}
   288  				if string(valueBytes) != "testfilename" {
   289  					t.Fatalf("unexpected fname")
   290  				}
   291  			case "mimeType":
   292  				value := components[1]
   293  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   294  				if err != nil {
   295  					t.Fatal(err)
   296  				}
   297  				if string(valueBytes) != "application/json" {
   298  					t.Fatalf("unexpected mimeType")
   299  				}
   300  			case "x-qn-meta-a":
   301  				value := components[1]
   302  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   303  				if err != nil {
   304  					t.Fatal(err)
   305  				}
   306  				if string(valueBytes) != "b" {
   307  					t.Fatalf("unexpected x-qn-meta-a")
   308  				}
   309  			case "x-qn-meta-c":
   310  				value := components[1]
   311  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   312  				if err != nil {
   313  					t.Fatal(err)
   314  				}
   315  				if string(valueBytes) != "d" {
   316  					t.Fatalf("unexpected x-qn-meta-c")
   317  				}
   318  			case "x:a":
   319  				value := components[1]
   320  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   321  				if err != nil {
   322  					t.Fatal(err)
   323  				}
   324  				if string(valueBytes) != "b" {
   325  					t.Fatalf("unexpected x:a")
   326  				}
   327  			case "x:c":
   328  				value := components[1]
   329  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   330  				if err != nil {
   331  					t.Fatal(err)
   332  				}
   333  				if string(valueBytes) != "d" {
   334  					t.Fatalf("unexpected x:c")
   335  				}
   336  			default:
   337  				t.Fatalf("unexpected component key: %s", components[0])
   338  			}
   339  			components = components[2:]
   340  		}
   341  		actualBody, err := internal_io.ReadAll(r.Body)
   342  		if err != nil {
   343  			t.Fatal(err)
   344  		}
   345  		if string(actualBody) != "testctx1,testctx2" {
   346  			t.Fatalf("unexpected body")
   347  		}
   348  		w.Header().Add("X-ReqId", "fakereqid")
   349  		w.Write([]byte(`{"ok":true}`))
   350  	}).Methods(http.MethodPost)
   351  	server = httptest.NewServer(serveMux)
   352  	defer server.Close()
   353  
   354  	tmpFileStat, err := tmpFile.Stat()
   355  	if err != nil {
   356  		t.Fatal(err)
   357  	}
   358  	tmpFileSourceID := fmt.Sprintf("%d:%d:%s", tmpFileStat.Size(), tmpFileStat.ModTime().UnixNano(), tmpFile.Name())
   359  
   360  	resumableRecorder := resumablerecorder.NewJsonFileSystemResumableRecorder(tmpDir)
   361  	medium := resumableRecorder.OpenForCreatingNew(&resumablerecorder.ResumableRecorderOpenArgs{
   362  		AccessKey:   "testak",
   363  		BucketName:  "testbucket",
   364  		ObjectName:  "testkey",
   365  		SourceID:    tmpFileSourceID,
   366  		PartSize:    4 * 1024 * 1024,
   367  		TotalSize:   5 * 1024 * 1024,
   368  		UpEndpoints: region.Endpoints{Preferred: []string{server.URL}},
   369  	})
   370  	if err = medium.Write(&resumablerecorder.ResumableRecord{
   371  		PartID:     "testctx1",
   372  		Offset:     0,
   373  		PartSize:   4 * 1024 * 1024,
   374  		PartNumber: 1,
   375  		ExpiredAt:  time.Now().Add(1 * time.Hour),
   376  	}); err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	if err = medium.Write(&resumablerecorder.ResumableRecord{
   380  		PartID:     "testctx2",
   381  		Offset:     4 * 1024 * 1024,
   382  		PartSize:   1 * 1024 * 1024,
   383  		PartNumber: 2,
   384  		ExpiredAt:  time.Now().Add(1 * time.Hour),
   385  	}); err != nil {
   386  		t.Fatal(err)
   387  	}
   388  	if err = medium.Close(); err != nil {
   389  		t.Fatal(err)
   390  	}
   391  
   392  	multiPartsUploader := newMultiPartsUploader(newConcurrentMultiPartsUploaderScheduler(
   393  		NewMultiPartsUploaderV1(&MultiPartsUploaderOptions{
   394  			ResumableRecorder: resumableRecorder,
   395  			Options: http_client.Options{
   396  				Regions:     &region.Region{Up: region.Endpoints{Preferred: []string{server.URL}}},
   397  				Credentials: credentials.NewCredentials("testak", "testsk"),
   398  			},
   399  		}), &concurrentMultiPartsUploaderSchedulerOptions{PartSize: 1 << 22, Concurrency: 2},
   400  	))
   401  
   402  	var (
   403  		key         = "testkey"
   404  		returnValue struct {
   405  			Ok bool `json:"ok"`
   406  		}
   407  		lastUploaded uint64
   408  	)
   409  	if err = multiPartsUploader.UploadFile(context.Background(), tmpFile.Name(), &ObjectOptions{
   410  		BucketName:  "testbucket",
   411  		ObjectName:  &key,
   412  		FileName:    "testfilename",
   413  		ContentType: "application/json",
   414  		Metadata:    map[string]string{"a": "b", "c": "d"},
   415  		CustomVars:  map[string]string{"a": "b", "c": "d"},
   416  		OnUploadingProgress: func(progress *UploadingProgress) {
   417  			if progress.TotalSize != 5*1024*1024 {
   418  				t.Fatalf("unexpected file size")
   419  			} else if progress.Uploaded > progress.TotalSize {
   420  				t.Fatalf("unexpected uploaded")
   421  			} else if lu := atomic.SwapUint64(&lastUploaded, progress.Uploaded); lu > progress.Uploaded || lu > progress.TotalSize {
   422  				t.Fatalf("unexpected uploaded")
   423  			}
   424  		},
   425  	}, &returnValue); err != nil {
   426  		t.Fatal(err)
   427  	} else if !returnValue.Ok {
   428  		t.Fatalf("unexpected response body")
   429  	}
   430  }
   431  
   432  func TestMultiPartsUploaderRetry(t *testing.T) {
   433  	tmpFile, err := os.CreateTemp("", "multi-parts-uploader-test-*")
   434  	if err != nil {
   435  		t.Fatal(err)
   436  	}
   437  	defer os.Remove(tmpFile.Name())
   438  	defer tmpFile.Close()
   439  
   440  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   441  	if _, err = io.CopyN(tmpFile, r, 5*1024*1024); err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	if _, err = tmpFile.Seek(0, io.SeekStart); err != nil {
   445  		t.Fatal(err)
   446  	}
   447  
   448  	var handlerCalled_1, handlerCalled_2, handlerCalled_3 uint64
   449  	serveMux_1 := mux.NewRouter()
   450  	serveMux_1.HandleFunc("/mkblk/{blockSize}", func(w http.ResponseWriter, r *http.Request) {
   451  		atomic.AddUint64(&handlerCalled_1, 1)
   452  		w.Header().Add("X-ReqId", "fakereqid")
   453  		w.WriteHeader(599)
   454  	}).Methods(http.MethodPost)
   455  	server_1 := httptest.NewServer(serveMux_1)
   456  	defer server_1.Close()
   457  
   458  	serveMux_2 := mux.NewRouter()
   459  	serveMux_2.HandleFunc("/mkblk/{blockSize}", func(w http.ResponseWriter, r *http.Request) {
   460  		atomic.AddUint64(&handlerCalled_2, 1)
   461  		w.WriteHeader(200)
   462  	}).Methods(http.MethodPost)
   463  	server_2 := httptest.NewServer(serveMux_2)
   464  	defer server_2.Close()
   465  
   466  	var server_3 *httptest.Server
   467  	serveMux_3 := mux.NewRouter()
   468  	serveMux_3.HandleFunc("/mkblk/4194304", func(w http.ResponseWriter, r *http.Request) {
   469  		atomic.AddUint64(&handlerCalled_3, 1)
   470  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
   471  			t.Fatalf("unexpected authorization")
   472  		}
   473  		actualBody, err := internal_io.ReadAll(r.Body)
   474  		if err != nil {
   475  			t.Fatal(err)
   476  		}
   477  		expectedBody, err := internal_io.ReadAll(io.NewSectionReader(tmpFile, 0, 4*1024*1024))
   478  		if err != nil {
   479  			t.Fatal(err)
   480  		}
   481  		if !bytes.Equal(actualBody, expectedBody) {
   482  			t.Fatalf("unexpected body")
   483  		}
   484  		jsonBody, err := json.Marshal(&apis.ResumableUploadV1MakeBlockResponse{
   485  			Ctx:       "testctx1",
   486  			Checksum:  "testchecksum1",
   487  			Crc32:     int64(crc32.ChecksumIEEE(actualBody)),
   488  			Host:      server_3.URL,
   489  			ExpiredAt: time.Now().Add(1 * time.Hour).Unix(),
   490  		})
   491  		if err != nil {
   492  			t.Fatal(err)
   493  		}
   494  		w.Header().Add("X-ReqId", "fakereqid")
   495  		w.Write(jsonBody)
   496  	}).Methods(http.MethodPost)
   497  	serveMux_3.HandleFunc("/mkblk/1048576", func(w http.ResponseWriter, r *http.Request) {
   498  		atomic.AddUint64(&handlerCalled_3, 1)
   499  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
   500  			t.Fatalf("unexpected authorization")
   501  		}
   502  		actualBody, err := internal_io.ReadAll(r.Body)
   503  		if err != nil {
   504  			t.Fatal(err)
   505  		}
   506  		expectedBody, err := internal_io.ReadAll(io.NewSectionReader(tmpFile, 4*1024*1024, 1*1024*1024))
   507  		if err != nil {
   508  			t.Fatal(err)
   509  		}
   510  		if !bytes.Equal(actualBody, expectedBody) {
   511  			t.Fatalf("unexpected body")
   512  		}
   513  		jsonBody, err := json.Marshal(&apis.ResumableUploadV1MakeBlockResponse{
   514  			Ctx:       "testctx2",
   515  			Checksum:  "testchecksum2",
   516  			Crc32:     int64(crc32.ChecksumIEEE(actualBody)),
   517  			Host:      server_3.URL,
   518  			ExpiredAt: time.Now().Add(1 * time.Hour).Unix(),
   519  		})
   520  		if err != nil {
   521  			t.Fatal(err)
   522  		}
   523  		w.Header().Add("X-ReqId", "fakereqid")
   524  		w.Write(jsonBody)
   525  	}).Methods(http.MethodPost)
   526  	serveMux_3.PathPrefix("/mkfile/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   527  		atomic.AddUint64(&handlerCalled_3, 1)
   528  		if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") {
   529  			t.Fatalf("unexpected authorization")
   530  		}
   531  		components := strings.Split(strings.TrimPrefix(r.URL.Path, "/mkfile/"), "/")
   532  		if components[0] != strconv.FormatInt(5*1024*1024, 10) {
   533  			t.Fatalf("unexpected fileSize")
   534  		}
   535  		components = components[1:]
   536  		for len(components) > 0 {
   537  			switch components[0] {
   538  			case "key":
   539  				value := components[1]
   540  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   541  				if err != nil {
   542  					t.Fatal(err)
   543  				}
   544  				if string(valueBytes) != "testkey" {
   545  					t.Fatalf("unexpected key")
   546  				}
   547  			case "fname":
   548  				value := components[1]
   549  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   550  				if err != nil {
   551  					t.Fatal(err)
   552  				}
   553  				if string(valueBytes) != "testfilename" {
   554  					t.Fatalf("unexpected fname")
   555  				}
   556  			case "mimeType":
   557  				value := components[1]
   558  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   559  				if err != nil {
   560  					t.Fatal(err)
   561  				}
   562  				if string(valueBytes) != "application/json" {
   563  					t.Fatalf("unexpected mimeType")
   564  				}
   565  			case "x-qn-meta-a":
   566  				value := components[1]
   567  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   568  				if err != nil {
   569  					t.Fatal(err)
   570  				}
   571  				if string(valueBytes) != "b" {
   572  					t.Fatalf("unexpected x-qn-meta-a")
   573  				}
   574  			case "x-qn-meta-c":
   575  				value := components[1]
   576  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   577  				if err != nil {
   578  					t.Fatal(err)
   579  				}
   580  				if string(valueBytes) != "d" {
   581  					t.Fatalf("unexpected x-qn-meta-c")
   582  				}
   583  			case "x:a":
   584  				value := components[1]
   585  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   586  				if err != nil {
   587  					t.Fatal(err)
   588  				}
   589  				if string(valueBytes) != "b" {
   590  					t.Fatalf("unexpected x:a")
   591  				}
   592  			case "x:c":
   593  				value := components[1]
   594  				valueBytes, err := base64.URLEncoding.DecodeString(value)
   595  				if err != nil {
   596  					t.Fatal(err)
   597  				}
   598  				if string(valueBytes) != "d" {
   599  					t.Fatalf("unexpected x:c")
   600  				}
   601  			default:
   602  				t.Fatalf("unexpected component key: %s", components[0])
   603  			}
   604  			components = components[2:]
   605  		}
   606  		actualBody, err := internal_io.ReadAll(r.Body)
   607  		if err != nil {
   608  			t.Fatal(err)
   609  		}
   610  		if string(actualBody) != "testctx1,testctx2" {
   611  			t.Fatalf("unexpected body")
   612  		}
   613  		w.Header().Add("X-ReqId", "fakereqid")
   614  		w.Write([]byte(`{"ok":true}`))
   615  	}).Methods(http.MethodPost)
   616  	server_3 = httptest.NewServer(serveMux_3)
   617  	defer server_3.Close()
   618  
   619  	multiPartsUploader := newMultiPartsUploader(newSerialMultiPartsUploaderScheduler(
   620  		NewMultiPartsUploaderV1(&MultiPartsUploaderOptions{
   621  			Options: http_client.Options{
   622  				Regions: regions{[]*region.Region{
   623  					{Up: region.Endpoints{Preferred: []string{server_1.URL}}},
   624  					{Up: region.Endpoints{Preferred: []string{server_2.URL}}},
   625  					{Up: region.Endpoints{Preferred: []string{server_3.URL}}},
   626  				}},
   627  				Credentials: credentials.NewCredentials("testak", "testsk"),
   628  			},
   629  		}), &serialMultiPartsUploaderSchedulerOptions{PartSize: 1 << 22},
   630  	))
   631  
   632  	var (
   633  		key         = "testkey"
   634  		returnValue struct {
   635  			Ok bool `json:"ok"`
   636  		}
   637  		lastUploaded uint64
   638  	)
   639  	if err = multiPartsUploader.UploadFile(context.Background(), tmpFile.Name(), &ObjectOptions{
   640  		BucketName:  "testbucket",
   641  		ObjectName:  &key,
   642  		FileName:    "testfilename",
   643  		ContentType: "application/json",
   644  		Metadata:    map[string]string{"a": "b", "c": "d"},
   645  		CustomVars:  map[string]string{"a": "b", "c": "d"},
   646  		OnUploadingProgress: func(progress *UploadingProgress) {
   647  			if progress.TotalSize != 5*1024*1024 {
   648  				t.Fatalf("unexpected file size")
   649  			}
   650  			atomic.StoreUint64(&lastUploaded, progress.Uploaded)
   651  		},
   652  	}, &returnValue); err != nil {
   653  		t.Fatal(err)
   654  	} else if !returnValue.Ok {
   655  		t.Fatalf("unexpected response body")
   656  	}
   657  
   658  	if fileSize := atomic.LoadUint64(&lastUploaded); fileSize != 5*1024*1024 {
   659  		t.Fatalf("unexpected file size: %d", fileSize)
   660  	}
   661  	if count := atomic.LoadUint64(&handlerCalled_1); count != 4 {
   662  		t.Fatalf("unexpected handler call count: %d", count)
   663  	}
   664  	if count := atomic.LoadUint64(&handlerCalled_2); count != 4 {
   665  		t.Fatalf("unexpected handler call count: %d", count)
   666  	}
   667  	if count := atomic.LoadUint64(&handlerCalled_3); count != 3 {
   668  		t.Fatalf("unexpected handler call count: %d", count)
   669  	}
   670  }