setName('backup:verify') ->setDescription('Checksum audit of a snapshot: every blob it needs must be in the (freshly re-checked) index and its pack present') ->addArgument('target', InputArgument::REQUIRED, 'Location name or id') ->addArgument('snapshot', InputArgument::REQUIRED, 'Snapshot id (see backup:list)') ->addOption('deep', null, InputOption::VALUE_NONE, 'Also download and decrypt every blob (verifies its authentication tag and content hash, not just that it is listed); much slower') ->setHelp(<<<'HELP' Without --deep this re-downloads and re-authenticates the index (never trusting the local cache) and confirms every blob the snapshot needs is listed and its pack is present -- fast, no blob content is read, so it cannot by itself catch a truncated or bit-rotted pack. With --deep every blob is also downloaded and decrypted, which fails loudly on exactly that even if the index still lists it -- the same verification an actual restore would do, run here deliberately instead of waiting to find out during one. Run this periodically (cron, picking a sample of recent snapshots) rather than only right before you'd need to rely on a restore. Examples: occ backup:verify offsite 537bf7e9c1a4... occ backup:verify offsite 537bf7e9c1a4... --deep HELP); } protected function execute(InputInterface $input, OutputInterface $output): int { try { $target = $this->targets->get($input->getArgument('target')); $result = $this->verify->verify($target, (string)$input->getArgument('snapshot'), (bool)$input->getOption('deep')); } catch (\Throwable $e) { $output->writeln('' . $e->getMessage() . ''); return 1; } $output->writeln(sprintf('%d files, %d blobs checked%s.', $result['filesChecked'], $result['blobsChecked'], $input->getOption('deep') ? sprintf(' (%.1f MiB decrypted)', $result['bytesChecked'] / 1048576) : '')); if ($result['missing'] !== []) { $output->writeln('' . count($result['missing']) . ' blob(s) missing from the index: ' . implode(', ', $result['missing'])); } if ($result['failed'] !== []) { $output->writeln('' . count($result['failed']) . ' blob(s) failed verification (corrupted or tampered): ' . implode(', ', $result['failed'])); } if ($result['missing'] === [] && $result['failed'] === []) { $output->writeln('OK'); return 0; } return 1; } }