Files
kuber/server/cas.ts
T
2026-09-03 11:28:30 +07:00

100 lines
2.6 KiB
TypeScript

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<boolean> {
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<Sha256Digest> {
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<Uint8Array> {
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;
}
}