fix: stream and consistently copy state snapshots

This commit is contained in:
Dotta committed 2026-08-14 07:18:11 +00:00
1 parent f9a60389d6
commit b89d6adb39
2 files changed
+14 -4

No files matched your search

+10 -3
View File
@@ -87,6 +87,11 @@ async function expandPattern(pattern: string): Promise<string[]> {
async function copyEntry(entry: StateClassEntry, source: string, destination: string): Promise<void> {
const stat = await fs.stat(source);
await fs.mkdir(path.dirname(destination), { recursive: true });
if (entry.consistency === "sqlite_backup" && stat.isDirectory()) {
await fs.mkdir(destination, { recursive: true });
for (const child of await fs.readdir(source)) await copyEntry(entry, path.join(source, child), path.join(destination, child));
return;
}
if (entry.consistency === "sqlite_backup" && stat.isFile() && /\.(sqlite|sqlite3|db)$/i.test(source)) {
await execFileAsync("sqlite3", [source, `.backup '${destination.replace(/'/g, "''")}'`]);
return;
@@ -138,10 +143,12 @@ export function createInstanceStateSnapshotService(opts: {
await execFileAsync("tar", ["-czf", archivePath, "-C", stageDir, "."]);
const encryptedPath = `${archivePath}.enc`;
await opts.encryptionProvider.encrypt(archivePath, encryptedPath);
const body = await fs.readFile(encryptedPath);
const encryptedStat = await fs.stat(encryptedPath);
const objectKey = `instance-state/${context.instanceId ?? "default"}/${startedAt.toISOString().replace(/[:.]/g, "-")}.tar.gz.enc`;
await opts.storageProvider.putObject({ objectKey, body, contentType: "application/vnd.paperclip.state-snapshot", contentLength: body.length });
const result = { objectKey, sizeBytes: body.length, sha256: createHash("sha256").update(body).digest("hex"), entryCount, startedAt: startedAt.toISOString(), finishedAt: new Date().toISOString() };
const sha256 = createHash("sha256");
for await (const chunk of createReadStream(encryptedPath)) sha256.update(chunk);
await opts.storageProvider.putObject({ objectKey, body: createReadStream(encryptedPath), contentType: "application/vnd.paperclip.state-snapshot", contentLength: encryptedStat.size });
const result = { objectKey, sizeBytes: encryptedStat.size, sha256: sha256.digest("hex"), entryCount, startedAt: startedAt.toISOString(), finishedAt: new Date().toISOString() };
await fs.mkdir(markerDir, { recursive: true });
await fs.writeFile(path.join(markerDir, "state-snapshot.success.json"), JSON.stringify(result));
await fs.rm(path.join(markerDir, "state-snapshot.failure"), { force: true });
+4 -1
View File
@@ -1,4 +1,6 @@
import { createReadStream, promises as fs } from "node:fs";
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import path from "node:path";
import type { StorageProvider, GetObjectResult, HeadObjectResult } from "./types.js";
import { notFound, badRequest } from "../errors.js";
@@ -47,7 +49,8 @@ export function createLocalDiskStorageProvider(baseDir: string): StorageProvider
await fs.mkdir(dir, { recursive: true });
const tempPath = `${targetPath}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await fs.writeFile(tempPath, input.body);
if (Buffer.isBuffer(input.body)) await fs.writeFile(tempPath, input.body);
else await pipeline(input.body, createWriteStream(tempPath));
await fs.rename(tempPath, targetPath);
},