SPB Git
4.1 KB · 55 lines typescript
1import { execFile } from "node:child_process";2import { promisify } from "node:util";3import { createHash } from "node:crypto";4import { createReadStream, promises as fs } from "node:fs";5import { join, dirname } from "node:path";6import { STORAGE_DIR } from "@/lib/env";7const run = promisify(execFile);8const BIN = (name: string) => process.env.POPPLER_BIN ? join(process.env.POPPLER_BIN, name) : name;9const PATH_ENV = { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH ?? ""}` };1011export async function sha256File(path: string): Promise<string> {12  return new Promise((res, rej) => { const h = createHash("sha256"); createReadStream(path).on("data", (d) => h.update(d)).on("end", () => res(h.digest("hex"))).on("error", rej); });13}14export type PdfInfo = { pages: number | null; title: string | null; author: string | null; subject: string | null; keywords: string | null; creator: string | null; producer: string | null; creationDate: string | null; modDate: string | null; pdfVersion: string | null; encrypted: boolean; pageSize: string | null; raw: Record<string, string> };15export async function pdfInfo(path: string): Promise<PdfInfo> {16  const { stdout } = await run(BIN("pdfinfo"), [path], { env: PATH_ENV, maxBuffer: 1 << 20 });17  const raw: Record<string, string> = {};18  for (const line of stdout.split("\n")) { const i = line.indexOf(":"); if (i > 0) raw[line.slice(0, i).trim()] = line.slice(i + 1).trim(); }19  const g = (k: string) => raw[k] ?? null;20  return { pages: raw.Pages ? Number(raw.Pages) : null, title: g("Title"), author: g("Author"), subject: g("Subject"), keywords: g("Keywords"), creator: g("Creator"), producer: g("Producer"), creationDate: g("CreationDate"), modDate: g("ModDate"), pdfVersion: g("PDF version"), encrypted: (raw.Encrypted ?? "no").toLowerCase() !== "no", pageSize: g("Page size"), raw };21}22/** Integrity: the file must parse, report ≥ 1 page, not be encrypted, and the first page must yield text (or be flagged scanned). */23export async function pdfIntegrity(path: string): Promise<{ ok: boolean; reason?: string; info?: PdfInfo }> {24  try {25    const info = await pdfInfo(path);26    if (!info.pages || info.pages < 1) return { ok: false, reason: "no pages", info };27    if (info.encrypted) return { ok: false, reason: "encrypted", info };28    return { ok: true, info };29  } catch (e) { return { ok: false, reason: (e as Error).message.slice(0, 200) }; }30}31export async function pdfText(path: string, opts: { first?: number; last?: number } = {}): Promise<string> {32  const args = ["-layout", "-enc", "UTF-8"]; if (opts.first) args.push("-f", String(opts.first)); if (opts.last) args.push("-l", String(opts.last)); args.push(path, "-");33  const { stdout } = await run(BIN("pdftotext"), args, { env: PATH_ENV, maxBuffer: 64 << 20 });34  return stdout;35}36/** Full text split per page (form-feed separated by pdftotext). */37export async function pdfTextPages(path: string): Promise<string[]> {38  const t = await pdfText(path); return t.split("\f").map((p) => p.replace(/[ \t]+\n/g, "\n").trim());39}40export async function pdfThumbnail(path: string, outPngBase: string, dpi = 50): Promise<string> {41  await fs.mkdir(dirname(outPngBase), { recursive: true });42  await run(BIN("pdftoppm"), ["-png", "-r", String(dpi), "-f", "1", "-l", "1", "-singlefile", path, outPngBase], { env: PATH_ENV });43  return `${outPngBase}.png`;44}45export function storagePath(key: string) { return join(/*turbopackIgnore: true*/ STORAGE_DIR, key); }46export async function ensureStorage() { for (const d of ["files", "previews", "inbox", "tmp"]) await fs.mkdir(join(/*turbopackIgnore: true*/ STORAGE_DIR, d), { recursive: true }); }47/** Content-addressed storage key: files/<2 hex>/<sha256>.pdf — the same manuscript is never stored twice. */48export function manuscriptKey(sha: string, ext = "pdf") { return `files/${sha.slice(0, 2)}/${sha}.${ext}`; }49export async function storeFile(srcPath: string, sha: string, ext = "pdf"): Promise<string> {50  const key = manuscriptKey(sha, ext); const dest = storagePath(key);51  await fs.mkdir(dirname(dest), { recursive: true });52  try { await fs.access(dest); } catch { await fs.copyFile(srcPath, dest); await fs.chmod(dest, 0o640); }53  return key;54}55
Files · main
Menu

Theme