import { NextRequest, NextResponse } from "next/server"; import { ingestFile } from "@/lib/ingest/ingest"; import { assertLocalRequest } from "@/lib/api/localOnly"; import { rawDb } from "@/lib/db/client"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; // Hard cap on a single uploaded file. Pinkudex stores images and short // covers; anything beyond this is almost certainly a mistake (or an // attack). Without the cap, `await file.arrayBuffer()` happily buffers // multi-GB POSTs and OOMs the Node process. const MAX_UPLOAD_BYTES = 512 * 1024 * 1024; export async function POST(req: NextRequest) { const blocked = assertLocalRequest(req); if (blocked) return blocked; const contentLength = Number(req.headers.get("content-length") ?? ""); if (Number.isFinite(contentLength) && contentLength > MAX_UPLOAD_BYTES) { return NextResponse.json({ error: "Upload too large" }, { status: 413 }); } const form = await req.formData(); const file = form.get("file"); if (!(file instanceof File)) { return NextResponse.json({ error: "missing file" }, { status: 400 }); } if (file.size > MAX_UPLOAD_BYTES) { return NextResponse.json({ error: "Upload too large" }, { status: 413 }); } const buf = Buffer.from(await file.arrayBuffer()); const nfoFile = form.get("nfo"); const nfoXml = nfoFile instanceof File ? await nfoFile.text() : undefined; const autoTag = form.get("autoTag"); const autoCollection = form.get("autoCollection"); let autoCollectionId: number | undefined; if (typeof autoCollection === "string" && autoCollection.trim()) { const parsed = Number(autoCollection); if (!Number.isInteger(parsed) || parsed <= 0) { return NextResponse.json({ error: "invalid collection" }, { status: 400 }); } const exists = rawDb.prepare(`SELECT id FROM collections WHERE id = ?`).get(parsed) as { id: number } | undefined; if (!exists) { return NextResponse.json({ error: "collection not found" }, { status: 400 }); } autoCollectionId = parsed; } const autoAssign = (typeof autoTag === "string" && autoTag.trim()) || autoCollectionId != null ? { tagName: typeof autoTag === "string" ? autoTag : undefined, collectionId: autoCollectionId, } : undefined; const parentImageIdRaw = form.get("parentImageId"); const parentImageId = typeof parentImageIdRaw === "string" && parentImageIdRaw ? Number(parentImageIdRaw) : undefined; const targetFilenameRaw = form.get("targetFilename"); const targetFilename = typeof targetFilenameRaw === "string" && targetFilenameRaw.trim() ? targetFilenameRaw.trim() : undefined; const actressNamesRaw = form.get("actressNames"); let actressNames: string[] | undefined; if (typeof actressNamesRaw === "string" && actressNamesRaw.trim()) { try { const parsed = JSON.parse(actressNamesRaw); if (Array.isArray(parsed)) actressNames = parsed.filter((s): s is string => typeof s === "string"); } catch { // ignore } } const onCollisionRaw = form.get("onCollision"); const onCollision = onCollisionRaw === "replace" || onCollisionRaw === "skip" ? onCollisionRaw : "detect"; try { const result = await ingestFile(buf, file.name, { nfoXml, autoAssign, parentImageId, targetFilename, actressNames, onCollision, }); return NextResponse.json(result); } catch (err) { console.error("ingest failed", err); return NextResponse.json({ error: (err as Error).message }, { status: 500 }); } }