cache.ts

2.4 KB · raw

1import type { Database } from "bun:sqlite";2import { type Bucket, BucketWriteError, KEPR_PREFIX } from "../bucket/reader.ts";3import { filesAt, objectBytes, tarballGeneration } from "../mirror/queries.ts";4import { knownSave } from "../mirror/store.ts";5import { buildTarball } from "./tar.ts";67const tarballsOf = (repo: string) => `${KEPR_PREFIX}tarballs/${repo}/`;89/**10 * A save's tarball, kept in the bucket under Kepr's own folder (June, 2026-09-30) as11 * `.kepr/tarballs/<repo>/<generation>/<id>.tar.gz` once first requested (`tarballGeneration`).12 * If the bucket refuses the write nothing is kept, and each request builds it again. Null when the13 * save is gone from the mirror, as it is for a moment while a rebuild runs.14 */15export async function cachedTarball(16	bucket: Bucket,17	db: Database,18	repo: string,19	saveId: string,20): Promise<Uint8Array | null> {21	for (;;) {22		const generation = tarballGeneration(db);23		const key = `${tarballsOf(repo)}${generation}/${saveId}.tar.gz`;24		const stored = await bucket.get(key);25		// A strip, a hidden-path change or a rebuild may land during any await, and the sync's thread26		// can land one between any two statements: the generation and the files are read in one27		// snapshot, so what is built matches the key it files under, and bytes are served only for28		// the generation still current.29		const read = db.transaction(() => {30			if (tarballGeneration(db) !== generation) return "moved";31			if (stored) return "stored";32			if (!knownSave(db, saveId)) return "unknown";33			return filesAt(db, saveId).map((file) => {34				const bytes = objectBytes(db, file.hash);35				if (!bytes) throw new Error(`${repo}: object ${file.hash} is missing from the mirror`);36				return { path: file.path, bytes, executable: file.executable };37			});38		})();39		if (read === "moved") continue;40		if (read === "stored") return stored;41		if (read === "unknown") return null;42		const tarball = buildTarball(`${repo}-${saveId.slice(0, 8)}`, read);43		try {44			await bucket.put(key, tarball, "application/gzip");45		} catch (error) {46			if (!(error instanceof BucketWriteError)) throw error;47		}48		if (tarballGeneration(db) === generation) return tarball;49	}50}5152/** Deletes a repo's kept tarballs to free space; stale ones are never looked up anyway. */53export async function dropTarballs(bucket: Bucket, repo: string): Promise<void> {54	for (const key of await bucket.list(tarballsOf(repo))) await bucket.remove(key);55}