remove.ts

3.4 KB · raw

1import type { Database } from "bun:sqlite";2import { rmSync } from "node:fs";3import { join } from "node:path";4import { inTurn } from "../bucket/reader.ts";5import type { SyncTarget } from "../bucket/sync.ts";6import { closeMirror } from "../mirror/store.ts";7import { keepState, setting } from "./db.ts";89// One repo delete at a time. Its progress lives in settings rather than in this process, so the10// console can show it and a Kepr restarted mid-delete can be asked to finish the job.11let running: Promise<void> | null = null;1213export type Removal = { repo: string; done: number; total: number; error: string };1415/** The delete in progress, or null. */16export function removal(kepr: Database): Removal | null {17	const repo = setting(kepr, "removing_repo");18	if (repo === null) return null;19	return {20		repo,21		done: Number(setting(kepr, "removing_done") ?? 0),22		total: Number(setting(kepr, "removing_total") ?? 0),23		error: setting(kepr, "removing_error") ?? "",24	};25}2627/** Starts deleting a whole repo (§24); false when one is already being deleted. */28export function startRemoval(target: SyncTarget, repo: string): boolean {29	if (running) return false;30	running = deleteRepo(target, repo).finally(() => {31		running = null;32	});33	return true;34}3536/** The delete under way, or null when none is: what a caller waits on to see it finish. */37export function removalRun(): Promise<void> | null {38	return running;39}4041async function deleteRepo(target: SyncTarget, repo: string): Promise<void> {42	const { bucket, kepr, mirrorDir } = target;43	keepState(kepr, "removing_repo", repo);44	keepState(kepr, "removing_error", null);45	try {46		const keys = await bucket.repoKeys(repo);47		keepState(kepr, "removing_total", String(keys.length));48		keepState(kepr, "removing_done", "0");49		let done = 0;50		// A few keys at a time: a repo holds thousands, and one request each could take an hour.51		await inTurn(keys, 4, async (key) => {52			await bucket.removeRepoKey(repo, key);53			done += 1;54			if (done % 20 === 0) keepState(kepr, "removing_done", String(done));55		});56		keepState(kepr, "removing_done", String(done));57		forgetRepo(kepr, mirrorDir, repo);58		keepState(kepr, "removing_repo", null);59	} catch (error) {60		// The keys already gone stay gone; starting again lists what is left and finishes the job.61		keepState(kepr, "removing_error", error instanceof Error ? error.message : String(error));62	}63}6465/** Everything Kepr holds for a repo: its mirror, and its rows. Its bucket keys are already gone. */66export function forgetRepo(kepr: Database, mirrorDir: string, repo: string): void {67	closeMirror(mirrorDir, repo);68	for (const suffix of [".db", ".db-wal", ".db-shm"]) {69		rmSync(join(mirrorDir, `${repo}${suffix}`), { force: true });70	}71	const children = ["issue_entries", "issue_files", "issue_followers"];72	// Every table with a `repo` column, plus the rename record that spells both names.73	const own = [74		"blocked_reporters",75		"issues",76		"release_settings",77		"downloads",78		"download_days",79		"installs",80		"repo_text",81	];82	kepr.transaction(() => {83		for (const table of children) {84			kepr85				.query(`DELETE FROM ${table} WHERE issue_id IN (SELECT id FROM issues WHERE repo = ?)`)86				.run(repo);87		}88		for (const table of own) kepr.query(`DELETE FROM ${table} WHERE repo = ?`).run(repo);89		kepr.query("DELETE FROM renamed WHERE old = ? OR new = ?").run(repo, repo);90		kepr.query("DELETE FROM repos WHERE name = ?").run(repo);91	})();92}