mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { createInterface } from "node:readline";
|
|
import { openPromise } from "yauzl";
|
|
import type { SnapshotDocument } from "./snapshotPolicy";
|
|
|
|
export async function* readSnapshotTable(
|
|
snapshotPath: string,
|
|
table: string,
|
|
): AsyncGenerator<SnapshotDocument> {
|
|
const child = spawn("unzip", ["-p", snapshotPath, `${table}/documents.jsonl`], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
const errors: Buffer[] = [];
|
|
child.stderr.on("data", (chunk: Buffer) => errors.push(chunk));
|
|
const completion = childCompletion(child);
|
|
const lines = createInterface({ input: child.stdout, crlfDelay: Number.POSITIVE_INFINITY });
|
|
let completed = false;
|
|
try {
|
|
for await (const line of lines) {
|
|
if (line.trim()) yield JSON.parse(line) as SnapshotDocument;
|
|
}
|
|
completed = true;
|
|
} finally {
|
|
if (!completed && child.exitCode === null) child.kill("SIGTERM");
|
|
}
|
|
const status = await completion;
|
|
if (completed && status !== 0) {
|
|
throw new Error(
|
|
`Failed to read ${table} from snapshot: ${Buffer.concat(errors).toString("utf8").trim()}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function readSnapshotEntry(snapshotPath: string, entry: string) {
|
|
const child = spawn("unzip", ["-p", snapshotPath, entry], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
const chunks: Buffer[] = [];
|
|
const errors: Buffer[] = [];
|
|
const completion = childCompletion(child);
|
|
child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
child.stderr.on("data", (chunk: Buffer) => errors.push(chunk));
|
|
const status = await completion;
|
|
if (status !== 0) {
|
|
throw new Error(
|
|
`Failed to read ${entry} from snapshot: ${Buffer.concat(errors).toString("utf8").trim()}`,
|
|
);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
export async function* readSelectedSnapshotEntries(
|
|
snapshotPath: string,
|
|
selectedEntries: ReadonlySet<string>,
|
|
): AsyncGenerator<{ entry: string; bytes: Buffer }> {
|
|
if (selectedEntries.size === 0) return;
|
|
|
|
const found = new Set<string>();
|
|
const zip = await openPromise(snapshotPath, { autoClose: false, lazyEntries: true });
|
|
try {
|
|
for await (const file of zip.eachEntry()) {
|
|
if (!selectedEntries.has(file.fileName)) continue;
|
|
if (!file.canDecodeFileData()) {
|
|
throw new Error(`Cannot decode selected snapshot entry: ${file.fileName}`);
|
|
}
|
|
const stream = await zip.openReadStreamPromise(file);
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
found.add(file.fileName);
|
|
yield { entry: file.fileName, bytes: Buffer.concat(chunks) };
|
|
}
|
|
} finally {
|
|
zip.close();
|
|
}
|
|
|
|
const missing = [...selectedEntries].filter((entry) => !found.has(entry));
|
|
if (missing.length > 0) {
|
|
throw new Error(`Snapshot is missing ${missing.length} selected entries`);
|
|
}
|
|
}
|
|
|
|
export async function listSelectedStorageEntries(
|
|
snapshotPath: string,
|
|
storageIds: ReadonlySet<string>,
|
|
) {
|
|
const entries = new Map<string, string>();
|
|
for await (const entry of listSnapshotEntries(snapshotPath)) {
|
|
if (!entry.startsWith("_storage/") || entry.endsWith("/documents.jsonl")) continue;
|
|
const basename = entry.slice("_storage/".length);
|
|
const storageId = basename.includes(".") ? basename.slice(0, basename.indexOf(".")) : basename;
|
|
if (storageIds.has(storageId)) entries.set(storageId, entry);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
export async function* listSnapshotEntries(snapshotPath: string) {
|
|
const child = spawn("unzip", ["-Z1", snapshotPath], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
const errors: Buffer[] = [];
|
|
const completion = childCompletion(child);
|
|
child.stderr.on("data", (chunk: Buffer) => errors.push(chunk));
|
|
const lines = createInterface({ input: child.stdout, crlfDelay: Number.POSITIVE_INFINITY });
|
|
for await (const entry of lines) yield entry;
|
|
const status = await completion;
|
|
if (status !== 0) {
|
|
throw new Error(`Failed to list snapshot: ${Buffer.concat(errors).toString("utf8").trim()}`);
|
|
}
|
|
}
|
|
|
|
export async function runCommand(command: string, args: string[], cwd?: string) {
|
|
const child = spawn(command, args, {
|
|
cwd,
|
|
stdio: "inherit",
|
|
});
|
|
const status = await childCompletion(child);
|
|
if (status !== 0) throw new Error(`${command} exited with status ${status ?? "unknown"}`);
|
|
}
|
|
|
|
function childCompletion(child: ReturnType<typeof spawn>) {
|
|
return new Promise<number | null>((resolve, reject) => {
|
|
child.once("error", reject);
|
|
child.once("close", resolve);
|
|
});
|
|
}
|