feat: MC Report System - MySQL + Express + Vanilla JS SPA
This commit is contained in:
75
backend/middleware/upload.js
Normal file
75
backend/middleware/upload.js
Normal file
@@ -0,0 +1,75 @@
|
||||
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': () => buffer.includes(Buffer.from('ftyp')),
|
||||
'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();
|
||||
for (const file of req.files) {
|
||||
const buf = fs.readFileSync(file.path);
|
||||
if (!ALLOWED_MIME[file.mimetype]) {
|
||||
fs.unlinkSync(file.path);
|
||||
return res.status(400).json({ error: `不支持的文件类型: ${file.mimetype}` });
|
||||
}
|
||||
if (!validateMagicBytes(buf, file.mimetype)) {
|
||||
fs.unlinkSync(file.path);
|
||||
return res.status(400).json({ error: '文件内容与声明类型不符,可能是恶意文件' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { upload, finalizeUpload, validateMagicBytes, UPLOAD_DIR, ALLOWED_MIME };
|
||||
Reference in New Issue
Block a user