import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; import { canManageMaterial, type MaterialAuthorizationUser, } from '@/lib/material-authorization'; import { buildStoredUploadPath, getUploadRootDir } from '@/lib/upload-paths'; import { ensureSafeDirectoryWithinRoot, isPathFreeOfSymbolicLinks, normalizeSafeFileName, resolveStrictChildPath, } from '@/lib/path-security'; import { randomBytes } from 'crypto'; import { writeFile } from 'fs/promises'; import path from 'path'; import { boundedMultipartRequestLimiter, getBoundedMultipartFailure, } from '@/lib/multipart-route-limits'; const MAX_PSD_FILE_BYTES = 128 * 1024 * 1024; const MAX_PSD_REQUEST_BYTES = MAX_PSD_FILE_BYTES + 2 * 1024 * 1024; const ALLOWED_PSD_MIME_TYPES = new Set([ '', 'application/octet-stream', 'application/vnd.adobe.photoshop', 'application/x-photoshop', 'image/vnd.adobe.photoshop', 'image/x-photoshop', ]); function detectPhotoshopDocumentType(buffer: Buffer): '.psd' | '.psb' | null { if (buffer.length < 6 || buffer.subarray(0, 4).toString('ascii') !== '8BPS') { return null; } const version = buffer.readUInt16BE(4); if (version === 1) return '.psd'; if (version === 2) return '.psb'; return null; } // POST - 上传 PSD 文件到本地 export async function POST(request: NextRequest) { let releaseRequestSlot: (() => void) | null = null; try { const session = await getServerSession(authOptions); const userId = String((session?.user as { id?: string } | undefined)?.id || '').trim(); if (!userId) { return NextResponse.json({ error: '请先登录' }, { status: 401 }); } const contentLength = Number(request.headers.get('content-length')); if (!Number.isFinite(contentLength) || contentLength <= 0) { return NextResponse.json({ error: '缺少有效的请求大小' }, { status: 411 }); } if (contentLength > MAX_PSD_REQUEST_BYTES) { return NextResponse.json({ error: 'PSD 文件过大' }, { status: 413 }); } releaseRequestSlot = await boundedMultipartRequestLimiter.acquire(); const formData = await request.formData(); const file = formData.get('file'); const materialId = String(formData.get('materialId') || '').trim(); if ( !file || typeof (file as File).name !== 'string' || typeof (file as File).arrayBuffer !== 'function' || !materialId ) { return NextResponse.json({ error: '缺少文件或素材ID' }, { status: 400 }); } const uploadFile = file as File; const safeOriginalFileName = normalizeSafeFileName(uploadFile.name); const extension = path.extname(safeOriginalFileName || '').toLowerCase(); if ( !safeOriginalFileName || (extension !== '.psd' && extension !== '.psb') || !ALLOWED_PSD_MIME_TYPES.has(String(uploadFile.type || '').toLowerCase()) ) { return NextResponse.json({ error: '只支持有效的 PSD 或 PSB 文件' }, { status: 400 }); } if (uploadFile.size <= 0 || uploadFile.size > MAX_PSD_FILE_BYTES) { return NextResponse.json({ error: 'PSD 文件大小无效或超过限制' }, { status: 413 }); } const material = await prisma.material.findUnique({ where: { id: materialId }, select: { id: true, userId: true, deletedAt: true, }, }); if (!material || material.deletedAt) { return NextResponse.json({ error: '素材不存在' }, { status: 404 }); } if ( !(await canManageMaterial( session!.user as MaterialAuthorizationUser, material, )) ) { return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 }); } if (normalizeSafeFileName(material.id) !== material.id) { throw new Error('素材存储标识不安全'); } const buffer = Buffer.from(await uploadFile.arrayBuffer()); const detectedExtension = detectPhotoshopDocumentType(buffer); if (!detectedExtension || detectedExtension !== extension) { return NextResponse.json({ error: 'PSD 文件头或版本无效' }, { status: 400 }); } const uploadRoot = getUploadRootDir(); const uploadDir = await ensureSafeDirectoryWithinRoot( uploadRoot, 'product-package', material.id, 'psd', ); if (!uploadDir) { throw new Error('PSD 上传目录不安全'); } const fileName = `psd-${Date.now()}-${randomBytes(8).toString('hex')}${detectedExtension}`; const filePath = resolveStrictChildPath(uploadDir, fileName); if ( !filePath || !(await isPathFreeOfSymbolicLinks(uploadRoot, filePath, { allowMissingLeaf: true })) ) { throw new Error('PSD 最终文件路径不安全'); } await writeFile(filePath, buffer, { flag: 'wx', mode: 0o600 }); // 返回相对路径 const relativePath = buildStoredUploadPath('product-package', material.id, 'psd', fileName); return NextResponse.json({ path: relativePath }); } catch (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) } } : {}), }, ); } console.error('[PSD Upload] Error:', error); return NextResponse.json( { error: '上传失败: ' + (error instanceof Error ? error.message : '未知错误') }, { status: 500 } ); } finally { releaseRequestSlot?.(); } }