WuYongJie d1c6efc79d
Some checks failed
CI Check / check (push) Has been cancelled
feat: NAS 部署与 Harbor 推送流程
2025-12-03 14:39:06 +08:00

276 lines
8.8 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 { getS3ObjectStream, isS3Url } from '@/lib/s3'
import { ensureMaterialDownloadLogsTable } from '@/lib/material-download-log'
import { readFile, access } from 'fs/promises'
import path from 'path'
import { Readable } from 'stream'
export const dynamic = 'force-dynamic'
export async function GET(req: NextRequest) {
try {
// Check if user is logged in
const session = await getServerSession(authOptions)
if (!session) {
console.log('[Download] No session found')
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
)
}
// Check if user has download permission (skip check for admins)
const userId = (session.user as any).id
const userRole = (session.user as any).role
const userEmail = (session.user as any).email
console.log(`[Download] User ${userId} (${userEmail}, role: ${userRole}) attempting download`)
if (userRole !== 'ADMIN') {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { canDownload: true, email: true, username: true }
})
console.log(`[Download] User ${userId} canDownload status:`, user?.canDownload)
if (!user) {
console.error(`[Download] User ${userId} not found in database`)
return NextResponse.json(
{ error: '用户不存在,请联系管理员' },
{ status: 403 }
)
}
if (!user.canDownload) {
console.warn(`[Download] User ${userId} (${user.email}) download permission is disabled`)
return NextResponse.json(
{ error: '您的下载权限已被限制,请联系管理员' },
{ status: 403 }
)
}
} else {
console.log(`[Download] Admin user ${userId} skipping permission check`)
}
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ error: '缺少素材ID' },
{ status: 400 }
)
}
const material = await prisma.material.findUnique({
where: { id },
select: {
id: true,
userId: true,
productModel: true,
zipFilePath: true,
zipFileName: true,
materialType: true,
categoryName: true,
sceneName: true,
authorName: true,
}
})
if (!material) {
console.error(`[Download] Material ${id} not found`)
return NextResponse.json(
{ error: '素材不存在' },
{ status: 404 }
)
}
console.log(`[Download] Material ${id} (${material.productModel}) found. Owner: ${material.userId}, Requester: ${userId}`)
const isOwnMaterial = material.userId === userId
let shouldIncrementDownload = false
if (!isOwnMaterial) {
try {
await ensureMaterialDownloadLogsTable()
const now = new Date()
shouldIncrementDownload = true
// 每次下载都记录一条日志
await prisma.$executeRaw`
INSERT INTO material_download_logs (id, "materialId", "userId", "downloadedAt")
VALUES (gen_random_uuid(), ${id}, ${userId}, ${now})
`
console.log(`[Download] Created download log for material ${id}, user ${userId}`)
} catch (logError) {
console.error(`[Download] Error handling download log:`, logError)
// 如果日志记录失败,仍然允许下载,但不增加计数
shouldIncrementDownload = false
console.warn(`[Download] Continuing download without logging due to error`)
}
} else {
// 自己下载自己的素材,不记录下载日志,也不增加下载次数
console.log(`[Download] User ${userId} downloaded own material ${id}. Not incrementing count.`)
}
if (shouldIncrementDownload) {
await prisma.material.update({
where: { id },
data: {
downloadCount: {
increment: 1,
},
},
})
}
if (!material.zipFilePath) {
console.error(`[Download] Material ${id} has no zipFilePath`)
return NextResponse.json(
{ error: '没有找到素材文件,请重新上传' },
{ status: 404 }
)
}
console.log(`[Download] Material ${id} file path: ${material.zipFilePath}`)
// 生成新的文件名:SPU_一级分类_二级分类_用户名.扩展名
// 获取原文件扩展名
const zipNameFallback = material.zipFileName || ''
const originalExt = path.extname(zipNameFallback) || path.extname(material.zipFilePath) || ''
const newFileName = `${material.productModel}_${material.categoryName}_${material.sceneName}_${material.authorName}${originalExt}`
console.log('Download filename:', newFileName)
// 根据文件类型和素材类型确定Content-Type
let contentType = 'application/zip'
let contentDisposition = `attachment; filename="${encodeURIComponent(newFileName)}"`
const rangeHeader = req.headers.get('range')
// 如果是视频类型,使用视频的Content-Type
if (material.materialType === '视频') {
const ext = originalExt.toLowerCase()
const videoTypes: { [key: string]: string } = {
'.mp4': 'video/mp4',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.wmv': 'video/x-ms-wmv',
'.flv': 'video/x-flv',
'.mkv': 'video/x-matroska',
'.webm': 'video/webm',
}
contentType = videoTypes[ext] || 'video/mp4'
if (!rangeHeader) {
contentDisposition = `inline; filename="${encodeURIComponent(newFileName)}"`
}
}
const contentDispositionHeader = `${contentDisposition}; filename*=UTF-8''${encodeURIComponent(newFileName)}`
if (isS3Url(material.zipFilePath)) {
const s3Object = await getS3ObjectStream(material.zipFilePath, rangeHeader)
const stream = toWebStream(s3Object.body)
const headers: Record<string, string> = {
'Content-Type': s3Object.contentType || contentType,
'Content-Disposition': contentDispositionHeader,
'Accept-Ranges': s3Object.acceptRanges || 'bytes',
'Cache-Control': 'no-store',
}
if (rangeHeader && s3Object.contentRange) {
headers['Content-Range'] = s3Object.contentRange
}
if (s3Object.contentLength) {
headers['Content-Length'] = s3Object.contentLength.toString()
}
const status = rangeHeader ? 206 : 200
return new NextResponse(stream as any, {
status,
headers,
})
}
// Read legacy local files (backward compatibility)
let filePath = material.zipFilePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
}
console.log('Attempting to download legacy file:', filePath)
try {
await access(filePath)
} catch (error) {
console.error('File not found:', filePath)
return NextResponse.json(
{ error: '文件不存在,可能已被删除' },
{ status: 404 }
)
}
const fileBuffer = await readFile(filePath)
if (material.materialType === '视频' && rangeHeader) {
const fileSize = fileBuffer.length
const parts = rangeHeader.replace(/bytes=/, '').split('-')
const start = parseInt(parts[0], 10)
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1
const chunkSize = end - start + 1
const chunk = fileBuffer.slice(start, end + 1)
return new NextResponse(chunk as any, {
status: 206,
headers: {
'Content-Type': contentType,
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize.toString(),
},
})
}
return new NextResponse(fileBuffer as any, {
headers: {
'Content-Type': contentType,
'Content-Disposition': contentDispositionHeader,
'Content-Length': fileBuffer.length.toString(),
'Accept-Ranges': 'bytes', // 支持Range请求(用于视频流式传输)
},
})
} catch (error) {
console.error('[Download] Error:', error)
const errorMessage = error instanceof Error ? error.message : '未知错误'
console.error('[Download] Error details:', {
message: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{ error: '下载失败: ' + errorMessage },
{ status: 500 }
)
}
}
function toWebStream(body: any) {
if (!body) {
throw new Error('S3 object body is empty')
}
if (typeof (body as any).transformToWebStream === 'function') {
return (body as any).transformToWebStream()
}
if (body instanceof Readable) {
return Readable.toWeb(body)
}
return body as ReadableStream<any>
}