diff --git a/app/api/database/restore-initial/route.ts b/app/api/database/restore-initial/route.ts index b90c7fb..5f626a0 100644 --- a/app/api/database/restore-initial/route.ts +++ b/app/api/database/restore-initial/route.ts @@ -4,7 +4,7 @@ import path from 'path'; import JSZip from 'jszip'; import prisma from '../../db'; import { withAuthContext, ApiResponse, AuthResult } from '../../utils/auth'; -import { reloadEnvFile, ensureEnvDefaults } from '../../utils/env-reload'; +import { reloadEnvFile, ensureEnvDefaults, parseEnvFile, replaceEnvVar } from '../../utils/env-reload'; import { isSQLite } from '../../utils/db-provider'; import { importFromSQLiteFile, importFromJSON } from '../../utils/db-backup'; @@ -99,22 +99,42 @@ async function handler(request: NextRequest, authContext: AuthResult): Promise name.endsWith('.backup.env')); if (envFiles.length > 0) { const envFile = zip.file(envFiles[0]); if (envFile) { const envContent = await envFile.async('string'); - // Backup existing .env + // Save current DB connection params before overwriting + const preserveKeys = ['DATABASE_PROVIDER', 'DATABASE_URL', 'LOG_DATABASE_URL']; + const preserved: Record = {}; if (fs.existsSync(envPath)) { + const currentEnvContent = fs.readFileSync(envPath, 'utf-8'); + const currentVars = parseEnvFile(currentEnvContent); + for (const key of preserveKeys) { + if (currentVars[key]) preserved[key] = currentVars[key]; + } + + // Backup existing .env const envBackupPath = `${envPath}.backup-${dateStr}`; await fs.promises.copyFile(envPath, envBackupPath); console.log('✓ Existing .env backed up'); } + // Write the backup's .env await fs.promises.writeFile(envPath, envContent); console.log('✓ .env file restored successfully'); + + // Re-apply preserved DB connection params + if (Object.keys(preserved).length > 0) { + let restoredContent = await fs.promises.readFile(envPath, 'utf-8'); + for (const [key, value] of Object.entries(preserved)) { + restoredContent = replaceEnvVar(restoredContent, key, value); + } + await fs.promises.writeFile(envPath, restoredContent); + console.log('✓ Database connection parameters preserved'); + } } } diff --git a/app/api/database/route.ts b/app/api/database/route.ts index 8672318..d0061bc 100644 --- a/app/api/database/route.ts +++ b/app/api/database/route.ts @@ -4,7 +4,7 @@ import path from 'path'; import JSZip from 'jszip'; import prisma from '../db'; import { withSysAdminAuth, ApiResponse } from '../utils/auth'; -import { reloadEnvFile, ensureEnvDefaults } from '../utils/env-reload'; +import { reloadEnvFile, ensureEnvDefaults, parseEnvFile, replaceEnvVar } from '../utils/env-reload'; import { isSQLite } from '../utils/db-provider'; import { importFromSQLiteFile, importFromJSON, exportToJSON } from '../utils/db-backup'; @@ -143,20 +143,41 @@ async function postHandler(req: NextRequest): Promise name.endsWith('.backup.env')); if (envFiles.length > 0) { const envFile = zip.file(envFiles[0]); if (envFile) { const envContent = await envFile.async('string'); - // Backup existing .env - if (await fs.promises.access(envPath).then(() => true).catch(() => false)) { + // Save current DB connection params before overwriting + const preserveKeys = ['DATABASE_PROVIDER', 'DATABASE_URL', 'LOG_DATABASE_URL']; + const preserved: Record = {}; + const currentEnvExists = await fs.promises.access(envPath).then(() => true).catch(() => false); + if (currentEnvExists) { + const currentEnvContent = await fs.promises.readFile(envPath, 'utf-8'); + const currentVars = parseEnvFile(currentEnvContent); + for (const key of preserveKeys) { + if (currentVars[key]) preserved[key] = currentVars[key]; + } + + // Backup existing .env const envBackupPath = `${envPath}.backup-${dateStr}`; await fs.promises.copyFile(envPath, envBackupPath); } + // Write the backup's .env await fs.promises.writeFile(envPath, envContent); + + // Re-apply preserved DB connection params + if (Object.keys(preserved).length > 0) { + let restoredContent = await fs.promises.readFile(envPath, 'utf-8'); + for (const [key, value] of Object.entries(preserved)) { + restoredContent = replaceEnvVar(restoredContent, key, value); + } + await fs.promises.writeFile(envPath, restoredContent); + console.log('Database connection parameters preserved during .env restore'); + } } } diff --git a/app/api/utils/env-reload.ts b/app/api/utils/env-reload.ts index 0665e53..ff5c1d3 100644 --- a/app/api/utils/env-reload.ts +++ b/app/api/utils/env-reload.ts @@ -6,7 +6,7 @@ import { execSync } from 'child_process'; * Parses a .env file content and returns key-value pairs * Handles comments, empty lines, quoted values, and unquoted values */ -function parseEnvFile(content: string): Record { +export function parseEnvFile(content: string): Record { const envVars: Record = {}; const lines = content.split('\n'); @@ -51,12 +51,20 @@ function parseEnvFile(content: string): Record { } /** - * Reloads environment variables from the .env file into process.env - * This is useful after restoring a backup that includes a new .env file - * - * @param envFilePath Optional path to the .env file. Defaults to './.env' - * @returns True if the reload was successful, false otherwise + * Replaces or appends an environment variable in .env file content. + * If the key exists, its value is updated in-place. If not, it's appended. */ +export function replaceEnvVar(content: string, key: string, value: string): string { + const regex = new RegExp(`^${key}=.*$`, 'm'); + const needsQuotes = value.includes(' ') || value.includes('://'); + const newLine = needsQuotes ? `${key}="${value}"` : `${key}=${value}`; + if (regex.test(content)) { + return content.replace(regex, newLine); + } + const suffix = content.endsWith('\n') ? '' : '\n'; + return content + suffix + newLine + '\n'; +} + /** * Ensures all required environment variables exist in the .env file. * Adds missing vars with correct defaults without overwriting existing values.