({ partNumber, url }: PartUrl)
| 434 | |
| 435 | try { |
| 436 | const uploadPart = async ({ partNumber, url }: PartUrl): Promise<CompletedPart> => { |
| 437 | const start = (partNumber - 1) * CHUNK_SIZE |
| 438 | const end = Math.min(start + CHUNK_SIZE, file.size) |
| 439 | const chunk = file.slice(start, end) |
| 440 | |
| 441 | for (let attempt = 0; attempt <= MULTIPART_MAX_RETRIES; attempt++) { |
| 442 | try { |
| 443 | const partResponse = await fetch(url, { |
| 444 | method: 'PUT', |
| 445 | body: chunk, |
| 446 | signal, |
| 447 | headers: { 'Content-Type': getFileContentType(file) }, |
| 448 | }) |
| 449 | |
| 450 | if (!partResponse.ok) { |
| 451 | throw new DirectUploadError( |
| 452 | `Failed to upload part ${partNumber}: ${partResponse.statusText}`, |
| 453 | 'MULTIPART_ERROR', |
| 454 | undefined, |
| 455 | partResponse.status |
| 456 | ) |
| 457 | } |
| 458 | |
| 459 | const etag = partResponse.headers.get('ETag') || undefined |
| 460 | completedBytes[partNumber - 1] = end - start |
| 461 | reportProgress() |
| 462 | |
| 463 | return { partNumber, etag: etag?.replace(/"/g, '') } |
| 464 | } catch (partError) { |
| 465 | const isClientError = |
| 466 | partError instanceof DirectUploadError && |
| 467 | partError.status !== undefined && |
| 468 | partError.status >= 400 && |
| 469 | partError.status < 500 |
| 470 | if (isAbortError(partError) || isClientError || attempt >= MULTIPART_MAX_RETRIES) { |
| 471 | throw partError |
| 472 | } |
| 473 | const delay = MULTIPART_RETRY_DELAY_MS * MULTIPART_RETRY_BACKOFF ** attempt |
| 474 | logger.warn( |
| 475 | `Part ${partNumber} failed (attempt ${attempt + 1}), retrying in ${Math.round(delay / 1000)}s` |
| 476 | ) |
| 477 | await sleep(delay) |
| 478 | } |
| 479 | } |
| 480 | |
| 481 | throw new DirectUploadError(`Retries exhausted for part ${partNumber}`, 'MULTIPART_ERROR') |
| 482 | } |
| 483 | |
| 484 | const partResults = await runWithConcurrency( |
| 485 | presignedUrls, |
nothing calls this directly
no test coverage detected