first commit

This commit is contained in:
2025-09-23 07:35:11 +00:00
commit a5dd3f1335
110 changed files with 46108 additions and 0 deletions

262
api/routes/upload.ts Executable file
View File

@@ -0,0 +1,262 @@
/**
* 文件上传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;