91 lines
3.5 KiB
TypeScript
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 });
|
|
}
|
|
}
|