import type { Database } from "bun:sqlite"; import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { filesAt, objectBytes, stripCount } from "../mirror/queries.ts"; import { buildTarball } from "./tar.ts"; /** * A save's tarball, built on first request into `///.tar.gz`. The * generation is how many strip events the mirror holds, so once a strip lands no older file is ever * looked up again, even if Kepr stops before the sync's cleanup runs. Built and written in one * synchronous run, so a strip can't land mid-build either. */ export function cachedTarball( tarballDir: string, db: Database, repo: string, saveId: string, ): Uint8Array { const dir = join(tarballDir, repo, String(stripCount(db))); const path = join(dir, `${saveId}.tar.gz`); try { return readFileSync(path); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } const files = filesAt(db, saveId).map((file) => { const bytes = objectBytes(db, file.hash); if (!bytes) throw new Error(`${repo}: object ${file.hash} is missing from the mirror`); return { path: file.path, bytes, executable: file.executable }; }); const tarball = buildTarball(`${repo}-${saveId.slice(0, 8)}`, files); mkdirSync(dir, { recursive: true }); writeFileSync(`${path}.tmp`, tarball); renameSync(`${path}.tmp`, path); return tarball; } /** Clears a repo's cached tarballs after a strip; cleanup only, since the new generation never reads them. */ export function dropTarballs(tarballDir: string, repo: string): void { rmSync(join(tarballDir, repo), { recursive: true, force: true }); }