import { NextRequest, NextResponse } from "next/server"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; import { revalidatePath } from "next/cache"; import { rawDb } from "@/lib/db/client"; import { safeJoin } from "@/lib/safePath"; import { assertLocalRequest } from "@/lib/api/localOnly"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const COVER_ROOT = path.join(process.cwd(), "data", "collection-covers"); const ALLOWED_EXT = new Set([".jpg", ".jpeg", ".png", ".webp"]); const SLOT_COLS: Record = { portrait: { path: "cover_portrait_path", zoom: "cover_portrait_zoom", ox: "cover_portrait_offset_x", oy: "cover_portrait_offset_y" }, landscape: { path: "cover_landscape_path", zoom: "cover_landscape_zoom", ox: "cover_landscape_offset_x", oy: "cover_landscape_offset_y" }, }; export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const blocked = assertLocalRequest(req); if (blocked) return blocked; const { id } = await ctx.params; const numId = Number(id); if (!Number.isFinite(numId)) return NextResponse.json({ error: "bad id" }, { status: 400 }); const url = new URL(req.url); const slot = (url.searchParams.get("slot") ?? "portrait") as keyof typeof SLOT_COLS; const cols = SLOT_COLS[slot]; if (!cols) return NextResponse.json({ error: "bad slot" }, { status: 400 }); const coll = rawDb.prepare(`SELECT id, slug, ${cols.path} AS prevPath FROM collections WHERE id = ?`).get(numId) as | { id: number; slug: string; prevPath: string | null } | undefined; if (!coll) return NextResponse.json({ error: "collection not found" }, { status: 404 }); const form = await req.formData(); const file = form.get("file"); if (!(file instanceof File)) return NextResponse.json({ error: "missing file" }, { status: 400 }); const ext = path.extname(file.name).toLowerCase(); if (!ALLOWED_EXT.has(ext)) return NextResponse.json({ error: "unsupported format" }, { status: 415 }); const buf = Buffer.from(await file.arrayBuffer()); const sha = crypto.createHash("sha256").update(buf).digest("hex").slice(0, 16); const filename = `${coll.id}-${slot}-${sha}${ext}`; await fs.mkdir(COVER_ROOT, { recursive: true }); await fs.writeFile(path.join(COVER_ROOT, filename), buf); if (coll.prevPath && coll.prevPath !== filename) { const prevAbs = safeJoin(COVER_ROOT, coll.prevPath); if (prevAbs) await fs.rm(prevAbs, { force: true }).catch(() => {}); } rawDb.prepare(` UPDATE collections SET ${cols.path} = ?, ${cols.zoom} = 1, ${cols.ox} = 0, ${cols.oy} = 0 WHERE id = ? `).run(filename, coll.id); revalidatePath("/collection"); revalidatePath(`/collection/${coll.slug}`); return NextResponse.json({ coverPath: filename }); }