mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 23:24:52 +09:00
Give each safeOutputFile call its own temporary file (#1026)
Two writes to the same file at the same time shared one temporary name, so the second write clobbered the first's temporary and the first rename left the second call with nothing to rename, failing with ENOENT. The host does exactly that when it saves an instance config while the instance saves it too. The temporary now carries a random tag and is created exclusively, so concurrent writers never touch each other's file. Since the temporary is always new, the mode is applied on creation and the chmod for reused leftovers goes away. A failed rename removes its temporary instead of leaving it for the next write to overwrite. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7024c8f72
commit
f4aa894fe7
2 files changed
+38
-15
No files matched your search
+26
-4
@@ -87,29 +87,51 @@ describe("lib/file_ops", function() {
|
||||
});
|
||||
|
||||
describe("safeOutputFile()", function() {
|
||||
async function assertNoTemporary(target) {
|
||||
let { dir, name } = path.parse(target);
|
||||
let entries = (await fs.readdir(dir)).filter(
|
||||
entry => entry.startsWith(`${name}.`) && entry !== path.basename(target)
|
||||
);
|
||||
assert.deepEqual(entries, [], "temporary was left behind");
|
||||
}
|
||||
it("should write new target file", async function() {
|
||||
let target = path.join(baseDir, "safe", "simple.txt");
|
||||
await lib.safeOutputFile(target, "a text file", "utf8");
|
||||
await assert.rejects(fs.access(target.replace(".txt", ".tmp.txt")), "temporary was left behind");
|
||||
await assertNoTemporary(target);
|
||||
assert.equal(await fs.readFile(target, "utf8"), "a text file");
|
||||
});
|
||||
it("should overwrite existing target file", async function() {
|
||||
let target = path.join(baseDir, "safe", "exists.txt");
|
||||
await fs.writeFile(target, "previous", "utf8");
|
||||
await lib.safeOutputFile(target, "current", "utf8");
|
||||
await assert.rejects(fs.access(target.replace(".txt", ".tmp.txt")), "temporary was left behind");
|
||||
await assertNoTemporary(target);
|
||||
assert.equal(await fs.readFile(target, "utf8"), "current");
|
||||
});
|
||||
it("should apply mode to a leftover temporary file", async function() {
|
||||
it("should apply mode to the target file", async function() {
|
||||
if (process.platform === "win32") {
|
||||
this.skip();
|
||||
}
|
||||
let target = path.join(baseDir, "safe", "mode.txt");
|
||||
await fs.writeFile(target.replace(".txt", ".tmp.txt"), "stale", { mode: 0o644 });
|
||||
await lib.safeOutputFile(target, "private", { mode: 0o600 });
|
||||
assert.equal((await fs.stat(target)).mode & 0o777, 0o600);
|
||||
assert.equal(await fs.readFile(target, "utf8"), "private");
|
||||
});
|
||||
it("should not interfere between concurrent writes to the same file", async function() {
|
||||
let target = path.join(baseDir, "safe", "concurrent.txt");
|
||||
let writes = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
writes.push(lib.safeOutputFile(target, `write ${i}`, "utf8"));
|
||||
}
|
||||
await Promise.all(writes);
|
||||
await assertNoTemporary(target);
|
||||
assert.match(await fs.readFile(target, "utf8"), /^write \d+$/);
|
||||
});
|
||||
it("should remove the temporary file when the rename fails", async function() {
|
||||
let target = path.join(baseDir, "safe", "rename-fails");
|
||||
await fs.mkdir(target, { recursive: true });
|
||||
await assert.rejects(lib.safeOutputFile(target, "data", "utf8"));
|
||||
await assertNoTemporary(target);
|
||||
});
|
||||
it("should handle creating file in current working directory", async function() {
|
||||
let target = "temporary-file-made-to-test-cwd.txt";
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user