- Add generateUniqueFileName function using crypto.randomBytes - Replace Date.now() with timestamp + random hex for all file names - Ensures unique file names even when multiple users upload simultaneously - Fixes: temp PSD files, temp preview files, S3 preview files, and material files - Resolves reported issue of upload errors with concurrent users
309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
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'
|
||
import { randomBytes } from 'crypto'
|
||
|
||
// 生成唯一文件名(避免并发冲突)
|
||
function generateUniqueFileName(prefix: string, ext: string): string {
|
||
const timestamp = Date.now()
|
||
const randomId = randomBytes(8).toString('hex') // 16个字符的随机字符串
|
||
return `${prefix}-${timestamp}-${randomId}${ext}`
|
||
}
|
||
|
||
// 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 = generateUniqueFileName('temp-psd', `.${zipFile.name.split('.').pop()}`)
|
||
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, generateUniqueFileName('temp-preview', '.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,
|
||
maxHeight: 1000,
|
||
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 = generateUniqueFileName('preview', '-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,缩放适配300x500显示
|
||
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,
|
||
maxHeight: 1000,
|
||
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)
|
||
const fileExt = path.extname(processedFileName)
|
||
previewFileName = generateUniqueFileName('preview', fileExt)
|
||
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 zipFileExt = path.extname(zipFile.name)
|
||
const zipFileName = generateUniqueFileName('material', zipFileExt)
|
||
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 }
|
||
)
|
||
}
|
||
}
|
||
|