Uploading large files to Amazon S3 can be efficiently managed using multipart uploads with presigned URLs. This approach allows you to upload file parts directly from the client-side without exposing your AWS credentials. In this article, we’ll guide you through the process of implementing S3 multipart upload using presigned URLs with Node.js on the server-side and React on the client-side.
Prerequisites
Node.js: Installed on your system.
React: A basic understanding of React and a React app set up using create-react-app.
AWS SDK for Node.js: Installed in your Node.js project.
AWS Account: Access to an AWS account with permissions to upload files to S3.
Server-Side Setup (Node.js)
First, create a new Node.js project and install the required dependencies:
mkdir s3-multipart-upload cd s3-multipart-upload npm init -y npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner express dotenv cors body-parser
Create a .env file in the root directory to store your AWS credentials and configuration:
AWS_ACCESS_KEY_ID=your-access-key-id AWS_SECRET_ACCESS_KEY=your-secret-access-key AWS_REGION=your-region S3_BUCKET_NAME=your-bucket-name
Step 1: Initialize the Server
Create a file named server.js and initialize the server:
require('dotenv').config(); const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const { S3Client, CreateMultipartUploadCommand, CompleteMultipartUploadCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const app = express(); app.use(cors()); app.use(bodyParser.json()); // Initialize s3Client const s3Client = new S3Client({ region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, }); const bucketName = process.env.S3_BUCKET_NAME; app.listen(3000, () => { console.log('Server is running on port 3000'); });
Step 2: Create Multipart Upload
Add an endpoint to create a multipart upload and get an upload ID:
const { S3Client, CreateMultipartUploadCommand, } = require('@aws-sdk/client-s3') app.post('/create-multipart-upload', async (req, res) => { const { fileName } = req.body; const params = { Bucket: bucketName, Key: fileName, }; try { const command = new CreateMultipartUploadCommand(params); const response = await s3Client.send(command); res.json({ uploadId: response.UploadId }); } catch (error) { res.status(500).json({ error: error.message }); } });
Step 3: Generate Presigned URLs for Parts
Add an endpoint to generate presigned URLs for uploading parts:
const {getSignedUrl} = require('@aws-sdk/s3-request-presigner') const { S3Client, UploadPartCommand, } = require('@aws-sdk/client-s3') app.post('/presigned-url', async (req, res) => { const { uploadId, parts, fileName } = req.body; const params = { Bucket: bucketName, Key: fileName, UploadId: uploadId, }; try { let promises = [] for (let index = 1; index <= parts; index++) { const uploadPartCommand = new UploadPartCommand({ ...params, PartNumber: index, }) promises.push(getSignedUrl(s3Client, uploadPartCommand, {expiresIn: 3600})) } const signedUrls = await Promise.all(promises) const partSignedUrlList = signedUrls.map((signedUrl) => { return signedUrl }) res.status(200).send(partSignedUrlList) } catch (error) { res.status(500).json({ error: error.message })}
Step 4: Complete Multipart Upload
Add an endpoint to complete the multipart upload:
const { S3Client, CompleteMultipartUploadCommand, } = require('@aws-sdk/client-s3') const _ = require('lodash') app.post('/complete-multipart-upload', async (req, res) => { const { uploadId, fileName, parts } = req.body; const multipartParams = { Bucket: bucketName, Key: fileName, UploadId: uploadId, MultipartUpload: { Parts: _.orderBy(parts, ['PartNumber'], ['asc']), }, } let completeMultipartUploadCommand = new CompleteMultipartUploadCommand( multipartParams, ) try { await s3Client.send(completeMultipartUploadCommand) res.status(200).json({error: error.message}) } catch (error) { res.status(500).json({error: error.message}) } });
Client-Side Setup (React)
First, create a new React project:
npx create-react-app s3-multipart-upload-client cd s3-multipart-upload-client
Install the necessary packages:
Step 1: Service to Handle Multipart Upload
Create a service to handle the multipart upload logic. In src, create a file named uploadService.js:
export async function createMultipartUpload(fileName) { const response = await axios.post(`${serverUrl}/create-multipart-upload`, { fileName }); return response.data.uploadId; }
Get Presigned urls:
export async function getPresignedUrl(uploadId, partNumber, fileName) { const response = await axios.post(`${serverUrl}/presigned-url`, { uploadId, parts, fileName, }); return response.data.partSignedUrlList; }
Complete multipart upload:
export async function completeMultipartUpload(uploadId, fileName, parts) { const response = await axios.post(`${serverUrl}/complete-multipart-upload`, { uploadId, fileName, parts, }); return response.data.response; }
Upload File:
export async function uploadFile(file, setProgress) { const fileName = file.name const uploadId = await createMultipartUpload(fileName) const partSize = 20 * 1024 * 1024 // 20 MB const totalParts = Math.ceil(file.size / partSize) const presignedUrls = await getPresignedUrl(uploadId, totalParts, fileName)const parts = [] let partNumber = 1 for (let start = 0; start < file.size; start += partSize) { const end = Math.min(start + partSize, file.size) const blob = file.slice(start, end) await axios.put(presignedUrls[partNumber - 1], blob, { headers: { 'Content-Type': file.type, }, onUploadProgress: (progressEvent) => { const progress = Math.round( (progressEvent.loaded * 100) / progressEvent.total, ) setProgress((prev) => ({ ...prev, [partNumber]: progress, })) }, }) parts.push({ ETag: (await axios.head(presignedUrls[index])).headers.etag, PartNumber: partNumber, }) partNumber++ } return completeMultipartUpload(uploadId, fileName, parts) }
Step 2: Implement Upload Component
Create a component to handle the file input and upload logic. In src, create a new file named Upload.js:
import React, { useState } from 'react'; import { uploadFile } from './uploadService'; const Upload = () => { const [progress, setProgress] = useState({}); const handleFileChange = async (event) => { const file = event.target.files[0]; if (file) { try { const response = await uploadFile(file, setProgress); console.log('Upload complete:', response); } catch (error) { console.error('Upload error:', error); } } }; return ( <div > <input type="file" onChange={handleFileChange} /> <div > {Object.keys(progress).map((partNumber) => ( <div key={partNumber}>Part {partNumber}: {progress[partNumber]}%</div > ))} </div > </div > ); }; export default Upload;
Step 3: Update App Component
Integrate the Upload component into your main application component. In src/App.js, update the file as follows:
import React from 'react'; import Upload from './Upload'; function App() { return ( <div className="App"> <h1 >S3 Multipart Upload</h1 > <Upload /> </div > ); } export default App;
Step 4: Run the Application
Finally, run both the server and the React client application:
# Start the Node.js server node server.js # In a new terminal, start the React app npm start