Never create a repository where backup data lies but no config could be read

This commit is contained in:
2026-10-21 22:25:48 +02:00
parent 827e46381e
commit 9bc4e4a4ab
3 changed files with 25 additions and 0 deletions
+3
View File
@@ -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),
+8
View File
@@ -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'))];
+14
View File
@@ -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);