From e0ace0864e320d262b913dc9924e89fc940a41b3 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sun, 31 May 2026 22:48:56 +0200 Subject: [PATCH] fix(restore): preserve operator-meta settings across restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the chicken-and-egg where `restore_allow_force` (and its auto-upgrade tracking flag) got overwritten on every restore by whatever value happened to be in the backup. Net effect: 1. Admin enables Force Restore (via tonight's default-ON migration edit, or hand-SQL on older installs). 2. Restore runs successfully. 3. Restored DB has `restore_allow_force = `. 4. Next restore attempt: "Force restore is not allowed by system settings" — admin needs the SQL workaround AGAIN. Cure: snapshot a small list of operator-meta keys BEFORE the DROP DATABASE (while we still have a working pool against the OLD DB), then UPSERT them back AFTER the psql restore + migrate.latest. The preserved set is intentionally narrow — currently just `restore_allow_force` and `restore_allow_force_auto_upgraded`. These are about how the operator wants the install to behave, not user- facing state. Adding more keys is a one-line addition to the PRESERVED_META_KEYS constant. Survives both: - backup is OLDER than the operator's most recent setting change - backup is NEWER but had a different operator policy Either way, the post-restore install reflects the LIVE operator policy, not the backup's snapshot of it. --- backend/src/services/restoreService.js | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/backend/src/services/restoreService.js b/backend/src/services/restoreService.js index fb6d31ac..0ed1cbe6 100644 --- a/backend/src/services/restoreService.js +++ b/backend/src/services/restoreService.js @@ -829,6 +829,36 @@ class RestoreService { const { host, port, user, password, database } = knexConfig.connection; const env = { ...process.env, PGPASSWORD: password }; + // Snapshot operator-meta settings BEFORE the DROP so we can + // restore them after the psql load. These keys are about how + // the operator wants the install to behave (force-restore + // permission, auto-upgrade tracking), not user-facing state — + // they should NOT be overwritten by whatever values the backup + // happens to contain. + // + // Chicken-and-egg this closes: `restore_allow_force` defaults + // to true (post tonight's migration 032 edit), but every + // restore would overwrite it with whatever the backup carried. + // Admin sets it to true → restores → wakes up with the row + // back to whatever was in the backup. Two consecutive restores + // needed the SQL workaround again. With this snapshot/replay, + // the operator's policy persists across restores. + const PRESERVED_META_KEYS = [ + 'restore_allow_force', + 'restore_allow_force_auto_upgraded', + ]; + let preservedMeta = []; + try { + preservedMeta = await db('app_settings') + .whereIn('setting_key', PRESERVED_META_KEYS) + .select('setting_key', 'setting_value', 'setting_type'); + this.log('info', `Snapshotted ${preservedMeta.length} restore-meta setting(s) for post-restore replay`, { + keys: preservedMeta.map(r => r.setting_key), + }); + } catch (err) { + this.log('warn', `Could not snapshot restore-meta settings (continuing): ${err.message}`); + } + // `psql` with no `-d` defaults to a database whose name matches // the connecting user, NOT a maintenance DB. So on installs // where the user's home DB doesn't exist (e.g. user=`picpeak`, @@ -976,6 +1006,33 @@ END $$;` this.log('info', 'Running database migrations...'); await db.migrate.latest(); + // Replay the snapshotted operator-meta settings on top of the + // restored DB. UPSERT by setting_key — if the backup had the + // same key with a different value, we overwrite it; if the row + // doesn't exist in the backup, we insert it. Either way the + // operator's pre-restore policy survives. + if (preservedMeta.length > 0) { + try { + for (const row of preservedMeta) { + await db('app_settings') + .insert({ + setting_key: row.setting_key, + setting_value: row.setting_value, + setting_type: row.setting_type || 'restore', + updated_at: new Date(), + }) + .onConflict('setting_key') + .merge({ + setting_value: row.setting_value, + updated_at: new Date(), + }); + } + this.log('info', `Replayed ${preservedMeta.length} restore-meta setting(s) post-restore`); + } catch (err) { + this.log('warn', `Could not replay restore-meta settings (admin may need to re-set them): ${err.message}`); + } + } + return { success: true }; } catch (error) {