// 上传预览图API import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { uploadToS3 } from '@/lib/s3'; import { compressImage, sanitizeFileName } from '@/lib/image-processor'; import { randomBytes } from 'crypto'; import { assertBoundedMultipartHeaders, assertMultipartFiles } from '@/lib/multipart-request-guard'; import { boundedMultipartRequestLimiter, getBoundedMultipartFailure, MULTIPART_ROUTE_LIMITS, } from '@/lib/multipart-route-limits'; export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export const maxDuration = 10800 // 生成唯一文件名 function generateUniqueFilename(prefix: string, ext: string): string { const timestamp = Date.now(); const random = randomBytes(8).toString('hex'); return `${prefix}-${timestamp}-${random}${ext}`; } // POST - 上传预览图 export async function POST(request: NextRequest) { let releaseRequestSlot: (() => void) | null = null; try { const session = await getServerSession(authOptions); if (!session) { return NextResponse.json( { error: '请先登录' }, { status: 401 } ); } assertBoundedMultipartHeaders(request.headers, MULTIPART_ROUTE_LIMITS.previewRequestBytes); releaseRequestSlot = await boundedMultipartRequestLimiter.acquire(); const formData = await request.formData(); assertMultipartFiles(formData, { fields: { file: { maxCount: 1, maxFileBytes: MULTIPART_ROUTE_LIMITS.previewFileBytes, allowedMimeTypes: ['image/*'], }, }, maxFiles: 1, maxTotalBytes: MULTIPART_ROUTE_LIMITS.previewFileBytes, }); const file = formData.get('file') as File; if (!file) { return NextResponse.json( { error: '请选择文件' }, { status: 400 } ); } // 验证文件类型 if (!file.type.startsWith('image/')) { return NextResponse.json( { error: '只能上传图片文件' }, { status: 400 } ); } // 验证文件大小(50MB限制) if (file.size > MULTIPART_ROUTE_LIMITS.previewFileBytes) { return NextResponse.json( { error: '预览图过大,请使用小于50MB的图片' }, { status: 400 } ); } // 转换为Buffer const buffer = Buffer.from(await file.arrayBuffer()); // 压缩预览图 console.log('[Preview Upload] Compressing preview image...'); const compressedBuffer = await compressImage(buffer, { maxSizeKB: 300, maxWidth: 1200, maxHeight: 1600, format: 'webp' }); // 生成文件名并上传到S3 const filename = generateUniqueFilename('preview', '.webp'); console.log('[Preview Upload] Uploading compressed preview image to S3...'); const url = await uploadToS3(compressedBuffer, filename, 'image/webp'); console.log('[Preview Upload] Preview image uploaded:', url); return NextResponse.json({ url }); } catch (error) { console.error('上传预览图失败:', error); const boundedFailure = getBoundedMultipartFailure(error); if (boundedFailure) { return NextResponse.json( { error: boundedFailure.message, code: boundedFailure.code }, { status: boundedFailure.status, ...(boundedFailure.retryAfterSeconds ? { headers: { 'Retry-After': String(boundedFailure.retryAfterSeconds) } } : {}), }, ); } return NextResponse.json( { error: '上传失败,请稍后重试' }, { status: 500 } ); } finally { releaseRequestSlot?.(); } }