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 };
|