mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 21:51:28 +00:00
infra: preserve symlink write semantics
This commit is contained in:
@@ -142,6 +142,24 @@ describe("json-file helpers", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"does not create missing target directories through an existing symlink",
|
||||
async () => {
|
||||
await withTempDir({ prefix: "openclaw-json-file-" }, async (root) => {
|
||||
const missingTargetDir = path.join(root, "missing-target");
|
||||
const targetPath = path.join(missingTargetDir, "config.json");
|
||||
const linkPath = path.join(root, "config-link.json");
|
||||
fs.symlinkSync(targetPath, linkPath);
|
||||
|
||||
expect(() => saveJsonFile(linkPath, SAVED_PAYLOAD)).toThrow(
|
||||
expect.objectContaining({ code: "ENOENT" }),
|
||||
);
|
||||
expect(fs.existsSync(missingTargetDir)).toBe(false);
|
||||
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("falls back to copy when rename-based overwrite fails", async () => {
|
||||
await withJsonPath(({ root, pathname }) => {
|
||||
writeExistingJson(pathname);
|
||||
|
||||
@@ -36,9 +36,10 @@ function readSymlinkTargetPath(linkPath: string): string {
|
||||
return path.resolve(path.dirname(linkPath), target);
|
||||
}
|
||||
|
||||
function resolveJsonWriteTarget(pathname: string): string {
|
||||
function resolveJsonWriteTarget(pathname: string): { targetPath: string; followsSymlink: boolean } {
|
||||
let currentPath = pathname;
|
||||
const visited = new Set<string>();
|
||||
let followsSymlink = false;
|
||||
|
||||
for (;;) {
|
||||
let stat: fs.Stats;
|
||||
@@ -48,11 +49,11 @@ function resolveJsonWriteTarget(pathname: string): string {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
return currentPath;
|
||||
return { targetPath: currentPath, followsSymlink };
|
||||
}
|
||||
|
||||
if (!stat.isSymbolicLink()) {
|
||||
return currentPath;
|
||||
return { targetPath: currentPath, followsSymlink };
|
||||
}
|
||||
|
||||
if (visited.has(currentPath)) {
|
||||
@@ -64,6 +65,7 @@ function resolveJsonWriteTarget(pathname: string): string {
|
||||
}
|
||||
|
||||
visited.add(currentPath);
|
||||
followsSymlink = true;
|
||||
currentPath = readSymlinkTargetPath(currentPath);
|
||||
}
|
||||
}
|
||||
@@ -104,11 +106,13 @@ export function loadJsonFile<T = unknown>(pathname: string): T | undefined {
|
||||
}
|
||||
|
||||
export function saveJsonFile(pathname: string, data: unknown) {
|
||||
const targetPath = resolveJsonWriteTarget(pathname);
|
||||
const { targetPath, followsSymlink } = resolveJsonWriteTarget(pathname);
|
||||
const tmpPath = `${targetPath}.${randomUUID()}.tmp`;
|
||||
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
||||
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: JSON_DIR_MODE });
|
||||
if (!followsSymlink) {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: JSON_DIR_MODE });
|
||||
}
|
||||
try {
|
||||
writeTempJsonFile(tmpPath, payload);
|
||||
trySetSecureMode(tmpPath);
|
||||
|
||||
Reference in New Issue
Block a user