This commit is contained in:
2026-05-23 22:47:34 +08:00
parent 690ebac266
commit 5fdc70c645
3 changed files with 138 additions and 15 deletions

View File

@@ -2,6 +2,7 @@ import * as express from 'express';
import * as path from 'path';
import * as fs from 'fs';
import * as morgan from 'morgan';
import { v4 as uuid } from 'uuid';
import signaling from './signaling';
import { log, LogLevel } from './log';
import Options from './class/options';
@@ -11,6 +12,34 @@ import { initSwagger } from './swagger';
const cors = require('cors');
const multer = require('multer');
const AVATAR_UPLOAD_LIMIT_BYTES = 2 * 1024 * 1024;
const ALLOWED_AVATAR_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
const ALLOWED_AVATAR_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
function safeAvatarExtension(file: any): string {
const originalExt = path.extname(file.originalname || '').toLowerCase();
if (ALLOWED_AVATAR_EXTENSIONS.has(originalExt)) {
return originalExt;
}
switch (file.mimetype) {
case 'image/jpeg':
return '.jpg';
case 'image/png':
return '.png';
case 'image/webp':
return '.webp';
case 'image/gif':
return '.gif';
default:
return '';
}
}
function isAllowedAvatar(file: any): boolean {
const ext = path.extname(file.originalname || '').toLowerCase();
return ALLOWED_AVATAR_MIME_TYPES.has(file.mimetype) && ALLOWED_AVATAR_EXTENSIONS.has(ext);
}
export const createServer = (config: Options): express.Express => {
const app: express.Express = express();
resetHandler(config.mode);
@@ -62,30 +91,58 @@ export const createServer = (config: Options): express.Express => {
}
});
const upload = multer({ storage: storage });
const upload = multer({
storage: storage,
limits: {
fileSize: AVATAR_UPLOAD_LIMIT_BYTES
},
fileFilter: (_req: express.Request, file: any, cb: (error: Error | null, acceptFile?: boolean) => void) => {
if (!isAllowedAvatar(file)) {
cb(new Error('Only jpg, png, webp, or gif avatars are allowed'));
return;
}
cb(null, true);
}
});
// 头像上传API
app.post('/api/upload/avatar', upload.single('avatar'), (req: any, res: express.Response) => {
if (!req.file) {
return res.status(400).json({ success: false, message: 'No file uploaded' });
}
app.post('/api/upload/avatar', (req: express.Request, res: express.Response) => {
upload.single('avatar')(req, res, (error: Error) => {
if (error) {
log(LogLevel.warn, 'Avatar upload rejected:', error.message);
const isSizeLimit = error.name === 'MulterError' && (error as any).code === 'LIMIT_FILE_SIZE';
return res.status(400).json({
success: false,
message: isSizeLimit ? 'Avatar file is too large' : error.message
});
}
const userId = req.body.userId || 'unknown';
const ext = path.extname(req.file.originalname);
const oldPath = req.file.path;
const newFilename = `${userId}${ext}`;
const newPath = path.join(path.dirname(oldPath), newFilename);
const request = req as any;
if (!request.file) {
return res.status(400).json({ success: false, message: 'No file uploaded' });
}
const ext = safeAvatarExtension(request.file);
if (!ext) {
fs.unlink(request.file.path, () => undefined);
return res.status(400).json({ success: false, message: 'Unsupported avatar file type' });
}
const oldPath = request.file.path;
const newFilename = `avatar_${uuid()}${ext}`;
const newPath = path.join(path.dirname(oldPath), newFilename);
// 重命名文件
fs.rename(oldPath, newPath, (err) => {
if (err) {
log(LogLevel.error, 'Error renaming file:', err);
fs.rename(oldPath, newPath, (err) => {
if (err) {
log(LogLevel.error, 'Error renaming file:', err);
return res.status(500).json({ success: false, message: '文件重命名失败' });
}
const avatarUrl = `/uploads/avatars/${newFilename}`;
res.json({ success: true, avatarUrl: avatarUrl });
});
});
});
// 确保uploads目录可访问