Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
74 lines
3.1 KiB
JavaScript
74 lines
3.1 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("node:fs");
|
|
|
|
// Publish a prepared regular file without replacing any destination entry.
|
|
// Hardlinks make complete bytes visible atomically. FAT-like filesystems use an
|
|
// exclusively created handle instead: partial contents can be visible there,
|
|
// but no pathname cleanup may remove a concurrent writer's replacement.
|
|
// Resolves with the published inode identity ({ dev, ino, size }) so callers can
|
|
// verify an open destination handle before later metadata stamping.
|
|
async function publishLocalFileExclusive(source, target, assertNotCancelled = () => {}, preparedHandle) {
|
|
assertNotCancelled();
|
|
try {
|
|
await fs.promises.link(source, target);
|
|
// The hardlink shares the published inode; stat either name.
|
|
const linkedStat = await fs.promises.lstat(source);
|
|
return { dev: linkedStat.dev, ino: linkedStat.ino, size: linkedStat.size };
|
|
} catch (error) {
|
|
if (!["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM", "EACCES", "EXDEV"].includes(error?.code)) throw error;
|
|
}
|
|
|
|
let input;
|
|
let output;
|
|
let failure;
|
|
let publishedIdentity;
|
|
try {
|
|
input = preparedHandle ?? await fs.promises.open(source, "r");
|
|
const stat = await input.stat();
|
|
assertNotCancelled();
|
|
output = await fs.promises.open(target, "wx", stat.mode & 0o7777);
|
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
let position = 0;
|
|
while (position < stat.size) {
|
|
assertNotCancelled();
|
|
const { bytesRead } = await input.read(buffer, 0, Math.min(buffer.length, stat.size - position), position);
|
|
if (!bytesRead) throw new Error("Prepared local file ended before publication completed");
|
|
let written = 0;
|
|
while (written < bytesRead) {
|
|
assertNotCancelled();
|
|
const { bytesWritten } = await output.write(buffer, written, bytesRead - written, position + written);
|
|
if (!bytesWritten) throw new Error("Local publication made no write progress");
|
|
written += bytesWritten;
|
|
}
|
|
position += bytesRead;
|
|
}
|
|
assertNotCancelled();
|
|
// Restore metadata after writes (which may clear special permission bits),
|
|
// through the owned handle rather than a potentially replaced pathname.
|
|
await output.chmod(stat.mode & 0o7777);
|
|
await output.utimes(stat.atime, stat.mtime);
|
|
const ownedStat = await output.stat();
|
|
const targetStat = await fs.promises.lstat(target);
|
|
if (!targetStat.isFile() || targetStat.dev !== ownedStat.dev || targetStat.ino !== ownedStat.ino) {
|
|
throw new Error("Local download target changed during replacement");
|
|
}
|
|
publishedIdentity = { dev: ownedStat.dev, ino: ownedStat.ino, size: ownedStat.size };
|
|
} catch (error) {
|
|
failure = error;
|
|
} finally {
|
|
for (const handle of [output, preparedHandle ? undefined : input]) {
|
|
try { await handle?.close(); } catch (error) { failure ??= error; }
|
|
}
|
|
}
|
|
if (failure) {
|
|
// Once exclusive creation succeeded, leave the partial destination intact.
|
|
// The caller must retain its complete prepared file and any original backup.
|
|
if (output) failure.localPublicationIncomplete = true;
|
|
throw failure;
|
|
}
|
|
return publishedIdentity;
|
|
}
|
|
|
|
module.exports = { publishLocalFileExclusive };
|