Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { getServerSession } from 'next-auth';
|
||
import { authOptions } from '@/lib/auth';
|
||
import { uploadToS3 } from '@/lib/s3';
|
||
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; // original-main/original-preview等
|
||
|
||
if (!file) {
|
||
return NextResponse.json({ error: '缺少文件' }, { status: 400 });
|
||
}
|
||
|
||
// 文件大小检查(原图最大100MB)
|
||
if (file.size > MULTIPART_ROUTE_LIMITS.legacyS3FileBytes) {
|
||
return NextResponse.json(
|
||
{ error: `文件过大,请使用小于100MB的图片` },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
console.log(`[S3 Original Upload] ========== Processing original file ==========`);
|
||
console.log(`[S3 Original Upload] Type: ${type}`);
|
||
console.log(`[S3 Original Upload] Original fileName: ${file.name}`);
|
||
console.log(`[S3 Original Upload] File size: ${file.size} bytes (${(file.size / 1024 / 1024).toFixed(2)} MB)`);
|
||
console.log(`[S3 Original Upload] ⚠️ NO COMPRESSION - Uploading as-is`);
|
||
|
||
// 读取文件buffer(不压缩)
|
||
const buffer = Buffer.from(await file.arrayBuffer());
|
||
|
||
// 生成文件路径(保持原文件扩展名)
|
||
const timestamp = Date.now();
|
||
const randomSuffix = Math.random().toString(36).substring(2, 9);
|
||
const extension = file.name.split('.').pop() || 'jpg';
|
||
const fileName = `${type}-${timestamp}-${randomSuffix}.${extension}`;
|
||
const s3Path = `product-package/${fileName}`;
|
||
|
||
console.log(`[S3 Original Upload] Generated fileName: ${fileName}`);
|
||
console.log(`[S3 Original Upload] S3 path: ${s3Path}`);
|
||
|
||
// 直接上传原图到 S3(不压缩)
|
||
const url = await uploadToS3(buffer, s3Path, file.type || 'image/jpeg');
|
||
|
||
console.log(`[S3 Original Upload] Uploaded successfully: url=${url}`);
|
||
console.log(`[S3 Original Upload] ==========================================`);
|
||
|
||
return NextResponse.json({ url });
|
||
} catch (error) {
|
||
console.error('[S3 Original 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?.();
|
||
}
|
||
}
|