This repository has been archived on 2026-06-15. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MediaLore/src/app/api/game-cover/route.ts
Garret Patti eecee9bc5f add auth
2026-04-05 17:44:24 -04:00

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