SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
3.6 KB · 125 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : src/lib/cache.mjs8 *  Purpose : Filesystem cache keyed by <repo>@<sha> — busted on push9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import {14  readFileSync, writeFileSync, mkdirSync, rmSync, existsSync,15  readdirSync, statSync, renameSync,16} from 'node:fs';17import { join, dirname } from 'node:path';18import { randomBytes } from 'node:crypto';19import { safeJoin } from './util.mjs';2021/**22 * Content cache under `cacheDir`. Layout:23 *   <cacheDir>/repos/<repo>/<sha>/<kind>          per-commit artifacts24 *   <cacheDir>/global/<kind>                       cross-repo artifacts (search index, heatmap)25 * Push hooks call {@link Cache#bustRepo} which nukes the repo subtree.26 */27export class Cache {28  /** @param {string} cacheDir */29  constructor(cacheDir) {30    this.root = cacheDir;31  }3233  /** @param {...string} segments @returns {string} */34  path(...segments) {35    const cleaned = segments.map((s) => String(s).replaceAll('/', '_'));36    return safeJoin(this.root, ...cleaned);37  }3839  /** Per-repo, per-commit key. @returns {string} */40  repoPath(repo, sha, kind) {41    return safeJoin(this.root, 'repos', repo, sha, kind.replaceAll('/', '_'));42  }4344  /** @returns {Buffer|null} */45  getBuffer(path) {46    try {47      return readFileSync(path);48    } catch {49      return null;50    }51  }5253  /** @returns {any|null} */54  getJSON(path) {55    const buf = this.getBuffer(path);56    if (buf === null) return null;57    try {58      return JSON.parse(buf.toString('utf8'));59    } catch {60      return null;61    }62  }6364  /** Atomic write (tmp + rename). */65  set(path, data) {66    mkdirSync(dirname(path), { recursive: true });67    const tmp = `${path}.${randomBytes(4).toString('hex')}.tmp`;68    writeFileSync(tmp, data);69    renameSync(tmp, path);70  }7172  setJSON(path, value) {73    this.set(path, JSON.stringify(value));74  }7576  /**77   * Read-through helper for JSON artifacts.78   * @param {string} path79   * @param {() => Promise<any>|any} compute80   */81  async remember(path, compute) {82    const hit = this.getJSON(path);83    if (hit !== null) return hit;84    const value = await compute();85    if (value !== undefined) this.setJSON(path, value);86    return value;87  }8889  /** Drop every cached artifact for a repo (all shas). @param {string} repo */90  bustRepo(repo) {91    rmSync(safeJoin(this.root, 'repos', repo), { recursive: true, force: true });92    rmSync(safeJoin(this.root, 'global'), { recursive: true, force: true });93  }9495  /** @returns {{files: number, bytes: number}} recursive cache footprint */96  footprint() {97    let files = 0;98    let bytes = 0;99    const walk = (dir) => {100      let entries;101      try {102        entries = readdirSync(dir);103      } catch {104        return;105      }106      for (const entry of entries) {107        const full = join(dir, entry);108        let st;109        try {110          st = statSync(full);111        } catch {112          continue;113        }114        if (st.isDirectory()) walk(full);115        else {116          files += 1;117          bytes += st.size;118        }119      }120    };121    if (existsSync(this.root)) walk(this.root);122    return { files, bytes };123  }124}125