github.com/qiniu/go-sdk/v7@v7.27.0/storagev2/uploader/multi_parts_uploader_v2_test.go (about) 1 //go:build unit 2 // +build unit 3 4 package uploader_test 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/md5" 10 "encoding/base64" 11 "encoding/hex" 12 "encoding/json" 13 "fmt" 14 "io" 15 "math/rand" 16 "net/http" 17 "net/http/httptest" 18 "os" 19 "strings" 20 "testing" 21 "time" 22 23 "github.com/gorilla/mux" 24 internal_io "github.com/qiniu/go-sdk/v7/internal/io" 25 "github.com/qiniu/go-sdk/v7/storagev2/apis" 26 "github.com/qiniu/go-sdk/v7/storagev2/credentials" 27 "github.com/qiniu/go-sdk/v7/storagev2/http_client" 28 "github.com/qiniu/go-sdk/v7/storagev2/region" 29 "github.com/qiniu/go-sdk/v7/storagev2/uploader" 30 resumablerecorder "github.com/qiniu/go-sdk/v7/storagev2/uploader/resumable_recorder" 31 "github.com/qiniu/go-sdk/v7/storagev2/uploader/source" 32 ) 33 34 func TestMultiPartsUploaderV2(t *testing.T) { 35 tmpFile, err := os.CreateTemp("", "multi-parts-uploader-test-*") 36 if err != nil { 37 t.Fatal(err) 38 } 39 defer os.Remove(tmpFile.Name()) 40 defer tmpFile.Close() 41 42 r := rand.New(rand.NewSource(time.Now().UnixNano())) 43 if _, err = io.CopyN(tmpFile, r, 5*1024*1024); err != nil { 44 t.Fatal(err) 45 } 46 if _, err = tmpFile.Seek(0, io.SeekStart); err != nil { 47 t.Fatal(err) 48 } 49 50 serveMux := mux.NewRouter() 51 serveMux.HandleFunc("/buckets/{bucketName}/objects/{encodedObjectName}/uploads", func(w http.ResponseWriter, r *http.Request) { 52 if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") { 53 t.Fatalf("unexpected authorization") 54 } 55 vars := mux.Vars(r) 56 if vars["bucketName"] != "testbucket" { 57 t.Fatalf("unexpected bucket name") 58 } 59 objectBytes, err := base64.URLEncoding.DecodeString(vars["encodedObjectName"]) 60 if err != nil { 61 t.Fatal(err) 62 } else if string(objectBytes) != "testkey" { 63 t.Fatalf("unexpected object name") 64 } 65 jsonBytes, err := json.Marshal(&apis.ResumableUploadV2InitiateMultipartUploadResponse{ 66 UploadId: "testuploadID", 67 ExpiredAt: time.Now().Add(1 * time.Hour).Unix(), 68 }) 69 if err != nil { 70 t.Fatal(err) 71 } 72 w.Header().Add("X-ReqId", "fakereqid") 73 w.Write(jsonBytes) 74 }).Methods(http.MethodPost) 75 serveMux.HandleFunc("/buckets/{bucketName}/objects/{encodedObjectName}/uploads/{uploadID}/{partNumber}", func(w http.ResponseWriter, r *http.Request) { 76 if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") { 77 t.Fatalf("unexpected authorization") 78 } 79 vars := mux.Vars(r) 80 if vars["bucketName"] != "testbucket" { 81 t.Fatalf("unexpected bucket name") 82 } 83 objectBytes, err := base64.URLEncoding.DecodeString(vars["encodedObjectName"]) 84 if err != nil { 85 t.Fatal(err) 86 } else if string(objectBytes) != "testkey" { 87 t.Fatalf("unexpected object name") 88 } 89 if vars["uploadID"] != "testuploadID" { 90 t.Fatalf("unexpected upload id") 91 } 92 actualBody, err := internal_io.ReadAll(r.Body) 93 if err != nil { 94 t.Fatal(err) 95 } 96 var expectedBody, jsonBody []byte 97 switch vars["partNumber"] { 98 case "1": 99 expectedBody, err = internal_io.ReadAll(io.NewSectionReader(tmpFile, 0, 4*1024*1024)) 100 if err != nil { 101 t.Fatal(err) 102 } 103 case "2": 104 expectedBody, err = internal_io.ReadAll(io.NewSectionReader(tmpFile, 4*1024*1024, 1024*1024)) 105 if err != nil { 106 t.Fatal(err) 107 } 108 default: 109 t.Fatalf("unexpected part number") 110 } 111 if !bytes.Equal(actualBody, expectedBody) { 112 t.Fatalf("unexpected body") 113 } 114 md5Sum := md5.Sum(actualBody) 115 if r.Header.Get("Content-MD5") != hex.EncodeToString(md5Sum[:]) { 116 t.Fatalf("unexpected content-md5") 117 } 118 switch vars["partNumber"] { 119 case "1": 120 jsonBody, err = json.Marshal(&apis.ResumableUploadV2UploadPartResponse{ 121 Etag: "testetag1", 122 Md5: r.Header.Get("Content-MD5"), 123 }) 124 case "2": 125 jsonBody, err = json.Marshal(&apis.ResumableUploadV2UploadPartResponse{ 126 Etag: "testetag2", 127 Md5: r.Header.Get("Content-MD5"), 128 }) 129 } 130 if err != nil { 131 t.Fatal(err) 132 } 133 w.Header().Add("X-ReqId", "fakereqid") 134 w.Write(jsonBody) 135 }).Methods(http.MethodPut) 136 serveMux.HandleFunc("/buckets/{bucketName}/objects/{encodedObjectName}/uploads/{uploadID}", func(w http.ResponseWriter, r *http.Request) { 137 if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") { 138 t.Fatalf("unexpected authorization") 139 } 140 vars := mux.Vars(r) 141 if vars["bucketName"] != "testbucket" { 142 t.Fatalf("unexpected bucket name") 143 } 144 objectBytes, err := base64.URLEncoding.DecodeString(vars["encodedObjectName"]) 145 if err != nil { 146 t.Fatal(err) 147 } else if string(objectBytes) != "testkey" { 148 t.Fatalf("unexpected object name") 149 } 150 if vars["uploadID"] != "testuploadID" { 151 t.Fatalf("unexpected upload id") 152 } 153 requestBodyBytes, err := internal_io.ReadAll(r.Body) 154 if err != nil { 155 t.Fatal(err) 156 } 157 var body apis.ResumableUploadV2CompleteMultipartUploadRequest 158 if err = body.UnmarshalJSON(requestBodyBytes); err != nil { 159 t.Fatalf("unexpected request body") 160 } 161 if len(body.Parts) != 2 { 162 t.Fatalf("unexpected parts") 163 } else if body.Parts[0].PartNumber != 1 { 164 t.Fatalf("unexpected part number") 165 } else if body.Parts[0].Etag != "testetag1" { 166 t.Fatalf("unexpected part number") 167 } else if body.Parts[1].PartNumber != 2 { 168 t.Fatalf("unexpected part number") 169 } else if body.Parts[1].Etag != "testetag2" { 170 t.Fatalf("unexpected part number") 171 } 172 if body.FileName != "testfilename" { 173 t.Fatalf("unexpected fileName") 174 } 175 if body.MimeType != "application/json" { 176 t.Fatalf("unexpected mimeType") 177 } 178 if len(body.Metadata) != 2 { 179 t.Fatalf("unexpected metadata") 180 } else if body.Metadata["x-qn-meta-a"] != "b" { 181 t.Fatalf("unexpected x-qn-meta-a") 182 } else if body.Metadata["x-qn-meta-c"] != "d" { 183 t.Fatalf("unexpected x-qn-meta-c") 184 } else if body.CustomVars["x:a"] != "b" { 185 t.Fatalf("unexpected x:a") 186 } else if body.CustomVars["x:c"] != "d" { 187 t.Fatalf("unexpected x:c") 188 } 189 w.Header().Add("X-ReqId", "fakereqid") 190 w.Write([]byte(`{"ok":true}`)) 191 }).Methods(http.MethodPost) 192 server := httptest.NewServer(serveMux) 193 defer server.Close() 194 195 multiPartsUploaderV2 := uploader.NewMultiPartsUploaderV2(&uploader.MultiPartsUploaderOptions{ 196 Options: http_client.Options{ 197 Regions: ®ion.Region{Up: region.Endpoints{Preferred: []string{server.URL}}}, 198 Credentials: credentials.NewCredentials("testak", "testsk"), 199 }, 200 }) 201 202 src, err := source.NewFileSource(tmpFile.Name()) 203 if err != nil { 204 t.Fatal(err) 205 } 206 key := "testkey" 207 initializedPart, err := multiPartsUploaderV2.InitializeParts(context.Background(), src, &uploader.MultiPartsObjectOptions{ 208 uploader.ObjectOptions{ 209 BucketName: "testbucket", 210 ObjectName: &key, 211 FileName: "testfilename", 212 ContentType: "application/json", 213 Metadata: map[string]string{"a": "b", "c": "d"}, 214 CustomVars: map[string]string{"a": "b", "c": "d"}, 215 }, 216 4 * 1024 * 1024, 217 }) 218 if err != nil { 219 t.Fatal(err) 220 } 221 defer initializedPart.Close() 222 223 part, err := src.Slice(4 * 1024 * 1024) 224 if err != nil { 225 t.Fatal(err) 226 } 227 lastUploaded := uint64(0) 228 uploadedPart_1, err := multiPartsUploaderV2.UploadPart(context.Background(), initializedPart, part, &uploader.UploadPartOptions{ 229 OnUploadingProgress: func(progress *uploader.UploadingPartProgress) { 230 if progress.PartSize != 4*1024*1024 { 231 t.Fatalf("unexpected partSize") 232 } 233 if progress.Uploaded < lastUploaded || progress.Uploaded > progress.PartSize { 234 t.Fatalf("unexpected uploaded") 235 } 236 lastUploaded = progress.Uploaded 237 }, 238 }) 239 if err != nil { 240 t.Fatal(err) 241 } 242 243 part, err = src.Slice(4 * 1024 * 1024) 244 if err != nil { 245 t.Fatal(err) 246 } 247 lastUploaded = 0 248 uploadedPart_2, err := multiPartsUploaderV2.UploadPart(context.Background(), initializedPart, part, &uploader.UploadPartOptions{ 249 OnUploadingProgress: func(progress *uploader.UploadingPartProgress) { 250 if progress.PartSize != 1*1024*1024 { 251 t.Fatalf("unexpected partSize") 252 } 253 if progress.Uploaded < lastUploaded || progress.Uploaded > progress.PartSize { 254 t.Fatalf("unexpected uploaded") 255 } 256 lastUploaded = progress.Uploaded 257 }, 258 }) 259 if err != nil { 260 t.Fatal(err) 261 } 262 263 var returnValue struct { 264 Ok bool `json:"ok"` 265 } 266 err = multiPartsUploaderV2.CompleteParts(context.Background(), initializedPart, []uploader.UploadedPart{uploadedPart_1, uploadedPart_2}, &returnValue) 267 if err != nil { 268 t.Fatal(err) 269 } else if !returnValue.Ok { 270 t.Fatalf("unexpected response body") 271 } 272 } 273 274 func TestMultiPartsUploaderV2Resuming(t *testing.T) { 275 tmpDir, err := os.MkdirTemp("", "multi-parts-uploader-test-*") 276 if err != nil { 277 t.Fatal(err) 278 } 279 defer os.RemoveAll(tmpDir) 280 281 tmpFile, err := os.CreateTemp("", "multi-parts-uploader-test-*") 282 if err != nil { 283 t.Fatal(err) 284 } 285 defer os.Remove(tmpFile.Name()) 286 defer tmpFile.Close() 287 288 r := rand.New(rand.NewSource(time.Now().UnixNano())) 289 if _, err = io.CopyN(tmpFile, r, 5*1024*1024); err != nil { 290 t.Fatal(err) 291 } 292 if _, err = tmpFile.Seek(0, io.SeekStart); err != nil { 293 t.Fatal(err) 294 } 295 296 serveMux := mux.NewRouter() 297 serveMux.HandleFunc("/buckets/{bucketName}/objects/{encodedObjectName}/uploads/{uploadID}", func(w http.ResponseWriter, r *http.Request) { 298 if !strings.HasPrefix(r.Header.Get("Authorization"), "UpToken testak:") { 299 t.Fatalf("unexpected authorization") 300 } 301 vars := mux.Vars(r) 302 if vars["bucketName"] != "testbucket" { 303 t.Fatalf("unexpected bucket name") 304 } 305 objectBytes, err := base64.URLEncoding.DecodeString(vars["encodedObjectName"]) 306 if err != nil { 307 t.Fatal(err) 308 } else if string(objectBytes) != "testkey" { 309 t.Fatalf("unexpected object name") 310 } 311 if vars["uploadID"] != "testuploadID" { 312 t.Fatalf("unexpected upload id") 313 } 314 requestBodyBytes, err := internal_io.ReadAll(r.Body) 315 if err != nil { 316 t.Fatal(err) 317 } 318 var body apis.ResumableUploadV2CompleteMultipartUploadRequest 319 if err = body.UnmarshalJSON(requestBodyBytes); err != nil { 320 t.Fatalf("unexpected request body") 321 } 322 if len(body.Parts) != 2 { 323 t.Fatalf("unexpected parts") 324 } else if body.Parts[0].PartNumber != 1 { 325 t.Fatalf("unexpected part number") 326 } else if body.Parts[0].Etag != "testetag1" { 327 t.Fatalf("unexpected part number") 328 } else if body.Parts[1].PartNumber != 2 { 329 t.Fatalf("unexpected part number") 330 } else if body.Parts[1].Etag != "testetag2" { 331 t.Fatalf("unexpected part number") 332 } 333 if body.FileName != "testfilename" { 334 t.Fatalf("unexpected fileName") 335 } 336 if body.MimeType != "application/json" { 337 t.Fatalf("unexpected mimeType") 338 } 339 if len(body.Metadata) != 2 { 340 t.Fatalf("unexpected metadata") 341 } else if body.Metadata["x-qn-meta-a"] != "b" { 342 t.Fatalf("unexpected x-qn-meta-a") 343 } else if body.Metadata["x-qn-meta-c"] != "d" { 344 t.Fatalf("unexpected x-qn-meta-c") 345 } else if body.CustomVars["x:a"] != "b" { 346 t.Fatalf("unexpected x:a") 347 } else if body.CustomVars["x:c"] != "d" { 348 t.Fatalf("unexpected x:c") 349 } 350 w.Header().Add("X-ReqId", "fakereqid") 351 w.Write([]byte(`{"ok":true}`)) 352 }).Methods(http.MethodPost) 353 server := httptest.NewServer(serveMux) 354 defer server.Close() 355 356 tmpFileStat, err := tmpFile.Stat() 357 if err != nil { 358 t.Fatal(err) 359 } 360 tmpFileSourceID := fmt.Sprintf("%d:%d:%s", tmpFileStat.Size(), tmpFileStat.ModTime().UnixNano(), tmpFile.Name()) 361 362 resumableRecorder := resumablerecorder.NewJsonFileSystemResumableRecorder(tmpDir) 363 medium := resumableRecorder.OpenForCreatingNew(&resumablerecorder.ResumableRecorderOpenArgs{ 364 AccessKey: "testak", 365 BucketName: "testbucket", 366 ObjectName: "testkey", 367 SourceID: tmpFileSourceID, 368 PartSize: 4 * 1024 * 1024, 369 TotalSize: 5 * 1024 * 1024, 370 UpEndpoints: region.Endpoints{Preferred: []string{server.URL}}, 371 }) 372 if err = medium.Write(&resumablerecorder.ResumableRecord{ 373 UploadID: "testuploadID", 374 PartID: "testetag1", 375 Offset: 0, 376 PartSize: 4 * 1024 * 1024, 377 PartNumber: 1, 378 ExpiredAt: time.Now().Add(1 * time.Hour), 379 }); err != nil { 380 t.Fatal(err) 381 } 382 if err = medium.Write(&resumablerecorder.ResumableRecord{ 383 UploadID: "testuploadID", 384 PartID: "testetag2", 385 Offset: 4 * 1024 * 1024, 386 PartSize: 1 * 1024 * 1024, 387 PartNumber: 2, 388 ExpiredAt: time.Now().Add(1 * time.Hour), 389 }); err != nil { 390 t.Fatal(err) 391 } 392 if err = medium.Close(); err != nil { 393 t.Fatal(err) 394 } 395 396 multiPartsUploaderV2 := uploader.NewMultiPartsUploaderV2(&uploader.MultiPartsUploaderOptions{ 397 ResumableRecorder: resumableRecorder, 398 Options: http_client.Options{ 399 Regions: ®ion.Region{Up: region.Endpoints{Preferred: []string{server.URL}}}, 400 Credentials: credentials.NewCredentials("testak", "testsk"), 401 }, 402 }) 403 404 src, err := source.NewFileSource(tmpFile.Name()) 405 if err != nil { 406 t.Fatal(err) 407 } 408 key := "testkey" 409 initializedPart := multiPartsUploaderV2.TryToResume(context.Background(), src, &uploader.MultiPartsObjectOptions{ 410 uploader.ObjectOptions{ 411 BucketName: "testbucket", 412 ObjectName: &key, 413 FileName: "testfilename", 414 ContentType: "application/json", 415 Metadata: map[string]string{"a": "b", "c": "d"}, 416 CustomVars: map[string]string{"a": "b", "c": "d"}, 417 }, 418 4 * 1024 * 1024, 419 }) 420 if initializedPart == nil { 421 t.Fatalf("initializedPart is nil") 422 } 423 defer initializedPart.Close() 424 425 part, err := src.Slice(4 * 1024 * 1024) 426 if err != nil { 427 t.Fatal(err) 428 } 429 uploadedPart_1, err := multiPartsUploaderV2.UploadPart(context.Background(), initializedPart, part, &uploader.UploadPartOptions{ 430 OnUploadingProgress: func(progress *uploader.UploadingPartProgress) { 431 if progress.PartSize != 4*1024*1024 { 432 t.Fatalf("unexpected partSize") 433 } 434 if progress.Uploaded != 4*1024*1024 { 435 t.Fatalf("unexpected uploaded") 436 } 437 }, 438 }) 439 if err != nil { 440 t.Fatal(err) 441 } 442 443 part, err = src.Slice(4 * 1024 * 1024) 444 if err != nil { 445 t.Fatal(err) 446 } 447 uploadedPart_2, err := multiPartsUploaderV2.UploadPart(context.Background(), initializedPart, part, &uploader.UploadPartOptions{ 448 OnUploadingProgress: func(progress *uploader.UploadingPartProgress) { 449 if progress.PartSize != 1024*1024 { 450 t.Fatalf("unexpected partSize") 451 } 452 if progress.Uploaded != 1024*1024 { 453 t.Fatalf("unexpected uploaded") 454 } 455 }, 456 }) 457 if err != nil { 458 t.Fatal(err) 459 } 460 461 var returnValue struct { 462 Ok bool `json:"ok"` 463 } 464 err = multiPartsUploaderV2.CompleteParts(context.Background(), initializedPart, []uploader.UploadedPart{uploadedPart_1, uploadedPart_2}, &returnValue) 465 if err != nil { 466 t.Fatal(err) 467 } else if !returnValue.Ok { 468 t.Fatalf("unexpected response body") 469 } 470 }