Learn how to easily add file upload and management features to your web app with this step-by-step guide.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Introduction: Why File Management Matters
Let's face it—nearly every modern web application needs to handle file uploads. Whether it's profile pictures, product images, documents, or media files, implementing a robust file management system is crucial. What seems like a simple feature ("just let users upload files!") quickly reveals itself as a complex challenge involving storage decisions, security considerations, and user experience design.
First, let's understand what we're building:
The HTML Basics
Let's start with a clean, accessible upload component:
<form id="upload-form" enctype="multipart/form-data">
<div class="upload-container">
<label for="file-input" class="upload-label">
<div class="upload-placeholder">
<svg><!-- Your icon SVG here --></svg>
<span>Drag files here or click to browse</span>
</div>
</label>
<input type="file" id="file-input" multiple class="file-input" />
</div>
<div class="file-preview-container"></div>
<div class="upload-actions">
<button type="submit" class="upload-button">Upload Files</button>
</div>
</form>
Making It Interactive with JavaScript
The magic happens when we add JavaScript to handle drag-and-drop, previews, and AJAX uploads:
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('upload-form');
const fileInput = document.getElementById('file-input');
const previewContainer = document.querySelector('.file-preview-container');
const uploadContainer = document.querySelector('.upload-container');
// Handle drag and drop events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadContainer.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop area when dragging over it
['dragenter', 'dragover'].forEach(eventName => {
uploadContainer.addEventListener(eventName, () => {
uploadContainer.classList.add('highlight');
});
});
['dragleave', 'drop'].forEach(eventName => {
uploadContainer.addEventListener(eventName, () => {
uploadContainer.classList.remove('highlight');
});
});
// Handle dropped files
uploadContainer.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
});
// Handle selected files
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
});
function handleFiles(files) {
files = [...files]; // Convert FileList to array
files.forEach(previewFile);
files.forEach(uploadFile);
}
function previewFile(file) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
const filePreview = document.createElement('div');
filePreview.className = 'file-preview';
// Create appropriate preview based on file type
if (file.type.startsWith('image/')) {
filePreview.innerHTML = `
<img src="${reader.result}" alt="${file.name}" />
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${formatFileSize(file.size)}</span>
</div>
`;
} else {
// For non-image files, show an icon based on file type
filePreview.innerHTML = `
<div class="file-icon">${getFileIcon(file.type)}</div>
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${formatFileSize(file.size)}</span>
</div>
`;
}
previewContainer.appendChild(filePreview);
};
}
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
// Update UI to show successful upload
// You could add a progress indicator or success message here
})
.catch(error => {
console.error('Error:', error);
// Handle errors, show message to user
});
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function getFileIcon(mimeType) {
// Return appropriate icon based on file type
if (mimeType.startsWith('image/')) return '📷';
if (mimeType.startsWith('video/')) return '🎬';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('word')) return '📝';
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return '📊';
return '📁';
}
});
Let's look at implementation options in different languages:
// Server-side code with Express and multer for file handling
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const app = express();
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, 'uploads');
// Create directory if it doesn't exist
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
// Generate unique filename with original extension
const fileExt = path.extname(file.originalname);
const fileName = `${uuidv4()}${fileExt}`;
cb(null, fileName);
}
});
// File filter to control which files are accepted
const fileFilter = (req, file, cb) => {
// Define allowed mime types
const allowedTypes = [
'image/jpeg', 'image/png', 'image/gif',
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true); // Accept file
} else {
cb(new Error('Invalid file type. Only images, PDFs, and Word documents are allowed.'), false);
}
};
// Configure upload middleware
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
}
});
// Handle file uploads
app.post('/api/upload', upload.single('file'), (req, res) => {
try {
// File was uploaded and is available in req.file
const fileData = {
id: path.parse(req.file.filename).name, // UUID without extension
originalName: req.file.originalname,
filename: req.file.filename,
path: req.file.path,
size: req.file.size,
mimetype: req.file.mimetype,
uploadDate: new Date()
};
// Here you would typically save the file metadata to a database
// For this example, we'll just return the file data
res.status(200).json({
success: true,
file: fileData
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
});
// Error handler for multer errors
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// A Multer error occurred when uploading
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: 'File is too large. Maximum size is 10MB.'
});
}
}
// For any other errors
res.status(500).json({
success: false,
message: err.message
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
<?php
// upload_handler.php
header('Content-Type: application/json');
// Define the upload directory and create it if it doesn't exist
$uploadDir = __DIR__ . '/uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Check if file was uploaded
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
$errorMessage = 'Upload failed. ';
if (isset($_FILES['file'])) {
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$errorMessage .= 'File is too large.';
break;
case UPLOAD_ERR_PARTIAL:
$errorMessage .= 'The file was only partially uploaded.';
break;
case UPLOAD_ERR_NO_FILE:
$errorMessage .= 'No file was uploaded.';
break;
default:
$errorMessage .= 'Unknown error occurred.';
}
}
echo json_encode([
'success' => false,
'message' => $errorMessage
]);
exit;
}
// Validate file type
$allowedTypes = [
'image/jpeg', 'image/png', 'image/gif',
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (!in_array($_FILES['file']['type'], $allowedTypes)) {
echo json_encode([
'success' => false,
'message' => 'Invalid file type. Only images, PDFs, and Word documents are allowed.'
]);
exit;
}
// Validate file size (10MB max)
$maxFileSize = 10 * 1024 * 1024; // 10MB in bytes
if ($_FILES['file']['size'] > $maxFileSize) {
echo json_encode([
'success' => false,
'message' => 'File is too large. Maximum size is 10MB.'
]);
exit;
}
// Generate a unique filename with the original extension
$fileExt = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$fileName = uniqid() . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
// Move the uploaded file to the target location
if (move_uploaded_file($_FILES['file']['tmp_name'], $targetFile)) {
// File upload successful
$fileData = [
'id' => pathinfo($fileName, PATHINFO_FILENAME), // Unique ID without extension
'originalName' => $_FILES['file']['name'],
'filename' => $fileName,
'path' => $targetFile,
'size' => $_FILES['file']['size'],
'mimetype' => $_FILES['file']['type'],
'uploadDate' => date('Y-m-d H:i:s')
];
// Here you would typically save the file metadata to a database
// For this example, we'll just return the file data
echo json_encode([
'success' => true,
'file' => $fileData
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to move uploaded file.'
]);
}
?>
import os
import uuid
from flask import Flask, request, jsonify
from werkzeug.utils import secure_filename
from datetime import datetime
app = Flask(__name__)
# Configure upload settings
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx'}
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
# Ensure the upload directory exists
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/api/upload', methods=['POST'])
def upload_file():
# Check if the post request has the file part
if 'file' not in request.files:
return jsonify({
'success': False,
'message': 'No file part in the request'
}), 400
file = request.files['file']
# If user does not select file, browser might
# submit an empty file without a filename
if file.filename == '':
return jsonify({
'success': False,
'message': 'No file selected'
}), 400
if file and allowed_file(file.filename):
# Create a unique filename while preserving the original extension
original_filename = secure_filename(file.filename)
file_ext = os.path.splitext(original_filename)[1]
unique_filename = f"{uuid.uuid4()}{file_ext}"
# Save the file
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(file_path)
# Create file metadata
file_data = {
'id': str(uuid.uuid4()),
'originalName': original_filename,
'filename': unique_filename,
'path': file_path,
'size': os.path.getsize(file_path),
'mimetype': file.content_type,
'uploadDate': datetime.now().isoformat()
}
# Here you would typically save the file metadata to a database
# For this example, we'll just return the file data
return jsonify({
'success': True,
'file': file_data
})
return jsonify({
'success': False,
'message': 'File type not allowed'
}), 400
@app.errorhandler(413)
def request_entity_too_large(error):
return jsonify({
'success': False,
'message': 'File is too large. Maximum size is 10MB.'
}), 413
if __name__ == '__main__':
app.run(debug=True, port=3000)
Local Storage vs. Cloud Storage
When it comes to storing files, you have two primary options:
Implementing Amazon S3 Storage
Cloud storage is the industry standard for production applications. Here's how to implement it with AWS S3:
// Node.js implementation with AWS S3
const AWS = require('aws-sdk');
const multer = require('multer');
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const app = express();
// Configure AWS
AWS.config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});
const s3 = new AWS.S3();
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
// Configure multer for memory storage (files will be buffered in memory)
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
const allowedTypes = [
'image/jpeg', 'image/png', 'image/gif',
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only images, PDFs, and Word documents are allowed.'), false);
}
}
});
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
// Generate unique file name
const fileId = uuidv4();
const originalName = req.file.originalname;
const fileExt = originalName.split('.').pop();
const fileName = `${fileId}.${fileExt}`;
// Set up S3 upload parameters
const params = {
Bucket: BUCKET_NAME,
Key: `uploads/${fileName}`, // Path in the bucket
Body: req.file.buffer, // File content
ContentType: req.file.mimetype,
Metadata: {
'originalname': originalName
}
};
// Upload to S3
const s3Upload = await s3.upload(params).promise();
// Prepare response data
const fileData = {
id: fileId,
originalName: originalName,
filename: fileName,
location: s3Upload.Location, // Public URL
size: req.file.size,
mimetype: req.file.mimetype,
uploadDate: new Date()
};
// Here you would typically save the file metadata to a database
res.status(200).json({
success: true,
file: fileData
});
} catch (error) {
console.error('Error uploading to S3:', error);
res.status(500).json({
success: false,
message: 'Error uploading file',
error: error.message
});
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Creating a File Gallery View
Once files are uploaded, users need a way to view, manage, and use them:
<div class="file-manager">
<div class="file-manager-header">
<h3>Your Files</h3>
<div class="file-manager-actions">
<select id="sort-files">
<option value="date-desc">Newest First</option>
<option value="date-asc">Oldest First</option>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="size-desc">Size (Largest)</option>
<option value="size-asc">Size (Smallest)</option>
</select>
<div class="view-toggles">
<button id="grid-view" class="active">
<svg><!-- Grid icon --></svg>
</button>
<button id="list-view">
<svg><!-- List icon --></svg>
</button>
</div>
</div>
</div>
<div class="file-search">
<input type="text" placeholder="Search files..." id="file-search-input" />
</div>
<div class="file-grid" id="file-container">
<!-- Files will be populated here via JavaScript -->
</div>
<div class="file-pagination">
<button id="prev-page" disabled>Previous</button>
<span id="page-indicator">Page 1 of 1</span>
<button id="next-page" disabled>Next</button>
</div>
</div>
The JavaScript to Power It
document.addEventListener('DOMContentLoaded', () => {
// File manager elements
const fileContainer = document.getElementById('file-container');
const sortSelect = document.getElementById('sort-files');
const gridViewBtn = document.getElementById('grid-view');
const listViewBtn = document.getElementById('list-view');
const searchInput = document.getElementById('file-search-input');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const pageIndicator = document.getElementById('page-indicator');
// State management
let files = []; // Will store all files
let filteredFiles = []; // Will store files filtered by search
let currentPage = 1;
let filesPerPage = 20;
let currentView = 'grid'; // 'grid' or 'list'
let currentSort = 'date-desc';
// Fetch files from server
async function fetchFiles() {
try {
const response = await fetch('/api/files');
const data = await response.json();
if (data.success) {
files = data.files;
filteredFiles = [...files];
sortFiles(currentSort);
renderFiles();
updatePagination();
} else {
console.error('Failed to fetch files:', data.message);
}
} catch (error) {
console.error('Error fetching files:', error);
}
}
// Sort files based on selected criteria
function sortFiles(sortBy) {
currentSort = sortBy;
switch (sortBy) {
case 'date-desc':
filteredFiles.sort((a, b) => new Date(b.uploadDate) - new Date(a.uploadDate));
break;
case 'date-asc':
filteredFiles.sort((a, b) => new Date(a.uploadDate) - new Date(b.uploadDate));
break;
case 'name-asc':
filteredFiles.sort((a, b) => a.originalName.localeCompare(b.originalName));
break;
case 'name-desc':
filteredFiles.sort((a, b) => b.originalName.localeCompare(a.originalName));
break;
case 'size-desc':
filteredFiles.sort((a, b) => b.size - a.size);
break;
case 'size-asc':
filteredFiles.sort((a, b) => a.size - b.size);
break;
}
renderFiles();
}
// Filter files based on search input
function filterFiles(searchTerm) {
if (!searchTerm) {
filteredFiles = [...files];
} else {
searchTerm = searchTerm.toLowerCase();
filteredFiles = files.filter(file =>
file.originalName.toLowerCase().includes(searchTerm)
);
}
currentPage = 1;
sortFiles(currentSort);
updatePagination();
}
// Render files to the container
function renderFiles() {
// Calculate pagination
const startIndex = (currentPage - 1) * filesPerPage;
const endIndex = startIndex + filesPerPage;
const filesToRender = filteredFiles.slice(startIndex, endIndex);
// Clear container
fileContainer.innerHTML = '';
if (filesToRender.length === 0) {
fileContainer.innerHTML = '<div class="no-files">No files found</div>';
return;
}
// Render each file
filesToRender.forEach(file => {
const fileElement = document.createElement('div');
fileElement.className = `file-item ${currentView === 'grid' ? 'grid-item' : 'list-item'}`;
fileElement.dataset.id = file.id;
// Determine file icon or preview
let filePreview;
if (file.mimetype.startsWith('image/')) {
filePreview = `<img src="${file.location || '/api/files/' + file.id}" alt="${file.originalName}" class="file-preview" />`;
} else {
filePreview = `<div class="file-icon">${getFileIcon(file.mimetype)}</div>`;
}
// Format date for display
const uploadDate = new Date(file.uploadDate);
const formattedDate = uploadDate.toLocaleDateString() + ' ' +
uploadDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
// Create file item HTML
fileElement.innerHTML = `
<div class="file-preview-container">
${filePreview}
</div>
<div class="file-details">
<div class="file-name">${file.originalName}</div>
<div class="file-meta">
<span class="file-size">${formatFileSize(file.size)}</span>
<span class="file-date">${formattedDate}</span>
</div>
</div>
<div class="file-actions">
<button class="file-action download-file" title="Download">
<svg><!-- Download icon --></svg>
</button>
<button class="file-action delete-file" title="Delete">
<svg><!-- Delete icon --></svg>
</button>
</div>
`;
// Add event listeners to buttons
const downloadBtn = fileElement.querySelector('.download-file');
const deleteBtn = fileElement.querySelector('.delete-file');
downloadBtn.addEventListener('click', () => downloadFile(file));
deleteBtn.addEventListener('click', () => deleteFile(file));
fileContainer.appendChild(fileElement);
});
}
// Update pagination controls
function updatePagination() {
const totalPages = Math.ceil(filteredFiles.length / filesPerPage);
pageIndicator.textContent = `Page ${currentPage} of ${totalPages || 1}`;
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
}
// Download a file
function downloadFile(file) {
const downloadUrl = file.location || `/api/files/${file.id}/download`;
const a = document.createElement('a');
a.href = downloadUrl;
a.download = file.originalName; // Suggest the original filename
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Delete a file with confirmation
async function deleteFile(file) {
if (confirm(`Are you sure you want to delete "${file.originalName}"?`)) {
try {
const response = await fetch(`/api/files/${file.id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
// Remove from arrays
files = files.filter(f => f.id !== file.id);
filteredFiles = filteredFiles.filter(f => f.id !== file.id);
// Re-render and update pagination
renderFiles();
updatePagination();
} else {
alert(`Failed to delete file: ${data.message}`);
}
} catch (error) {
console.error('Error deleting file:', error);
alert('An error occurred while deleting the file.');
}
}
}
// Utility: Format file size for display
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Utility: Get icon for file type
function getFileIcon(mimeType) {
// Return appropriate icon based on file type
if (mimeType.startsWith('image/')) return '📷';
if (mimeType.startsWith('video/')) return '🎬';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('word')) return '📝';
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return '📊';
return '📁';
}
// Event listeners
sortSelect.addEventListener('change', () => {
sortFiles(sortSelect.value);
});
gridViewBtn.addEventListener('click', () => {
currentView = 'grid';
fileContainer.className = 'file-grid';
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
renderFiles();
});
listViewBtn.addEventListener('click', () => {
currentView = 'list';
fileContainer.className = 'file-list';
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
renderFiles();
});
searchInput.addEventListener('input', () => {
filterFiles(searchInput.value);
});
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
renderFiles();
updatePagination();
}
});
nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(filteredFiles.length / filesPerPage);
if (currentPage < totalPages) {
currentPage++;
renderFiles();
updatePagination();
}
});
// Initial load
fetchFiles();
});
Complete API for File Operations
To support the file management interface, we need backend endpoints:
// Node.js Express API routes for file management
const express = require('express');
const router = express.Router();
const multer = require('multer');
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const db = require('./database'); // Your database connection
// Configure AWS
AWS.config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});
const s3 = new AWS.S3();
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
// Configure multer
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
}
});
// Get all files
router.get('/files', async (req, res) => {
try {
// Get files from database
const files = await db.query('SELECT * FROM files WHERE user_id = ?', [req.user.id]);
res.json({
success: true,
files: files
});
} catch (error) {
console.error('Error fetching files:', error);
res.status(500).json({
success: false,
message: 'Error fetching files',
error: error.message
});
}
});
// Get a single file
router.get('/files/:id', async (req, res) => {
try {
const fileId = req.params.id;
// Get file from database
const [file] = await db.query(
'SELECT * FROM files WHERE id = ? AND user_id = ?',
[fileId, req.user.id]
);
if (!file) {
return res.status(404).json({
success: false,
message: 'File not found'
});
}
res.json({
success: true,
file: file
});
} catch (error) {
console.error('Error fetching file:', error);
res.status(500).json({
success: false,
message: 'Error fetching file',
error: error.message
});
}
});
// Download a file
router.get('/files/:id/download', async (req, res) => {
try {
const fileId = req.params.id;
// Get file from database
const [file] = await db.query(
'SELECT * FROM files WHERE id = ? AND user_id = ?',
[fileId, req.user.id]
);
if (!file) {
return res.status(404).json({
success: false,
message: 'File not found'
});
}
// Check if using S3 or local storage
if (file.storage_type === 's3') {
// For S3, generate a signed URL
const params = {
Bucket: BUCKET_NAME,
Key: file.storage_path,
Expires: 60, // URL valid for 60 seconds
ResponseContentDisposition: `attachment; filename="${file.original_name}"`
};
const signedUrl = s3.getSignedUrl('getObject', params);
// Redirect to the signed URL
return res.redirect(signedUrl);
} else {
// For local storage, serve the file directly
const filePath = file.storage_path;
res.download(filePath, file.original_name);
}
} catch (error) {
console.error('Error downloading file:', error);
res.status(500).json({
success: false,
message: 'Error downloading file',
error: error.message
});
}
});
// Upload a file
router.post('/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
// Generate a unique ID for the file
const fileId = uuidv4();
// Extract file info
const originalName = req.file.originalname;
const mimeType = req.file.mimetype;
const size = req.file.size;
const fileExt = originalName.split('.').pop();
const storageFilename = `${fileId}.${fileExt}`;
const storageKey = `uploads/${req.user.id}/${storageFilename}`;
// Upload to S3
const params = {
Bucket: BUCKET_NAME,
Key: storageKey,
Body: req.file.buffer,
ContentType: mimeType,
Metadata: {
'originalname': originalName
}
};
const s3Upload = await s3.upload(params).promise();
// Save file metadata to database
const fileData = {
id: fileId,
user_id: req.user.id,
original_name: originalName,
storage_filename: storageFilename,
storage_path: storageKey,
storage_type: 's3',
location: s3Upload.Location,
size: size,
mimetype: mimeType,
upload_date: new Date()
};
await db.query(
'INSERT INTO files (id, user_id, original_name, storage_filename, storage_path, storage_type, location, size, mimetype, upload_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[fileData.id, fileData.user_id, fileData.original_name, fileData.storage_filename, fileData.storage_path, fileData.storage_type, fileData.location, fileData.size, fileData.mimetype, fileData.upload_date]
);
res.status(200).json({
success: true,
file: fileData
});
} catch (error) {
console.error('Error uploading file:', error);
res.status(500).json({
success: false,
message: 'Error uploading file',
error: error.message
});
}
});
// Delete a file
router.delete('/files/:id', async (req, res) => {
try {
const fileId = req.params.id;
// Get file from database
const [file] = await db.query(
'SELECT * FROM files WHERE id = ? AND user_id = ?',
[fileId, req.user.id]
);
if (!file) {
return res.status(404).json({
success: false,
message: 'File not found'
});
}
// Delete from storage
if (file.storage_type === 's3') {
// Delete from S3
const params = {
Bucket: BUCKET_NAME,
Key: file.storage_path
};
await s3.deleteObject(params).promise();
} else {
// Delete from local filesystem
const fs = require('fs');
if (fs.existsSync(file.storage_path)) {
fs.unlinkSync(file.storage_path);
}
}
// Delete from database
await db.query('DELETE FROM files WHERE id = ?', [fileId]);
res.json({
success: true,
message: 'File deleted successfully'
});
} catch (error) {
console.error('Error deleting file:', error);
res.status(500).json({
success: false,
message: 'Error deleting file',
error: error.message
});
}
});
module.exports = router;
Database Schema for File Management
Your database needs to track file metadata efficiently:
-- For MySQL/PostgreSQL
CREATE TABLE files (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
original_name VARCHAR(255) NOT NULL,
storage_filename VARCHAR(255) NOT NULL,
storage_path VARCHAR(512) NOT NULL,
storage_type ENUM('local', 's3', 'azure', 'gcp') NOT NULL,
location VARCHAR(512) NULL,
size BIGINT NOT NULL,
mimetype VARCHAR(127) NOT NULL,
upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_accessed TIMESTAMP NULL,
is_public BOOLEAN DEFAULT FALSE,
folder_path VARCHAR(512) DEFAULT '/',
tags TEXT NULL,
INDEX idx_user_id (user_id),
INDEX idx_upload_date (upload_date),
INDEX idx_folder_path (folder_path)
);
-- Optional table for file sharing
CREATE TABLE file_shares (
id VARCHAR(36) PRIMARY KEY,
file_id VARCHAR(36) NOT NULL,
shared_by VARCHAR(36) NOT NULL,
shared_with VARCHAR(36) NULL,
share_link VARCHAR(64) NULL,
access_type ENUM('view', 'edit', 'download') NOT NULL,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
INDEX idx_file_id (file_id),
INDEX idx_shared_with (shared_with),
INDEX idx_share_link (share_link)
);
-- Optional table for file versioning
CREATE TABLE file_versions (
id VARCHAR(36) PRIMARY KEY,
file_id VARCHAR(36) NOT NULL,
version_number INT NOT NULL,
storage_filename VARCHAR(255) NOT NULL,
storage_path VARCHAR(512) NOT NULL,
size BIGINT NOT NULL,
created_by VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
UNIQUE KEY uk_file_version (file_id, version_number),
INDEX idx_file_id (file_id)
);
Image Processing for Thumbnails & Previews
// Node.js image processing with Sharp
const sharp = require('sharp');
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
async function generateThumbnail(fileBuffer, mimeType, fileId) {
// Only process images
if (!mimeType.startsWith('image/')) {
return null;
}
try {
// Resize to thumbnail size (maintain aspect ratio)
const thumbnailBuffer = await sharp(fileBuffer)
.resize({
width: 200,
height: 200,
fit: sharp.fit.inside,
withoutEnlargement: true
})
.toBuffer();
// Upload thumbnail to S3
const thumbnailKey = `thumbnails/${fileId}.jpg`;
await s3.upload({
Bucket: process.env.S3_BUCKET_NAME,
Key: thumbnailKey,
Body: thumbnailBuffer,
ContentType: 'image/jpeg'
}).promise();
return `https://${process.env.S3_BUCKET_NAME}.s3.amazonaws.com/${thumbnailKey}`;
} catch (error) {
console.error('Error generating thumbnail:', error);
return null;
}
}
// When processing uploads:
router.post('/upload', upload.single('file'), async (req, res) => {
try {
// ... existing upload code ...
// Generate thumbnail if it's an image
let thumbnailUrl = null;
if (req.file.mimetype.startsWith('image/')) {
thumbnailUrl = await generateThumbnail(req.file.buffer, req.file.mimetype, fileId);
}
// Add thumbnail to file data
if (thumbnailUrl) {
fileData.thumbnail_url = thumbnailUrl;
// Update database with thumbnail URL
await db.query(
'UPDATE files SET thumbnail_url = ? WHERE id = ?',
[thumbnailUrl, fileId]
);
}
// ... rest of upload code ...
} catch (error) {
// ... error handling ...
}
});
Progress Tracking for Large Uploads
// Client-side upload with progress tracking using XHR
function uploadFileWithProgress(file, progressCallback, completedCallback) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
// Add file to form data
formData.append('file', file);
// Set up progress tracking
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = Math.round((event.loaded / event.total) * 100);
progressCallback(percentComplete, file);
}
});
// Set up completion handler
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
const response = JSON.parse(xhr.responseText);
completedCallback(response, file);
} else {
console.error('Upload failed:', xhr.statusText);
}
});
// Handle errors
xhr.addEventListener('error', () => {
console.error('Upload failed due to network error');
});
// Open and send the request
xhr.open('POST', '/api/upload', true);
xhr.send(formData);
// Return the XHR object so it can be aborted if needed
return xhr;
}
// Usage example
const fileInput = document.getElementById('file-input');
const progressBars = {};
fileInput.addEventListener('change', () => {
Array.from(fileInput.files).forEach(file => {
// Create a progress bar for this file
const progressBar = createProgressBarForFile(file);
progressBars[file.name] = progressBar;
// Start upload with progress tracking
uploadFileWithProgress(
file,
(percent) => {
// Update progress bar
updateProgressBar(progressBar, percent);
},
(response) => {
// Handle completed upload
finishProgressBar(progressBar);
// Add file to UI
addFileToGallery(response.file);
}
);
});
});
function createProgressBarForFile(file) {
const progressContainer = document.createElement('div');
progressContainer.className = 'upload-progress-container';
progressContainer.innerHTML = `
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${formatFileSize(file.size)}</span>
</div>
<div class="progress-bar-outer">
<div class="progress-bar-inner" style="width: 0%"></div>
</div>
<div class="progress-text">0%</div>
`;
document.getElementById('progress-container').appendChild(progressContainer);
return progressContainer;
}
function updateProgressBar(progressBar, percent) {
const progressInner = progressBar.querySelector('.progress-bar-inner');
const progressText = progressBar.querySelector('.progress-text');
progressInner.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
}
function finishProgressBar(progressBar) {
progressBar.classList.add('completed');
// Optionally remove it after a delay
setTimeout(() => {
progressBar.classList.add('fade-out');
setTimeout(() => {
progressBar.remove();
}, 500);
}, 2000);
}
Drag-and-Drop Folder Uploads
// Handle folder uploads
const folderInput = document.getElementById('folder-input');
// For folder input
folderInput.addEventListener('change', async (event) => {
const files = event.target.files;
await processFiles(files);
});
// For drag and drop
uploadContainer.addEventListener('drop', async (event) => {
event.preventDefault();
// Check if items are available (for folder support)
if (event.dataTransfer.items) {
await processDataTransferItems(event.dataTransfer.items);
} else if (event.dataTransfer.files) {
// Fallback to files
await processFiles(event.dataTransfer.files);
}
});
// Process DataTransferItemList (supports folders)
async function processDataTransferItems(items) {
const filePromises = [];
// Process each item
for (let i = 0; i < items.length; i++) {
const item = items[i];
// Handle directories
if (item.webkitGetAsEntry && item.webkitGetAsEntry().isDirectory) {
const entry = item.webkitGetAsEntry();
const dirFiles = await readDirectoryRecursively(entry);
filePromises.push(...dirFiles);
}
// Handle files
else if (item.kind === 'file') {
const file = item.getAsFile();
filePromises.push(Promise.resolve({ file, path: file.name }));
}
}
// Wait for all files to be processed
const fileObjects = await Promise.all(filePromises);
// Upload files with their paths
for (const { file, path } of fileObjects) {
await uploadFileWithPath(file, path);
}
}
// Recursively read all files from a directory
async function readDirectoryRecursively(directoryEntry) {
const reader = directoryEntry.createReader();
const entries = await new Promise((resolve) => {
const entries = [];
function readEntries() {
reader.readEntries((results) => {
if (results.length) {
entries.push(...results);
readEntries();
} else {
resolve(entries);
}
});
}
readEntries();
});
const filePromises = [];
for (const entry of entries) {
// Handle subdirectories
if (entry.isDirectory) {
const subDirFiles = await readDirectoryRecursively(entry);
filePromises.push(...subDirFiles);
}
// Handle files
else {
const filePromise = new Promise((resolve) => {
entry.file((file) => {
// Construct path relative to the uploaded folder
const path = entry.fullPath.substring(1); // Remove leading slash
resolve({ file, path });
});
});
filePromises.push(filePromise);
}
}
return filePromises;
}
// Upload a file with its path
async function uploadFileWithPath(file, path) {
const formData = new FormData();
formData.append('file', file);
formData.append('path', path);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
return data;
} catch (error) {
console.error('Error uploading file:', error);
throw error;
}
}
1. Performance Optimization Tips
2. Security Considerations
3. Monitoring and Maintenance
Conclusion: Putting It All Together
Building a robust file management system is a significant undertaking, but breaking it down into manageable components makes it achievable. The foundation is a solid frontend interface, reliable backend handlers, and a flexible storage strategy.
For most production applications, cloud storage solutions like AWS S3, Google Cloud Storage, or Azure Blob Storage offer the best balance of scalability, reliability, and cost-effectiveness. They handle the heavy lifting of redundancy, availability, and global distribution.
Remember that file management is rarely "set it and forget it"—user needs evolve, storage requirements grow, and security considerations change. Plan for iterative improvements based on usage patterns and feedback.
By following this guide, you'll have implemented a full-featured file upload and management system that will serve as a solid foundation for your application's needs, ready to scale as your user base grows.
Explore the top 3 file upload and management use cases to enhance your web app’s functionality and user experience.
A centralized repository where teams store, organize, and share documents with version control. Enables businesses to maintain a single source of truth for contracts, policies, and operational materials while controlling access through permissions.
A specialized system for handling rich media files including images, videos, and audio. Transforms raw uploads into optimized formats for different devices and platforms while preserving originals for future editing needs.
Systems that allow customers or community members to contribute content directly. Creates engagement through participation while building valuable content libraries that enhance product value over time.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.