import { mkdir, readdir, rm, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; const JOB_TTL_MS = 15 * 60 * 1000; const JOBS_ROOT = path.join(tmpdir(), "kk-card-jobs"); const JOB_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; export function isJobId(id: string): boolean { return JOB_ID_RE.test(id); } export function jobDir(id: string): string { return path.join(JOBS_ROOT, id); } export function jobZipPath(id: string): string { return path.join(jobDir(id), "ids.zip"); } export async function sweepExpiredJobs(): Promise { let names: string[] = []; try { names = await readdir(JOBS_ROOT); } catch { return; } const cutoff = Date.now() - JOB_TTL_MS; await Promise.all( names.map(async (name) => { const dir = path.join(JOBS_ROOT, name); try { const info = await stat(dir); if (info.mtimeMs >= cutoff) return; await rm(dir, { recursive: true, force: true }); } catch { // ignore missing or locked job folders } }), ); } export async function createJobDir(): Promise<{ id: string; dir: string; pngDir: string; imagePath: string; zipPath: string; }> { await sweepExpiredJobs(); const id = crypto.randomUUID(); const dir = jobDir(id); const pngDir = path.join(dir, "png"); await mkdir(pngDir, { recursive: true }); return { id, dir, pngDir, imagePath: path.join(dir, "base"), zipPath: jobZipPath(id), }; } export async function getJobZip( id: string, ): Promise<{ zipPath: string; size: number } | null> { if (!isJobId(id)) return null; const zipPath = jobZipPath(id); try { const info = await stat(zipPath); if (!info.isFile() || info.size === 0) return null; return { zipPath, size: info.size }; } catch { return null; } }