Introduction to File Handling
File handling is a fundamental aspect of most modern web applications. From profile pictures to document sharing, from media galleries to data imports, the ability to upload, process, and serve files is crucial for creating rich, interactive user experiences.
Today, we'll explore comprehensive strategies for handling files in full stack JavaScript applications. We'll cover everything from basic uploads to advanced cloud storage integration and media processing techniques.
Common File Handling Use Cases
- Profile Images: User avatar uploads and management
- Media Galleries: Photo and video collections
- Document Management: PDFs, spreadsheets, presentations
- Content Creation: Rich text editors with image insertion
- Data Import/Export: CSV files, data backups
- File Sharing: Collaborative workspaces, shared folders
File Upload Fundamentals
Before diving into advanced techniques, let's understand the basics of file uploads in web applications.
flowchart TD
A[User Selects File] -->|Browser| B[File Input Element]
B --> C{File Validation}
C -->|Invalid| D[Show Error]
C -->|Valid| E[Prepare for Upload]
E --> F[File Transfer to Server]
F --> G{Server Processing}
G --> H[Store File]
G --> I[Process File]
H --> J[Generate Access URL]
I --> J
J --> K[Return Response to Client]
HTML5 File Input
The foundation of file uploads is the HTML5 file input element, which allows users to select files from their device.
<!-- Basic file input -->
<input type="file" id="fileUpload" name="fileUpload">
<!-- Multiple file selection -->
<input type="file" id="multipleFiles" name="files[]" multiple>
<!-- File type filtering -->
<input type="file" id="imageUpload" accept="image/*">
<!-- Specific file types -->
<input type="file" id="documentUpload" accept=".pdf,.doc,.docx,.txt">
Drag and Drop Uploads
Modern web applications often implement drag-and-drop file uploads for a better user experience. This is achieved using the HTML5 Drag and Drop API combined with the FileReader API.
// HTML structure
<div id="dropZone" class="drop-zone">
<p>Drag & drop files here or</p>
<button id="browseBtn" class="browse-btn">Browse Files</button>
<input type="file" id="fileInput" hidden multiple>
</div>
<div id="previewContainer" class="preview-container"></div>
// JavaScript implementation
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const previewContainer = document.getElementById('previewContainer');
// Trigger file selection when the browse button is clicked
browseBtn.addEventListener('click', () => {
fileInput.click();
});
// Handle file selection via input
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
// Setup drag and drop event listeners
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Add visual feedback during drag
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.add('highlight');
});
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.remove('highlight');
});
});
// Handle dropped files
dropZone.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
handleFiles(files);
});
// Process files for preview and upload
function handleFiles(files) {
Array.from(files).forEach(file => {
// Validate file type and size
if (!validateFile(file)) return;
// Create preview for images
if (file.type.startsWith('image/')) {
createImagePreview(file);
} else {
createFilePreview(file);
}
// Prepare for upload
uploadFile(file);
});
}
function validateFile(file) {
// Example validation
const maxSize = 5 * 1024 * 1024; // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (file.size > maxSize) {
alert(`File too large: ${file.name}`);
return false;
}
if (!allowedTypes.includes(file.type)) {
alert(`File type not supported: ${file.name}`);
return false;
}
return true;
}
function createImagePreview(file) {
const reader = new FileReader();
reader.onload = (e) => {
const preview = document.createElement('div');
preview.className = 'preview-item';
preview.innerHTML = `
<img src="${e.target.result}" alt="${file.name}">
<p>${file.name}</p>
<div class="progress-bar"><div class="progress"></div></div>
`;
previewContainer.appendChild(preview);
// Store reference to progress bar
file.previewElement = preview;
};
reader.readAsDataURL(file);
}
function createFilePreview(file) {
const preview = document.createElement('div');
preview.className = 'preview-item file-preview';
preview.innerHTML = `
<div class="file-icon">${getFileIcon(file.type)}</div>
<p>${file.name}</p>
<div class="progress-bar"><div class="progress"></div></div>
`;
previewContainer.appendChild(preview);
// Store reference to progress bar
file.previewElement = preview;
}
function getFileIcon(fileType) {
// Return appropriate icon based on file type
if (fileType === 'application/pdf') {
return '<i class="far fa-file-pdf"></i>';
} else if (fileType.includes('spreadsheet') || fileType.includes('excel')) {
return '<i class="far fa-file-excel"></i>';
} else if (fileType.includes('document') || fileType.includes('word')) {
return '<i class="far fa-file-word"></i>';
}
return '<i class="far fa-file"></i>';
}
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
// Track upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
updateProgress(file, percentComplete);
}
});
xhr.onload = function() {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
showUploadSuccess(file, response);
} else {
showUploadError(file);
}
};
xhr.onerror = function() {
showUploadError(file);
};
xhr.send(formData);
}
function updateProgress(file, percent) {
const progressBar = file.previewElement.querySelector('.progress');
progressBar.style.width = `${percent}%`;
}
function showUploadSuccess(file, response) {
file.previewElement.classList.add('upload-success');
file.previewElement.dataset.fileUrl = response.url;
// Update UI to show success and provide link to file
const progressBar = file.previewElement.querySelector('.progress-bar');
progressBar.innerHTML = `<span class="success-icon">✓</span>`;
// Add copy link button
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-link-btn';
copyBtn.innerHTML = 'Copy Link';
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(response.url);
copyBtn.innerHTML = 'Copied!';
setTimeout(() => { copyBtn.innerHTML = 'Copy Link'; }, 2000);
});
file.previewElement.appendChild(copyBtn);
}
function showUploadError(file) {
file.previewElement.classList.add('upload-error');
// Update UI to show error
const progressBar = file.previewElement.querySelector('.progress-bar');
progressBar.innerHTML = `<span class="error-icon">×</span> Upload failed`;
// Add retry button
const retryBtn = document.createElement('button');
retryBtn.className = 'retry-btn';
retryBtn.innerHTML = 'Retry';
retryBtn.addEventListener('click', () => {
// Reset progress bar
progressBar.innerHTML = `<div class="progress"></div>`;
file.previewElement.classList.remove('upload-error');
// Retry upload
uploadFile(file);
});
file.previewElement.appendChild(retryBtn);
}
Form-based Uploads
For simple use cases, HTML forms with multipart/form-data encoding can be used to upload files.
<!-- HTML Form -->
<form action="/api/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="fileUpload">
<button type="submit">Upload</button>
</form>
Server-side File Handling with Express
In Node.js applications, the most common library for handling file uploads is Multer, which is a middleware for Express designed to handle multipart/form-data.
Basic Multer Setup
// server.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
// Define storage configuration
const storage = multer.diskStorage({
destination: function(req, file, cb) {
const uploadDir = 'uploads/';
// Create directory if it doesn't exist
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function(req, file, cb) {
// Create unique filename: timestamp + original name
const uniqueFileName = Date.now() + '-' + file.originalname.replace(/\s+/g, '-');
cb(null, uniqueFileName);
}
});
// Create multer instance with storage configuration
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: function(req, file, cb) {
// Validate file types
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid file type. Only JPEG, PNG and PDF are allowed.'), false);
}
cb(null, true);
}
});
// Single file upload route
app.post('/api/upload', upload.single('file'), (req, res) => {
try {
// req.file contains information about the uploaded file
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Generate URL for accessing the file
const fileUrl = `/files/${req.file.filename}`;
res.status(200).json({
success: true,
file: {
name: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
url: fileUrl
}
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
// Multiple file upload route
app.post('/api/upload-multiple', upload.array('files', 5), (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
// Process all uploaded files
const uploadedFiles = req.files.map(file => ({
name: file.originalname,
size: file.size,
mimetype: file.mimetype,
url: `/files/${file.filename}`
}));
res.status(200).json({
success: true,
files: uploadedFiles
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
// Serve uploaded files
app.use('/files', express.static('uploads'));
// Handle Multer errors
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// A Multer error occurred during upload
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File size exceeds the limit (10MB)' });
}
return res.status(400).json({ error: err.message });
} else if (err) {
// A general error occurred
return res.status(500).json({ error: err.message });
}
next();
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Security Considerations
flowchart TD
A[File Upload] --> B{Security Checks}
B --> C[File Type Validation]
B --> D[Size Limiting]
B --> E[Virus Scanning]
B --> F[Filename Sanitization]
B --> G[Content Inspection]
B --> H[Storage Isolation]
C & D & E & F & G & H --> I[Storage Processing]
When handling file uploads, security is paramount. Here are key considerations:
- Validate file types: Always check MIME types and file extensions
- Limit file sizes: Prevent denial of service via huge file uploads
- Sanitize filenames: Avoid path traversal attacks and ensure safe filenames
- Store files outside web root: Prevent direct access to uploaded files
- Scan for malware: Consider implementing virus scanning for uploaded files
- Use CDNs or dedicated services: Offload file storage to specialized services
- Implement proper access controls: Ensure only authorized users can access files
Advanced Multer Configuration with Security Features
// Advanced multer configuration with enhanced security
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const multer = require('multer');
const { lookup } = require('mime-types');
// Create a secure upload handling middleware
function createSecureUploadMiddleware(options = {}) {
const {
destination = 'uploads/',
allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'],
maxFileSize = 10 * 1024 * 1024, // 10MB
maxFiles = 5,
storageStrategy = 'disk' // 'disk', 'memory', or 'temp'
} = options;
// Ensure upload directory exists and is secure
if (storageStrategy === 'disk') {
// Create directory if it doesn't exist
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination, { recursive: true, mode: 0o750 }); // rwxr-x---
}
// Check if directory is writable
try {
fs.accessSync(destination, fs.constants.W_OK);
} catch (error) {
throw new Error(`Upload directory ${destination} is not writable`);
}
}
// Configure storage based on chosen strategy
let storage;
if (storageStrategy === 'disk') {
storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, destination);
},
filename: function(req, file, cb) {
// Create secure filename with random hash
crypto.randomBytes(16, (err, buf) => {
if (err) return cb(err);
// Get file extension from mimetype
const ext = getSecureExtension(file.mimetype);
// Generate unique filename with timestamp, hash, and extension
const filename = `${Date.now()}-${buf.toString('hex')}${ext}`;
cb(null, filename);
});
}
});
} else if (storageStrategy === 'memory') {
storage = multer.memoryStorage();
} else if (storageStrategy === 'temp') {
// Use disk storage but with temp directory
const os = require('os');
const tempDir = path.join(os.tmpdir(), 'app-uploads');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, tempDir);
},
filename: function(req, file, cb) {
crypto.randomBytes(16, (err, buf) => {
if (err) return cb(err);
const ext = getSecureExtension(file.mimetype);
const filename = `${Date.now()}-${buf.toString('hex')}${ext}`;
cb(null, filename);
});
}
});
}
// Create multer instance with configured storage
const upload = multer({
storage: storage,
limits: {
fileSize: maxFileSize,
files: maxFiles
},
fileFilter: function(req, file, cb) {
// Validate file type using both mimetype and extension
if (!validateFileType(file, allowedTypes)) {
return cb(new Error(`Invalid file type. Allowed types: ${allowedTypes.join(', ')}`), false);
}
cb(null, true);
}
});
return upload;
}
// Helper function to get secure file extension
function getSecureExtension(mimetype) {
// Map known safe types to their extensions
const typeMap = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'image/webp': '.webp',
'application/pdf': '.pdf',
'text/plain': '.txt',
'text/csv': '.csv',
'application/msword': '.doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx'
// Add more as needed
};
return typeMap[mimetype] || '';
}
// Helper function to validate file type
function validateFileType(file, allowedTypes) {
// Check if mimetype is in allowed list
if (!allowedTypes.includes(file.mimetype)) {
return false;
}
// Double-check extension matches the mimetype
const originalExt = path.extname(file.originalname).toLowerCase();
const expectedExt = getSecureExtension(file.mimetype);
if (expectedExt && originalExt) {
// If we have both, they should match (accounting for .jpg vs .jpeg)
if (originalExt === '.jpeg' && expectedExt === '.jpg') {
return true;
}
if (originalExt !== expectedExt) {
return false; // Extension doesn't match mimetype
}
}
return true;
}
// Usage example
const imageUpload = createSecureUploadMiddleware({
destination: 'uploads/images/',
allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
maxFileSize: 5 * 1024 * 1024 // 5MB
});
const documentUpload = createSecureUploadMiddleware({
destination: 'uploads/documents/',
allowedTypes: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
maxFileSize: 20 * 1024 * 1024 // 20MB
});
// Routes
app.post('/api/upload/images', imageUpload.array('images', 10), handleFileUpload);
app.post('/api/upload/documents', documentUpload.array('documents', 5), handleFileUpload);
// Generic handler for file uploads
function handleFileUpload(req, res) {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
// Process files based on upload strategy
let uploadedFiles;
if (req.files[0].buffer) {
// Memory storage - files are in buffer
uploadedFiles = req.files.map(file => ({
name: file.originalname,
size: file.size,
mimetype: file.mimetype,
buffer: file.buffer // Note: You'd typically process this further
}));
} else {
// Disk storage - files are saved to disk
uploadedFiles = req.files.map(file => ({
name: file.originalname,
size: file.size,
mimetype: file.mimetype,
path: file.path,
url: `/files/${path.basename(file.path)}`
}));
}
res.status(200).json({
success: true,
files: uploadedFiles
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
}
Cloud Storage Integration
For production applications, storing files on the same server as your application can lead to scaling issues, storage limitations, and potential security risks. Cloud storage services like AWS S3, Google Cloud Storage, or Azure Blob Storage offer robust, scalable alternatives.
flowchart LR
A[Client] -->|Upload File| B[Application Server]
B -->|Process & Validate| C{Transformation Needed?}
C -->|Yes| D[Process Image/File]
C -->|No| E[Direct Upload]
D --> F[Upload to Cloud]
E --> F
F --> G[Generate Signed URL]
G --> H[Save Reference in Database]
H --> I[Return URL to Client]
AWS S3 Integration
Amazon S3 (Simple Storage Service) is one of the most popular cloud storage solutions. Here's how to integrate it with a Node.js application:
// Install required packages
// npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer
const express = require('express');
const multer = require('multer');
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const crypto = require('crypto');
const path = require('path');
const app = express();
// Configure multer to use memory storage for temporary handling
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024 // 10MB
},
fileFilter: (req, file, cb) => {
// Validate file types
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid file type. Only JPEG, PNG and PDF are allowed.'), false);
}
cb(null, true);
}
});
// Configure AWS S3 client
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
// S3 bucket name
const bucketName = process.env.AWS_S3_BUCKET;
// Generate unique file name
function generateUniqueFileName(originalname) {
const timestamp = Date.now();
const randomString = crypto.randomBytes(8).toString('hex');
const extension = path.extname(originalname);
return `${timestamp}-${randomString}${extension}`;
}
// Upload file to S3
async function uploadToS3(fileBuffer, fileName, fileType) {
const params = {
Bucket: bucketName,
Key: fileName,
Body: fileBuffer,
ContentType: fileType
};
const command = new PutObjectCommand(params);
try {
const response = await s3Client.send(command);
return fileName; // Return the file name for database storage
} catch (error) {
console.error('S3 upload error:', error);
throw error;
}
}
// Generate signed URL for accessing the file
async function getSignedFileUrl(fileName) {
const params = {
Bucket: bucketName,
Key: fileName
};
const command = new GetObjectCommand(params);
try {
// URL expires in 1 hour
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return signedUrl;
} catch (error) {
console.error('Error generating signed URL:', error);
throw error;
}
}
// Upload route
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Generate unique file name
const fileName = generateUniqueFileName(req.file.originalname);
// Upload to S3
await uploadToS3(req.file.buffer, fileName, req.file.mimetype);
// Generate signed URL for client access
const fileUrl = await getSignedFileUrl(fileName);
// In a real application, you would save file metadata to your database here
// const fileRecord = await db.files.create({
// originalName: req.file.originalname,
// fileName: fileName,
// fileType: req.file.mimetype,
// fileSize: req.file.size,
// userId: req.user.id,
// s3Key: fileName
// });
res.status(200).json({
success: true,
file: {
name: req.file.originalname,
size: req.file.size,
type: req.file.mimetype,
url: fileUrl,
key: fileName // Store this in your database
}
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
// Generate a new signed URL for a file (useful when URLs expire)
app.get('/api/files/:fileName/url', async (req, res) => {
try {
const { fileName } = req.params;
// In a real application, verify user has access to this file
// const fileRecord = await db.files.findOne({
// where: { fileName, userId: req.user.id }
// });
//
// if (!fileRecord) {
// return res.status(404).json({ error: 'File not found' });
// }
const fileUrl = await getSignedFileUrl(fileName);
res.status(200).json({
success: true,
url: fileUrl
});
} catch (error) {
console.error('Error generating URL:', error);
res.status(500).json({ error: 'Failed to generate file URL' });
}
});
// Start server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Direct-to-S3 Uploads
For improved performance, especially with large files, you can implement direct-to-S3 uploads. This approach generates pre-signed URLs that allow clients to upload directly to S3, bypassing your server for the file transfer.
// Server-side code for generating pre-signed URLs
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
// Configure S3 client
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
// Route to generate pre-signed URL for direct upload
app.post('/api/get-upload-url', async (req, res) => {
try {
const { fileName, fileType } = req.body;
if (!fileName || !fileType) {
return res.status(400).json({ error: 'fileName and fileType are required' });
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(fileType)) {
return res.status(400).json({ error: 'Invalid file type' });
}
// Generate unique key
const key = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}-${fileName}`;
// Create pre-signed URL
const putObjectParams = {
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
ContentType: fileType
};
const command = new PutObjectCommand(putObjectParams);
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
res.json({
success: true,
uploadUrl,
key
});
} catch (error) {
console.error('Error generating upload URL:', error);
res.status(500).json({ error: 'Failed to generate upload URL' });
}
});
// Frontend JavaScript for direct S3 upload
async function uploadFileToS3(file) {
try {
// First, get a pre-signed URL from our server
const urlResponse = await fetch('/api/get-upload-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: file.name,
fileType: file.type
})
});
const { uploadUrl, key } = await urlResponse.json();
// Now upload directly to S3 using the pre-signed URL
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
},
body: file
});
if (uploadResponse.ok) {
// Notify our server of the successful upload
await fetch('/api/complete-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
key,
fileName: file.name,
fileSize: file.size,
fileType: file.type
})
});
return {
success: true,
key,
fileName: file.name
};
} else {
throw new Error('Upload failed');
}
} catch (error) {
console.error('Error uploading to S3:', error);
throw error;
}
}
// Example usage in a web app
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadFileToS3(file);
console.log('Upload successful:', result);
} catch (error) {
console.error('Upload failed:', error);
}
});
Google Cloud Storage Integration
Google Cloud Storage offers features similar to AWS S3 and can be easily integrated with Node.js applications.
// Install required packages
// npm install @google-cloud/storage multer
const express = require('express');
const multer = require('multer');
const { Storage } = require('@google-cloud/storage');
const crypto = require('crypto');
const path = require('path');
const app = express();
// Configure multer to use memory storage
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024 // 10MB
},
fileFilter: (req, file, cb) => {
// Validate file types
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid file type. Only JPEG, PNG and PDF are allowed.'), false);
}
cb(null, true);
}
});
// Configure Google Cloud Storage
const storage = new Storage({
projectId: process.env.GCP_PROJECT_ID,
keyFilename: process.env.GCP_KEY_FILE
});
const bucketName = process.env.GCP_BUCKET_NAME;
const bucket = storage.bucket(bucketName);
// Generate unique file name
function generateUniqueFileName(originalname) {
const timestamp = Date.now();
const randomString = crypto.randomBytes(8).toString('hex');
const extension = path.extname(originalname);
return `${timestamp}-${randomString}${extension}`;
}
// Upload file to Google Cloud Storage
async function uploadToGCS(fileBuffer, fileName, fileType) {
const file = bucket.file(fileName);
// Set appropriate metadata
const metadata = {
contentType: fileType
};
try {
// Upload file to GCS
await file.save(fileBuffer, {
metadata: metadata
});
return fileName;
} catch (error) {
console.error('GCS upload error:', error);
throw error;
}
}
// Generate signed URL for accessing the file
async function getSignedFileUrl(fileName) {
try {
const options = {
version: 'v4',
action: 'read',
expires: Date.now() + 1000 * 60 * 60 // 1 hour
};
const [url] = await bucket.file(fileName).getSignedUrl(options);
return url;
} catch (error) {
console.error('Error generating signed URL:', error);
throw error;
}
}
// Upload route
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Generate unique file name
const fileName = generateUniqueFileName(req.file.originalname);
// Upload to Google Cloud Storage
await uploadToGCS(req.file.buffer, fileName, req.file.mimetype);
// Generate signed URL for client access
const fileUrl = await getSignedFileUrl(fileName);
res.status(200).json({
success: true,
file: {
name: req.file.originalname,
size: req.file.size,
type: req.file.mimetype,
url: fileUrl,
key: fileName // Store this in your database
}
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
// Start server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Image Processing with Sharp
In web applications, image processing is a common requirement. Tasks like resizing images, generating thumbnails, applying watermarks, or converting between formats are often necessary. Sharp is a high-performance Node.js image processing library that makes these tasks straightforward.
Basic Sharp Integration
// Install required packages
// npm install sharp multer @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const crypto = require('crypto');
const path = require('path');
const app = express();
// Configure multer for memory storage
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 20 * 1024 * 1024 // 20MB limit
},
fileFilter: (req, file, cb) => {
// Only allow image uploads
if (!file.mimetype.startsWith('image/')) {
return cb(new Error('Only images are allowed'), false);
}
// For image processing, we'll accept these formats
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid image type. Only JPEG, PNG, WebP and GIF are allowed.'), false);
}
cb(null, true);
}
});
// Configure AWS S3 client
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
const bucketName = process.env.AWS_S3_BUCKET;
// Generate unique file name
function generateUniqueFileName(baseName, suffix = '', extension = '.jpg') {
const timestamp = Date.now();
const randomString = crypto.randomBytes(8).toString('hex');
return `${baseName}-${timestamp}-${randomString}${suffix}${extension}`;
}
// Upload buffer to S3
async function uploadToS3(buffer, fileName, contentType) {
const params = {
Bucket: bucketName,
Key: fileName,
Body: buffer,
ContentType: contentType
};
const command = new PutObjectCommand(params);
try {
await s3Client.send(command);
return fileName;
} catch (error) {
console.error('S3 upload error:', error);
throw error;
}
}
// Generate signed URL for S3 object
async function getSignedFileUrl(fileName) {
const params = {
Bucket: bucketName,
Key: fileName
};
const command = new GetObjectCommand(params);
try {
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return signedUrl;
} catch (error) {
console.error('Error generating signed URL:', error);
throw error;
}
}
// Process image using Sharp and upload to S3
async function processAndUploadImage(buffer, originalName, options = {}) {
const {
width,
height,
fit = 'cover',
position = 'centre',
withoutEnlargement = true,
quality = 80,
format = 'jpeg',
generateThumbnail = true,
thumbnailSize = 200,
watermark = null
} = options;
// Create a base name for the file
const baseName = path.parse(originalName).name.replace(/\s+/g, '-').toLowerCase();
// Create Sharp instance
let imageProcessor = sharp(buffer);
// Get image metadata
const metadata = await imageProcessor.metadata();
// Initialize results object
const results = {
original: {
width: metadata.width,
height: metadata.height,
format: metadata.format
},
processed: {},
thumbnail: null
};
// Resize main image if dimensions provided
if (width || height) {
imageProcessor = imageProcessor.resize({
width,
height,
fit,
position,
withoutEnlargement
});
results.processed.width = width || Math.round(metadata.width * (height / metadata.height));
results.processed.height = height || Math.round(metadata.height * (width / metadata.width));
}
// Add watermark if provided
if (watermark && watermark.path) {
// Overlay watermark image
imageProcessor = imageProcessor.composite([
{
input: watermark.path,
gravity: watermark.gravity || 'southeast',
opacity: watermark.opacity || 0.5
}
]);
}
// Convert to desired format
const outputFormat = format.toLowerCase();
switch (outputFormat) {
case 'jpeg':
case 'jpg':
imageProcessor = imageProcessor.jpeg({ quality });
results.processed.format = 'jpeg';
results.processed.contentType = 'image/jpeg';
break;
case 'png':
imageProcessor = imageProcessor.png({ quality });
results.processed.format = 'png';
results.processed.contentType = 'image/png';
break;
case 'webp':
imageProcessor = imageProcessor.webp({ quality });
results.processed.format = 'webp';
results.processed.contentType = 'image/webp';
break;
case 'avif':
imageProcessor = imageProcessor.avif({ quality });
results.processed.format = 'avif';
results.processed.contentType = 'image/avif';
break;
default:
imageProcessor = imageProcessor.jpeg({ quality });
results.processed.format = 'jpeg';
results.processed.contentType = 'image/jpeg';
}
// Process the main image
const processedBuffer = await imageProcessor.toBuffer();
// Upload processed image to S3
const extension = `.${results.processed.format}`;
const fileName = generateUniqueFileName(baseName, '', extension);
await uploadToS3(processedBuffer, fileName, results.processed.contentType);
results.processed.key = fileName;
results.processed.url = await getSignedFileUrl(fileName);
// Generate and upload thumbnail if requested
if (generateThumbnail) {
const thumbnailProcessor = sharp(buffer)
.resize({
width: thumbnailSize,
height: thumbnailSize,
fit: 'cover',
position
});
// Use same format as main image
switch (outputFormat) {
case 'jpeg':
case 'jpg':
thumbnailProcessor.jpeg({ quality });
break;
case 'png':
thumbnailProcessor.png({ quality });
break;
case 'webp':
thumbnailProcessor.webp({ quality });
break;
case 'avif':
thumbnailProcessor.avif({ quality });
break;
default:
thumbnailProcessor.jpeg({ quality });
}
const thumbnailBuffer = await thumbnailProcessor.toBuffer();
// Upload thumbnail to S3
const thumbnailFileName = generateUniqueFileName(baseName, '-thumb', extension);
await uploadToS3(thumbnailBuffer, thumbnailFileName, results.processed.contentType);
results.thumbnail = {
width: thumbnailSize,
height: thumbnailSize,
format: results.processed.format,
contentType: results.processed.contentType,
key: thumbnailFileName,
url: await getSignedFileUrl(thumbnailFileName)
};
}
return results;
}
// Upload and process image route
app.post('/api/upload/image', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
// Extract processing options from request
const {
width,
height,
format,
quality,
generateThumbnail
} = req.body;
// Process image with Sharp and upload to S3
const processingOptions = {
width: width ? parseInt(width) : null,
height: height ? parseInt(height) : null,
format: format || 'jpeg',
quality: quality ? parseInt(quality) : 80,
generateThumbnail: generateThumbnail !== 'false'
};
const results = await processAndUploadImage(
req.file.buffer,
req.file.originalname,
processingOptions
);
// Return results to client
res.status(200).json({
success: true,
originalName: req.file.originalname,
originalSize: req.file.size,
processed: results.processed,
thumbnail: results.thumbnail
});
} catch (error) {
console.error('Image processing error:', error);
res.status(500).json({ error: 'Image processing failed' });
}
});
// Example route for image manipulation with watermark
app.post('/api/images/watermark', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
// Create a watermark using Sharp
const watermarkBuffer = await sharp({
create: {
width: 300,
height: 100,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
})
.composite([{
input: {
text: {
text: 'WATERMARK',
font: 'Arial',
fontSize: 48,
rgba: true
}
}
}])
.png()
.toBuffer();
// Create temporary file for watermark
const os = require('os');
const fs = require('fs');
const watermarkPath = path.join(os.tmpdir(), 'watermark.png');
fs.writeFileSync(watermarkPath, watermarkBuffer);
// Process image with watermark
const results = await processAndUploadImage(
req.file.buffer,
req.file.originalname,
{
width: req.body.width ? parseInt(req.body.width) : null,
height: req.body.height ? parseInt(req.body.height) : null,
watermark: {
path: watermarkPath,
gravity: 'southeast',
opacity: 0.7
}
}
);
// Clean up temporary watermark file
fs.unlinkSync(watermarkPath);
// Return results
res.status(200).json({
success: true,
result: results
});
} catch (error) {
console.error('Watermark error:', error);
res.status(500).json({ error: 'Watermark processing failed' });
}
});
// Example route for image format conversion
app.post('/api/images/convert', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
const { format = 'webp', quality = 80 } = req.body;
// Validate format
const validFormats = ['jpeg', 'jpg', 'png', 'webp', 'avif'];
if (!validFormats.includes(format.toLowerCase())) {
return res.status(400).json({ error: 'Invalid format. Supported formats: jpeg, png, webp, avif' });
}
// Process the image
const results = await processAndUploadImage(
req.file.buffer,
req.file.originalname,
{
format: format,
quality: parseInt(quality)
}
);
res.status(200).json({
success: true,
result: results
});
} catch (error) {
console.error('Format conversion error:', error);
res.status(500).json({ error: 'Format conversion failed' });
}
});
// Start server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Advanced Image Processing Features
Sharp supports a wide range of image processing operations beyond basic resizing and format conversion.
// Example of advanced image processing functions with Sharp
// Function for adding text to images
async function addTextToImage(imageBuffer, text, options = {}) {
const {
fontSize = 24,
fontColor = { r: 255, g: 255, b: 255, alpha: 1 },
backgroundColor = { r: 0, g: 0, b: 0, alpha: 0.5 },
position = 'south',
padding = 10
} = options;
// Create text overlay using SVG
const textSvg = Buffer.from(`
`);
// Get image metadata
const metadata = await sharp(imageBuffer).metadata();
// Determine position coordinates
const svgWidth = metadata.width;
const svgHeight = Math.min(metadata.height / 4, fontSize * 2 + padding * 2);
let overlayOptions = {};
switch (position) {
case 'north':
overlayOptions = { top: 0, left: 0 };
break;
case 'south':
overlayOptions = { bottom: 0, left: 0 };
break;
case 'east':
overlayOptions = { right: 0, top: 0 };
break;
case 'west':
overlayOptions = { left: 0, top: 0 };
break;
default:
overlayOptions = { bottom: 0, left: 0 };
}
// Apply the text overlay
return sharp(imageBuffer)
.composite([
{
input: {
create: {
width: svgWidth,
height: svgHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
},
...overlayOptions
},
{
input: textSvg,
...overlayOptions
}
])
.toBuffer();
}
// Function for applying filters to images
async function applyImageFilter(imageBuffer, filter) {
let processor = sharp(imageBuffer);
switch (filter) {
case 'grayscale':
processor = processor.grayscale();
break;
case 'sepia':
// Sepia effect using color manipulation
processor = processor
.modulate({
brightness: 1,
saturation: 0.5,
hue: 40
})
.tint({ r: 112, g: 66, b: 20 });
break;
case 'vintage':
processor = processor
.modulate({
brightness: 1.1,
saturation: 0.8,
hue: 15
})
.tint({ r: 255, g: 210, b: 170 });
break;
case 'sharpen':
processor = processor.sharpen({
sigma: 1.5,
m1: 1,
m2: 1.5,
x1: 0.5,
y2: 0.5,
y3: 0.7
});
break;
case 'blur':
processor = processor.blur(8);
break;
case 'negative':
processor = processor.negate();
break;
default:
// No filter applied
break;
}
return processor.toBuffer();
}
// Function for creating image collages
async function createImageCollage(imageBuffers, options = {}) {
const {
columns = 2,
margin = 10,
background = { r: 255, g: 255, b: 255, alpha: 1 }
} = options;
if (!imageBuffers || imageBuffers.length === 0) {
throw new Error('No images provided for collage');
}
// Process all images to the same height
const processedImages = await Promise.all(
imageBuffers.map(async (buffer) => {
const metadata = await sharp(buffer).metadata();
// Resize to uniform size
return {
buffer: await sharp(buffer)
.resize({
height: 300,
fit: 'cover',
position: 'centre'
})
.toBuffer(),
metadata: await sharp(buffer)
.resize({
height: 300,
fit: 'cover',
position: 'centre'
})
.metadata()
};
})
);
// Calculate rows needed
const rows = Math.ceil(processedImages.length / columns);
// Calculate canvas size
const imageWidth = processedImages[0].metadata.width;
const imageHeight = processedImages[0].metadata.height;
const canvasWidth = (imageWidth * columns) + (margin * (columns + 1));
const canvasHeight = (imageHeight * rows) + (margin * (rows + 1));
// Create composite array for sharp
const composites = processedImages.map((img, index) => {
const row = Math.floor(index / columns);
const col = index % columns;
const left = margin + (col * (imageWidth + margin));
const top = margin + (row * (imageHeight + margin));
return {
input: img.buffer,
top,
left
};
});
// Create collage
return sharp({
create: {
width: canvasWidth,
height: canvasHeight,
channels: 4,
background
}
})
.composite(composites)
.toBuffer();
}
// API routes for these functions
app.post('/api/images/text', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
const { text, fontSize, position } = req.body;
if (!text) {
return res.status(400).json({ error: 'Text content is required' });
}
// Add text to image
const processedBuffer = await addTextToImage(req.file.buffer, text, {
fontSize: fontSize ? parseInt(fontSize) : 24,
position: position || 'south'
});
// Save to S3 or process further as needed
const fileName = generateUniqueFileName('text-image', '', '.jpg');
await uploadToS3(processedBuffer, fileName, 'image/jpeg');
const url = await getSignedFileUrl(fileName);
res.status(200).json({
success: true,
key: fileName,
url
});
} catch (error) {
console.error('Text overlay error:', error);
res.status(500).json({ error: 'Text overlay failed' });
}
});
app.post('/api/images/filter', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
const { filter } = req.body;
if (!filter) {
return res.status(400).json({ error: 'Filter type is required' });
}
// Apply filter to image
const processedBuffer = await applyImageFilter(req.file.buffer, filter);
// Save to S3 or process further as needed
const fileName = generateUniqueFileName('filtered-image', `-${filter}`, '.jpg');
await uploadToS3(processedBuffer, fileName, 'image/jpeg');
const url = await getSignedFileUrl(fileName);
res.status(200).json({
success: true,
filter,
key: fileName,
url
});
} catch (error) {
console.error('Filter application error:', error);
res.status(500).json({ error: 'Filter application failed' });
}
});
app.post('/api/images/collage', upload.array('images', 9), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No images uploaded' });
}
if (req.files.length < 2) {
return res.status(400).json({ error: 'At least 2 images are required for a collage' });
}
const { columns } = req.body;
// Create a collage from the uploaded images
const imageBuffers = req.files.map(file => file.buffer);
const collageBuffer = await createImageCollage(imageBuffers, {
columns: columns ? parseInt(columns) : 2
});
// Save to S3
const fileName = generateUniqueFileName('collage', '', '.jpg');
await uploadToS3(collageBuffer, fileName, 'image/jpeg');
const url = await getSignedFileUrl(fileName);
res.status(200).json({
success: true,
key: fileName,
url,
imageCount: req.files.length
});
} catch (error) {
console.error('Collage creation error:', error);
res.status(500).json({ error: 'Collage creation failed' });
}
});
Advanced File Handling Patterns
Chunked File Uploads
When dealing with very large files, it's often better to split them into smaller chunks for upload. This approach has several advantages:
- Improved reliability for large file uploads
- Ability to resume interrupted uploads
- Better progress tracking
- Reduced memory usage on both client and server
sequenceDiagram
participant Client
participant Server
participant Storage
Client->>Client: Split file into chunks
Client->>Server: Upload chunk 1
Server->>Storage: Store chunk 1
Server->>Client: Acknowledge chunk 1
Client->>Server: Upload chunk 2
Server->>Storage: Store chunk 2
Server->>Client: Acknowledge chunk 2
Client->>Server: Upload chunk N
Server->>Storage: Store chunk N
Server->>Client: Acknowledge chunk N
Client->>Server: Request file completion
Server->>Storage: Merge chunks
Storage->>Server: Return final file
Server->>Client: Upload complete
Client-side Implementation
// Client-side chunked upload implementation
class ChunkedUploader {
constructor(file, options = {}) {
this.file = file;
this.options = {
chunkSize: 1024 * 1024 * 5, // 5MB chunks
retries: 3,
retryDelay: 1000,
concurrentUploads: 3,
onProgress: null,
onComplete: null,
onError: null,
...options
};
this.uploadId = null;
this.chunks = this.prepareChunks();
this.uploadedChunks = [];
this.activeUploads = 0;
this.totalChunks = this.chunks.length;
this.uploadedBytes = 0;
this.status = 'ready'; // ready, uploading, paused, completed, error
}
prepareChunks() {
const chunks = [];
let start = 0;
let end = this.options.chunkSize;
while (start < this.file.size) {
chunks.push({
index: chunks.length,
start,
end: Math.min(end, this.file.size),
status: 'pending', // pending, uploading, completed, error
retries: 0
});
start = end;
end = start + this.options.chunkSize;
}
return chunks;
}
async start() {
if (this.status === 'uploading') {
return;
}
this.status = 'uploading';
try {
// Initialize upload session
await this.initializeUpload();
// Start uploading chunks concurrently
this.processNextChunks();
} catch (error) {
this.handleError(error);
}
}
async initializeUpload() {
try {
const response = await fetch('/api/uploads/chunked/initialize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: this.file.name,
fileSize: this.file.size,
fileType: this.file.type,
totalChunks: this.totalChunks
})
});
if (!response.ok) {
throw new Error(`Failed to initialize upload: ${response.statusText}`);
}
const data = await response.json();
this.uploadId = data.uploadId;
} catch (error) {
console.error('Failed to initialize upload:', error);
throw error;
}
}
processNextChunks() {
// Find pending chunks to upload
const pendingChunks = this.chunks.filter(chunk => chunk.status === 'pending');
if (pendingChunks.length === 0 && this.activeUploads === 0) {
// All chunks uploaded, complete the upload
this.completeUpload();
return;
}
// Calculate how many new uploads to start
const availableSlots = Math.max(0, this.options.concurrentUploads - this.activeUploads);
const chunksToUpload = pendingChunks.slice(0, availableSlots);
// Start new uploads
chunksToUpload.forEach(chunk => {
this.uploadChunk(chunk);
});
}
async uploadChunk(chunk) {
// Mark chunk as uploading
chunk.status = 'uploading';
this.activeUploads++;
try {
// Get chunk data as Blob
const chunkData = this.file.slice(chunk.start, chunk.end);
// Create FormData with chunk data
const formData = new FormData();
formData.append('uploadId', this.uploadId);
formData.append('chunkIndex', chunk.index);
formData.append('totalChunks', this.totalChunks);
formData.append('chunkData', chunkData);
// Upload chunk
const response = await fetch('/api/uploads/chunked/chunk', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Failed to upload chunk ${chunk.index}: ${response.statusText}`);
}
// Mark chunk as completed
chunk.status = 'completed';
this.uploadedChunks.push(chunk.index);
this.uploadedBytes += (chunk.end - chunk.start);
// Report progress
if (this.options.onProgress) {
const percentage = (this.uploadedBytes / this.file.size) * 100;
this.options.onProgress(percentage, this.uploadedBytes, this.file.size);
}
} catch (error) {
console.error(`Chunk ${chunk.index} upload error:`, error);
// Handle retry logic
if (chunk.retries < this.options.retries) {
chunk.retries++;
chunk.status = 'pending';
// Delay retry
setTimeout(() => {
// Only process next chunks if we're still uploading
if (this.status === 'uploading') {
this.processNextChunks();
}
}, this.options.retryDelay);
} else {
chunk.status = 'error';
this.handleError(error);
}
} finally {
this.activeUploads--;
// Only process next chunks if we're still uploading
if (this.status === 'uploading') {
this.processNextChunks();
}
}
}
async completeUpload() {
try {
const response = await fetch('/api/uploads/chunked/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uploadId: this.uploadId
})
});
if (!response.ok) {
throw new Error(`Failed to complete upload: ${response.statusText}`);
}
const data = await response.json();
this.status = 'completed';
if (this.options.onComplete) {
this.options.onComplete(data);
}
} catch (error) {
console.error('Failed to complete upload:', error);
this.handleError(error);
}
}
pause() {
if (this.status === 'uploading') {
this.status = 'paused';
}
}
resume() {
if (this.status === 'paused') {
this.status = 'uploading';
this.processNextChunks();
}
}
cancel() {
this.status = 'cancelled';
// Notify server to clean up
fetch(`/api/uploads/chunked/cancel/${this.uploadId}`, {
method: 'POST'
}).catch(error => {
console.error('Failed to cancel upload on server:', error);
});
}
handleError(error) {
this.status = 'error';
if (this.options.onError) {
this.options.onError(error);
}
}
}
// Example usage
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const progressBar = document.getElementById('progressBar');
const uploadBtn = document.getElementById('uploadBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const cancelBtn = document.getElementById('cancelBtn');
const uploader = new ChunkedUploader(file, {
onProgress: (percentage, uploaded, total) => {
progressBar.style.width = `${percentage}%`;
progressBar.textContent = `${Math.round(percentage)}%`;
const uploadedMB = (uploaded / (1024 * 1024)).toFixed(2);
const totalMB = (total / (1024 * 1024)).toFixed(2);
document.getElementById('uploadStatus').textContent =
`Uploaded ${uploadedMB} MB of ${totalMB} MB`;
},
onComplete: (data) => {
progressBar.style.width = '100%';
progressBar.textContent = 'Completed';
document.getElementById('uploadStatus').textContent = 'Upload completed';
// Display file URL
const fileLink = document.createElement('a');
fileLink.href = data.url;
fileLink.textContent = 'Access your file';
fileLink.target = '_blank';
document.getElementById('fileLink').appendChild(fileLink);
},
onError: (error) => {
document.getElementById('uploadStatus').textContent = `Error: ${error.message}`;
progressBar.classList.add('error');
}
});
uploadBtn.addEventListener('click', () => uploader.start());
pauseBtn.addEventListener('click', () => uploader.pause());
resumeBtn.addEventListener('click', () => uploader.resume());
cancelBtn.addEventListener('click', () => uploader.cancel());
});
Server-side Implementation
// Server-side implementation of chunked uploads
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { S3Client, PutObjectCommand, CompleteMultipartUploadCommand,
CreateMultipartUploadCommand, UploadPartCommand, AbortMultipartUploadCommand } = require('@aws-sdk/client-s3');
const crypto = require('crypto');
const app = express();
// Configure multer for handling chunk uploads
const upload = multer({
storage: multer.diskStorage({
destination: function(req, file, cb) {
const uploadId = req.body.uploadId;
const tempDir = path.join(__dirname, 'temp', uploadId);
// Create directory if it doesn't exist
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
cb(null, tempDir);
},
filename: function(req, file, cb) {
// Use chunk index as filename
const chunkIndex = req.body.chunkIndex;
cb(null, `chunk-${chunkIndex}`);
}
})
});
// Configure S3 client
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
const bucketName = process.env.AWS_S3_BUCKET;
// In-memory storage for upload sessions
const uploadSessions = new Map();
// Initialize a new chunked upload
app.post('/api/uploads/chunked/initialize', async (req, res) => {
try {
const { fileName, fileSize, fileType, totalChunks } = req.body;
if (!fileName || !fileSize || !totalChunks) {
return res.status(400).json({ error: 'Missing required parameters' });
}
// Generate upload ID
const uploadId = crypto.randomBytes(16).toString('hex');
// Generate a final target filename
const fileExtension = path.extname(fileName);
const baseName = path.basename(fileName, fileExtension).replace(/\s+/g, '-').toLowerCase();
const finalFileName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}-${baseName}${fileExtension}`;
// Store upload session info
uploadSessions.set(uploadId, {
fileName,
fileSize: parseInt(fileSize),
fileType,
totalChunks: parseInt(totalChunks),
finalFileName,
receivedChunks: 0,
chunkStatus: Array(parseInt(totalChunks)).fill(false),
createdAt: new Date(),
s3UploadId: null,
s3Parts: []
});
// Create temp directory for chunks
const tempDir = path.join(__dirname, 'temp', uploadId);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// For S3 multipart upload, initialize the multipart upload
if (process.env.STORAGE_TYPE === 's3') {
const command = new CreateMultipartUploadCommand({
Bucket: bucketName,
Key: finalFileName,
ContentType: fileType
});
const { UploadId } = await s3Client.send(command);
// Store S3 multipart upload ID
const session = uploadSessions.get(uploadId);
session.s3UploadId = UploadId;
uploadSessions.set(uploadId, session);
}
res.status(200).json({
uploadId,
maxChunkSize: 1024 * 1024 * 5 // 5MB, for client reference
});
// Set up cleanup for abandoned uploads (e.g., after 24 hours)
setTimeout(() => cleanupUpload(uploadId), 24 * 60 * 60 * 1000);
} catch (error) {
console.error('Upload initialization error:', error);
res.status(500).json({ error: 'Failed to initialize upload' });
}
});
// Handle chunk upload
app.post('/api/uploads/chunked/chunk', upload.single('chunkData'), async (req, res) => {
try {
const { uploadId, chunkIndex, totalChunks } = req.body;
// Validate parameters
if (!uploadId || chunkIndex === undefined || !totalChunks) {
return res.status(400).json({ error: 'Missing required parameters' });
}
// Get upload session
const session = uploadSessions.get(uploadId);
if (!session) {
return res.status(404).json({ error: 'Upload session not found' });
}
const index = parseInt(chunkIndex);
// For S3 multipart upload
if (process.env.STORAGE_TYPE === 's3' && session.s3UploadId) {
// Read the chunk file
const chunkFile = req.file.path;
const fileContent = fs.readFileSync(chunkFile);
// Upload part to S3
const command = new UploadPartCommand({
Bucket: bucketName,
Key: session.finalFileName,
UploadId: session.s3UploadId,
PartNumber: index + 1, // S3 part numbers start at 1
Body: fileContent
});
const { ETag } = await s3Client.send(command);
// Store part info for completing the multipart upload later
session.s3Parts[index] = {
ETag,
PartNumber: index + 1
};
// Clean up temp file
fs.unlinkSync(chunkFile);
}
// Update session status
session.chunkStatus[index] = true;
session.receivedChunks++;
uploadSessions.set(uploadId, session);
res.status(200).json({
success: true,
uploadId,
chunkIndex: index,
receivedChunks: session.receivedChunks,
totalChunks: session.totalChunks
});
} catch (error) {
console.error('Chunk upload error:', error);
res.status(500).json({ error: 'Failed to upload chunk' });
}
});
// Complete the upload and merge chunks
app.post('/api/uploads/chunked/complete', async (req, res) => {
try {
const { uploadId } = req.body;
if (!uploadId) {
return res.status(400).json({ error: 'Upload ID is required' });
}
// Get upload session
const session = uploadSessions.get(uploadId);
if (!session) {
return res.status(404).json({ error: 'Upload session not found' });
}
// Verify all chunks are received
if (session.receivedChunks !== session.totalChunks) {
return res.status(400).json({
error: 'Not all chunks have been uploaded',
receivedChunks: session.receivedChunks,
totalChunks: session.totalChunks
});
}
let fileUrl;
// Complete the multipart upload for S3
if (process.env.STORAGE_TYPE === 's3' && session.s3UploadId) {
// Sort parts by part number
const sortedParts = session.s3Parts
.filter(part => part !== null)
.sort((a, b) => a.PartNumber - b.PartNumber);
// Complete the multipart upload
const command = new CompleteMultipartUploadCommand({
Bucket: bucketName,
Key: session.finalFileName,
UploadId: session.s3UploadId,
MultipartUpload: {
Parts: sortedParts
}
});
await s3Client.send(command);
// Generate URL for the file
fileUrl = `https://${bucketName}.s3.amazonaws.com/${session.finalFileName}`;
} else {
// For local storage, merge chunks into a single file
const tempDir = path.join(__dirname, 'temp', uploadId);
const uploadDir = path.join(__dirname, 'uploads');
// Ensure uploads directory exists
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
const outputPath = path.join(uploadDir, session.finalFileName);
// Create write stream for the final file
const writeStream = fs.createWriteStream(outputPath);
// Merge chunks
for (let i = 0; i < session.totalChunks; i++) {
const chunkPath = path.join(tempDir, `chunk-${i}`);
// Read chunk and append to the final file
const chunkData = fs.readFileSync(chunkPath);
writeStream.write(chunkData);
// Clean up chunk file
fs.unlinkSync(chunkPath);
}
// Close write stream
writeStream.end();
// Generate URL for the file
fileUrl = `/files/${session.finalFileName}`;
}
// Clean up temp directory
const tempDir = path.join(__dirname, 'temp', uploadId);
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
// Remove upload session from memory
uploadSessions.delete(uploadId);
res.status(200).json({
success: true,
file: {
name: session.fileName,
size: session.fileSize,
type: session.fileType,
url: fileUrl
}
});
} catch (error) {
console.error('Complete upload error:', error);
res.status(500).json({ error: 'Failed to complete upload' });
}
});
// Cancel upload and clean up
app.post('/api/uploads/chunked/cancel/:uploadId', async (req, res) => {
try {
const { uploadId } = req.params;
if (!uploadId) {
return res.status(400).json({ error: 'Upload ID is required' });
}
// Get upload session
const session = uploadSessions.get(uploadId);
if (!session) {
return res.status(404).json({ error: 'Upload session not found' });
}
// For S3, abort multipart upload
if (process.env.STORAGE_TYPE === 's3' && session.s3UploadId) {
const command = new AbortMultipartUploadCommand({
Bucket: bucketName,
Key: session.finalFileName,
UploadId: session.s3UploadId
});
await s3Client.send(command);
}
// Clean up temp directory
const tempDir = path.join(__dirname, 'temp', uploadId);
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
// Remove upload session from memory
uploadSessions.delete(uploadId);
res.status(200).json({ success: true });
} catch (error) {
console.error('Cancel upload error:', error);
res.status(500).json({ error: 'Failed to cancel upload' });
}
});
// Helper function to clean up abandoned uploads
function cleanupUpload(uploadId) {
try {
const session = uploadSessions.get(uploadId);
if (!session) return;
// For S3, abort multipart upload
if (process.env.STORAGE_TYPE === 's3' && session.s3UploadId) {
const command = new AbortMultipartUploadCommand({
Bucket: bucketName,
Key: session.finalFileName,
UploadId: session.s3UploadId
});
s3Client.send(command).catch(error => {
console.error('Failed to abort S3 multipart upload:', error);
});
}
// Clean up temp directory
const tempDir = path.join(__dirname, 'temp', uploadId);
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
// Remove upload session from memory
uploadSessions.delete(uploadId);
} catch (error) {
console.error('Cleanup upload error:', error);
}
}
// Serve static files
app.use('/files', express.static('uploads'));
// Start server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
File Type Validation with Magic Numbers
MIME types sent by browsers can be spoofed. For more secure file validation, you can check file "magic numbers" - signatures in the file's binary data that identify its true type.
// Install required packages
// npm install file-type multer
const express = require('express');
const multer = require('multer');
const { fileTypeFromBuffer } = require('file-type');
const fs = require('fs');
const path = require('path');
const app = express();
// Configure multer for memory storage
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024 // 10MB
}
});
// Validate file type using magic numbers
async function validateFileType(buffer, allowedTypes) {
try {
// Detect file type from buffer
const fileType = await fileTypeFromBuffer(buffer);
// If file type couldn't be detected
if (!fileType) {
return { valid: false, detectedType: 'unknown' };
}
// Check if detected type is in allowed types
const isValid = allowedTypes.some(type => {
// Match MIME type (e.g., 'image/jpeg')
if (type === fileType.mime) {
return true;
}
// Match category (e.g., 'image/*')
if (type.endsWith('/*')) {
const category = type.split('/')[0];
return fileType.mime.startsWith(`${category}/`);
}
return false;
});
return {
valid: isValid,
detectedType: fileType.mime,
extension: fileType.ext
};
} catch (error) {
console.error('File type validation error:', error);
return { valid: false, detectedType: 'error' };
}
}
// Upload route with magic number validation
app.post('/api/upload/secure', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Define allowed types based on upload context
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
// Validate file using magic numbers
const validation = await validateFileType(req.file.buffer, allowedTypes);
if (!validation.valid) {
return res.status(400).json({
error: 'Invalid file type',
detectedType: validation.detectedType,
allowedTypes
});
}
// At this point, the file type is validated
// Continue with upload processing...
// For example, save the file with the correct extension
const fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}.${validation.extension}`;
const filePath = path.join(__dirname, 'uploads', fileName);
// Ensure uploads directory exists
if (!fs.existsSync(path.join(__dirname, 'uploads'))) {
fs.mkdirSync(path.join(__dirname, 'uploads'), { recursive: true });
}
// Write the file
fs.writeFileSync(filePath, req.file.buffer);
res.status(200).json({
success: true,
file: {
name: req.file.originalname,
size: req.file.buffer.length,
type: validation.detectedType,
url: `/files/${fileName}`
}
});
} catch (error) {
console.error('Secure upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
// Serve uploaded files
app.use('/files', express.static('uploads'));
// Start server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Practice Activities
Activity 1: Basic File Upload System
Build a simple yet secure file upload system with the following features:
- Drag-and-drop file upload interface
- File type validation (allow only images and PDFs)
- File size limit (5MB)
- Progress tracking
- Preview for images
Use Express and Multer on the backend to handle the uploads and store files locally. Implement proper error handling and security best practices.
Activity 2: Cloud Storage Integration
Extend the basic file upload system to use cloud storage. Choose one of the following:
- AWS S3
- Google Cloud Storage
- Azure Blob Storage
Implement direct-to-cloud uploads for better performance and create a database model to track uploaded files. Add the ability to list, rename, and delete files.
Activity 3: Image Processing Service
Create an image processing service with the following features:
- Upload images in various formats
- Resize images to different dimensions
- Apply filters (grayscale, sepia, etc.)
- Add watermarks
- Convert between formats (JPG, PNG, WebP)
- Generate optimized thumbnails
Use Sharp for image processing and implement a RESTful API for the service. Store original and processed images in cloud storage.
Activity 4: Large File Upload System
Create a system for handling large file uploads with the following features:
- Chunked file uploads
- Resume interrupted uploads
- Progress tracking
- Client-side hash verification
Implement both client-side and server-side code. Test with files of various sizes, including very large files (>1GB).
Real-world File Handling Systems
Document Management System
A document management system needs robust file handling capabilities. Key features include:
- Support for multiple file types (documents, images, PDFs, etc.)
- Version control
- Preview generation
- Full-text search
- Metadata extraction
- Access control
- Collaborative editing
For this type of system, you might use:
- Cloud storage for scalable file storage
- Queue system for processing files asynchronously
- Specialized services for extracting text from PDFs and other documents
- Image processing for generating previews
- WebSockets for real-time collaboration
Media Sharing Platform
A platform for sharing photos and videos requires specialized file handling:
- Support for high-resolution images and videos
- Transcoding videos to multiple formats and resolutions
- Generating responsive image sets
- Content moderation
- Content delivery optimization
Technologies that might be used:
- CDN integration for fast global delivery
- Video processing services like FFmpeg
- Image processing for optimizing images
- AI-based content moderation
- Multi-region storage for reduced latency
E-commerce Product Management
Managing product images in an e-commerce system:
- Multiple product images per product
- Variations for different screen sizes
- Zoom-capable high-resolution images
- Bulk upload capabilities
- Automated background removal
Implementation considerations:
- Standardized image processing pipelines
- CDN integration for fast loading
- Lazy loading strategies
- Responsive image techniques (srcset, sizes)
- Batch processing for bulk uploads
Further Resources
File Upload Libraries
- Multer - Node.js middleware for handling multipart/form-data
- Uppy - Elegant, modular file uploader for browsers
- Dropzone.js - JavaScript library that turns any HTML element into a dropzone
- express-fileupload - Simple Express middleware for uploading files
- backblaze-b2 - Node.js Library for Backblaze B2 Cloud Storage
Image Processing
Cloud Storage Services
- Amazon S3 - Simple Storage Service by AWS
- Google Cloud Storage - Object storage for companies of all sizes
- Azure Blob Storage - Microsoft's object storage solution
- Backblaze B2 - Low-cost cloud storage
- Wasabi - Hot cloud storage
Security Resources
- OWASP File Upload Cheat Sheet - Security best practices for file uploads
- file-type - Detect file types using magic numbers
- is-svg - Check if a string or buffer is SVG
- clamav.js - Node.js library for ClamAV antivirus scanning
- helmet - Help secure Express apps with various HTTP headers