shelf.ts
3.2 kB · typescript · 89 lines
1import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";2import { dirname, join, resolve } from "node:path";34const org = resolve(import.meta.dir, "..");5const data = join(org, "data");6const home = join(data, "shelf");7const REPO = process.env.SHELF_REPO ?? "";8const TARBALL = REPO ? `https://codeload.github.com/${REPO}/tar.gz/main` : "";9const KEEP = "research/";1011/* TAR */1213const text = (bytes: Uint8Array, from: number, to: number) => {14 const end = bytes.indexOf(0, from);15 return new TextDecoder().decode(bytes.subarray(from, end < 0 || end > to ? to : end));16};17const octal = (bytes: Uint8Array, from: number, to: number) => parseInt(text(bytes, from, to).trim() || "0", 8);1819function pax(block: Uint8Array) {20 const out: Record<string, string> = {};21 const body = new TextDecoder().decode(block);22 for (const line of body.split("\n")) {23 const m = line.match(/^\d+ ([^=]+)=(.*)$/);24 if (m) out[m[1]] = m[2];25 }26 return out;27}2829function untar(tar: Uint8Array, into: string) {30 let at = 0;31 let next: Record<string, string> = {};32 let files = 0;33 while (at + 512 <= tar.length) {34 const head = tar.subarray(at, at + 512);35 if (head.every((b) => b === 0)) break;36 const size = octal(head, 124, 136);37 const kind = String.fromCharCode(head[156]);38 const prefix = text(head, 345, 500);39 let name = next.path ?? (prefix ? `${prefix}/${text(head, 0, 100)}` : text(head, 0, 100));40 const body = tar.subarray(at + 512, at + 512 + size);41 at += 512 + Math.ceil(size / 512) * 512;42 if (kind === "x") {43 next = pax(body);44 continue;45 }46 next = {};47 if (kind === "g" || kind === "L") continue;48 name = name.replace(/^[^/]+\//, "");49 if (!name.startsWith(KEEP)) continue;50 const path = join(into, name);51 if (kind === "5" || name.endsWith("/")) {52 mkdirSync(path, { recursive: true });53 } else if (kind === "0" || kind === "\0") {54 mkdirSync(dirname(path), { recursive: true });55 writeFileSync(path, body);56 files++;57 }58 }59 return files;60}6162/* FETCH */6364export async function shelf(): Promise<string> {65 const local = process.env.MRLY_SHELF;66 if (local) return resolve(local);67 const cached = join(home, "research");68 const fallback = (why: string) => {69 if (!existsSync(join(cached, "README.md"))) throw new Error(`shelf: ${why} and no cache at ${cached}; set MRLY_SHELF to a local checkout or SHELF_REPO to an owner/repo`);70 console.warn(`shelf: ${why}, building from the cached copy`);71 return cached;72 };73 if (!TARBALL) return fallback("no SHELF_REPO in the environment");74 try {75 const res = await fetch(TARBALL);76 if (!res.ok) throw new Error(`HTTP ${res.status}`);77 const tar = Bun.gunzipSync(new Uint8Array(await res.arrayBuffer()));78 const fresh = join(data, "shelf.next");79 rmSync(fresh, { recursive: true, force: true });80 const files = untar(tar, fresh);81 if (!existsSync(join(fresh, "research", "README.md"))) throw new Error("tarball carries no research/README.md");82 rmSync(home, { recursive: true, force: true });83 renameSync(fresh, home);84 console.log(`shelf: fetched ${files} files from GitHub`);85 } catch (reason) {86 return fallback(`fetch failed (${reason})`);87 }88 return cached;89}