148 lines
5.0 KiB
TypeScript

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';
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) {
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 });
}
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) {
console.error('[PSD Upload] Error:', error);
return NextResponse.json(
{ error: '上传失败: ' + (error instanceof Error ? error.message : '未知错误') },
{ status: 500 }
);
}
}