Files
ggl/api/routes/upload.ts
2025-09-23 07:35:11 +00:00

262 lines
7.9 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件上传API路由
* Handle file uploads for videos and cover images
*/
import { Router, type Request, type Response } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { authenticateToken } from '../middleware/auth.js';
const router = Router();
// Multer错误处理中间件
const handleMulterError = (error: any, req: Request, res: Response, next: any) => {
console.error('Multer错误:', error);
if (error instanceof multer.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
return res.status(400).json({
success: false,
message: '文件大小超出限制最大100MB'
});
case 'LIMIT_FILE_COUNT':
return res.status(400).json({
success: false,
message: '文件数量超出限制'
});
case 'LIMIT_UNEXPECTED_FILE':
return res.status(400).json({
success: false,
message: '意外的文件字段'
});
default:
return res.status(400).json({
success: false,
message: `文件上传错误: ${error.message}`
});
}
}
if (error.message) {
return res.status(400).json({
success: false,
message: error.message
});
}
return res.status(500).json({
success: false,
message: '文件上传失败'
});
};
// 确保上传目录存在
const ensureUploadDir = (dir: string) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
};
// 配置multer用于视频文件上传
const videoStorage = multer.memoryStorage();
const videoUpload = multer({
storage: videoStorage,
limits: {
fileSize: 100 * 1024 * 1024 // 100MB限制
},
fileFilter: (req, file, cb) => {
console.log('视频文件过滤器检查,文件类型:', file.mimetype, '文件名:', file.originalname);
// 获取文件扩展名
const fileExtension = path.extname(file.originalname).toLowerCase();
const mimeType = file.mimetype ? file.mimetype.toLowerCase() : '';
// 支持的MIME类型与前端保持一致
const allowedTypes = [
// 标准视频MIME类型
'video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo',
'video/x-ms-wmv', 'video/x-flv', 'video/webm', 'video/ogg',
'video/3gpp', 'video/3gpp2', 'video/x-matroska', 'video/mp2t',
'video/x-ms-asf', 'video/x-m4v', 'video/divx', 'video/xvid',
// 应用程序类型(某些视频格式)
'application/octet-stream', 'application/x-msvideo',
'application/vnd.rn-realmedia', 'application/x-shockwave-flash'
];
// 支持的文件扩展名
const allowedExtensions = [
'.mp4', '.webm', '.ogg', '.avi', '.mov', '.3gp', '.3g2',
'.wmv', '.flv', '.mkv', '.m4v', '.ts', '.asf', '.mpg',
'.mpeg', '.m2v', '.rmvb', '.rm', '.divx', '.xvid',
'.f4v', '.m2ts', '.mts', '.vob', '.dat', '.amv'
];
// 验证逻辑:
// 1. 扩展名匹配(优先级最高)
// 2. MIME类型完全匹配
// 3. MIME类型以video/开头且扩展名正确
// 4. MIME类型为空但扩展名正确
const isExtensionValid = allowedExtensions.includes(fileExtension);
const isMimeTypeValid = allowedTypes.includes(mimeType);
const isVideoMimeType = mimeType.startsWith('video/');
const isValid = isExtensionValid || isMimeTypeValid || (isVideoMimeType && isExtensionValid) || (!mimeType && isExtensionValid);
console.log('文件验证结果:', {
fileExtension,
mimeType,
isExtensionValid,
isMimeTypeValid,
isVideoMimeType,
isValid
});
if (isValid) {
console.log('视频文件类型检查通过');
cb(null, true);
} else {
console.log('视频文件类型检查失败,不支持的格式:', mimeType, '扩展名:', fileExtension);
cb(new Error(`不支持的视频格式。文件类型: ${mimeType}, 扩展名: ${fileExtension}`));
}
}
});
// 配置multer用于封面图片上传
const coverStorage = multer.memoryStorage();
const coverUpload = multer({
storage: coverStorage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB限制
},
fileFilter: (req, file, cb) => {
console.log('封面图片过滤器检查,文件类型:', file.mimetype);
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
console.log('封面图片类型检查通过');
cb(null, true);
} else {
console.log('封面图片类型检查失败,不支持的格式:', file.mimetype);
cb(new Error('不支持的图片格式'));
}
}
});
/**
* 上传视频文件
* POST /api/upload/video
*/
router.post('/video', authenticateToken, videoUpload.single('video'), handleMulterError, async (req: Request, res: Response): Promise<void> => {
try {
console.log('视频文件上传请求开始');
const file = req.file;
if (!file) {
console.log('错误: 没有视频文件');
res.status(400).json({
success: false,
message: '请选择要上传的视频文件'
});
return;
}
// 生成文件名
const timestamp = Date.now();
const originalName = file.originalname;
const ext = path.extname(originalName);
const filename = `video_${timestamp}_${Math.random().toString(36).substring(2)}${ext}`;
// 确保上传目录存在
const uploadDir = path.join(process.cwd(), 'uploads', 'videos');
ensureUploadDir(uploadDir);
// 保存文件
const filePath = path.join(uploadDir, filename);
const relativePath = path.join('uploads', 'videos', filename).replace(/\\/g, '/');
console.log('开始保存视频文件到:', filePath);
fs.writeFileSync(filePath, file.buffer);
console.log('视频文件保存成功');
// 返回文件URL
const fileUrl = `/${relativePath}`;
res.status(200).json({
success: true,
message: '视频文件上传成功',
data: {
url: fileUrl,
filename: filename,
size: file.size,
mimetype: file.mimetype
}
});
} catch (error) {
console.error('视频文件上传失败:', error);
res.status(500).json({
success: false,
message: '视频文件上传失败'
});
}
});
/**
* 上传封面图片
* POST /api/upload/cover
*/
router.post('/cover', authenticateToken, coverUpload.single('cover'), handleMulterError, async (req: Request, res: Response): Promise<void> => {
try {
console.log('封面图片上传请求开始');
const file = req.file;
if (!file) {
console.log('错误: 没有封面图片文件');
res.status(400).json({
success: false,
message: '请选择要上传的封面图片'
});
return;
}
// 生成文件名
const timestamp = Date.now();
const originalName = file.originalname;
const ext = path.extname(originalName);
const filename = `cover_${timestamp}_${Math.random().toString(36).substring(2)}${ext}`;
// 确保上传目录存在
const uploadDir = path.join(process.cwd(), 'uploads', 'covers');
ensureUploadDir(uploadDir);
// 保存文件
const filePath = path.join(uploadDir, filename);
const relativePath = path.join('uploads', 'covers', filename).replace(/\\/g, '/');
console.log('开始保存封面图片到:', filePath);
fs.writeFileSync(filePath, file.buffer);
console.log('封面图片保存成功');
// 返回文件URL
const fileUrl = `/${relativePath}`;
res.status(200).json({
success: true,
message: '封面图片上传成功',
data: {
url: fileUrl,
filename: filename,
size: file.size,
mimetype: file.mimetype
}
});
} catch (error) {
console.error('封面图片上传失败:', error);
res.status(500).json({
success: false,
message: '封面图片上传失败'
});
}
});
export default router;