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 }