getValue($b); $total = 0; foreach ($b->list('packs') as $p) { $total += filesize("$root/$p"); } return $total; } public function testForgetAndPruneFreesSpaceAndKeepsTheRest(): void { $src = $this->tempDir(); $backend = new LocalBackend($this->tempDir()); $keys = KeyRing::generate(); $repo = Repository::initWithKey($backend, $keys, $keys->wrap('pw')); file_put_contents("$src/keep.bin", random_bytes(6 * 1048576)); file_put_contents("$src/old.bin", random_bytes(40 * 1048576)); $old = $repo->backupDirectory($src); unlink("$src/old.bin"); file_put_contents("$src/new.bin", random_bytes(3 * 1048576)); $new = $repo->backupDirectory($src, $old['snapshot']); $expected = self::hashTree($src); $before = $this->du($backend); $dry = $repo->prune(true); $this->assertSame(0, $dry['freedBytes'], 'nothing is unused while both snapshots exist'); $preview = $repo->prune(true, 0.5, [$old['snapshot']]); $this->assertGreaterThan(35 * 1048576, $preview['freedBytes'], 'dry run can preview forgetting a snapshot'); $repo->forget($old['snapshot'], 'tester'); $this->assertSame(0, $repo->prune(true)['freedBytes'], 'a snapshot in the trash keeps its data'); $this->assertArrayHasKey($old['snapshot'], $repo->trash()); $this->assertSame([], $repo->purgeTrash(3600), 'not purged before the delay'); $this->assertSame([$old['snapshot']], $repo->purgeTrash(0)); $plan = $repo->prune(true); $this->assertSame($plan, $repo->prune(true, 0.5, [])); $this->assertGreaterThan(35 * 1048576, $plan['freedBytes']); $this->assertSame($before, $this->du($backend), 'dry run changes nothing'); $result = $repo->prune(); $this->assertGreaterThan(0, $result['deleted'] + $result['repacked']); $this->assertLessThan($before - 35 * 1048576, $this->du($backend)); $reopened = Repository::openWithKey($backend, $keys); $target = $this->tempDir(); $reopened->restore($new['snapshot'], $target); $this->assertSame($expected, self::hashTree($target)); $this->assertSame([$new['snapshot']], $reopened->listSnapshots()); } public function testRepackKeepsUsedBlobsOfMixedPacks(): void { $src = $this->tempDir(); $backend = new LocalBackend($this->tempDir()); $keys = KeyRing::generate(); $repo = Repository::initWithKey($backend, $keys, $keys->wrap('pw')); // Small files share packs: deleting most of them leaves mixed packs to repack. for ($i = 0; $i < 40; $i++) { file_put_contents("$src/f$i", random_bytes(500000)); } $a = $repo->backupDirectory($src); for ($i = 0; $i < 40; $i++) { if ($i % 5 !== 0) { unlink("$src/f$i"); } } $b = $repo->backupDirectory($src, $a['snapshot']); $repo->forget($a['snapshot']); $repo->purgeTrash(0); $r = $repo->prune(false, 0.5); $this->assertGreaterThan(0, $r['repacked']); $target = $this->tempDir(); Repository::openWithKey($backend, $keys)->restore($b['snapshot'], $target); $this->assertSame(self::hashTree($src), self::hashTree($target)); } public function testRestoreFollowsPacksRepackedByAnotherServer(): void { // A disaster recovery test on a second server reads the location while the original // server's weekly cleanup repacks it: the restore must not stop at a pack that moved. $src = $this->tempDir(); $backend = new LocalBackend($this->tempDir()); $keys = KeyRing::generate(); $repo = Repository::initWithKey($backend, $keys, $keys->wrap('pw')); for ($i = 0; $i < 40; $i++) { file_put_contents("$src/f$i", random_bytes(500000)); } $a = $repo->backupDirectory($src); for ($i = 0; $i < 40; $i++) { if ($i % 5 !== 0) { unlink("$src/f$i"); } } $b = $repo->backupDirectory($src, $a['snapshot']); $reader = Repository::openWithKey($backend, $keys); // index loaded before the cleanup $repo->forget($a['snapshot']); $repo->purgeTrash(0); $this->assertGreaterThan(0, $repo->prune(false, 0.5)['repacked']); $target = $this->tempDir(); $reader->restore($b['snapshot'], $target); $this->assertSame(self::hashTree($src), self::hashTree($target)); } public function testUntrashRestoresAWorkingSnapshot(): void { $src = $this->tempDir(); $backend = new LocalBackend($this->tempDir()); $keys = KeyRing::generate(); $repo = Repository::initWithKey($backend, $keys, $keys->wrap('pw')); file_put_contents("$src/a", random_bytes(2 * 1048576)); $repo->putObject('db/x', '{"tables":{}}'); $s = $repo->backupDirectory($src); $expected = self::hashTree($src); unlink("$src/a"); file_put_contents("$src/b", 'other'); $s2 = $repo->backupDirectory($src, $s['snapshot']); $repo->forget($s['snapshot'], 'attacker'); $this->assertNotContains($s['snapshot'], $repo->listSnapshots()); $repo->prune(); // must not free the trashed snapshot's data $repo->untrash($s['snapshot']); $this->assertContains($s['snapshot'], $repo->listSnapshots()); $this->assertSame([], $repo->trash()); $target = $this->tempDir(); Repository::openWithKey($backend, $keys)->restore($s['snapshot'], $target); $this->assertSame($expected, self::hashTree($target)); } }