mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Keep a symlinked config.json linked when Paseo saves it (#6023)
* fix(server): keep a symlinked config.json linked when saving Paseo saved config.json by renaming a temporary file over its path, which replaced a symlink with a regular file and left the linked file unchanged. Private file writes now resolve the symlink and replace the file it points at. Closes #5932 * test(server): check a symlinked config.json stays private --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,14 @@
|
||||
import { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
chmodSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
@@ -843,3 +853,35 @@ describe.skipIf(process.platform === "win32")("persisted config file permissions
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"config.json symlinked from another directory",
|
||||
() => {
|
||||
test("saving writes through the symlink and leaves it in place", () => {
|
||||
const parent = createTempHome();
|
||||
const home = path.join(parent, "home");
|
||||
const dotfiles = path.join(parent, "dotfiles");
|
||||
const linkedConfigPath = path.join(dotfiles, "config.json");
|
||||
const configPath = path.join(home, "config.json");
|
||||
try {
|
||||
mkdirSync(home);
|
||||
mkdirSync(dotfiles, { mode: 0o755 });
|
||||
chmodSync(dotfiles, 0o755);
|
||||
writeFileSync(linkedConfigPath, '{ "version": 1 }\n');
|
||||
symlinkSync(linkedConfigPath, configPath);
|
||||
|
||||
savePersistedConfig(home, { version: 1, daemon: { relay: { enabled: false } } });
|
||||
|
||||
expect(lstatSync(configPath).isSymbolicLink()).toBe(true);
|
||||
expect(JSON.parse(readFileSync(linkedConfigPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
daemon: { relay: { enabled: false } },
|
||||
});
|
||||
expect(modeOf(dotfiles)).toBe(0o755);
|
||||
expect(modeOf(linkedConfigPath)).toBe(PRIVATE_FILE_MODE);
|
||||
} finally {
|
||||
rmSync(parent, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -28,14 +28,27 @@ export function writePrivateFileAtomicSync(
|
||||
data: string | NodeJS.ArrayBufferView,
|
||||
): void {
|
||||
ensurePrivateDirectory(path.dirname(filePath));
|
||||
const parent = path.dirname(filePath);
|
||||
const temporary = path.join(parent, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}`);
|
||||
const destination = resolveSymlinkedFile(filePath);
|
||||
const temporary = path.join(
|
||||
path.dirname(destination),
|
||||
`.${path.basename(destination)}.${process.pid}.${randomUUID()}`,
|
||||
);
|
||||
try {
|
||||
writeFileSync(temporary, data, { mode: PRIVATE_FILE_MODE });
|
||||
renameSync(temporary, filePath);
|
||||
ensurePrivateFile(filePath);
|
||||
renameSync(temporary, destination);
|
||||
ensurePrivateFile(destination);
|
||||
} catch (error) {
|
||||
rmSync(temporary, { force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Renaming onto a symlink replaces the link, so write to the file it points at.
|
||||
function resolveSymlinkedFile(filePath: string): string {
|
||||
try {
|
||||
return realpathSync(filePath);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return filePath;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user