// Re-derive the stored tree shape for every cached share snapshot, from the folder rows already in // Postgres — no peer contact, so it works with everyone offline and is deterministic. // // Written for the backfill when the tree columns were added, but kept because it's the answer to any // future change in tree shape: it runs the SAME buildTree + finishSoulseekBrowse the sidecar's ingest // runs, so there's no second implementation to drift. // // bun scripts/rebuild-soulseek-tree.ts import type { BrowsedFile } from 'officerdb'; import { eq, asc } from 'drizzle-orm'; import { db, finishSoulseekBrowse } from 'officerdb'; import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/schema'; import { buildTree } from '../src/servers/sidecar/slskd/browse'; const snapshots = await db .select({ id: soulseekBrowseSnapshots.id, userId: soulseekBrowseSnapshots.userId, username: soulseekBrowseSnapshots.username, status: soulseekBrowseSnapshots.status, }) .from(soulseekBrowseSnapshots) .orderBy(asc(soulseekBrowseSnapshots.id)); console.log(`${snapshots.length} snapshot(s) to rebuild`); for (const snap of snapshots) { const started = Date.now(); const rows = await db .select({ name: soulseekBrowseDirs.name, files: soulseekBrowseDirs.files }) .from(soulseekBrowseDirs) .where(eq(soulseekBrowseDirs.snapshotId, snap.id)); if (!rows.length) { console.log(` #${snap.id} ${snap.username}: no folder rows, skipped`); continue; } const tree = buildTree(rows.map((r) => ({ name: r.name, files: r.files as BrowsedFile[] }))); await finishSoulseekBrowse(snap.id, tree); const roots = tree.filter((t) => t.parentPath === null); console.log( ` #${snap.id} ${snap.username}: ${rows.length} -> ${tree.length} rows ` + `(+${tree.length - rows.length} synthesized), ${roots.length} root(s), ` + `${roots.reduce((n, r) => n + r.subtreeFileCount, 0)} files, ${Date.now() - started}ms`, ); } process.exit(0);