mirror of https://github.com/openclaw/openclaw.git
fix: write backup archives atomically
This commit is contained in:
parent
bef8409562
commit
88e52ff19d
|
|
@ -0,0 +1,62 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
|
||||
|
||||
const tarCreateMock = vi.hoisted(() => vi.fn());
|
||||
const backupVerifyCommandMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("tar", () => ({
|
||||
c: tarCreateMock,
|
||||
}));
|
||||
|
||||
vi.mock("./backup-verify.js", () => ({
|
||||
backupVerifyCommand: backupVerifyCommandMock,
|
||||
}));
|
||||
|
||||
const { backupCreateCommand } = await import("./backup.js");
|
||||
|
||||
describe("backupCreateCommand atomic archive write", () => {
|
||||
let tempHome: TempHomeEnv;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempHome = await createTempHomeEnv("openclaw-backup-atomic-test-");
|
||||
tarCreateMock.mockReset();
|
||||
backupVerifyCommandMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await tempHome.restore();
|
||||
});
|
||||
|
||||
it("does not leave a partial final archive behind when tar creation fails", async () => {
|
||||
const stateDir = path.join(tempHome.home, ".openclaw");
|
||||
const archiveDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-failure-"));
|
||||
try {
|
||||
await fs.writeFile(path.join(stateDir, "openclaw.json"), JSON.stringify({}), "utf8");
|
||||
await fs.writeFile(path.join(stateDir, "state.txt"), "state\n", "utf8");
|
||||
|
||||
tarCreateMock.mockRejectedValueOnce(new Error("disk full"));
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const outputPath = path.join(archiveDir, "backup.tar.gz");
|
||||
|
||||
await expect(
|
||||
backupCreateCommand(runtime, {
|
||||
output: outputPath,
|
||||
}),
|
||||
).rejects.toThrow(/disk full/i);
|
||||
|
||||
await expect(fs.access(outputPath)).rejects.toThrow();
|
||||
const remaining = await fs.readdir(archiveDir);
|
||||
expect(remaining).toEqual([]);
|
||||
} finally {
|
||||
await fs.rm(archiveDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
|
@ -119,6 +120,10 @@ async function assertOutputPathReady(outputPath: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
function buildTempArchivePath(outputPath: string): string {
|
||||
return `${outputPath}.${randomUUID()}.tmp`;
|
||||
}
|
||||
|
||||
async function canonicalizePathForContainment(targetPath: string): Promise<string> {
|
||||
const resolved = path.resolve(targetPath);
|
||||
try {
|
||||
|
|
@ -265,6 +270,7 @@ export async function backupCreateCommand(
|
|||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-"));
|
||||
const manifestPath = path.join(tempDir, "manifest.json");
|
||||
const tempArchivePath = buildTempArchivePath(outputPath);
|
||||
try {
|
||||
const manifest = buildManifest({
|
||||
createdAt,
|
||||
|
|
@ -281,7 +287,7 @@ export async function backupCreateCommand(
|
|||
|
||||
await tar.c(
|
||||
{
|
||||
file: outputPath,
|
||||
file: tempArchivePath,
|
||||
gzip: true,
|
||||
portable: true,
|
||||
preservePaths: true,
|
||||
|
|
@ -295,7 +301,9 @@ export async function backupCreateCommand(
|
|||
},
|
||||
[manifestPath, ...result.assets.map((asset) => asset.sourcePath)],
|
||||
);
|
||||
await fs.rename(tempArchivePath, outputPath);
|
||||
} finally {
|
||||
await fs.rm(tempArchivePath, { force: true }).catch(() => undefined);
|
||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue