openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
170 lines (169 loc) • 7.69 kB
JavaScript
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
import { a as resolveImmutableSqliteFileUri, t as openNodeSqliteDatabase } from "./node-sqlite-DWTcT2ek.mjs";
import "./openclaw-state-db-contract-CQDemVBb.mjs";
import { c as resolveRuntimeServiceCommit, s as resolveRuntimeServiceBuildId, t as VERSION } from "./version-Dx-YMDJx.mjs";
import { o as getOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle-8N3pIeYH.mjs";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-BvBEgHyD.mjs";
import { p as syncDirectorySync, s as requireDirectorySync } from "./directory-durability-B6uYEZLt.mjs";
import { i as sameFileMutationFingerprint } from "./file-descriptor-B4yBUiAP.mjs";
import { n as assertSqliteIntegrity } from "./sqlite-integrity-Da9mtGpK.mjs";
import "./openclaw-agent-db-contract-ClUH-Vmp.mjs";
import { t as DoctorMaintenanceRefusalError } from "./update-doctor-result-T_IRct7a.mjs";
import { closeSync, existsSync, fsyncSync, lstatSync, openSync, realpathSync, statSync, unlinkSync } from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
//#region src/commands/doctor-migration-backup.ts
/** Preserve the old database generation before Doctor advances its schemas. */
async function backupDoctorMigrationDatabases(params) {
const { detectOpenClawStateDatabaseSchemaMigrations } = await import("./openclaw-state-db-schema-discovery-DqT9B2pv.mjs");
const sharedPath = resolveOpenClawStateSqlitePath(params.env);
const pending = new Set(params.pendingDatabasePaths);
if (detectOpenClawStateDatabaseSchemaMigrations({ env: params.env }).length > 0) pending.add(sharedPath);
if (pending.size === 0) return {
changes: [],
warnings: []
};
if (existsSync(sharedPath)) pending.add(sharedPath);
const maintenance = getOpenClawDatabaseMaintenanceScope();
if (!maintenance?.ownsSchemaMaintenance) throw new Error("Pre-migration SQLite backups require Doctor maintenance ownership.");
maintenance.assertAdmission();
const { createVerifiedSqliteSnapshot } = await import("./sqlite-snapshot-C2VNcJ2i.mjs");
const { sanitizeOpenClawStateLeaseRows } = await import("./openclaw-state-snapshot-sanitizer-ZOMM0m6s.mjs");
maintenance.assertAdmission();
const sources = [...new Set([...pending].map((pathname) => realpathSync.native(pathname)))];
if (sources.every((sourcePath) => {
const { dev, ino } = statSync(sourcePath);
return dev !== 0 && ino !== 0 && params.verifiedSnapshots?.some((snapshot) => snapshot.dev === dev && snapshot.ino === ino);
})) return {
changes: [],
warnings: []
};
const inventory = [.../* @__PURE__ */ new Set([...sources, ...params.databasePaths.map((pathname) => realpathSync.native(pathname))])].toSorted().map((pathname) => ({
path: pathname,
identity: statSync(pathname, { bigint: true })
}));
const backupDigest = createHash("sha256").update(JSON.stringify([
VERSION,
resolveRuntimeServiceBuildId(),
resolveRuntimeServiceCommit(),
19,
24,
inventory.map(({ path, identity }) => [
path,
String(identity.dev),
String(identity.ino)
])
])).digest("hex");
const backupId = [
backupDigest.slice(0, 8),
backupDigest.slice(8, 12),
`8${backupDigest.slice(13, 16)}`,
`8${backupDigest.slice(17, 20)}`,
backupDigest.slice(20, 32)
].join("-");
const assertInventory = () => {
maintenance.assertAdmission();
for (const { path: pathname, identity } of inventory) {
const current = statSync(pathname, { bigint: true });
if (!current.isFile() || current.dev !== identity.dev || current.ino !== identity.ino) throw new Error(`Pre-migration SQLite backup source changed: ${pathname}`);
}
};
const changes = [];
const targetFor = (sourcePath) => `${sourcePath}.pre-startup-migration-${backupId}.bak`;
const capturePath = `${targetFor(existsSync(sharedPath) ? realpathSync.native(sharedPath) : inventory[0].path)}.capturing`;
let capture = lstatSync(capturePath, {
bigint: true,
throwIfNoEntry: false
});
const hasSnapshots = inventory.some(({ path: sourcePath }) => lstatSync(targetFor(sourcePath), { throwIfNoEntry: false }) !== void 0);
if (!capture && !hasSnapshots) {
assertInventory();
const descriptor = openSync(capturePath, "wx", 384);
try {
fsyncSync(descriptor);
} finally {
closeSync(descriptor);
}
capture = lstatSync(capturePath, { bigint: true });
}
const assertCapture = () => {
assertInventory();
if (capture && (!capture.isFile() || capture.size !== 0n || !sameFileMutationFingerprint(capture, lstatSync(capturePath, { bigint: true })))) throw new DoctorMaintenanceRefusalError(`Pre-migration SQLite backup capture marker changed: ${capturePath}`, {
kind: "data-at-risk",
reason: "incomplete-migration"
});
};
const syncCaptureDirectory = () => requireDirectorySync(syncDirectorySync(path.dirname(capturePath)), "Migration backup directory");
if (capture) {
assertCapture();
syncCaptureDirectory();
for (const { path: sourcePath } of inventory) {
const targetPath = targetFor(sourcePath);
const incomplete = lstatSync(targetPath, {
bigint: true,
throwIfNoEntry: false
});
if (!incomplete) continue;
assertCapture();
if (!incomplete.isFile() || !sameFileMutationFingerprint(incomplete, lstatSync(targetPath, { bigint: true }))) throw new Error(`Incomplete pre-migration SQLite backup changed: ${targetPath}`);
unlinkSync(targetPath);
changes.push(`Discarded incomplete pre-migration SQLite backup: ${targetPath}`);
}
}
for (const { path: sourcePath } of inventory) {
assertCapture();
const targetPath = targetFor(sourcePath);
const existing = lstatSync(targetPath, {
bigint: true,
throwIfNoEntry: false
});
if (existing) {
try {
if (!existing.isFile()) throw new Error("Backup must be a regular file");
const snapshot = openNodeSqliteDatabase(resolveImmutableSqliteFileUri(targetPath), { readOnly: true });
try {
snapshot.exec("PRAGMA trusted_schema = OFF;");
assertCapture();
assertSqliteIntegrity(snapshot, targetPath);
} finally {
snapshot.close();
}
if (!sameFileMutationFingerprint(existing, lstatSync(targetPath, { bigint: true }))) throw new Error("Backup changed during verification");
} catch (cause) {
throw new DoctorMaintenanceRefusalError(`Cannot verify pre-migration SQLite backup: ${targetPath}. ${formatErrorMessage(cause)}. Restore the matching backup group before retrying; surviving snapshots were preserved.`, {
kind: "data-at-risk",
reason: "incomplete-migration"
}, { cause });
}
changes.push(`Reused pre-migration SQLite backup: ${targetPath}`);
continue;
}
if (!capture) throw new DoctorMaintenanceRefusalError(`Completed pre-migration SQLite backup group is missing: ${targetPath}. Restore the matching backup group before retrying; surviving snapshots were preserved.`, {
kind: "data-at-risk",
reason: "incomplete-migration"
});
const backup = await createVerifiedSqliteSnapshot({
sourcePath,
targetPath,
preserveRowIds: true,
transform: sanitizeOpenClawStateLeaseRows,
beforePublish: assertCapture
});
assertCapture();
changes.push(`Saved pre-migration SQLite backup: ${backup.path}`);
}
assertCapture();
if (capture) unlinkSync(capturePath);
syncCaptureDirectory();
assertInventory();
if (lstatSync(capturePath, { throwIfNoEntry: false })) throw new DoctorMaintenanceRefusalError(`Pre-migration SQLite backup capture marker appeared during verification: ${capturePath}`, {
kind: "data-at-risk",
reason: "incomplete-migration"
});
return {
changes,
warnings: []
};
}
//#endregion
export { backupDoctorMigrationDatabases };