chenpei e69f15337d feat: Add automatic PSD preview generation
- Add psd-processor library to auto-generate previews from PSD files
- Update upload API to detect PSD files and generate previews automatically
- Make preview image optional for PSD file uploads
- Update UploadModal UI to show PSD files don't require manual preview upload
- Install ag-psd package for PSD processing
2025-10-26 15:49:33 +08:00

297 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { uploadToS3 } from '@/lib/s3'
import { processImage, getProcessedFileName } from '@/lib/image-processor'
import { generatePsdPreview, hasPsdExtension } from '@/lib/psd-processor'
import { writeFile, mkdir, unlink } from 'fs/promises'
import path from 'path'
// Configure route to handle large files
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
// Increase timeout for large file uploads
export const maxDuration = 10800 // 3 hours timeout (10800 seconds)
export async function POST(req: NextRequest) {
try {
console.log('[Upload] Starting upload process...')
const session = await getServerSession(authOptions)
console.log('[Upload] Session checked:', !!session)
if (!session) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
)
}
console.log('[Upload] Parsing form data...')
const formData = await req.formData()
const previewImage = formData.get('previewImage') as File
const zipFile = formData.get('zipFile') as File
const productModel = formData.get('productModel') as string
const categoryName = formData.get('categoryName') as string
const sceneName = formData.get('sceneName') as string
const materialType = (formData.get('materialType') as string) || '3DMAX'
const renderer = (formData.get('renderer') as string) || 'CR'
console.log('[Upload] Form data received:', {
hasPreviewImage: !!previewImage,
previewImageSize: previewImage?.size,
hasZipFile: !!zipFile,
zipFileSize: zipFile?.size,
productModel,
categoryName,
sceneName,
materialType
})
// Check if file is PSD
const isPsdFile = zipFile && hasPsdExtension(zipFile.name)
console.log('[Upload] File type check:', { isPsdFile, fileName: zipFile?.name })
// Validation - preview image is optional for PSD files
if (!zipFile || !productModel || !categoryName || !sceneName) {
console.log('[Upload] Validation failed - missing required fields')
return NextResponse.json(
{ error: '产品型号、分类、场景和文件都是必填的' },
{ status: 400 }
)
}
// For non-PSD files, preview image is required
if (!isPsdFile && !previewImage) {
console.log('[Upload] Validation failed - non-PSD file requires preview image')
return NextResponse.json(
{ error: '请上传预览图,或者上传PSD文件以自动生成预览图' },
{ status: 400 }
)
}
// 获取用户信息作为作者花名
console.log('[Upload] Fetching user info...')
const user = await prisma.user.findUnique({
where: { id: (session.user as any).id },
select: { username: true },
})
if (!user) {
console.log('[Upload] User not found')
return NextResponse.json(
{ error: '用户不存在' },
{ status: 404 }
)
}
const authorName = user.username
console.log('[Upload] User found:', authorName)
let processedBuffer: Buffer
let previewFileName: string
let previewUrl: string
// Handle preview image generation
if (isPsdFile && !previewImage) {
// Auto-generate preview from PSD file
console.log('[Upload] PSD file detected, auto-generating preview...')
try {
// Save PSD file temporarily
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const tempPsdFileName = `temp-psd-${Date.now()}-${zipFile.name}`
const tempPsdPath = path.join(uploadDir, tempPsdFileName)
const psdBuffer = Buffer.from(await zipFile.arrayBuffer())
await writeFile(tempPsdPath, psdBuffer)
console.log('[Upload] Temporary PSD saved:', tempPsdPath)
// Generate preview image
const tempPreviewPath = path.join(uploadDir, `temp-preview-${Date.now()}.jpg`)
const result = await generatePsdPreview(tempPsdPath, tempPreviewPath, 800)
if (!result.success || !result.previewPath) {
// Clean up temp files
await unlink(tempPsdPath).catch(() => {})
await unlink(tempPreviewPath).catch(() => {})
console.error('[Upload] PSD preview generation failed:', result.error)
return NextResponse.json(
{ error: `PSD预览图生成失败:${result.error}。请手动上传预览图。` },
{ status: 500 }
)
}
console.log('[Upload] PSD preview generated successfully')
// Read the generated preview and process it
const { readFile } = await import('fs/promises')
const generatedBuffer = await readFile(tempPreviewPath)
// Process the generated preview (compress and optimize)
processedBuffer = await processImage(generatedBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
// Clean up temporary files
await unlink(tempPsdPath).catch(() => {})
await unlink(tempPreviewPath).catch(() => {})
console.log('[Upload] Temporary files cleaned up')
// Upload to S3
previewFileName = `preview-${Date.now()}-generated.webp`
previewUrl = await uploadToS3(processedBuffer, previewFileName, 'image/webp')
console.log('[Upload] Auto-generated preview uploaded to S3:', previewUrl)
} catch (psdError) {
console.error('[Upload] PSD processing error:', psdError)
return NextResponse.json(
{ error: `PSD文件处理失败:${psdError instanceof Error ? psdError.message : '未知错误'}。请手动上传预览图。` },
{ status: 500 }
)
}
} else {
// Use provided preview image
if (!previewImage) {
return NextResponse.json(
{ error: '请上传预览图' },
{ status: 400 }
)
}
// 验证预览图大小(限制50MB)
const maxPreviewSize = 50 * 1024 * 1024 // 50MB
if (previewImage.size > maxPreviewSize) {
console.log(`[Upload] Preview image too large: ${(previewImage.size / 1024 / 1024).toFixed(2)}MB`)
return NextResponse.json(
{ error: `预览图过大,请使用小于50MB的图片(当前:${(previewImage.size / 1024 / 1024).toFixed(2)}MB)` },
{ status: 400 }
)
}
// 验证预览图格式
const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
if (!allowedImageTypes.includes(previewImage.type)) {
console.log(`[Upload] Invalid image type: ${previewImage.type}`)
return NextResponse.json(
{ error: `不支持的图片格式:${previewImage.type},请使用 JPG、PNG、WebP 或 GIF 格式` },
{ status: 400 }
)
}
// 处理预览图:压缩到300KB以内,转换为webP,缩放到600px
console.log(`[Upload] Processing image... (${previewImage.type}, ${(previewImage.size / 1024).toFixed(2)}KB)`)
try {
const originalBuffer = Buffer.from(await previewImage.arrayBuffer())
processedBuffer = await processImage(originalBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
console.log('[Upload] Image processed successfully')
} catch (imageError) {
console.error('[Upload] Image processing error:', imageError)
return NextResponse.json(
{ error: `图片处理失败,请确保图片文件未损坏且格式正确。错误:${imageError instanceof Error ? imageError.message : '未知错误'}` },
{ status: 500 }
)
}
// Upload processed preview image to S3
console.log('[Upload] Uploading image to S3...')
const processedFileName = getProcessedFileName(previewImage.name)
previewFileName = `preview-${Date.now()}-${processedFileName}`
previewUrl = await uploadToS3(processedBuffer, previewFileName, 'image/webp')
console.log('[Upload] Image uploaded to S3:', previewUrl)
}
// Save zip/PSD file locally
console.log('[Upload] Saving file locally...')
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const zipFileName = `zip-${Date.now()}-${zipFile.name}`
const zipFilePath = path.join(uploadDir, zipFileName)
// If it's a PSD file, we already have the buffer from above
// Otherwise, read the buffer now
let zipBuffer: Buffer
if (isPsdFile && !previewImage) {
// Reread the PSD file for storage (we cleaned up the temp file already)
zipBuffer = Buffer.from(await zipFile.arrayBuffer())
} else {
zipBuffer = Buffer.from(await zipFile.arrayBuffer())
}
await writeFile(zipFilePath, zipBuffer)
const zipFileSize = BigInt(zipBuffer.length) // 记录文件大小(字节)
console.log('[Upload] File saved:', zipFilePath, `(${(Number(zipFileSize) / 1024 / 1024).toFixed(2)} MB)`)
// Create material record
console.log('[Upload] Creating database record...')
const material = await prisma.material.create({
data: {
previewUrl,
productModel,
categoryName,
sceneName,
materialType,
renderer,
authorName,
zipFileName,
zipFilePath,
zipFileSize,
userId: (session.user as any).id,
},
include: {
user: {
select: {
username: true,
},
},
},
})
console.log('[Upload] Database record created:', material.id)
// 转换BigInt为字符串以支持JSON序列化
const materialData = {
...material,
zipFileSize: material.zipFileSize.toString()
}
console.log('[Upload] Upload completed successfully!')
return NextResponse.json(
{ message: '上传成功', material: materialData },
{ status: 201 }
)
} catch (error) {
console.error('Upload error:', error)
// Log detailed error information
if (error instanceof Error) {
console.error('Error name:', error.name)
console.error('Error message:', error.message)
console.error('Error stack:', error.stack)
}
// Return more specific error message for debugging
const errorMessage = error instanceof Error ? error.message : '上传失败,请稍后再试'
return NextResponse.json(
{
error: '上传失败,请稍后再试',
details: process.env.NODE_ENV === 'development' ? errorMessage : undefined
},
{ status: 500 }
)
}
}