199 lines
6.6 KiB
TypeScript
199 lines
6.6 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 { 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 }
|
||
)
|
||
}
|
||
}
|
||
|