From 7ed53ad0eb49cf05c39a772b9700600cdcbc47c6 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Thu, 22 Oct 2026 00:36:58 +0200 Subject: [PATCH] Full restore: after an error maintenance mode stays on, with the one line to switch it off and run again; maintenance that was on before stays on --- CHANGELOG.md | 8 +++++--- docs/DISASTER-RECOVERY.md | 7 ++++++- lib/Command/RestoreFull.php | 13 +++++++++++-- lib/Service/RestoreFullService.php | 19 +++++++++---------- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ccb29..4d3997c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,11 @@ stopped after 13 hours with "Blob not in index"; the backup itself was fine. backups record no ID counters, and they stayed at 1. Every counter of a restored table now goes after its data. Found in a disaster recovery simulation of 138 GB from MariaDB to PostgreSQL, where all 15,772 files and every count were identical. -- A full restore now runs in maintenance mode, switched off again at the end (also after an - error): a background job, a page visit or a user writing into the instance meanwhile could - collide with restored rows ("duplicate key") or be lost. +- A full restore now runs in maintenance mode: a background job, a page visit or a user writing + into the instance meanwhile could collide with restored rows ("duplicate key") or be lost. It + is switched off when the restore completes; after an error it stays on, and the message gives + the one line to switch it off and run the restore again. Maintenance mode that was on before + the restore stays on. - Adding a location whose repository config could not be read, while backup data is there, no longer creates a new repository over it: it stops with an explanation. A snapshot that cannot be taken over when a location is added is now logged instead of left out silently. diff --git a/docs/DISASTER-RECOVERY.md b/docs/DISASTER-RECOVERY.md index acef877..99db900 100644 --- a/docs/DISASTER-RECOVERY.md +++ b/docs/DISASTER-RECOVERY.md @@ -95,7 +95,12 @@ out with `--skip-missing-tables`. **8. Let the restore run.** It restores the database, then the data directory, and takes hours for a large backup. Nextcloud is in maintenance mode meanwhile, so nobody and no background job -writes into it; the restore switches it off again at the end, also after an error. If it stops halfway, fix the cause and run the same command again with `--force`. +writes into it; the restore switches it off when it completes. (If you had switched maintenance +mode on yourself before, it stays on: switch it off with `occ maintenance:mode --off` when ready.) + +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 same command again with `--force`, in one line: +`occ maintenance:mode --off && occ backup:restore:full --install-apps --force`. 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 e8867c1..3cf0b8c 100644 --- a/lib/Command/RestoreFull.php +++ b/lib/Command/RestoreFull.php @@ -88,11 +88,17 @@ HELP); $this->installApp($app, $output); } } - $output->writeln('Nextcloud is in maintenance mode while the restore runs (switched off again at the end, also after an error).'); + $output->writeln('Nextcloud is in maintenance mode while the restore runs; it is switched off when the restore completes.'); $result = $this->restoreFull->restoreFull($target, $snapshot, $skipMissingTables); } catch (\Throwable $e) { $output->writeln('' . $e->getMessage() . ''); - $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.'); + if (\OCP\Server::get(\OCP\IConfig::class)->getSystemValueBool('maintenance', false)) { + $output->writeln('The restore had started: the instance may be partly restored, and maintenance mode stays on so that nobody uses it like that.'); + $output->writeln('Fix the cause, then switch maintenance mode off and run the same command again with --force, in one line:'); + $output->writeln(' occ maintenance:mode --off && occ backup:restore:full ' . (string)$input->getArgument('target') . ' ' . (string)$input->getArgument('snapshot') . ' --force ...'); + } 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.'); + } $output->writeln('If occ then says that Nextcloud needs an upgrade (the database is already the old server\'s), run occ upgrade first.'); return 1; } @@ -122,6 +128,9 @@ HELP); if (!$result['dbConsistent']) { $output->writeln('Warning: this snapshot\'s database dump was made over several steps (web cron), so it is not one consistent point in time. Check the instance carefully; prefer a snapshot made with system cron.'); } + if ($result['maintenanceLeftOn']) { + $output->writeln('Maintenance mode was on before the restore and stays on: switch it off with occ maintenance:mode --off when the instance is ready.'); + } $output->writeln('Next: check trusted_domains and overwritehost/overwriteprotocol match this server, then occ maintenance:repair.'); return 0; } diff --git a/lib/Service/RestoreFullService.php b/lib/Service/RestoreFullService.php index 4b036dd..cd3a1fb 100644 --- a/lib/Service/RestoreFullService.php +++ b/lib/Service/RestoreFullService.php @@ -87,7 +87,7 @@ final class RestoreFullService { ]; } - /** @return array{dbReport:array, dataFiles:int, configKeys:int, dbConsistent:bool, missingApps:list} */ + /** @return array{dbReport:array, dataFiles:int, configKeys:int, dbConsistent:bool, missingApps:list, maintenanceLeftOn:bool} */ public function restoreFull(Target $target, string $snapshotId, bool $skipMissingTables = false): array { $check = $this->checkCompatibility($target, $snapshotId, $skipMissingTables); if (!$check['ok']) { @@ -96,18 +96,17 @@ final class RestoreFullService { // Maintenance mode for the whole restore: background jobs (cron.php stops in maintenance // mode), page visits (AJAX cron) and users must not write into tables or the data // directory while they are being replaced; a row written in between would collide with a - // restored one or be silently lost. Switched off again at the end, also after an error: - // occ only offers app commands outside maintenance mode, and the guide's next step is - // to run the same restore again with --force. + // restored one or be silently lost. Switched off after a restore that completed. After an + // error it stays on: the instance may be partly restored, and must not be used like that. + // occ only offers app commands outside maintenance mode, so the admin switches it off + // right before running the restore again (see RestoreFull). Already on before: left on. $wasInMaintenance = $this->config->getSystemValueBool('maintenance', false); $this->config->setSystemValue('maintenance', true); - try { - return $this->restoreInMaintenance($target, $snapshotId, $skipMissingTables, $check); - } finally { - if (!$wasInMaintenance) { - $this->config->setSystemValue('maintenance', false); - } + $result = $this->restoreInMaintenance($target, $snapshotId, $skipMissingTables, $check); + if (!$wasInMaintenance) { + $this->config->setSystemValue('maintenance', false); } + return $result + ['maintenanceLeftOn' => $wasInMaintenance]; } /** @return array{dbReport:array, dataFiles:int, configKeys:int, dbConsistent:bool, missingApps:list} */