diff --git a/app/server/cli/commands/import-config.ts b/app/server/cli/commands/import-config.ts index 93b15687..b3607f44 100644 --- a/app/server/cli/commands/import-config.ts +++ b/app/server/cli/commands/import-config.ts @@ -69,6 +69,7 @@ export const importConfigCommand = new Command("import-config") .option("--dry-run", "Validate the config without importing") .option("--json", "Output results in JSON format") .option("--log-level ", "Set log level (debug, info, warn, error)") + .option("--overwrite-recovery-key", "Overwrite existing recovery key (only allowed if database is empty)") .action(async (options) => { const jsonOutput = options.json; const out = createOutput(jsonOutput); @@ -135,7 +136,7 @@ export const importConfigCommand = new Command("import-config") runDbMigrations(); const { applyConfigImport } = await import("../../modules/lifecycle/config-import"); - const result = await applyConfigImport(config); + const result = await applyConfigImport(config, { overwriteRecoveryKey: options.overwriteRecoveryKey }); out.json({ ...result, success: result.errors === 0 }); diff --git a/app/server/modules/lifecycle/config-import.ts b/app/server/modules/lifecycle/config-import.ts index 587956c2..728dc070 100644 --- a/app/server/modules/lifecycle/config-import.ts +++ b/app/server/modules/lifecycle/config-import.ts @@ -3,7 +3,13 @@ import fs from "node:fs/promises"; import path from "node:path"; import slugify from "slugify"; import { db } from "../../db/db"; -import { usersTable } from "../../db/schema"; +import { + usersTable, + volumesTable, + repositoriesTable, + backupSchedulesTable, + notificationDestinationsTable, +} from "../../db/schema"; import { logger } from "../../utils/logger"; import { volumeService } from "../volumes/volume.service"; import type { NotificationConfig } from "~/schemas/notifications"; @@ -109,7 +115,31 @@ function mergeResults(target: ImportResult, source: ImportResult): void { target.errors += source.errors; } -async function writeRecoveryKeyFromConfig(recoveryKey: string | null): Promise { +/** + * Check if the database has any records in the main tables. + * Used to prevent recovery key overwrite when data already exists. + */ +async function isDatabaseEmpty(): Promise { + const [volumes, repositories, schedules, notifications, users] = await Promise.all([ + db.select({ id: volumesTable.id }).from(volumesTable).limit(1), + db.select({ id: repositoriesTable.id }).from(repositoriesTable).limit(1), + db.select({ id: backupSchedulesTable.id }).from(backupSchedulesTable).limit(1), + db.select({ id: notificationDestinationsTable.id }).from(notificationDestinationsTable).limit(1), + db.select({ id: usersTable.id }).from(usersTable).limit(1), + ]); + return ( + volumes.length === 0 && + repositories.length === 0 && + schedules.length === 0 && + notifications.length === 0 && + users.length === 0 + ); +} + +async function writeRecoveryKeyFromConfig( + recoveryKey: string | null, + overwriteRecoveryKey: boolean, +): Promise { const result: ImportResult = { succeeded: 0, skipped: 0, warnings: 0, errors: 0 }; try { @@ -129,11 +159,29 @@ async function writeRecoveryKeyFromConfig(recoveryKey: string | null): Promise { +type ImportOptions = { + overwriteRecoveryKey?: boolean; +}; + +async function runImport(config: ImportConfig, options: ImportOptions = {}): Promise { const result: ImportResult = { succeeded: 0, skipped: 0, warnings: 0, errors: 0 }; - mergeResults(result, await writeRecoveryKeyFromConfig(config.recoveryKey)); + mergeResults(result, await writeRecoveryKeyFromConfig(config.recoveryKey, options.overwriteRecoveryKey ?? false)); // Stop immediately if recovery key has errors (e.g., mismatch with existing key) if (result.errors > 0) { @@ -779,10 +831,10 @@ function logImportSummary(result: ImportResult): void { /** * Import configuration from a raw config object (used by CLI) */ -export async function applyConfigImport(configRaw: unknown): Promise { +export async function applyConfigImport(configRaw: unknown, options: ImportOptions = {}): Promise { logger.info("Starting config import..."); const config = parseImportConfig(configRaw); - const result = await runImport(config); + const result = await runImport(config, options); logImportSummary(result); return result; }