const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const multer = require("multer"); const uploadDirectory = path.resolve( process.env.UPLOAD_DIR || path.join(process.cwd(), "uploads"), ); const maxFileSize = Number.parseInt(process.env.MAX_UPLOAD_SIZE_BYTES, 10) || 5 * 1024 * 1024; const extensionByMimeType = { "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp", }; // File signatures (magic bytes) that must match the declared Content-Type. const signatureByMimeType = { "image/jpeg": [Buffer.from([0xff, 0xd8, 0xff])], "image/png": [Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])], "image/gif": [Buffer.from("GIF87a"), Buffer.from("GIF89a")], }; function hasValidImageSignature(file) { const signatures = signatureByMimeType[file.mimetype]; if (!signatures) { return isWebP(file.path); } const header = Buffer.alloc(12); const fd = fs.openSync(file.path, "r"); fs.readSync(fd, header, 0, 12, 0); fs.closeSync(fd); return signatures.some((signature) => header.subarray(0, signature.length).equals(signature), ); } function isWebP(filePath) { const header = Buffer.alloc(12); const fd = fs.openSync(filePath, "r"); fs.readSync(fd, header, 0, 12, 0); fs.closeSync(fd); return ( header.subarray(0, 4).equals(Buffer.from("RIFF")) && header.subarray(8, 12).equals(Buffer.from("WEBP")) ); } fs.mkdirSync(uploadDirectory, { recursive: true }); const storage = multer.diskStorage({ destination: uploadDirectory, filename: (req, file, callback) => { callback(null, `${crypto.randomUUID()}${extensionByMimeType[file.mimetype]}`); }, }); const uploadImage = multer({ storage, limits: { fileSize: maxFileSize, files: 1 }, fileFilter: (req, file, callback) => { if (!extensionByMimeType[file.mimetype]) { return callback(new Error("Only JPEG, PNG, GIF, and WebP images are allowed")); } callback(null, true); }, }).single("image"); const ALLOWED_IMAGE_PREFIXES = ["/posts/image/", "/profiles/image/"]; function handleImageUpload(req, res, next) { uploadImage(req, res, (err) => { if (!err) { if (req.file && !hasValidImageSignature(req.file)) { fs.promises.unlink(req.file.path).catch(() => {}); return res.status(400).send("Uploaded file is not a valid image"); } return next(); } if (err.code === "LIMIT_FILE_SIZE") { return res.status(413).send("Image exceeds the maximum allowed size"); } return res.status(400).send(err.message); }); } function handlePostUpload(req, res, next) { return handleImageUpload(req, res, next); } function handleProfileUpload(req, res, next) { return handleImageUpload(req, res, next); } function imageLinkForFile(file, prefix = "/posts/image/") { return file ? `${prefix}${file.filename}` : null; } function profileImageLinkForFile(file) { return imageLinkForFile(file, "/profiles/image/"); } async function removeImageLink(imageLink) { if (!imageLink || !ALLOWED_IMAGE_PREFIXES.some((prefix) => imageLink.startsWith(prefix))) { return; } const filename = path.basename(imageLink); await fs.promises.unlink(path.join(uploadDirectory, filename)).catch((err) => { if (err.code !== "ENOENT") throw err; }); } async function removeUploadedProfileFile(file) { if (file) await removeImageLink(profileImageLinkForFile(file)); } async function removeUploadedFile(file) { if (file) await removeImageLink(imageLinkForFile(file)); } module.exports = { handlePostUpload, handleProfileUpload, imageLinkForFile, profileImageLinkForFile, removeImageLink, removeUploadedFile, removeUploadedProfileFile, uploadDirectory, };