d48e2b7d9d
Major features: - Full backup & restore system (JSON snapshots of all 20 tables + settings) - Web UI, REST API, OCC CLI commands, scheduled background job - Self-update from Gitea releases with Ed25519 signature verification - Configurable column visibility on all data tables (persisted via localStorage) Fixes: - NC admin group fallback for PermissionService (IGroupManager) - Bundle import inline error correction (editable error rows) New files: BackupService, BackupSettingsService, BackupController, BackupJob, SelfUpdateService, 4 OCC commands, ColumnPicker component, Backup.vue, Ed25519 signing scripts, signature verification tests (18 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
3.2 KiB
PHP
83 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Mitgliederverwaltung\Command;
|
|
|
|
use OCA\Mitgliederverwaltung\Service\BackupService;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
|
|
|
class BackupRestoreCommand extends Command {
|
|
|
|
private BackupService $backupService;
|
|
|
|
public function __construct(BackupService $backupService) {
|
|
parent::__construct();
|
|
$this->backupService = $backupService;
|
|
}
|
|
|
|
protected function configure(): void {
|
|
$this->setName('mitgliederverwaltung:backup:restore')
|
|
->setDescription('Restore Mitgliederverwaltung from a backup file')
|
|
->addArgument('filename', InputArgument::REQUIRED, 'Backup filename')
|
|
->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'Password if backup is encrypted')
|
|
->addOption('yes', 'y', InputOption::VALUE_NONE, 'Skip confirmation prompt');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int {
|
|
$filename = $input->getArgument('filename');
|
|
$password = $input->getOption('password');
|
|
$skipConfirm = $input->getOption('yes');
|
|
|
|
// Show backup info
|
|
$info = $this->backupService->getBackupInfo($filename);
|
|
if ($info === null) {
|
|
$output->writeln('<error>Backup not found: ' . $filename . '</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$output->writeln('Backup info:');
|
|
$output->writeln(' Filename: ' . $info['filename']);
|
|
$output->writeln(' Created: ' . $info['createdAt']);
|
|
$output->writeln(' Version: ' . ($info['appVersion'] ?? 'unknown'));
|
|
$output->writeln(' Tables: ' . count($info['tables']));
|
|
|
|
$totalRows = 0;
|
|
foreach ($info['tables'] as $count) {
|
|
$totalRows += $count;
|
|
}
|
|
$output->writeln(' Total rows: ' . $totalRows);
|
|
$output->writeln('');
|
|
|
|
// Confirmation
|
|
if (!$skipConfirm) {
|
|
$output->writeln('<comment>WARNING: This will DELETE ALL existing data and replace it with the backup.</comment>');
|
|
$helper = $this->getHelper('question');
|
|
$question = new ConfirmationQuestion('Continue? [y/N] ', false);
|
|
if (!$helper->ask($input, $output, $question)) {
|
|
$output->writeln('Restore cancelled.');
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|
|
|
|
$output->writeln('Restoring...');
|
|
|
|
try {
|
|
$result = $this->backupService->restoreBackup($filename, $password);
|
|
$output->writeln('');
|
|
$output->writeln('<info>Restore completed successfully!</info>');
|
|
$output->writeln(' Tables restored: ' . $result['restoredTables']);
|
|
$output->writeln(' Total rows: ' . $result['totalRows']);
|
|
return Command::SUCCESS;
|
|
} catch (\Exception $e) {
|
|
$output->writeln('<error>Restore failed: ' . $e->getMessage() . '</error>');
|
|
return Command::FAILURE;
|
|
}
|
|
}
|
|
}
|