import { createHash } from 'node:crypto' import { createReadStream } from 'node:fs' import { open as fsOpen, mkdir, readdir, rename, rm, stat } from 'node:fs/promises' import { join } from 'node:path' import type { Readable } from 'node:stream' /** * Content-addressed immutable media store (spec: bytes outside SQLite, * retained forever, deduplicated by digest). */ export function casPath(root: string, digest: string): string { return join(root, digest.slice(0, 2), digest.slice(2, 4), digest) } export interface StoredMedia { digest: string size: number } /** Store bytes under their sha256; idempotent for duplicate content. */ export async function casStore(root: string, bytes: Buffer): Promise { const digest = createHash('sha256').update(bytes).digest('hex') const path = casPath(root, digest) try { const existing = await stat(path) return { digest, size: existing.size } } catch { // not stored yet } const dir = join(root, digest.slice(0, 2), digest.slice(2, 4)) await mkdir(dir, { recursive: true }) const tmp = join(dir, `.tmp-${process.pid}-${Date.now()}`) const fh = await fsOpen(tmp, 'w') try { await fh.writeFile(bytes) await fh.sync() } finally { await fh.close() } await rename(tmp, path) return { digest, size: bytes.length } } export function casReadStream(root: string, digest: string): Readable { return createReadStream(casPath(root, digest)) } /** Remove leftover .tmp-* files orphaned by crashes (maintenance reconcile). */ export async function casReconcile(root: string): Promise { let removed = 0 const walk = async (dir: string) => { let entries: import('node:fs').Dirent[] try { entries = await readdir(dir, { withFileTypes: true }) } catch { return } for (const e of entries) { const p = join(dir, e.name) if (e.isDirectory()) await walk(p) else if (e.name.startsWith('.tmp-')) { await rm(p, { force: true }) removed++ } } } await walk(root) return removed }