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

This commit is contained in:
2026-10-22 00:36:58 +02:00
parent bdcb4f0cae
commit 7ed53ad0eb
4 changed files with 31 additions and 16 deletions
+5 -3
View File
@@ -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.
+6 -1
View File
@@ -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 <location> <snapshot> --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`.
+11 -2
View File
@@ -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('<error>' . $e->getMessage() . '</error>');
$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('<comment>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.</comment>');
}
if ($result['maintenanceLeftOn']) {
$output->writeln('<comment>Maintenance mode was on before the restore and stays on: switch it off with occ maintenance:mode --off when the instance is ready.</comment>');
}
$output->writeln('Next: check trusted_domains and overwritehost/overwriteprotocol match this server, then occ maintenance:repair.');
return 0;
}
+9 -10
View File
@@ -87,7 +87,7 @@ final class RestoreFullService {
];
}
/** @return array{dbReport:array, dataFiles:int, configKeys:int, dbConsistent:bool, missingApps:list<string>} */
/** @return array{dbReport:array, dataFiles:int, configKeys:int, dbConsistent:bool, missingApps:list<string>, 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<string>} */