mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { resolveQpdf } from "./binaries.js";
|
|
|
|
/** @internal Shared qpdf CLI runner for doc-engine modules; not part of the public package API. */
|
|
export function runQpdf(args: string[], timeoutMs = 30_000): Promise<string> {
|
|
const bin = resolveQpdf();
|
|
if (!bin) throw new Error("qpdf binary not found (set QPDF_PATH or install qpdf)");
|
|
return new Promise<string>((resolvePromise, reject) => {
|
|
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
let out = "";
|
|
let err = "";
|
|
let settled = false;
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
child.kill("SIGKILL");
|
|
reject(new Error(`qpdf timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
}, timeoutMs);
|
|
child.stdout.on("data", (c: Buffer) => {
|
|
out += c.toString("utf8");
|
|
});
|
|
child.stderr.on("data", (c: Buffer) => {
|
|
err = (err + c.toString("utf8")).slice(-4096);
|
|
});
|
|
child.on("error", (e) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
reject(e);
|
|
});
|
|
child.on("close", (code, signal) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
if (code === 0) resolvePromise(out);
|
|
else
|
|
reject(new Error(`qpdf exited ${code ?? signal}: ${err.slice(-1000) || out.slice(-1000)}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Structural check; throws with qpdf's diagnostics on damage. */
|
|
export async function qpdfCheck(filePath: string): Promise<void> {
|
|
await runQpdf(["--check", filePath]);
|
|
}
|
|
|
|
/**
|
|
* True when the PDF requires a password to open. qpdf --requires-password
|
|
* exits 0 when a password is required (2 = not encrypted, 3 = encrypted but
|
|
* the correct password was supplied), so this cannot be a throw-on-nonzero
|
|
* call.
|
|
*
|
|
* Verified against qpdf 12.1.0:
|
|
* encrypted fixture (no pw) -> exit 0
|
|
* plain fixture -> exit 2
|
|
* encrypted + correct pw -> exit 3
|
|
* encrypted + wrong pw -> exit 0
|
|
*/
|
|
export async function qpdfRequiresPassword(filePath: string): Promise<boolean> {
|
|
const bin = resolveQpdf();
|
|
if (!bin) throw new Error("qpdf binary not found (set QPDF_PATH or install qpdf)");
|
|
return new Promise<boolean>((resolvePromise, reject) => {
|
|
const child = spawn(bin, ["--requires-password", filePath], {
|
|
stdio: ["ignore", "ignore", "pipe"],
|
|
});
|
|
let settled = false;
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
child.kill("SIGKILL");
|
|
reject(new Error("qpdf timed out after 30s"));
|
|
}, 30_000);
|
|
child.on("error", (e) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
reject(e);
|
|
});
|
|
child.on("close", (code) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolvePromise(code === 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function qpdfPageCount(filePath: string): Promise<number> {
|
|
const out = await runQpdf(["--show-npages", filePath]);
|
|
const n = Number(out.trim());
|
|
if (!Number.isFinite(n)) throw new Error(`qpdf returned a non-numeric page count: ${out.trim()}`);
|
|
return n;
|
|
}
|