125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { Readable } from 'stream'
|
|
import { getFileStreamByObjectName } from '@/lib/fileStorage'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { withCors } from '@/lib/middleware'
|
|
|
|
export const runtime = 'nodejs'
|
|
|
|
interface ScreenshotFileMetadata {
|
|
objectName: string
|
|
filename: string
|
|
contentType: string
|
|
fileSize: number
|
|
}
|
|
|
|
const MAX_SCREENSHOT_METADATA_CACHE_SIZE = 5000
|
|
const screenshotMetadataCache = new Map<string, ScreenshotFileMetadata>()
|
|
|
|
function cacheScreenshotMetadata(fileId: string, metadata: ScreenshotFileMetadata) {
|
|
if (screenshotMetadataCache.size >= MAX_SCREENSHOT_METADATA_CACHE_SIZE) {
|
|
const oldestFileId = screenshotMetadataCache.keys().next().value
|
|
if (oldestFileId) {
|
|
screenshotMetadataCache.delete(oldestFileId)
|
|
}
|
|
}
|
|
|
|
screenshotMetadataCache.set(fileId, metadata)
|
|
}
|
|
|
|
async function findScreenshotMetadata(fileId: string): Promise<ScreenshotFileMetadata | null> {
|
|
const cached = screenshotMetadataCache.get(fileId)
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
|
|
const screenshot = await prisma.screenshot.findFirst({
|
|
where: { fileId },
|
|
select: {
|
|
objectName: true,
|
|
filename: true,
|
|
contentType: true,
|
|
fileSize: true
|
|
}
|
|
})
|
|
|
|
if (screenshot) {
|
|
cacheScreenshotMetadata(fileId, screenshot)
|
|
}
|
|
|
|
return screenshot
|
|
}
|
|
|
|
function createScreenshotHeaders(fileId: string, screenshot: ScreenshotFileMetadata) {
|
|
const headers = new Headers({
|
|
'Content-Type': resolveScreenshotContentType(screenshot),
|
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
'ETag': `"${fileId}"`,
|
|
'X-Content-Type-Options': 'nosniff'
|
|
})
|
|
|
|
if (screenshot.fileSize > 0) {
|
|
headers.set('Content-Length', String(screenshot.fileSize))
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
function resolveScreenshotContentType(screenshot: ScreenshotFileMetadata) {
|
|
const storedContentType = screenshot.contentType?.trim()
|
|
const filePath = `${screenshot.filename} ${screenshot.objectName}`.toLowerCase()
|
|
|
|
if (storedContentType && storedContentType !== 'image/webp') {
|
|
return storedContentType
|
|
}
|
|
|
|
if (filePath.includes('.avif')) return 'image/avif'
|
|
if (filePath.includes('.webp')) return 'image/webp'
|
|
if (filePath.includes('.png')) return 'image/png'
|
|
if (filePath.includes('.jpg') || filePath.includes('.jpeg')) return 'image/jpeg'
|
|
|
|
return storedContentType || 'image/webp'
|
|
}
|
|
|
|
async function handleScreenshotFile(req: NextRequest) {
|
|
try {
|
|
const pathSegments = req.nextUrl.pathname.split('/')
|
|
const fileIdIndex = pathSegments.indexOf('screenshots') + 1
|
|
const fileId = pathSegments[fileIdIndex]
|
|
|
|
if (!fileId) {
|
|
return NextResponse.json({ error: '缺少文件ID' }, { status: 400 })
|
|
}
|
|
|
|
const screenshot = await findScreenshotMetadata(fileId)
|
|
|
|
if (!screenshot) {
|
|
return NextResponse.json({ error: '截图不存在' }, { status: 404 })
|
|
}
|
|
|
|
const headers = createScreenshotHeaders(fileId, screenshot)
|
|
if (req.headers.get('if-none-match') === headers.get('ETag')) {
|
|
headers.delete('Content-Length')
|
|
return new NextResponse(null, { status: 304, headers })
|
|
}
|
|
|
|
const file = await getFileStreamByObjectName(screenshot.objectName, {
|
|
contentType: resolveScreenshotContentType(screenshot),
|
|
filename: screenshot.filename,
|
|
size: screenshot.fileSize
|
|
})
|
|
|
|
if (!file) {
|
|
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
|
|
}
|
|
|
|
return new NextResponse(Readable.toWeb(file.stream) as unknown as BodyInit, { headers })
|
|
|
|
} catch (error) {
|
|
console.error('获取截图失败:', error)
|
|
return NextResponse.json({ error: '获取截图失败' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
export const GET = withCors(handleScreenshotFile)
|