Something went wrong. Try again.
Periodic backups of ATProto repos and blobs safekeeper.dummy.cafe
atproto backup
Something went wrong. Try again.
2.9 kB · 83 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384// Wipe an account from safekeeper's control plane and storage.//// After a wipe:// - every CAR file under repos/{did}/ is deleted from storage// - every blob under blobs/{did}/ is deleted from storage// - the account, tracked_account, repo_backup, blob_backup rows for the DID// are hard-deleted// - the DID is inserted into blocked_account so future scheduler ticks// ignore it even if a PDS still lists it//// This is destructive and irreversible from within the panel (unblock is a// manual DB delete). The controller layer is responsible for confirming// intent — this function assumes the caller has already done that.
import { eq } from 'drizzle-orm';import db from '../database';import { account, blob_backup, blocked_account, repo_backup, tracked_account} from '../database/schema';import { storage } from '../storage';
export interface WipeSummary { repoBackupsDeleted: number; blobBackupsDeleted: number; bytesFreed: number;}
export async function wipeAccount( did: string, reason: string | null): Promise<WipeSummary> { const s = storage();
// Remove the whole per-DID directories in one shot — cheaper than iterating // each file, and correct even if the DB and disk have drifted (any orphan // files under repos/{did} or blobs/{did} also go). LocalStorage.delete // uses `rm -rf`, S3 impls would need a list+delete-many. await s.delete(`repos/${did}`).catch(() => {}); await s.delete(`blobs/${did}`).catch(() => {});
// Tally what we're about to remove for the return summary. Include pruned // snapshots because they were once real bytes we captured (even if the // file is gone). const repos = await db .select({ id: repo_backup.id, size: repo_backup.size_bytes }) .from(repo_backup) .where(eq(repo_backup.account_did, did)); const blobs = await db .select({ cid: blob_backup.cid, size: blob_backup.size_bytes }) .from(blob_backup) .where(eq(blob_backup.account_did, did));
const summary: WipeSummary = { repoBackupsDeleted: repos.length, blobBackupsDeleted: blobs.length, bytesFreed: repos.reduce((n, r) => n + r.size, 0) + blobs.reduce((n, b) => n + b.size, 0) };
// Child rows first (they FK-reference account), then parent, then insert // into the blocklist. bun:sqlite doesn't expose a great transaction API // via drizzle-orm; a scheduler tick can't interleave with us here because // runBackupCycle is single-flight via the in-process mutex. await db.delete(blob_backup).where(eq(blob_backup.account_did, did)); await db.delete(repo_backup).where(eq(repo_backup.account_did, did)); await db.delete(tracked_account).where(eq(tracked_account.did, did)); await db.delete(account).where(eq(account.did, did));
await db .insert(blocked_account) .values({ did, reason }) .onConflictDoUpdate({ target: blocked_account.did, set: { reason } });
return summary;}