import type { Database } from "bun:sqlite"; import { rmSync } from "node:fs"; import { join } from "node:path"; import { inTurn } from "../bucket/reader.ts"; import type { SyncTarget } from "../bucket/sync.ts"; import { closeMirror } from "../mirror/store.ts"; import { keepState, setting } from "./db.ts"; // One repo delete at a time. Its progress lives in settings rather than in this process, so the // console can show it and a Kepr restarted mid-delete can be asked to finish the job. let running: Promise | null = null; export type Removal = { repo: string; done: number; total: number; error: string }; /** The delete in progress, or null. */ export function removal(kepr: Database): Removal | null { const repo = setting(kepr, "removing_repo"); if (repo === null) return null; return { repo, done: Number(setting(kepr, "removing_done") ?? 0), total: Number(setting(kepr, "removing_total") ?? 0), error: setting(kepr, "removing_error") ?? "", }; } /** Starts deleting a whole repo (ยง24); false when one is already being deleted. */ export function startRemoval(target: SyncTarget, repo: string): boolean { if (running) return false; running = deleteRepo(target, repo).finally(() => { running = null; }); return true; } /** The delete under way, or null when none is: what a caller waits on to see it finish. */ export function removalRun(): Promise | null { return running; } async function deleteRepo(target: SyncTarget, repo: string): Promise { const { bucket, kepr, mirrorDir } = target; keepState(kepr, "removing_repo", repo); keepState(kepr, "removing_error", null); try { const keys = await bucket.repoKeys(repo); keepState(kepr, "removing_total", String(keys.length)); keepState(kepr, "removing_done", "0"); let done = 0; // A few keys at a time: a repo holds thousands, and one request each could take an hour. await inTurn(keys, 4, async (key) => { await bucket.removeRepoKey(repo, key); done += 1; if (done % 20 === 0) keepState(kepr, "removing_done", String(done)); }); keepState(kepr, "removing_done", String(done)); forgetRepo(kepr, mirrorDir, repo); keepState(kepr, "removing_repo", null); } catch (error) { // The keys already gone stay gone; starting again lists what is left and finishes the job. keepState(kepr, "removing_error", error instanceof Error ? error.message : String(error)); } } /** Everything Kepr holds for a repo: its mirror, and its rows. Its bucket keys are already gone. */ export function forgetRepo(kepr: Database, mirrorDir: string, repo: string): void { closeMirror(mirrorDir, repo); for (const suffix of [".db", ".db-wal", ".db-shm"]) { rmSync(join(mirrorDir, `${repo}${suffix}`), { force: true }); } const children = ["issue_entries", "issue_files", "issue_followers"]; // Every table with a `repo` column, plus the rename record that spells both names. const own = [ "blocked_reporters", "issues", "release_settings", "downloads", "download_days", "installs", "repo_text", ]; kepr.transaction(() => { for (const table of children) { kepr .query(`DELETE FROM ${table} WHERE issue_id IN (SELECT id FROM issues WHERE repo = ?)`) .run(repo); } for (const table of own) kepr.query(`DELETE FROM ${table} WHERE repo = ?`).run(repo); kepr.query("DELETE FROM renamed WHERE old = ? OR new = ?").run(repo, repo); kepr.query("DELETE FROM repos WHERE name = ?").run(repo); })(); }