From fd0f786f5ffbab67db45c1395271f03211360272 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Thu, 22 Oct 2026 01:59:01 +0200 Subject: [PATCH] Full restore: a run again after an interrupted restore keeps maintenance mode on when its check refuses; retry text about apps --- docs/DISASTER-RECOVERY.md | 5 +++-- lib/Command/RestoreFull.php | 2 +- lib/Service/RestoreFullService.php | 23 +++++++++++++++------ tests/integration/full-restore-roundtrip.sh | 1 + 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/DISASTER-RECOVERY.md b/docs/DISASTER-RECOVERY.md index 9b5d179..06bbb47 100644 --- a/docs/DISASTER-RECOVERY.md +++ b/docs/DISASTER-RECOVERY.md @@ -101,8 +101,9 @@ mode on yourself before, it stays on: switch it off with `occ maintenance:mode - If it stops halfway, maintenance mode stays on (the instance may be partly restored). Fix the cause, then switch maintenance mode off and run the restore again with `--force`, in one line (the message prints it): `occ maintenance:mode --off && occ backup:restore:full --force`. -Leave out `--install-apps` this time: the apps are installed already, and the instance is then open -only for the seconds until the restore has switched maintenance mode on again. +Leave out `--install-apps` this time, so that the instance is open only for the seconds until the +restore has switched maintenance mode on again; apps the first run could not install are listed at +the end of the restore, install those afterwards. If occ then says that Nextcloud or one of the apps requires an upgrade, the database is already the old server's: run `occ upgrade` first, then the same command with `--force`. diff --git a/lib/Command/RestoreFull.php b/lib/Command/RestoreFull.php index 05d63ed..6c804c6 100644 --- a/lib/Command/RestoreFull.php +++ b/lib/Command/RestoreFull.php @@ -98,7 +98,7 @@ HELP); $again = 'occ backup:restore:full ' . escapeshellarg((string)$input->getArgument('target')) . ' ' . escapeshellarg((string)$input->getArgument('snapshot')) . ($input->getOption('skip-missing-tables') ? ' --skip-missing-tables' : '') . ' --force --no-interaction'; $output->writeln(' occ maintenance:mode --off && ' . $again); - $output->writeln('(Without --install-apps: the apps are installed already, and the instance is open again only for the seconds until the restore starts.)'); + $output->writeln('(Without --install-apps, so the instance is open only for the seconds until the restore starts again. Apps the first run could not install are listed at the end: install those afterwards.)'); } else { $output->writeln('If this happened after the restore had started, the instance may be partly restored: fix the cause and run the same command again with --force.'); } diff --git a/lib/Service/RestoreFullService.php b/lib/Service/RestoreFullService.php index 27e8edd..2a1978e 100644 --- a/lib/Service/RestoreFullService.php +++ b/lib/Service/RestoreFullService.php @@ -27,6 +27,9 @@ final class RestoreFullService { * Config keys that must keep pointing at THIS installation's actual, already-working * database and data directory (set by occ maintenance:install), never at the old server's. */ + /** App setting: when a full restore started that has not completed (see restoreFull()). */ + private const INTERRUPTED_KEY = 'restore_full_started'; + private const CONNECTION_KEYS = [ 'dbtype', 'dbhost', 'dbport', 'dbname', 'dbuser', 'dbpassword', 'dbpersistent', 'dbtableprefix', 'dbdriveroptions', 'datadirectory', @@ -98,23 +101,31 @@ final class RestoreFullService { // right before running the restore again (see RestoreFull). Already on before: left on. // On before the checks below, so that a run again after an error leaves the partly // restored instance open for seconds at most; off again when they refuse (nothing changed). + // A marker in ng_backup's own app settings (which the restore keeps) says a restore started + // and did not complete: then the instance may be partly restored, and a check that refuses + // a run again must not open it. + $appConfig = \OCP\Server::get(\OCP\IAppConfig::class); + $interrupted = $appConfig->getValueInt(\OCA\NgBackup\AppInfo\Application::APP_ID, self::INTERRUPTED_KEY, 0) > 0; $wasInMaintenance = $this->config->getSystemValueBool('maintenance', false); $this->config->setSystemValue('maintenance', true); + $reopen = function () use ($wasInMaintenance, $interrupted): void { + if (!$wasInMaintenance && !$interrupted) { + $this->config->setSystemValue('maintenance', false); + } + }; try { $check = $this->checkCompatibility($target, $snapshotId, $skipMissingTables); } catch (\Throwable $e) { - if (!$wasInMaintenance) { - $this->config->setSystemValue('maintenance', false); - } + $reopen(); throw $e; } if (!$check['ok']) { - if (!$wasInMaintenance) { - $this->config->setSystemValue('maintenance', false); - } + $reopen(); throw new \RuntimeException($this->describeBlockers($check)); } + $appConfig->setValueInt(\OCA\NgBackup\AppInfo\Application::APP_ID, self::INTERRUPTED_KEY, time()); $result = $this->restoreInMaintenance($target, $snapshotId, $skipMissingTables, $check); + $appConfig->deleteKey(\OCA\NgBackup\AppInfo\Application::APP_ID, self::INTERRUPTED_KEY); if (!$wasInMaintenance) { $this->config->setSystemValue('maintenance', false); } diff --git a/tests/integration/full-restore-roundtrip.sh b/tests/integration/full-restore-roundtrip.sh index f15e311..ab79807 100755 --- a/tests/integration/full-restore-roundtrip.sh +++ b/tests/integration/full-restore-roundtrip.sh @@ -174,6 +174,7 @@ OUT=$(run_on ngb-frt-restore backup:restore:full offsite "$SNAP" "${RESTORE_OPTS } printf '%s\n' "$OUT" | grep -q "maintenance mode while the restore runs" || fail "the restore did not say it runs in maintenance mode" run_on ngb-frt-restore status 2>&1 | grep -q "maintenance: false" || fail "maintenance mode is still on after the restore" +run_on ngb-frt-restore config:app:get ng_backup restore_full_started >/dev/null 2>&1 && fail "the restore left its 'started' marker behind" ok "backup:restore:full completed, in maintenance mode and out of it again" # Everything below must work in a FRESH occ process (a new PHP process per docker exec), since