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 }) }