UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

170 lines (169 loc) • 7.69 kB
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 };