1import type { Database } from "bun:sqlite";2import {3 clearMirror,4 knownEventIds,5 knownObject,6 knownReleases,7 knownSave,8 land,9 mirrorState,10 openMirror,11 type StoredObject,12} from "../mirror/store.ts";13import { hiddenPaths, recordSync, registerRepo } from "../server/db.ts";14import { RESERVED_NAMES } from "../server/reserved.ts";15import { dropTarballs } from "../tarball/cache.ts";16import type { Bucket } from "./reader.ts";17import {18 BucketError,19 checkFormat,20 type EventRecord,21 escapePath,22 isSafePath,23 listEventIds,24 listReleaseVersions,25 listRepos,26 OversizedObject,27 type ReleaseRecord,28 readEvent,29 readHead,30 readObject,31 readRelease,32 readReleasesLatest,33 readSave,34 type SaveRecord,35} from "./repo.ts";3637const INTERVAL_MS = 2 * 60 * 1000;3839export type SyncTarget = {40 bucket: Bucket;41 kepr: Database;42 mirrorDir: string;43};4445let running: Promise<void> | null = null;464748export function syncAll(target: SyncTarget): Promise<void> {49 running ??= syncEach(target).finally(() => {50 running = null;51 });52 return running;53}545556export async function rebuildRepo(target: SyncTarget, repo: string): Promise<void> {57 await syncAll(target);58 clearMirror(openMirror(target.mirrorDir, repo));59 await syncAll(target);60}6162export function startSyncLoop(target: SyncTarget): void {63 const tick = () => syncAll(target).catch((error) => console.error(`kepr: sync failed: ${error}`));64 tick();65 setInterval(tick, INTERVAL_MS);66}6768async function syncEach(target: SyncTarget): Promise<void> {69 for (const repo of await listRepos(target.bucket)) {70 registerRepo(target.kepr, repo);71 let error: string | null = null;72 if (RESERVED_NAMES.has(repo)) {73 error = `not synced: the name "${repo}" clashes with Kepr's own /${repo} route; rename the repo`;74 } else {75 try {76 const mirror = openMirror(target.mirrorDir, repo);77 const events = await syncRepo(target.bucket, mirror, repo, hiddenPaths(target.kepr, repo));78 if (events.some((event) => event.kind === "strip")) await dropTarballs(target.bucket, repo);79 } catch (cause) {80 error = cause instanceof Error ? cause.message : String(cause);81 }82 }83 recordSync(target.kepr, repo, Math.floor(Date.now() / 1000), error);84 }85}86878889909192939495export async function syncRepo(96 bucket: Bucket,97 mirror: Database,98 repo: string,99 hidden: string[] = [],100): Promise<EventRecord[]> {101 await checkFormat(bucket, repo);102 const head = await readHead(bucket, repo);103 if (head === null) return [];104 const saves: SaveRecord[] = [];105 const objects = new Map<string, StoredObject>();106 const oversized = new Set<string>();107 const quarantined: string[] = [];108 const seen = new Set<string>();109 for (let cursor = head; cursor !== "" && !knownSave(mirror, cursor); ) {110 if (seen.has(cursor)) throw new BucketError(`${repo}: save ${cursor} is its own ancestor`);111 seen.add(cursor);112 const save = await readSave(bucket, repo, cursor);113 quarantined.push(...save.quarantined);114 for (const file of save.files) {115 if (objects.has(file.hash) || oversized.has(file.hash) || knownObject(mirror, file.hash)) {116 continue;117 }118 try {119 const { compressed, bytes } = await readObject(bucket, repo, file.hash);120 objects.set(file.hash, { compressed, size: bytes.length });121 } catch (cause) {122 if (!(cause instanceof OversizedObject)) throw cause;123 oversized.add(file.hash);124 }125 }126 quarantined.push(127 ...save.files.filter((f) => oversized.has(f.hash)).map((f) => escapePath(f.path)),128 );129 save.files = save.files.filter((file) => !oversized.has(file.hash));130 saves.push(save);131 cursor = save.parent;132 }133 const known = knownEventIds(mirror);134 const events: EventRecord[] = [];135 for (const id of await listEventIds(bucket, repo)) {136 if (known.has(id)) continue;137 const event = await readEvent(bucket, repo, id);138 139 if (event.kind !== "strip" || isSafePath(event.path)) events.push(event);140 }141 const releasesLatest = (await readReleasesLatest(bucket, repo)) ?? "";142 const moved =143 mirrorState(mirror, "head") !== head ||144 mirrorState(mirror, "releases_latest") !== releasesLatest;145 const releases = moved ? await newReleases(bucket, mirror, repo) : [];146 if (!moved && events.length === 0) return [];147 land(mirror, {148 head,149 releasesLatest,150 saves: saves.reverse(),151 objects,152 events,153 releases,154 quarantined,155 hidden,156 });157 return events;158}159160async function newReleases(161 bucket: Bucket,162 mirror: Database,163 repo: string,164): Promise<ReleaseRecord[]> {165 const known = knownReleases(mirror);166 const releases: ReleaseRecord[] = [];167 for (const version of await listReleaseVersions(bucket, repo)) {168 if (!known.has(version)) releases.push(await readRelease(bucket, repo, version));169 }170 return releases;171}