mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
fix(config): fsync backup and rename metadata
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4203b34330
commit
2ba053e2c3
4 files changed
+62
-4
No files matched your search
@@ -114,15 +114,25 @@ describe("config store", () => {
|
||||
fs.writeFileSync(configPath, invalidBytes);
|
||||
fs.writeFileSync(`${configPath}.invalid-1`, "existing backup");
|
||||
|
||||
const open = vi.spyOn(fs, "openSync");
|
||||
const sync = vi.spyOn(fs, "fsyncSync");
|
||||
const backupPath = backupInvalidConfig(configPath);
|
||||
expect(backupPath).toBe(`${configPath}.invalid-2`);
|
||||
expect(fs.readFileSync(backupPath)).toEqual(invalidBytes);
|
||||
expect(open).toHaveBeenCalledWith(backupPath, "r");
|
||||
expect(open).toHaveBeenCalledWith(path.dirname(configPath), "r");
|
||||
expect(sync).toHaveBeenCalled();
|
||||
expect(() => writeConfig(defaultConfig(), configPath)).toThrow(/Refusing to overwrite invalid config/);
|
||||
expect(fs.readFileSync(configPath)).toEqual(invalidBytes);
|
||||
|
||||
open.mockClear();
|
||||
sync.mockClear();
|
||||
const rename = vi.spyOn(fs, "renameSync");
|
||||
expect(writeConfig(defaultConfig(), configPath, { invalidBackupPath: backupPath })).toBe(true);
|
||||
expect(rename).toHaveBeenCalledWith(expect.stringMatching(/config\.json\.tmp-\d+-\d+$/), configPath);
|
||||
expect(open).toHaveBeenCalledWith(path.dirname(configPath), "r");
|
||||
expect(open.mock.invocationCallOrder.at(-1)!).toBeGreaterThan(rename.mock.invocationCallOrder.at(-1)!);
|
||||
expect(sync.mock.invocationCallOrder.at(-1)!).toBeGreaterThan(open.mock.invocationCallOrder.at(-1)!);
|
||||
expect(readConfig(configPath)).not.toBeNull();
|
||||
expect(fs.readdirSync(path.dirname(configPath)).some((entry) => entry.includes(".tmp-"))).toBe(false);
|
||||
});
|
||||
|
||||
+31
-4
@@ -110,6 +110,34 @@ function effectiveConfig(config: PaperclipConfig): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function syncDirectory(directoryPath: string): void {
|
||||
let directoryDescriptor: number | null = null;
|
||||
try {
|
||||
directoryDescriptor = fs.openSync(directoryPath, "r");
|
||||
fs.fsyncSync(directoryDescriptor);
|
||||
} catch (error) {
|
||||
const code = error instanceof Error && "code" in error ? error.code : null;
|
||||
if (process.platform !== "win32" || !["EACCES", "EINVAL", "EISDIR", "ENOTSUP", "EPERM"].includes(String(code))) {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (directoryDescriptor !== null) fs.closeSync(directoryDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function durableCopyFile(sourcePath: string, destinationPath: string, flags = 0): void {
|
||||
fs.copyFileSync(sourcePath, destinationPath, flags);
|
||||
fs.chmodSync(destinationPath, 0o600);
|
||||
|
||||
const backupDescriptor = fs.openSync(destinationPath, "r");
|
||||
try {
|
||||
fs.fsyncSync(backupDescriptor);
|
||||
} finally {
|
||||
fs.closeSync(backupDescriptor);
|
||||
}
|
||||
syncDirectory(path.dirname(destinationPath));
|
||||
}
|
||||
|
||||
function atomicWriteFile(filePath: string, contents: string): void {
|
||||
let attempt = 0;
|
||||
|
||||
@@ -124,6 +152,7 @@ function atomicWriteFile(filePath: string, contents: string): void {
|
||||
fs.closeSync(fileDescriptor);
|
||||
fileDescriptor = null;
|
||||
fs.renameSync(temporaryPath, filePath);
|
||||
syncDirectory(path.dirname(filePath));
|
||||
return;
|
||||
} catch (error) {
|
||||
if (fileDescriptor !== null) fs.closeSync(fileDescriptor);
|
||||
@@ -144,8 +173,7 @@ export function backupInvalidConfig(configPath?: string): string {
|
||||
for (let suffix = 1; ; suffix += 1) {
|
||||
const backupPath = `${filePath}.invalid-${suffix}`;
|
||||
try {
|
||||
fs.copyFileSync(filePath, backupPath, fs.constants.COPYFILE_EXCL);
|
||||
fs.chmodSync(backupPath, 0o600);
|
||||
durableCopyFile(filePath, backupPath, fs.constants.COPYFILE_EXCL);
|
||||
return backupPath;
|
||||
} catch (error) {
|
||||
const code = error instanceof Error && "code" in error ? error.code : null;
|
||||
@@ -193,8 +221,7 @@ export function writeConfig(
|
||||
// Backup existing config before overwriting
|
||||
if (fs.existsSync(filePath)) {
|
||||
const backupPath = filePath + ".backup";
|
||||
fs.copyFileSync(filePath, backupPath);
|
||||
fs.chmodSync(backupPath, 0o600);
|
||||
durableCopyFile(filePath, backupPath);
|
||||
}
|
||||
|
||||
atomicWriteFile(filePath, JSON.stringify(nextConfig, null, 2) + "\n");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -1029,8 +1030,13 @@ describe("worktree config repair", () => {
|
||||
delete process.env.PORT;
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const open = vi.spyOn(fsSync, "openSync");
|
||||
const sync = vi.spyOn(fsSync, "fsyncSync");
|
||||
maybePersistWorktreeRuntimePorts({ serverPort: 3103, databasePort: 54335 });
|
||||
|
||||
expect(open).toHaveBeenCalledWith(paperclipDir, "r");
|
||||
expect(sync).toHaveBeenCalled();
|
||||
|
||||
const writtenConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
|
||||
expect(writtenConfig).toMatchObject({
|
||||
topLevelExtension: { enabled: true },
|
||||
|
||||
@@ -324,6 +324,21 @@ function atomicWriteFile(filePath: string, contents: string): void {
|
||||
fs.closeSync(fileDescriptor);
|
||||
fileDescriptor = null;
|
||||
fs.renameSync(temporaryPath, filePath);
|
||||
let directoryDescriptor: number | null = null;
|
||||
try {
|
||||
directoryDescriptor = fs.openSync(path.dirname(filePath), "r");
|
||||
fs.fsyncSync(directoryDescriptor);
|
||||
} catch (error) {
|
||||
const code = error instanceof Error && "code" in error ? error.code : null;
|
||||
if (
|
||||
process.platform !== "win32" ||
|
||||
!["EACCES", "EINVAL", "EISDIR", "ENOTSUP", "EPERM"].includes(String(code))
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (directoryDescriptor !== null) fs.closeSync(directoryDescriptor);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
if (fileDescriptor !== null) fs.closeSync(fileDescriptor);
|
||||
|
||||
Reference in new issue
Block a user