From 9bc4e4a4ab83465c65c674af796e4a1fff580f7b Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Wed, 21 Oct 2026 22:25:48 +0200 Subject: [PATCH] Never create a repository where backup data lies but no config could be read --- CHANGELOG.md | 3 +++ lib/Repository/Repository.php | 8 ++++++++ tests/Unit/Repository/RepositoryTest.php | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cd1c5..b454a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ 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. +- 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. - An SFTP connection that a Hetzner Storage Box dropped without phpseclib noticing is now recognised and replaced: the check asks the server and counts its answer. - The extra connection a full restore reads ahead on is replaced when it breaks (up to 20 times), diff --git a/lib/Repository/Repository.php b/lib/Repository/Repository.php index b761dcf..8408e38 100644 --- a/lib/Repository/Repository.php +++ b/lib/Repository/Repository.php @@ -72,6 +72,14 @@ final class Repository { if ($backend->exists('config')) { throw new RepositoryException('A repository already exists at this location'); } + // No config, but backup data: the config is missing (or could not be seen just now, a + // connection that failed). A new repository here would write a config over the one that + // opens this data; refuse instead. + foreach (['index', 'snapshots'] as $dir) { + if ($backend->listFiles($dir) !== []) { + throw new RepositoryException("This location holds backup data ($dir/) but no repository config could be read: not creating a new repository over it. Check the connection and the folder; a missing config can be restored from the recovery kit."); + } + } $id = bin2hex(random_bytes(16)); $config = ['format' => self::FORMAT, 'id' => $id, 'created' => gmdate('c'), 'key' => $wrappedKey, 'check' => base64_encode((new StreamCipher($keys))->encryptString($id, 'config-check'))]; diff --git a/tests/Unit/Repository/RepositoryTest.php b/tests/Unit/Repository/RepositoryTest.php index b8710d6..49ee92f 100644 --- a/tests/Unit/Repository/RepositoryTest.php +++ b/tests/Unit/Repository/RepositoryTest.php @@ -99,6 +99,20 @@ class RepositoryTest extends TestCase { Repository::init($this->backend, 'pw'); } + public function testInitRefusesALocationWithBackupDataButNoConfig(): void { + $repo = Repository::init($this->backend, 'pw'); + $repo->backupDirectory($this->src); + $this->backend->delete('config'); // missing, or not seen for a moment + + try { + Repository::init($this->backend, 'pw'); + $this->fail('a new repository over existing backup data'); + } catch (RepositoryException $e) { + $this->assertStringContainsString('holds backup data', $e->getMessage()); + } + $this->assertFalse($this->backend->exists('config'), 'no new config written'); + } + public function testTamperedPackIsDetected(): void { $repo = Repository::init($this->backend, 'pw'); $r = $repo->backupDirectory($this->src);