From 66a58a2d3d9921aa4a30cb4e70c439a7718c293b Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sat, 20 Dec 2025 12:15:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(API):=20=E6=B7=BB=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 POST /api/files/upload 端点 - 支持图片、PDF、Word、Excel、文本、代码等格式 - 限制最大文件大小为 20MB - 自动创建 public/uploads 目录存储上传文件 - 使用 nanoid 生成唯一文件名 - 返回文件信息包含 URL、大小、类型等 - 提供 GET 端点获取上传配置 --- src/app/api/files/upload/route.ts | 131 ++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/app/api/files/upload/route.ts diff --git a/src/app/api/files/upload/route.ts b/src/app/api/files/upload/route.ts new file mode 100644 index 0000000..71adee4 --- /dev/null +++ b/src/app/api/files/upload/route.ts @@ -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, + }); +}