122 lines
5.0 KiB
TypeScript
122 lines
5.0 KiB
TypeScript
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
import { copyFile, cp, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { DatabaseSync, backup as backupDatabase } from "node:sqlite";
|
|
|
|
async function copyIfExists(source: string, destination: string) {
|
|
if (!existsSync(source)) {
|
|
return;
|
|
}
|
|
await copyFile(source, destination);
|
|
}
|
|
|
|
export function buildBackupMethods(
|
|
deps: {
|
|
getDb: () => DatabaseSync;
|
|
setDb: (db: DatabaseSync) => void;
|
|
openConnection: () => DatabaseSync;
|
|
init: () => void;
|
|
close: () => void;
|
|
profileDir: string;
|
|
dbPath: string;
|
|
logsDir: string;
|
|
artifactsDir: string;
|
|
backupsDir: string;
|
|
},
|
|
) {
|
|
return {
|
|
exportBackup(targetDir: string) {
|
|
mkdirSync(targetDir, { recursive: true });
|
|
deps.getDb().exec("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
const copyJobs = [
|
|
backupDatabase(deps.getDb(), join(targetDir, "profile.sqlite")),
|
|
cp(deps.logsDir, join(targetDir, "logs"), { recursive: true, force: true }),
|
|
cp(deps.artifactsDir, join(targetDir, "artifacts"), { recursive: true, force: true }),
|
|
copyIfExists(join(deps.profileDir, "vault.key"), join(targetDir, "vault.key")),
|
|
copyIfExists(join(deps.profileDir, "agent-api-signing.key"), join(targetDir, "agent-api-signing.key")),
|
|
copyIfExists(`${deps.dbPath}-wal`, join(targetDir, "profile.sqlite-wal")),
|
|
copyIfExists(`${deps.dbPath}-shm`, join(targetDir, "profile.sqlite-shm")),
|
|
];
|
|
return Promise.all(copyJobs);
|
|
},
|
|
|
|
async restoreFromBackup(sourceDir: string) {
|
|
const sourceDbPath = join(sourceDir, "profile.sqlite");
|
|
if (!existsSync(sourceDbPath)) {
|
|
throw new Error("Backup directory is missing profile.sqlite.");
|
|
}
|
|
|
|
// Validate
|
|
const validationDb = new DatabaseSync(sourceDbPath);
|
|
try {
|
|
const requiredTables = ["settings", "companies", "connectors"] as const;
|
|
for (const table of requiredTables) {
|
|
const row = validationDb
|
|
.prepare("select name from sqlite_master where type = 'table' and name = ?")
|
|
.get(table) as { name: string } | undefined;
|
|
if (!row) {
|
|
throw new Error(`Backup is missing the required table "${table}".`);
|
|
}
|
|
}
|
|
} finally {
|
|
validationDb.close();
|
|
}
|
|
|
|
const recoveryDir = join(deps.backupsDir, `pre-restore-${Date.now()}`);
|
|
mkdirSync(recoveryDir, { recursive: true });
|
|
deps.getDb().exec("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
await Promise.all([
|
|
backupDatabase(deps.getDb(), join(recoveryDir, "profile.sqlite")),
|
|
cp(deps.logsDir, join(recoveryDir, "logs"), { recursive: true, force: true }),
|
|
cp(deps.artifactsDir, join(recoveryDir, "artifacts"), { recursive: true, force: true }),
|
|
copyIfExists(join(deps.profileDir, "vault.key"), join(recoveryDir, "vault.key")),
|
|
copyIfExists(join(deps.profileDir, "agent-api-signing.key"), join(recoveryDir, "agent-api-signing.key")),
|
|
copyIfExists(`${deps.dbPath}-wal`, join(recoveryDir, "profile.sqlite-wal")),
|
|
copyIfExists(`${deps.dbPath}-shm`, join(recoveryDir, "profile.sqlite-shm")),
|
|
]);
|
|
|
|
deps.close();
|
|
try {
|
|
await applyBackup(sourceDir);
|
|
} catch (error) {
|
|
await applyBackup(recoveryDir);
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`Restore failed. Previous profile was recovered from ${recoveryDir}. ${message}`);
|
|
}
|
|
},
|
|
};
|
|
|
|
async function applyBackup(sourceDir: string) {
|
|
await rm(deps.dbPath, { force: true });
|
|
await rm(`${deps.dbPath}-wal`, { force: true });
|
|
await rm(`${deps.dbPath}-shm`, { force: true });
|
|
|
|
await copyFile(join(sourceDir, "profile.sqlite"), deps.dbPath);
|
|
await copyIfExists(join(sourceDir, "profile.sqlite-wal"), `${deps.dbPath}-wal`);
|
|
await copyIfExists(join(sourceDir, "profile.sqlite-shm"), `${deps.dbPath}-shm`);
|
|
|
|
await rm(deps.logsDir, { recursive: true, force: true });
|
|
await rm(deps.artifactsDir, { recursive: true, force: true });
|
|
mkdirSync(deps.logsDir, { recursive: true });
|
|
mkdirSync(deps.artifactsDir, { recursive: true });
|
|
|
|
if (existsSync(join(sourceDir, "logs"))) {
|
|
await cp(join(sourceDir, "logs"), deps.logsDir, { recursive: true, force: true });
|
|
}
|
|
if (existsSync(join(sourceDir, "artifacts"))) {
|
|
await cp(join(sourceDir, "artifacts"), deps.artifactsDir, { recursive: true, force: true });
|
|
}
|
|
await copyIfExists(join(sourceDir, "vault.key"), join(deps.profileDir, "vault.key"));
|
|
await copyIfExists(join(sourceDir, "agent-api-signing.key"), join(deps.profileDir, "agent-api-signing.key"));
|
|
if (existsSync(join(deps.profileDir, "vault.key"))) {
|
|
chmodSync(join(deps.profileDir, "vault.key"), 0o600);
|
|
}
|
|
if (existsSync(join(deps.profileDir, "agent-api-signing.key"))) {
|
|
chmodSync(join(deps.profileDir, "agent-api-signing.key"), 0o600);
|
|
}
|
|
|
|
deps.setDb(deps.openConnection());
|
|
deps.init();
|
|
}
|
|
}
|