56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { stat } from "node:fs/promises";
|
|
|
|
import {
|
|
openRegularFile,
|
|
RegularFileChangedError,
|
|
sameRegularFileState,
|
|
} from "./RegularFile.js";
|
|
|
|
/** Hash one regular file from its admitted descriptor and verify its path state. */
|
|
export const hashStableFile = async (
|
|
path: string,
|
|
signal?: AbortSignal,
|
|
): Promise<{ readonly sha256: string; readonly bytes: number }> => {
|
|
const handle = await openRegularFile(path, {
|
|
symlinks: "follow",
|
|
signal,
|
|
});
|
|
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);
|
|
return { sha256: hash.digest("hex"), bytes: position };
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
};
|