Component setup

Start by setting up the React component with state variables for tracking upload progress and status:

  • isUploading: Boolean flag to track upload state and disable UI elements
  • progress: Number to track upload completion percentage

File input validation

Implement file input handling with proper error checking:

  • Get the file input element using getElementById
  • Extract the selected file from the input's files array
  • Validate that a file was actually selected before proceeding
  • Show user-friendly error message if no file is selected

Upload configuration

Set up the core configuration parameters for the multipart upload:

  • Base URL: Construct the API endpoint using ASSETS_PREFIX environment variable
  • File Key: Use the original filename as the storage key
  • Chunk Size: Set to 5MB for optimal performance
  • Total Parts: Calculate the number of chunks needed based on file size

Initiate upload

Begin the multipart upload by requesting an uploadId from the server:

  • Create the initiation URL with action=create parameter
  • Send POST request with file metadata (key and content type)

Store uploadId

The uploadId is a unique identifier that links all parts of the multipart upload together. Store it securely as it will be used for all subsequent API calls.

Prepare parts for upload

Set up the data structures and URL templates needed for uploading individual parts:

  • Parts Data Array: Initialize array to store part metadata (PartNumber and ETag)
  • Upload Part URL: Create URL template with required parameters:
    • action=upload-part
    • uploadId
    • key for file identification

Upload file parts

Implement the core chunking and uploading logic:

  • File Chunking: Use file.slice() to create 5MB chunks
  • Part Numbering: Assign sequential part numbers (1-based indexing)
  • Upload: Upload each part sequentially
  • Progress Tracking: Update progress bar after each successful part upload

Store part metadata

For each successfully uploaded part, store the identifying metadata in the partsData array:

  • Part Number: Sequential identifier for the part
  • ETag: Server-generated hash for integrity verification

This metadata is required to complete the multipart upload successfully.

Complete multipart upload

Finalize the upload by combining all parts into a single file:

  • Create completion URL with action=complete parameter
  • Send POST request with:
    • uploadId: Links all parts together
    • key: Final file name
    • parts: Array of part metadata

Error handling and cleanup

Handle errors and reset the state:

  • Try-Catch Block: Wrap entire upload process in error handling
  • User Feedback: Show success/error messages to the user
  • State Reset: Clean up upload state and progress in finally block

UI components

Create a clean, responsive UI for the upload functionality:

  • File Input: Standard HTML file picker
  • Upload Button: Disabled during upload with visual feedback
  • Progress Bar: Real-time visual progress indicator
  • Status Text: Percentage completion display

The UI provides immediate feedback and prevents multiple simultaneous uploads.

uploader.ts
ExpandClose
1
import { useState } from "react";
2

3
export default function SimpleMultipartUploader() {
4
const [isUploading, setIsUploading] = useState(false);
5
const [progress, setProgress] = useState(0);
6

7
// Initiate file upload
8
const uploadFile = async () => {
9
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
10
const file = fileInput?.files?.[0];
11

12
if (!file) {
13
alert("Please select a file first");
14
return;
15
}
16

17
setIsUploading(true);
18
setProgress(0);
19

20
try {
21
// Configuration
22
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
23
const key = file.name;
24
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
25
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
26

27
// Step 1: Initiate upload
28
const createUploadUrl = new URL(BASE_URL);
29
createUploadUrl.searchParams.append("action", "create");
30

31
const createResponse = await fetch(createUploadUrl, {
32
method: "POST",
33
headers: { "Content-Type": "application/json" },
34
body: JSON.stringify({ key, contentType: file.type }),
35
});
36

37
const createJson = await createResponse.json();
38
const uploadId = createJson.uploadId;
39

40
// Step 2: Upload parts
41
const partsData = [];
42
const uploadPartUrl = new URL(BASE_URL);
43
uploadPartUrl.searchParams.append("action", "upload-part");
44
uploadPartUrl.searchParams.append("uploadId", uploadId);
45
uploadPartUrl.searchParams.append("key", key);
46

47
for (let i = 0; i < totalParts; i++) {
48
const start = CHUNK_SIZE * i;
49
const end = Math.min(file.size, start + CHUNK_SIZE);
50
const blob = file.slice(start, end);
51
const partNumber = i + 1;
52

53
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
54

55
const uploadPartResponse = await fetch(uploadPartUrl, {
56
method: "PUT",
57
body: blob,
58
});
59

60
const uploadPartJson = await uploadPartResponse.json();
61
const eTag = uploadPartJson.etag;
62

63
partsData.push({ PartNumber: partNumber, ETag: eTag });
64

65
// Update progress
66
const currentProgress = ((i + 1) / totalParts) * 100;
67
setProgress(currentProgress);
68
}
69

70
// Step 3: Complete upload
71
const completeUploadUrl = new URL(BASE_URL);
72
completeUploadUrl.searchParams.append("action", "complete");
73

74
await fetch(completeUploadUrl, {
75
method: "POST",
76
headers: { "Content-Type": "application/json" },
77
body: JSON.stringify({ uploadId, key, parts: partsData }),
78
});
79

80
alert("File uploaded successfully!");
81
} catch (error) {
82
console.error("Upload failed:", error);
83
alert("Upload failed. Please try again.");
84
} finally {
85
setIsUploading(false);
86
setProgress(0);
87
}
88
};
89

90
return (
91
<div style={{ padding: "20px", maxWidth: "500px" }}>
92
<h2>Simple Multipart File Upload</h2>
93

94
<div style={{ marginBottom: "20px" }}>
95
<input
96
type="file"
97
id="fileUpload"
98
style={{ marginBottom: "10px", display: "block" }}
99
/>
100
<button
101
onClick={uploadFile}
102
disabled={isUploading}
103
style={{
104
padding: "10px 20px",
105
backgroundColor: isUploading ? "#ccc" : "#007bff",
106
color: "white",
107
border: "none",
108
borderRadius: "4px",
109
cursor: isUploading ? "not-allowed" : "pointer",
110
}}
111
>
112
{isUploading ? "Uploading..." : "Upload"}
113
</button>
114
</div>
115

116
{isUploading && (
117
<div style={{ marginTop: "20px" }}>
118
<div
119
style={{
120
width: "100%",
121
backgroundColor: "#f0f0f0",
122
borderRadius: "4px",
123
overflow: "hidden",
124
}}
125
>
126
<div
127
style={{
128
width: `${progress}%`,
129
height: "20px",
130
backgroundColor: "#007bff",
131
transition: "width 0.3s ease",
132
}}
133
/>
134
</div>
135
<p style={{ marginTop: "5px", fontSize: "14px" }}>
136
Progress: {Math.round(progress)}%
137
</p>
138
</div>
139
)}
140
</div>
141
);
142
}