主要改动: 1. TCP缓冲区优化(16MB)- 解决公网上传速度慢问题 2. 并发数降低到1-3 - 避免4并发导致ERR_CONNECTION_RESET 3. 动态并发控制 - 根据网络环境自动调整 4. Nginx超时增加到120s - 支持大分片上传 5. 上传进度修复 - 修复剩余时间计算 6. 新增任务-页面上传器和详情组件 7. 统一部署文档为deploy-remote-build.ps1 问题修复: - 公网用户上传从200-900KB/s提升到预期3-5MB/s - 内网用户上传更稳定,无连接重置错误 - UploadProgress组件剩余时间计算修正 Co-Authored-By: Claude <noreply@anthropic.com>
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
// 分片上传完成合并接口
|
||
// 负责将分片文件按顺序合并为完整文件,校验大小,并返回服务器侧可复用的文件路径
|
||
// 供前端在第二步调用正式上传接口时复用,避免重复上传大文件
|
||
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth'
|
||
import { authOptions } from '@/lib/auth'
|
||
import path from 'path'
|
||
import fs from 'fs/promises'
|
||
import { createWriteStream, createReadStream } from 'fs'
|
||
import { pipeline } from 'stream/promises'
|
||
import { getUploadSession, deleteUploadSession } from '@/lib/upload-sessions'
|
||
import { sanitizeFileName } from '@/lib/image-processor'
|
||
|
||
export const runtime = 'nodejs'
|
||
export const dynamic = 'force-dynamic'
|
||
export const maxDuration = 10800 // 最长3小时
|
||
|
||
export async function POST(req: NextRequest) {
|
||
try {
|
||
const session = await getServerSession(authOptions)
|
||
if (!session) {
|
||
return NextResponse.json({ error: '请先登录' }, { status: 401 })
|
||
}
|
||
|
||
const { uploadId, originalFileName } = await req.json()
|
||
|
||
if (!uploadId) {
|
||
return NextResponse.json({ error: '缺少 uploadId' }, { status: 400 })
|
||
}
|
||
|
||
const uploadDir = process.env.UPLOAD_DIR || './uploads'
|
||
const tempDir = path.join(uploadDir, 'temp', uploadId)
|
||
|
||
// 读取会话
|
||
const uploadSession = await getUploadSession(uploadId)
|
||
if (!uploadSession) {
|
||
return NextResponse.json({ error: '上传会话不存在或已过期' }, { status: 404 })
|
||
}
|
||
|
||
// 会话归属校验
|
||
if (uploadSession.userId !== (session.user as any).id) {
|
||
return NextResponse.json({ error: '无权访问此上传会话' }, { status: 403 })
|
||
}
|
||
|
||
const totalChunks = uploadSession.totalChunks
|
||
const fileSize = uploadSession.fileSize
|
||
|
||
// 收集分片文件
|
||
const chunkFiles = []
|
||
for (let i = 0; i < totalChunks; i++) {
|
||
const chunkPath = path.join(tempDir, `chunk-${i}`)
|
||
chunkFiles.push(chunkPath)
|
||
}
|
||
|
||
// 校验分片是否齐全
|
||
for (const chunkPath of chunkFiles) {
|
||
try {
|
||
await fs.access(chunkPath)
|
||
} catch {
|
||
return NextResponse.json({ error: '分片不完整,请重新上传' }, { status: 400 })
|
||
}
|
||
}
|
||
|
||
// 合并文件
|
||
await fs.mkdir(tempDir, { recursive: true })
|
||
// 处理文件名:提取纯文件名(去除路径),并清理特殊字符,避免中文路径或特殊字符导致的问题
|
||
const rawFileName = originalFileName || uploadSession.fileName || `upload-${uploadId}`
|
||
const baseFileName = path.basename(rawFileName) // 提取文件名部分,去除路径
|
||
const mergedFileName = sanitizeFileName(baseFileName) || `upload-${uploadId}` // 清理特殊字符
|
||
const mergedPath = path.join(tempDir, `merged-${mergedFileName}`)
|
||
|
||
const mergedStream = createWriteStream(mergedPath)
|
||
for (const chunkPath of chunkFiles) {
|
||
const chunkStream = createReadStream(chunkPath)
|
||
await pipeline(chunkStream, mergedStream, { end: false })
|
||
}
|
||
// 手动结束写入并等待关闭
|
||
mergedStream.end()
|
||
await new Promise<void>((resolve, reject) => {
|
||
mergedStream.on('close', () => resolve())
|
||
mergedStream.on('error', (err) => reject(err))
|
||
})
|
||
|
||
// 校验大小
|
||
const stat = await fs.stat(mergedPath)
|
||
if (stat.size !== fileSize) {
|
||
return NextResponse.json(
|
||
{ error: `合并后的文件大小不匹配,期望 ${fileSize} 实际 ${stat.size}` },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
// 清理分片,保留合并后的文件供后续正式上传使用
|
||
// 使用统一的文件删除工具
|
||
const { deleteFiles } = await import('@/lib/file-cleanup')
|
||
const result = await deleteFiles(chunkFiles)
|
||
if (result.failedCount > 0) {
|
||
console.warn('[UploadComplete] 清理分片失败:', result.errors)
|
||
}
|
||
|
||
// 上传完成后,立即删除会话(不再占用Redis内存)
|
||
try {
|
||
const { deleteUploadSession } = await import('@/lib/upload-sessions-redis')
|
||
await deleteUploadSession(uploadId)
|
||
console.log('[UploadComplete] 已删除会话:', uploadId)
|
||
} catch (deleteError) {
|
||
console.warn('[UploadComplete] 删除会话失败(忽略):', deleteError)
|
||
}
|
||
|
||
// 返回合并结果
|
||
return NextResponse.json({
|
||
message: '分片合并完成',
|
||
existingFilePath: mergedPath,
|
||
fileName: mergedFileName,
|
||
fileSize: stat.size,
|
||
uploadId,
|
||
})
|
||
} catch (error) {
|
||
console.error('[UploadComplete] Error:', error)
|
||
return NextResponse.json({ error: '合并分片失败' }, { status: 500 })
|
||
}
|
||
}
|
||
|