npm install multer sharp
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const storage = multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
const uniqueName =
`${Date.now()}-${Math.round(
Math.random() * 1E9
)}${path.extname(file.originalname)}`;
cb(null, uniqueName);
},
});
const upload = multer({
storage,
limits: {
fileSize: 25 * 1024 * 1024 // 25MB max
},
fileFilter: (req, file, cb) => {
const allowedTypes = [
'image/jpeg', 'image/png', 'image/webp'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(
'Invalid file type. Only JPEG, ' +
'PNG, and WebP are allowed.'
));
}
},
});
app.post('/api/images/upload',
authenticate, upload.single('image'),
async (req, res) => {
try {
// Generate thumbnail
const thumbPath = req.file.path
.replace(/(\.[^.]+)$/, '_thumb$1');
await sharp(req.file.path)
.resize(300, 300, { fit: 'inside' })
.toFile(thumbPath);
const metadata =
await sharp(req.file.path).metadata();
const image = await Image.create({
name: req.file.originalname,
filename: req.file.filename,
thumbnail: path.basename(thumbPath),
width: metadata.width,
height: metadata.height,
fileSize: req.file.size,
format: req.file.mimetype.split('/')[1],
owner: req.userId,
});
res.status(201).json(image);
} catch (error) {
res.status(500)
.json({ error: 'Upload failed' });
}
});
function uploadImage(file, onProgress) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('image', file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener(
'progress', (e) => {
if (e.lengthComputable)
onProgress(Math.round(
(e.loaded / e.total) * 100
));
});
xhr.addEventListener('load', () => {
if (xhr.status === 201)
resolve(JSON.parse(xhr.responseText));
else reject(new Error('Upload failed'));
});
xhr.open('POST',
`${API_URL}/api/images/upload`);
xhr.setRequestHeader('Authorization',
`Bearer ${localStorage.getItem('token')}`
);
xhr.send(formData);
});
}
Build an <UploadZone> component with drag-and-drop and progress bar. Drag an image onto the page → see upload progress → thumbnail appears in gallery.
git switch -c feature/PIXELCRAFT-050-file-upload
git add server/ src/
git commit -m "Add file upload with thumbnails and progress (PIXELCRAFT-050)"
git push origin feature/PIXELCRAFT-050-file-upload
# PR → Review → Merge → Close ticket ✅
Streams and buffering.
A 50MB image doesn't load entirely into RAM. It's streamed in chunks.