From 9eeaefd938461ca0ef8977cd4673dc9bde15a1c4 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Sun, 18 Oct 2026 02:39:14 +0200 Subject: [PATCH] Refuse a full restore when the table prefix differs --- CHANGELOG.md | 2 ++ lib/Db/DbDumper.php | 21 +++++++++++++++++++++ lib/Service/RestoreFullService.php | 14 ++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edd6468..054fe91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ All-in-One onto a plain Nextcloud with PostgreSQL). - A full restore no longer refuses when apps are missing: it restores their data and lists the apps to install afterwards. `nextcloud-aio` is never required, so a backup of Nextcloud All-in-One can be restored onto a plain Nextcloud. +- A full restore onto an installation with a different table prefix (`dbtableprefix`) is refused + before it starts, with what to change, instead of restoring into tables nobody uses. - All problems are listed at once, with the app each missing table probably belongs to. For backups made before this version, `--skip-missing-tables` leaves out tables whose structure is unknown. diff --git a/lib/Db/DbDumper.php b/lib/Db/DbDumper.php index bc5f981..716b63e 100644 --- a/lib/Db/DbDumper.php +++ b/lib/Db/DbDumper.php @@ -145,6 +145,11 @@ final class DbDumper { */ public function restore(Repository $repo, array $manifest, ?array &$report = null, ?callable $heartbeat = null, bool $skipMissing = false): int { $report = ['skipped' => [], 'restored' => [], 'created' => [], 'missing' => []]; + $source = self::dumpPrefix($manifest); + if ($source !== null && $source !== $this->prefix) { + // Every table would look missing and be created under the old prefix, unused. + throw new \RuntimeException("The backup's tables use the prefix '$source', this installation '{$this->prefix}'. Install Nextcloud again with 'dbtableprefix' => '$source' in config/config.php before occ maintenance:install, and restore again."); + } $missing = $this->missingTables($manifest); $unknown = array_values(array_filter($missing, fn (string $t) => !isset($manifest['tables'][$t]['schema']))); if ($unknown !== [] && !$skipMissing) { @@ -241,6 +246,22 @@ final class DbDumper { return $total; } + /** + * The table prefix of the instance the dump was made on (from its appconfig table), or null + * when the dump has none. + * + */ + public static function dumpPrefix(array $manifest): ?string { + /** @var list $tables table names */ + $tables = array_keys($manifest['tables'] ?? []); + foreach ($tables as $name) { + if (str_ends_with($name, 'appconfig') && !str_ends_with($name, '_ex_appconfig')) { + return substr($name, 0, -strlen('appconfig')); + } + } + return null; + } + /** * Tables in the dump that do not exist on this instance. * diff --git a/lib/Service/RestoreFullService.php b/lib/Service/RestoreFullService.php index 467f595..9a0e279 100644 --- a/lib/Service/RestoreFullService.php +++ b/lib/Service/RestoreFullService.php @@ -63,19 +63,25 @@ final class RestoreFullService { $enabled = array_flip($this->appManager->getEnabledApps()); $missingApps = array_values(array_diff(array_keys($meta['apps'] ?? []), array_keys($enabled), self::ENVIRONMENT_APPS)); $missingTables = []; + $prefixMismatch = null; if (isset($meta['db'])) { $manifest = json_decode($repo->getObject($meta['db']), true, 512, JSON_THROW_ON_ERROR); $prefix = $this->config->getSystemValueString('dbtableprefix', 'oc_'); + $sourcePrefix = DbDumper::dumpPrefix($manifest); + if ($sourcePrefix !== null && $sourcePrefix !== $prefix) { + $prefixMismatch = [$sourcePrefix, $prefix]; + } $dumper = new DbDumper($this->db->getInner(), $prefix); $missingTables = array_values(array_filter($dumper->missingTables($manifest), fn (string $t) => !isset($manifest['tables'][$t]['schema']) && !str_starts_with($t, $prefix . 'ngb_'))); } return [ - 'ok' => $currentMajor === $snapshotMajor && ($missingTables === [] || $skipMissingTables), + 'ok' => $currentMajor === $snapshotMajor && $prefixMismatch === null && ($missingTables === [] || $skipMissingTables), 'currentMajor' => $currentMajor, 'snapshotMajor' => $snapshotMajor, 'missingApps' => $missingApps, - 'missingTables' => $missingTables, + 'missingTables' => $prefixMismatch === null ? $missingTables : [], + 'prefixMismatch' => $prefixMismatch, ]; } @@ -224,6 +230,10 @@ final class RestoreFullService { if ($check['currentMajor'] !== $check['snapshotMajor']) { $parts[] = "this installation is Nextcloud {$check['currentMajor']}, the backup is from {$check['snapshotMajor']} (same major version required)"; } + if (($check['prefixMismatch'] ?? null) !== null) { + [$source, $here] = $check['prefixMismatch']; + $parts[] = "the backup's tables use the prefix '$source', this installation '$here': install Nextcloud again with 'dbtableprefix' => '$source' in config/config.php before occ maintenance:install, and restore again"; + } if ($check['missingTables'] !== []) { $prefix = $this->config->getSystemValueString('dbtableprefix', 'oc_'); $parts[] = DbDumper::describeMissing($check['missingTables'], $prefix);