_142import { useState } from "react";_142_142export 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 elementsprogress: Number to track upload completion percentage
File input validation
_142import { useState } from "react";_142_142export 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
filesarray - Validate that a file was actually selected before proceeding
- Show user-friendly error message if no file is selected
Upload configuration
_142import { useState } from "react";_142_142export 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_PREFIXenvironment 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
_142import { useState } from "react";_142_142export 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=createparameter - Send POST request with file metadata (key and content type)
Store uploadId
_142import { useState } from "react";_142_142export 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
_142import { useState } from "react";_142_142export 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 (
PartNumberandETag) - Upload Part URL: Create URL template with required parameters:
action=upload-partuploadIdkeyfor file identification
Upload file parts
_142import { useState } from "react";_142_142export 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
_142import { useState } from "react";_142_142export 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
_142import { useState } from "react";_142_142export 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=completeparameter - Send POST request with:
uploadId: Links all parts togetherkey: Final file nameparts: Array of part metadata
Error handling and cleanup
_142import { useState } from "react";_142_142export 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
finallyblock
UI components
_142import { useState } from "react";_142_142export 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 elementsprogress: 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
filesarray - 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_PREFIXenvironment 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=createparameter - 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 (
PartNumberandETag) - Upload Part URL: Create URL template with required parameters:
action=upload-partuploadIdkeyfor 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=completeparameter - Send POST request with:
uploadId: Links all parts togetherkey: Final file nameparts: 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
finallyblock
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.
_142import { useState } from "react";_142_142export 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}