104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import path from 'path'
|
|
import fs from 'fs'
|
|
import sharp from 'sharp'
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
|
|
import { requireAdmin } from '@/lib/auth'
|
|
|
|
const MAX_COVER_BYTES = 10 * 1024 * 1024 // 10 MB
|
|
|
|
type CoverType = 'cover' | 'widecover'
|
|
|
|
function isCoverType(s: string | null): s is CoverType {
|
|
return s === 'cover' || s === 'widecover'
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const auth = await requireAdmin(request)
|
|
if (auth instanceof NextResponse) return auth
|
|
|
|
const { searchParams } = request.nextUrl
|
|
const libraryId = searchParams.get('libraryId')
|
|
const itemId = searchParams.get('itemId')
|
|
const coverType = searchParams.get('coverType')
|
|
|
|
if (!libraryId || !itemId) {
|
|
return NextResponse.json({ error: 'Missing libraryId or itemId' }, { status: 400 })
|
|
}
|
|
if (!isCoverType(coverType)) {
|
|
return NextResponse.json({ error: 'coverType must be "cover" or "widecover"' }, { status: 400 })
|
|
}
|
|
|
|
const library = getLibrary(libraryId)
|
|
if (!library) {
|
|
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
|
}
|
|
if (library.type !== 'games') {
|
|
return NextResponse.json({ error: 'Library is not a games library' }, { status: 400 })
|
|
}
|
|
|
|
const libraryRoot = resolveLibraryRoot(library)
|
|
const folderPath = decodeURIComponent(itemId)
|
|
|
|
let resolvedDir: string
|
|
try {
|
|
resolvedDir = resolveAndJail(libraryRoot, folderPath)
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid item path' }, { status: 400 })
|
|
}
|
|
|
|
if (!fs.existsSync(resolvedDir)) {
|
|
return NextResponse.json({ error: 'Game folder not found' }, { status: 404 })
|
|
}
|
|
|
|
let formData: FormData
|
|
try {
|
|
formData = await request.formData()
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid form data' }, { status: 400 })
|
|
}
|
|
|
|
const file = formData.get('cover')
|
|
if (!(file instanceof File)) {
|
|
return NextResponse.json({ error: 'cover file is required' }, { status: 400 })
|
|
}
|
|
|
|
if (file.size > MAX_COVER_BYTES) {
|
|
return NextResponse.json({ error: 'File too large. Maximum size is 10 MB.' }, { status: 400 })
|
|
}
|
|
|
|
const rawBuffer = Buffer.from(await file.arrayBuffer())
|
|
|
|
let processedBuffer: Buffer
|
|
try {
|
|
processedBuffer = await sharp(rawBuffer).jpeg({ quality: 90 }).toBuffer()
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid or corrupt image file.' }, { status: 400 })
|
|
}
|
|
|
|
const destFilename = `${coverType}.jpg`
|
|
const destPath = path.join(resolvedDir, destFilename)
|
|
|
|
// Remove any existing file with the same base name but a different extension
|
|
const basePattern = new RegExp(`^${coverType}\\.`, 'i')
|
|
try {
|
|
for (const f of fs.readdirSync(resolvedDir)) {
|
|
if (basePattern.test(f) && f.toLowerCase() !== destFilename) {
|
|
fs.unlinkSync(path.join(resolvedDir, f))
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
fs.writeFileSync(destPath, processedBuffer)
|
|
|
|
const relPath = path.join(folderPath, destFilename)
|
|
|
|
// cover uses the thumbnail endpoint; widecover is served directly
|
|
const url =
|
|
coverType === 'cover'
|
|
? `/api/thumbnail?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(relPath)}`
|
|
: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(relPath)}`
|
|
|
|
return NextResponse.json({ url }, { status: 200 })
|
|
}
|