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