File Handling in Full Stack Applications

Understanding file upload strategies, cloud storage integration, and image processing

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(`
    
      
      ${text}
    
  `);
  
  // 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:

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

  • Sharp - High performance Node.js image processing library
  • libvips - Underlying C library that powers Sharp
  • Jimp - JavaScript Image Manipulation Program
  • gm - GraphicsMagick and ImageMagick for Node.js
  • Jimp - An image processing library written entirely in JavaScript

Cloud Storage Services

Security Resources