Make the container actually deployable, publish to GHCR
The Docker setup built but never came up. Three separate blockers, each found by running the full compose stack against a real Postgres: - The runner stage hand-copied prisma/@prisma/dotenv, which misses the CLI's transitive deps (valibot) and crash-looped the entrypoint. A dedicated migrator stage now installs the CLI at the exact lockfile version with its real dependency tree. - Docker sets HOSTNAME to the container id and Next's standalone server binds to it verbatim, so the app was unreachable on localhost inside the container and the healthcheck could never pass. Pinned to 0.0.0.0. - openssl was missing from the build stages but present at runtime, so Prisma detected a different platform and tried to download a matching query engine at boot, as an unprivileged user, into a root-owned directory. Aligned openssl across every stage that touches Prisma. Verified from a clean volume: migrations apply, both containers report healthy, /api/health returns 200, and the app serves from the image. Also: - Publish multi-arch images to GHCR on push to main and on v* tags, with an SBOM and a signed provenance attestation. - Default compose to the published image; add a build override for working on the app itself. Host port and image are configurable. - Drop two unused dependencies: canvas (19MB of native node-canvas; the pipeline uses @napi-rs/canvas) and multer (routes use formData()). - Add a root .gitignore. There wasn't one, so a real .env was one `git add .` away from being committed. Untrack .DS_Store. - Rewrite the README around what the tool does and how to run it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4130ce2e09
commit
7e7e4d9b1c
36 files changed
+2260
-1270
No files matched your search
@@ -2,13 +2,17 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { readFile, mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
import { readFile, writeFile, mkdir } from "fs/promises";
|
||||
import { loadPdf, renderPage } from "@/lib/pipeline";
|
||||
import { sourcePdfPath, resultsDir, resultPagePath } from "@/lib/storage";
|
||||
|
||||
/**
|
||||
* Server-side PDF page renderer: renders EVERY page of the PDF as a
|
||||
* clean PNG image (no processing). Used by the "Original" panel so
|
||||
* all three panels display consistently via server-rendered images.
|
||||
* Server-side PDF page renderer: renders EVERY page of the PDF as a clean PNG
|
||||
* image (no processing) via the shared pipeline. Used by the "Original" panel
|
||||
* so all three panels display consistently via server-rendered images.
|
||||
*
|
||||
* Result images are stored under STORAGE_DIR and served through the
|
||||
* authenticated /asset route (never as public static files).
|
||||
*/
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
@@ -33,64 +37,24 @@ export async function POST(
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const publicDir = `/results/${user.id}/${id}/original`;
|
||||
const resultsDir = path.join(process.cwd(), "public", publicDir);
|
||||
|
||||
// Always regenerate so stale cached renders don't persist.
|
||||
|
||||
try {
|
||||
const pdfPath = path.join(process.cwd(), "public", doc.filePath);
|
||||
const pdfBuffer = await readFile(pdfPath);
|
||||
|
||||
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
||||
const loadingTask = pdfjsLib.getDocument({
|
||||
data: new Uint8Array(pdfBuffer),
|
||||
});
|
||||
const pdfDoc = await loadingTask.promise;
|
||||
const pdfBuffer = await readFile(sourcePdfPath(doc.filePath));
|
||||
const pdfDoc = await loadPdf(pdfBuffer);
|
||||
const numPages = pdfDoc.numPages;
|
||||
|
||||
await mkdir(resultsDir, { recursive: true });
|
||||
await mkdir(resultsDir(user.id, id, "original"), { recursive: true });
|
||||
|
||||
const pages: { page: number; path: string }[] = [];
|
||||
const { createCanvas } = await import("@napi-rs/canvas");
|
||||
|
||||
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const scale = 2.0;
|
||||
const viewport = page.getViewport({ scale });
|
||||
const width = Math.floor(viewport.width);
|
||||
const height = Math.floor(viewport.height);
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Fill white background — node-canvas defaults to transparent
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
await page.render({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
canvasContext: ctx as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
canvas: canvas as any,
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
const resultFileName = `page_${pageNum}.png`;
|
||||
const resultPath = path.join(resultsDir, resultFileName);
|
||||
|
||||
// Write the raw rendered PNG — no sharp processing
|
||||
const buffer = canvas.toBuffer("image/png");
|
||||
const { writeFile } = await import("fs/promises");
|
||||
await writeFile(resultPath, buffer);
|
||||
|
||||
const { buffer } = await renderPage(pdfDoc, pageNum);
|
||||
await writeFile(resultPagePath(user.id, id, "original", pageNum), buffer);
|
||||
pages.push({
|
||||
page: pageNum,
|
||||
path: `${publicDir}/${resultFileName}`,
|
||||
path: `/api/documents/${id}/asset?type=original&page=${pageNum}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Update pageCount if not set
|
||||
if (doc.pageCount === 0) {
|
||||
await prisma.pdfDocument.update({
|
||||
where: { id },
|
||||
@@ -101,9 +65,6 @@ export async function POST(
|
||||
return NextResponse.json({ pages, pageCount: numPages });
|
||||
} catch (e) {
|
||||
console.error("Render error:", e);
|
||||
return NextResponse.json(
|
||||
{ error: "Page rendering failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: "Page rendering failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user