Files
2026-05-26 22:46:00 +02:00

91 lines
3.5 KiB
TypeScript

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