2025-10-26 09:11:04 +08:00

199 lines
6.6 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 { writeFile, mkdir } 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 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
})
// Validation
if (!previewImage || !zipFile || !productModel || !categoryName || !sceneName) {
console.log('[Upload] Validation failed - missing fields')
return NextResponse.json(
{ error: '所有字段都是必填的' },
{ 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)
// 验证预览图大小(限制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)`)
let processedBuffer: Buffer
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)
const previewFileName = `preview-${Date.now()}-${processedFileName}`
const previewUrl = await uploadToS3(
processedBuffer,
previewFileName,
'image/webp'
)
console.log('[Upload] Image uploaded to S3:', previewUrl)
// Save zip file locally
console.log('[Upload] Saving zip 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)
const zipBuffer = Buffer.from(await zipFile.arrayBuffer())
await writeFile(zipFilePath, zipBuffer)
const zipFileSize = BigInt(zipBuffer.length) // 记录文件大小(字节)
console.log('[Upload] Zip 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,
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 }
)
}
}