repositories / will
will
owned by admin
src/shared/cas.ts
Rawimport { 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<StoredMedia> {
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<number> {
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
}