fix: 修复 fileid索引
This commit is contained in:
parent
fea99d37fe
commit
681e3c7493
@ -1,8 +1,86 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getFileByObjectName } from '@/lib/fileStorage'
|
||||
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('/')
|
||||
@ -13,27 +91,29 @@ async function handleScreenshotFile(req: NextRequest) {
|
||||
return NextResponse.json({ error: '缺少文件ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 从数据库查找 objectName
|
||||
const screenshot = await prisma.screenshot.findFirst({
|
||||
where: { fileId },
|
||||
select: { objectName: true }
|
||||
})
|
||||
const screenshot = await findScreenshotMetadata(fileId)
|
||||
|
||||
if (!screenshot) {
|
||||
return NextResponse.json({ error: '截图不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
const file = await getFileByObjectName(screenshot.objectName)
|
||||
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(file.buffer, {
|
||||
headers: {
|
||||
'Content-Type': file.contentType || 'image/webp',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
}
|
||||
})
|
||||
return new NextResponse(Readable.toWeb(file.stream) as unknown as BodyInit, { headers })
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取截图失败:', error)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as Minio from 'minio'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { Readable } from 'stream'
|
||||
import { minioClient, BUCKET_NAME, initializeMinIO } from './minioClient'
|
||||
|
||||
export interface StoredFile {
|
||||
@ -20,6 +21,19 @@ export interface FileMetadata {
|
||||
objectName: string
|
||||
}
|
||||
|
||||
export interface StoredFileStream {
|
||||
stream: Readable
|
||||
contentType: string
|
||||
filename: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface StoredFileStreamMetadata {
|
||||
contentType?: string | null
|
||||
filename?: string | null
|
||||
size?: number | null
|
||||
}
|
||||
|
||||
// 生成对象名称,使用分层结构优化性能
|
||||
function generateObjectName(type: 'screenshot' | 'version' | 'other', hostname?: string): string {
|
||||
const now = new Date()
|
||||
@ -168,6 +182,27 @@ export async function getFileByObjectName(objectName: string): Promise<{ buffer:
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileStreamByObjectName(
|
||||
objectName: string,
|
||||
metadata: StoredFileStreamMetadata = {}
|
||||
): Promise<StoredFileStream | null> {
|
||||
try {
|
||||
await ensureMinIOReady()
|
||||
|
||||
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
|
||||
|
||||
return {
|
||||
stream,
|
||||
contentType: metadata.contentType || 'application/octet-stream',
|
||||
filename: metadata.filename || objectName.split('/').pop() || 'unknown',
|
||||
size: metadata.size ?? undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 获取文件流失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFile(objectName: string): Promise<boolean> {
|
||||
try {
|
||||
await ensureMinIOReady()
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "screenshots_fileId_idx" ON "screenshots"("fileId");
|
||||
@ -75,6 +75,7 @@ model Screenshot {
|
||||
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([recordId])
|
||||
@@index([fileId])
|
||||
@@index([objectName])
|
||||
@@map("screenshots")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user