import { promises as fs } from "node:fs"; import { join } from "node:path"; import { sql } from "@/db"; import { requireUser, jsonError } from "@/lib/auth/guard"; import { clientIp } from "@/lib/auth/session"; import { audit } from "@/lib/audit"; import { enqueue } from "@/lib/jobs/queue"; import { parseVersionLabel } from "@/lib/papers/identifiers"; import { ensureStorage, pdfIntegrity, sha256File, storeFile } from "@/lib/pdf/process"; import { STORAGE_DIR } from "@/lib/env"; import { headers } from "next/headers"; export const dynamic = "force-dynamic"; const MAX = 150 * 1024 * 1024; export async function POST(req: Request) { try { const u = await requireUser(req); const fd = await req.formData(); const file = fd.get("file"); if (!(file instanceof File)) throw new Error("file missing"); if (file.size > MAX) throw new Error("file too large (max 150 MB)"); if (file.type && file.type !== "application/pdf") throw new Error("PDF only"); const paperId = String(fd.get("paper_id") ?? ""); const [p] = await sql`SELECT id, slug FROM papers WHERE id = ${paperId}`; if (!p) throw new Error("paper not found"); await ensureStorage(); const tmp = join(STORAGE_DIR, "tmp", `${crypto.randomUUID()}.pdf`); await fs.writeFile(tmp, Buffer.from(await file.arrayBuffer()), { mode: 0o600 }); const head = await fs.readFile(tmp, { encoding: "latin1", flag: "r" }).then((s) => s.slice(0, 5)); if (!head.startsWith("%PDF")) { await fs.unlink(tmp); throw new Error("not a PDF file"); } const integ = await pdfIntegrity(tmp); if (!integ.ok) { await fs.unlink(tmp); throw new Error(`PDF integrity check failed: ${integ.reason}`); } const sha = await sha256File(tmp); let [f] = await sql`SELECT id FROM files WHERE sha256 = ${sha}`; if (!f) { const key = await storeFile(tmp, sha); [f] = await sql`INSERT INTO files (kind, storage_key, original_name, mime, bytes, sha256, pages, integrity_ok) VALUES ('manuscript', ${key}, ${file.name.slice(0, 200)}, 'application/pdf', ${file.size}, ${sha}, ${integ.info?.pages ?? null}, true) RETURNING id`; } await fs.unlink(tmp).catch(() => {}); const [dupv] = await sql`SELECT label FROM paper_versions WHERE paper_id = ${p.id} AND sha256 = ${sha}`; if (dupv) throw new Error(`This exact file is already version ${dupv.label}`); const [{ next }] = await sql`SELECT coalesce(max(version_no), 0) + 1 AS next FROM paper_versions WHERE paper_id = ${p.id}`; const label = parseVersionLabel(String(fd.get("label") ?? "")) ?? { versionNo: next, label: `v${next}` }; const dateRaw = String(fd.get("released_on") ?? "").trim(); const precision = /^\d{4}$/.test(dateRaw) ? "year" : /^\d{4}-\d{2}$/.test(dateRaw) ? "month" : "day"; const date = !dateRaw ? null : precision === "year" ? `${dateRaw}-01-01` : precision === "month" ? `${dateRaw}-01` : dateRaw; const makeCurrent = fd.get("make_current") === "on"; const [v] = await sql`INSERT INTO paper_versions (paper_id, version_no, label, released_on, released_precision, description, file_id, pages, sha256, bytes, is_current) VALUES (${p.id}, ${next}, ${label.label}, ${date}, ${precision}, ${String(fd.get("description") ?? "") || null}, ${f.id}, ${integ.info?.pages ?? null}, ${sha}, ${file.size}, ${makeCurrent}) RETURNING id`; if (makeCurrent) { await sql`UPDATE paper_versions SET is_current = false WHERE paper_id = ${p.id} AND id <> ${v.id}`; await sql`UPDATE papers SET current_version_id = ${v.id}, last_revised_on = coalesce(${date}, last_revised_on), last_revised_precision = ${date ? precision : sql`last_revised_precision`} WHERE id = ${p.id}`; } await enqueue("process_pdf", { fileId: f.id, paperId: makeCurrent ? p.id : undefined, versionId: v.id }); await audit({ id: u.id, label: u.email, ip: clientIp(await headers()) }, "version.upload", "paper_version", v.id, null, { paper: p.slug, label: label.label, sha256: sha, bytes: file.size }); return Response.json({ ok: true, versionId: v.id, label: label.label, sha256: sha }); } catch (e) { return jsonError(e); } }