b14b16c66c
- Record folders without files in the meta record of a snapshot and restore them (#27) - Errors writing to the restore directory stop the restore, also with --salvage - Resume: check the folder before skipping, clean temporaries in folders that are complete - Clean only read-ahead folders of our own; refresh the lease on skipped files - Drop tables a failed database restore created; keep --salvage from calling a restored database fresh - Retry a pack that fails to open on the storage (SMB) - Without cron, running backups take turns from the open page - Bin: read in parts with a counter and progress bar, show read errors, refresh when a run ends
111 lines
4.3 KiB
PHP
111 lines
4.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\NgBackup\Tests\Unit\Repository;
|
|
|
|
use OCA\NgBackup\Backend\LocalBackend;
|
|
use OCA\NgBackup\Crypto\KeyRing;
|
|
use OCA\NgBackup\Repository\ICatalogAnchor;
|
|
use OCA\NgBackup\Repository\Repository;
|
|
use OCA\NgBackup\Repository\RepositoryException;
|
|
use OCA\NgBackup\Tests\Unit\TempDirTrait;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class CatalogTest extends TestCase {
|
|
use TempDirTrait;
|
|
|
|
private LocalBackend $backend;
|
|
private KeyRing $keys;
|
|
private ICatalogAnchor $anchor;
|
|
private string $root;
|
|
private string $src;
|
|
|
|
protected function setUp(): void {
|
|
$this->root = $this->tempDir();
|
|
$this->backend = new LocalBackend($this->root);
|
|
$this->keys = KeyRing::generate();
|
|
$this->anchor = new class implements ICatalogAnchor {
|
|
public array $data = [];
|
|
public function get(string $r): ?array { return $this->data[$r] ?? null; }
|
|
public function set(string $r, int $g, string $h): void { $this->data[$r] = ['gen' => $g, 'hash' => $h]; }
|
|
};
|
|
$this->src = $this->tempDir();
|
|
file_put_contents("$this->src/a", random_bytes(100000));
|
|
}
|
|
|
|
private function open(): Repository {
|
|
return Repository::openWithKey($this->backend, $this->keys, null, $this->anchor);
|
|
}
|
|
|
|
public function testNormalUseAdvancesTheChain(): void {
|
|
$repo = Repository::initWithKey($this->backend, $this->keys, $this->keys->wrap('pw'), $this->anchor);
|
|
$s1 = $repo->backupDirectory($this->src);
|
|
file_put_contents("$this->src/b", 'x');
|
|
$s2 = $repo->backupDirectory($this->src, $s1['snapshot']);
|
|
$repo->forget($s1['snapshot']);
|
|
$again = $this->open();
|
|
$this->assertSame([$s2['snapshot']], $again->listSnapshots());
|
|
$this->assertArrayHasKey($s1['snapshot'], $again->trash());
|
|
$this->assertSame(4, $again->catalogGeneration());
|
|
}
|
|
|
|
public function testTheTrashIsReadInPartsThatTogetherAreAll(): void {
|
|
$repo = Repository::initWithKey($this->backend, $this->keys, $this->keys->wrap('pw'), $this->anchor);
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$repo->forget($repo->backupDirectory($this->src)['snapshot']);
|
|
}
|
|
$again = $this->open();
|
|
$seen = [];
|
|
// A deadline already past: one entry per request, as on a slow location
|
|
for ($request = 0; count($seen) < 3 && $request < 5; $request++) {
|
|
$page = $again->trashPage(count($seen), 0.0);
|
|
$this->assertSame(3, $page['total']);
|
|
$this->assertCount(1, $page['items']);
|
|
$seen += $page['items'];
|
|
}
|
|
$this->assertSame(array_keys($again->trash()), array_intersect(array_keys($again->trash()), array_keys($seen)));
|
|
$this->assertCount(3, $seen);
|
|
$this->assertSame(['total' => 3, 'items' => []], $again->trashPage(3, 0.0));
|
|
$this->assertCount(3, $again->trashPage(0, microtime(true) + 60)['items']);
|
|
}
|
|
|
|
public function testRollbackIsDetected(): void {
|
|
$repo = Repository::initWithKey($this->backend, $this->keys, $this->keys->wrap('pw'), $this->anchor);
|
|
$repo->backupDirectory($this->src);
|
|
$repo->backupDirectory($this->src);
|
|
// The location "loses" the newest generation and presents an older, validly encrypted state.
|
|
$catalogs = $this->backend->list('catalog');
|
|
unlink($this->root . '/' . end($catalogs));
|
|
$this->expectException(RepositoryException::class);
|
|
$this->expectExceptionMessage('rolled back');
|
|
$this->open();
|
|
}
|
|
|
|
public function testHiddenSnapshotIsDetected(): void {
|
|
$repo = Repository::initWithKey($this->backend, $this->keys, $this->keys->wrap('pw'), $this->anchor);
|
|
$s = $repo->backupDirectory($this->src);
|
|
unlink($this->root . '/snapshots/' . $s['snapshot']);
|
|
$this->expectException(RepositoryException::class);
|
|
$this->open()->listSnapshots();
|
|
}
|
|
|
|
public function testSwappedGenerationIsRejected(): void {
|
|
$repo = Repository::initWithKey($this->backend, $this->keys, $this->keys->wrap('pw'), $this->anchor);
|
|
$repo->backupDirectory($this->src);
|
|
$repo->backupDirectory($this->src);
|
|
$c = $this->backend->list('catalog');
|
|
// Put generation 2's bytes where generation 3 should be.
|
|
copy($this->root . '/' . $c[1], $this->root . '/' . $c[2]);
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->open();
|
|
}
|
|
|
|
public function testWithoutAnchorTheNewestChainIsAccepted(): void {
|
|
$repo = Repository::initWithKey($this->backend, $this->keys, $this->keys->wrap('pw'));
|
|
$s = $repo->backupDirectory($this->src);
|
|
// Disaster recovery on a new server: opened with the passphrase, no anchor.
|
|
$this->assertSame([$s['snapshot']], Repository::open($this->backend, 'pw')->listSnapshots());
|
|
}
|
|
}
|