uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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

uploader.ts

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}

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.

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

_142
import { useState } from "react";
_142
_142
export default function SimpleMultipartUploader() {
_142
const [isUploading, setIsUploading] = useState(false);
_142
const [progress, setProgress] = useState(0);
_142
_142
// Initiate file upload
_142
const uploadFile = async () => {
_142
const fileInput = document.getElementById("fileUpload") as HTMLInputElement;
_142
const file = fileInput?.files?.[0];
_142
_142
if (!file) {
_142
alert("Please select a file first");
_142
return;
_142
}
_142
_142
setIsUploading(true);
_142
setProgress(0);
_142
_142
try {
_142
// Configuration
_142
const BASE_URL = `${import.meta.env.ASSETS_PREFIX}/api/multipart-upload`;
_142
const key = file.name;
_142
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
_142
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
_142
_142
// Step 1: Initiate upload
_142
const createUploadUrl = new URL(BASE_URL);
_142
createUploadUrl.searchParams.append("action", "create");
_142
_142
const createResponse = await fetch(createUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ key, contentType: file.type }),
_142
});
_142
_142
const createJson = await createResponse.json();
_142
const uploadId = createJson.uploadId;
_142
_142
// Step 2: Upload parts
_142
const partsData = [];
_142
const uploadPartUrl = new URL(BASE_URL);
_142
uploadPartUrl.searchParams.append("action", "upload-part");
_142
uploadPartUrl.searchParams.append("uploadId", uploadId);
_142
uploadPartUrl.searchParams.append("key", key);
_142
_142
for (let i = 0; i < totalParts; i++) {
_142
const start = CHUNK_SIZE * i;
_142
const end = Math.min(file.size, start + CHUNK_SIZE);
_142
const blob = file.slice(start, end);
_142
const partNumber = i + 1;
_142
_142
uploadPartUrl.searchParams.set("partNumber", partNumber.toString());
_142
_142
const uploadPartResponse = await fetch(uploadPartUrl, {
_142
method: "PUT",
_142
body: blob,
_142
});
_142
_142
const uploadPartJson = await uploadPartResponse.json();
_142
const eTag = uploadPartJson.etag;
_142
_142
partsData.push({ PartNumber: partNumber, ETag: eTag });
_142
_142
// Update progress
_142
const currentProgress = ((i + 1) / totalParts) * 100;
_142
setProgress(currentProgress);
_142
}
_142
_142
// Step 3: Complete upload
_142
const completeUploadUrl = new URL(BASE_URL);
_142
completeUploadUrl.searchParams.append("action", "complete");
_142
_142
await fetch(completeUploadUrl, {
_142
method: "POST",
_142
headers: { "Content-Type": "application/json" },
_142
body: JSON.stringify({ uploadId, key, parts: partsData }),
_142
});
_142
_142
alert("File uploaded successfully!");
_142
} catch (error) {
_142
console.error("Upload failed:", error);
_142
alert("Upload failed. Please try again.");
_142
} finally {
_142
setIsUploading(false);
_142
setProgress(0);
_142
}
_142
};
_142
_142
return (
_142
<div style={{ padding: "20px", maxWidth: "500px" }}>
_142
<h2>Simple Multipart File Upload</h2>
_142
_142
<div style={{ marginBottom: "20px" }}>
_142
<input
_142
type="file"
_142
id="fileUpload"
_142
style={{ marginBottom: "10px", display: "block" }}
_142
/>
_142
<button
_142
onClick={uploadFile}
_142
disabled={isUploading}
_142
style={{
_142
padding: "10px 20px",
_142
backgroundColor: isUploading ? "#ccc" : "#007bff",
_142
color: "white",
_142
border: "none",
_142
borderRadius: "4px",
_142
cursor: isUploading ? "not-allowed" : "pointer",
_142
}}
_142
>
_142
{isUploading ? "Uploading..." : "Upload"}
_142
</button>
_142
</div>
_142
_142
{isUploading && (
_142
<div style={{ marginTop: "20px" }}>
_142
<div
_142
style={{
_142
width: "100%",
_142
backgroundColor: "#f0f0f0",
_142
borderRadius: "4px",
_142
overflow: "hidden",
_142
}}
_142
>
_142
<div
_142
style={{
_142
width: `${progress}%`,
_142
height: "20px",
_142
backgroundColor: "#007bff",
_142
transition: "width 0.3s ease",
_142
}}
_142
/>
_142
</div>
_142
<p style={{ marginTop: "5px", fontSize: "14px" }}>
_142
Progress: {Math.round(progress)}%
_142
</p>
_142
</div>
_142
)}
_142
</div>
_142
);
_142
}