Initial commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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", "category-covers");
|
||||
const ALLOWED_EXT = new Set([".jpg", ".jpeg", ".png", ".webp"]);
|
||||
|
||||
const SLOT_COLS: Record<string, { path: string; zoom: string; ox: string; oy: string }> = {
|
||||
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 cat = rawDb.prepare(`SELECT id, slug, ${cols.path} AS prevPath FROM tag_categories WHERE id = ?`).get(numId) as
|
||||
| { id: number; slug: string; prevPath: string | null }
|
||||
| undefined;
|
||||
if (!cat) return NextResponse.json({ error: "category 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 = `${cat.id}-${slot}-${sha}${ext}`;
|
||||
await fs.mkdir(COVER_ROOT, { recursive: true });
|
||||
await fs.writeFile(path.join(COVER_ROOT, filename), buf);
|
||||
|
||||
// Replace any previous file in this slot, unless the bytes happened to
|
||||
// hash to the same name (in which case we just kept it).
|
||||
if (cat.prevPath && cat.prevPath !== filename) {
|
||||
const prevAbs = safeJoin(COVER_ROOT, cat.prevPath);
|
||||
if (prevAbs) await fs.rm(prevAbs, { force: true }).catch(() => {});
|
||||
}
|
||||
|
||||
rawDb.prepare(`
|
||||
UPDATE tag_categories
|
||||
SET ${cols.path} = ?, ${cols.zoom} = 1, ${cols.ox} = 0, ${cols.oy} = 0
|
||||
WHERE id = ?
|
||||
`).run(filename, cat.id);
|
||||
|
||||
revalidatePath("/category");
|
||||
revalidatePath(`/category/${cat.slug}`);
|
||||
return NextResponse.json({ coverPath: filename });
|
||||
}
|
||||
Reference in New Issue
Block a user