const multer = require('multer'); const path = require('path'); const crypto = require('crypto'); const fs = require('fs'); const UPLOAD_DIR = path.join(__dirname, '..', '..', 'data', 'uploads'); if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }); const ALLOWED_MIME = { 'image/jpeg': ['.jpg', '.jpeg'], 'image/png': ['.png'], 'image/gif': ['.gif'], 'image/webp': ['.webp'], 'video/mp4': ['.mp4'], 'video/webm': ['.webm'], }; const ALLOWED_EXT = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.mp4', '.webm']; function validateMagicBytes(buffer, mime) { const head = buffer.slice(0, 12); const sigs = { 'image/jpeg': () => head[0] === 0xFF && head[1] === 0xD8, 'image/png': () => head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4E && head[3] === 0x47, 'image/gif': () => head.toString('ascii', 0, 6) === 'GIF89a' || head.toString('ascii', 0, 6) === 'GIF87a', 'image/webp': () => head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP', 'video/mp4': () => buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70, 'video/webm': () => head[0] === 0x1A && head[1] === 0x45 && head[2] === 0xDF && head[3] === 0xA3, }; if (!sigs[mime]) return false; try { return sigs[mime](); } catch { return false; } } const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOAD_DIR), filename: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); cb(null, crypto.randomUUID() + ext); }, }); function fileFilter(req, file, cb) { const ext = path.extname(file.originalname).toLowerCase(); if (!ALLOWED_EXT.includes(ext)) { return cb(new Error('不支持的文件类型,仅允许: ' + ALLOWED_EXT.join(', '))); } cb(null, true); } const upload = multer({ storage, fileFilter, limits: { fileSize: 50 * 1024 * 1024, files: 5, }, }); function finalizeUpload(req, res, next) { if (!req.files || req.files.length === 0) return next(); const filePaths = []; try { for (const file of req.files) { const fd = fs.openSync(file.path, 'r'); const buf = Buffer.alloc(256); fs.readSync(fd, buf, 0, 256, 0); fs.closeSync(fd); filePaths.push(file.path); if (!ALLOWED_MIME[file.mimetype]) throw new Error(`不支持的文件类型: ${file.mimetype}`); if (!validateMagicBytes(buf, file.mimetype)) throw new Error('文件内容与声明类型不符,可能是恶意文件'); } next(); } catch (e) { for (const fp of filePaths) { try { fs.unlinkSync(fp); } catch {} } return res.status(400).json({ error: e.message || '文件校验失败' }); } } module.exports = { upload, finalizeUpload, validateMagicBytes, UPLOAD_DIR, ALLOWED_MIME };