Files
nextcloud-ng_backup/lib/Command/KeyConfirm.php
T
dillard c54b2cebed M5: end-to-end test matrix and CI
- tests/integration/roundtrip.sh: key, kit code and confirmation,
  location, full and unchanged incremental backup, restore new-folder and
  replace (files identical, extra file in the trash), database dump and
  restore, retention, trash and restore from trash, no files_external
  errors in the log
- tests/integration/matrix.sh: fresh Nextcloud per version and database
  (SQLite, MariaDB, PostgreSQL), removes only its own containers and
  their volumes; locally green on NC32 and NC35 with all three databases
- GitHub Actions: unit tests on PHP 8.2-8.4, round trip on NC32-35 x
  SQLite/MariaDB/PostgreSQL

Fixes found by the matrix:
- files_external is no longer booted when it is disabled: only its
  classes are loaded and a private BackendService gets its backends from
  its Application class (booting registered its mount provider, which
  failed on installations where its tables were never created)
- SQLite: the database file is excluded from the file backup (it is
  covered by the logical dump and copying the live file is inconsistent)
- occ backup:key:confirm accepts --code and --confirm for scripts
2026-10-02 11:56:57 +02:00

60 lines
2.6 KiB
PHP

<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: 2026 Dillard Blom
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\NgBackup\Command;
use OCA\NgBackup\Service\KeyService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class KeyConfirm extends Command {
public const STATEMENT = 'I have downloaded the recovery kit and stored it safely, outside this server. '
. 'I understand that keeping these keys safe is my own responsibility and that backups cannot be restored without them.';
public const CONFIRMATION = 'Yes, I confirm';
public function __construct(
private KeyService $keys,
) {
parent::__construct();
}
protected function configure(): void {
$this->setName('backup:key:confirm')
->setDescription('Confirm that the recovery kit is stored safely (required before the first backup)')
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Admin user id recorded with the confirmation', 'occ')
->addOption('code', null, InputOption::VALUE_REQUIRED, 'Confirmation code from the kit file (for scripts; asked otherwise)')
->addOption('confirm', null, InputOption::VALUE_REQUIRED, 'The confirmation phrase "' . self::CONFIRMATION . '" (for scripts; asked otherwise)');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
if (!$this->keys->isInitialized()) {
$output->writeln('<error>No backup key yet: run occ backup:key:init first.</error>');
return 1;
}
$output->writeln('Key fingerprint: <info>' . $this->keys->fingerprint() . '</info>');
$output->writeln('');
$output->writeln(wordwrap(self::STATEMENT, 78));
$output->writeln('');
$code = $input->getOption('code') ?? (string)$this->getHelper('question')->ask($input, $output, new Question('Confirmation code from the recovery kit file ("confirmation_code"): '));
if (!$this->keys->checkKitCode($code)) {
$output->writeln('<error>That code does not match the current recovery kit (download it with occ backup:key:kit first).</error>');
return 1;
}
$answer = $input->getOption('confirm') ?? (string)$this->getHelper('question')->ask($input, $output, new Question('Type "' . self::CONFIRMATION . '" to proceed: '));
if (trim($answer) !== self::CONFIRMATION) {
$output->writeln('<error>Not confirmed.</error>');
return 1;
}
$this->keys->confirmRecoveryKit((string)$input->getOption('user'));
$output->writeln('Confirmed. Backups can now run.');
return 0;
}
}