import { createHash, randomBytes } from "node:crypto"; import { constants } from "node:fs"; import { lstat, mkdir, open, rename, rm } from "node:fs/promises"; import { dirname, join } from "node:path"; import { assertSha256Digest, type Sha256Digest, } from "../shared/build-protocol"; function digest(data: Uint8Array): Sha256Digest { return `sha256:${createHash("sha256").update(data).digest("hex")}`; } export class FilesystemCas { readonly root: string; constructor(root: string) { this.root = root; } private path(blobDigest: Sha256Digest): string { assertSha256Digest(blobDigest); return join( this.root, "sha256", blobDigest.slice(7, 9), blobDigest.slice(9), ); } async has(blobDigest: Sha256Digest): Promise { try { return (await lstat(this.path(blobDigest))).isFile(); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; } } async put(data: Uint8Array, expected?: Sha256Digest): Promise { const actual = digest(data); if (expected !== undefined) { assertSha256Digest(expected); if (actual !== expected) throw new Error( `CAS digest mismatch: expected ${expected}, got ${actual}`, ); } const target = this.path(actual); const directory = dirname(target); await mkdir(directory, { recursive: true, mode: 0o755 }); if (await this.has(actual)) { await this.get(actual); return actual; } const temporary = `${target}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`; const handle = await open( temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o644, ); try { await handle.writeFile(data); await handle.sync(); } finally { await handle.close(); } try { await rename(temporary, target); } finally { await rm(temporary, { force: true }); } return actual; } async get(blobDigest: Sha256Digest): Promise { const handle = await open( this.path(blobDigest), constants.O_RDONLY | constants.O_NOFOLLOW, ); let data: Uint8Array; try { const blobStat = await handle.stat(); if (!blobStat.isFile()) throw new Error(`CAS blob is not a regular file: ${blobDigest}`); data = await handle.readFile(); } finally { await handle.close(); } const actual = digest(data); if (actual !== blobDigest) throw new Error( `Corrupt CAS blob: expected ${blobDigest}, got ${actual}`, ); return data; } }