first commit
This commit is contained in:
262
api/routes/upload.ts
Executable file
262
api/routes/upload.ts
Executable 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;
|
||||
Reference in New Issue
Block a user