import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { uploadToS3 } from '@/lib/s3'; import path from 'path'; import { assertBoundedMultipartHeaders, assertMultipartFiles } from '@/lib/multipart-request-guard'; import { boundedMultipartRequestLimiter, getBoundedMultipartFailure, MULTIPART_ROUTE_LIMITS, } from '@/lib/multipart-route-limits'; // POST - 上传文件到 S3 // 任务-详情类型:保持原图质量,不做压缩处理,确保素材完整性 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.legacyS3RequestBytes); releaseRequestSlot = await boundedMultipartRequestLimiter.acquire(); const formData = await request.formData(); assertMultipartFiles(formData, { fields: { file: { maxCount: 1, maxFileBytes: MULTIPART_ROUTE_LIMITS.legacyS3FileBytes, allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/tiff', 'image/avif'], }, }, maxFiles: 1, maxTotalBytes: MULTIPART_ROUTE_LIMITS.legacyS3FileBytes, }); const file = formData.get('file') as File; const type = formData.get('type') as string; // main/sku/detail if (!file) { return NextResponse.json({ error: '缺少文件' }, { status: 400 }); } // 文件大小检查(任务-详情允许较大文件,最大100MB) if (file.size > MULTIPART_ROUTE_LIMITS.legacyS3FileBytes) { return NextResponse.json( { error: `图片过大,请使用小于100MB的图片` }, { status: 400 } ); } // 读取文件buffer const buffer = Buffer.from(await file.arrayBuffer()); // 获取原始文件扩展名 const originalExt = path.extname(file.name).toLowerCase() || '.jpg'; // 保持原始格式,不转换为webp const mimeType = file.type || 'image/jpeg'; console.log(`[S3 Upload] ========== Processing file (NO COMPRESSION) ==========`); console.log(`[S3 Upload] Type: ${type}`); console.log(`[S3 Upload] Original fileName: ${file.name}`); console.log(`[S3 Upload] Original file size: ${buffer.length} bytes (${(buffer.length / 1024 / 1024).toFixed(2)} MB)`); console.log(`[S3 Upload] MIME type: ${mimeType}`); console.log(`[S3 Upload] 任务-详情类型:保持原图质量,不做压缩处理`); // 生成文件路径(保持原始格式) const timestamp = Date.now(); const randomSuffix = Math.random().toString(36).substring(2, 9); // 7位随机字符串 const fileName = `${type}-${timestamp}-${randomSuffix}${originalExt}`; const s3Path = `product-package/${fileName}`; console.log(`[S3 Upload] Generated fileName: ${fileName}`); console.log(`[S3 Upload] S3 path: ${s3Path}`); // 直接上传原图到 S3(不压缩) const url = await uploadToS3(buffer, s3Path, mimeType); console.log(`[S3 Upload] Uploaded successfully: url=${url}`); console.log(`[S3 Upload] ====================================`); return NextResponse.json({ url }); } catch (error) { console.error('[S3 Upload] 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: '上传失败: ' + (error instanceof Error ? error.message : '未知错误') }, { status: 500 } ); } finally { releaseRequestSlot?.(); } }