Files
rea/src/filesystem/StableFileHash.ts
T
morluto 131fd5ddcd fix(filesystem): retain stable hash cleanup owners across callers
Preserve completed digests and primary failures when descriptor close fails.
Carry the exact handle owner through provider, readiness, cache, doctor and
verifier boundaries, and retry cleanup without rehashing or retrying a raw fd.
Close candidate-owned resources even when no target client was opened.
Keep verifier workspaces when provider shutdown remains incomplete.

Refs #4266
2026-10-16 06:46:25 +08:00

105 lines
3.1 KiB
TypeScript

import { createHash } from "node:crypto";
import { stat } from "node:fs/promises";
import {
OwnedRegularFileCleanupFailure,
openRegularFile,
RegularFileChangedError,
sameRegularFileState,
} from "./RegularFile.js";
import { OwnedFileHandle } from "./OwnedFileHandle.js";
export type StableFileHashResult = {
readonly sha256: string;
readonly bytes: number;
};
/** Hashing finished or failed, but the owned descriptor close was rejected. */
export class StableFileHashCleanupFailure extends OwnedRegularFileCleanupFailure {
constructor(
path: string,
primaryCause: unknown,
readonly hasPrimaryCause: boolean,
cleanupCause: unknown,
owner: OwnedFileHandle,
readonly completedHash: StableFileHashResult | undefined,
) {
super(
path,
hasPrimaryCause ? primaryCause : cleanupCause,
cleanupCause,
owner,
`Stable file hashing could not confirm descriptor close: ${path}`,
);
this.name = "StableFileHashCleanupFailure";
}
}
/** Hash one regular file from its admitted descriptor and verify its path state. */
export const hashStableFile = async (
path: string,
signal?: AbortSignal,
): Promise<StableFileHashResult> => {
const handle = await openRegularFile(path, {
symlinks: "follow",
signal,
});
const owner = new OwnedFileHandle(handle);
let primaryCause: unknown;
let hasPrimaryCause = false;
let completedHash: StableFileHashResult | undefined;
try {
const initial = await handle.stat();
if (!Number.isSafeInteger(initial.size) || initial.size < 0)
throw new RangeError(
`Regular file size cannot be represented safely: ${path}`,
);
const hash = createHash("sha256");
const chunk = Buffer.allocUnsafe(64 * 1024);
let position = 0;
while (position <= initial.size) {
signal?.throwIfAborted();
const length = Math.min(chunk.length, initial.size - position + 1);
if (length <= 0) break;
const read = await handle.read(chunk, 0, length, position);
signal?.throwIfAborted();
if (read.bytesRead === 0) break;
hash.update(chunk.subarray(0, read.bytesRead));
position += read.bytesRead;
if (position > initial.size) throw new RegularFileChangedError(path);
}
signal?.throwIfAborted();
const [opened, currentPath] = await Promise.all([
handle.stat(),
stat(path),
]);
signal?.throwIfAborted();
if (
position !== initial.size ||
!sameRegularFileState(initial, opened) ||
!sameRegularFileState(initial, currentPath)
)
throw new RegularFileChangedError(path);
completedHash = { sha256: hash.digest("hex"), bytes: position };
} catch (cause: unknown) {
primaryCause = cause;
hasPrimaryCause = true;
}
try {
await owner.close();
} catch (cleanupCause: unknown) {
throw new StableFileHashCleanupFailure(
path,
primaryCause,
hasPrimaryCause,
cleanupCause,
owner,
completedHash,
);
}
if (hasPrimaryCause) throw primaryCause;
if (completedHash === undefined)
throw new Error("Stable file hashing completed without a digest");
return completedHash;
};