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'; import { reset as resetHandler }from './class/httphandler'; 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); // logging http access if (config.logging != "none") { app.use(morgan(config.logging)); } // const signal = require('./signaling'); app.use(cors({origin: '*'})); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.get('/config', (req, res) => res.json({ useWebSocket: config.type == 'websocket', startupMode: config.mode, logging: config.logging, protocol: config.secure ? 'https' : 'http', port: config.port })); app.use('/signaling', signaling); app.use(express.static(path.join(__dirname, '../client/public'))); app.use('/module', express.static(path.join(__dirname, '../client/src'))); app.get('/', (req, res) => { const indexPagePath: string = path.join(__dirname, '../client/public/index.html'); fs.access(indexPagePath, (err) => { if (err) { log(LogLevel.warn, `Can't find file ' ${indexPagePath}`); res.status(404).send(`Can't find file ${indexPagePath}`); } else { res.sendFile(indexPagePath); } }); }); // 初始化Swagger initSwagger(app, config); // 配置multer存储 const storage = multer.diskStorage({ destination: function (req: any, file: any, cb: (error: Error | null, destination: string) => void) { // 确保上传目录存在 const uploadDir = path.join(__dirname, '../client/public/uploads/avatars'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: function (req: any, file: any, cb: (error: Error | null, filename: string) => void) { // 临时使用原始文件名,稍后在API处理中重命名 cb(null, file.originalname); } }); 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', (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 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); return res.status(500).json({ success: false, message: '文件重命名失败' }); } const avatarUrl = `/uploads/avatars/${newFilename}`; res.json({ success: true, avatarUrl: avatarUrl }); }); }); }); // 确保uploads目录可访问 app.use('/uploads', express.static(path.join(__dirname, '../client/public/uploads'))); return app; };