feat(API): 添加文件上传接口
- 实现 POST /api/files/upload 端点 - 支持图片、PDF、Word、Excel、文本、代码等格式 - 限制最大文件大小为 20MB - 自动创建 public/uploads 目录存储上传文件 - 使用 nanoid 生成唯一文件名 - 返回文件信息包含 URL、大小、类型等 - 提供 GET 端点获取上传配置
This commit is contained in:
parent
cb86380e7f
commit
66a58a2d3d
131
src/app/api/files/upload/route.ts
Normal file
131
src/app/api/files/upload/route.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writeFile, mkdir } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
// 允许的 MIME 类型
|
||||
const ALLOWED_MIME_TYPES = [
|
||||
// 图片
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
// PDF
|
||||
'application/pdf',
|
||||
// Word
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
// Excel
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
// 文本
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
'text/csv',
|
||||
// 代码
|
||||
'text/javascript',
|
||||
'text/typescript',
|
||||
'text/html',
|
||||
'text/css',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
];
|
||||
|
||||
// 最大文件大小 20MB
|
||||
const MAX_FILE_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
// 上传目录
|
||||
const UPLOAD_DIR = path.join(process.cwd(), 'public', 'uploads');
|
||||
|
||||
// 确保上传目录存在
|
||||
async function ensureUploadDir() {
|
||||
if (!existsSync(UPLOAD_DIR)) {
|
||||
await mkdir(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
function getExtension(filename: string): string {
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
return ext || '.bin';
|
||||
}
|
||||
|
||||
// POST /api/files/upload - 上传文件
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: '没有提供文件' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return NextResponse.json(
|
||||
{ error: `文件大小超过限制(最大 ${MAX_FILE_SIZE / 1024 / 1024}MB)` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查 MIME 类型(允许一些额外的代码文件类型)
|
||||
const isAllowedType = ALLOWED_MIME_TYPES.includes(file.type) ||
|
||||
file.type.startsWith('text/') ||
|
||||
file.type === 'application/octet-stream'; // 允许未知类型(通过扩展名判断)
|
||||
|
||||
if (!isAllowedType) {
|
||||
return NextResponse.json(
|
||||
{ error: `不支持的文件类型: ${file.type}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
await ensureUploadDir();
|
||||
|
||||
// 生成唯一文件名
|
||||
const fileId = nanoid();
|
||||
const ext = getExtension(file.name);
|
||||
const filename = `${fileId}${ext}`;
|
||||
const filepath = path.join(UPLOAD_DIR, filename);
|
||||
|
||||
// 读取文件内容
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
|
||||
// 写入文件
|
||||
await writeFile(filepath, buffer);
|
||||
|
||||
// 返回文件信息
|
||||
const fileUrl = `/uploads/${filename}`;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
fileId,
|
||||
filename,
|
||||
originalName: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type,
|
||||
url: fileUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('File upload error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '文件上传失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/files/upload - 获取上传配置
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
maxFileSize: MAX_FILE_SIZE,
|
||||
allowedMimeTypes: ALLOWED_MIME_TYPES,
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user