Arbeitspaket O. Der Teil, um den es geht, ist nicht das Sichern, sondern das Zurueckspielen: eine Sicherungsfunktion, deren Wiederherstellung nie gelaufen ist, ist eine Vermutung. Die Datenbank wird nicht kopiert, sondern von SQLite ausgeschrieben (VACUUM INTO). Ein copy() waehrend eines Schreibvorgangs ergibt eine Datei, die vorhanden aussieht und sich am Tag der Wiederherstellung nicht oeffnen laesst. Der Medienordner gehoert dazu - ohne die Dateien stellt die Sicherung eine Website voller kaputter Bilder her. .env gehoert nicht dazu: darin steht APP_SECRET, und eine Sicherung wird heruntergeladen und weitergereicht. Was nicht drin ist, steht im Manifest. Jede Sicherung traegt Zeitpunkt, Anlass, Fassung, Migrationsstand, Inhaltszahlen und die SHA-256 der Datenbank. Die Uebersicht zeigt "unversehrt" oder "Pruefsumme falsch" - eine Sicherung, von der man erst beim Zurueckspielen erfaehrt, dass sie kaputt ist, waere schlimmer als keine. Beim Zurueckspielen: neuere Fassung wird abgewiesen (mit --force geht es), zuerst wird der aktuelle Stand gesichert, Wartungszustand an, Medien werden ergaenzt statt ersetzt - eine Datei, die nach der Sicherung hochgeladen wurde, bleibt liegen. Die Massnahme gegen Datenverlust darf nicht selbst welchen verursachen. In der Verwaltung gibt es Liste und Anlegen-Knopf, aber kein Zurueckspielen: ein Knopf, der die ganze Datenbank ersetzt, gehoert nicht neben einen, der eine Seite speichert. Nachgewiesen, zweimal: acht Tests gegen eine wegwerfbare Installation (Sicherung, Seite und Bilddatei loeschen, zurueckspielen, beides wieder da) - und von Hand auf der Entwicklungsinstallation mit einer eigens angelegten Testseite: 5 Seiten, gesichert, geloescht (4), zurueckgespielt (5, Seite mit ihrer Adresse wieder da), Testseite entfernt (4). Damit ist der aelteste offene Punkt aus Paket I erledigt. Ein Fall, den erst der Test zeigte: zwei Sicherungen in derselben Sekunde - nicht akademisch, denn die Wiederherstellung legt selbst eine Sicherheitskopie an. Der Name bekommt jetzt eine Nummer, statt dass die zweite scheitert. Offen: MySQL (mysqldump fehlt) und ein Download ueber den Browser. 344 PHP-Tests / 1397 Assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.5 KiB
PHP
65 lines
2.5 KiB
PHP
<?php
|
|
|
|
namespace App\Command;
|
|
|
|
use App\Ops\Backup;
|
|
use App\Ops\Updater;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
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\Style\SymfonyStyle;
|
|
|
|
/**
|
|
* Restoring is deliberately console only.
|
|
*
|
|
* A button that replaces the whole database does not belong next to a button that saves a page.
|
|
* The administration shows which backups exist and whether they are intact; putting one back is a
|
|
* decision made with a shell in hand.
|
|
*/
|
|
#[AsCommand(name: 'cms:restore', description: 'Puts a backup back in place: checks, safety copy, database, media.')]
|
|
final class RestoreCommand extends Command
|
|
{
|
|
public function __construct(private Backup $backup, private Updater $updater)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->addArgument('name', InputArgument::REQUIRED, 'Name der Sicherung, etwa sicherung-20260912-181159.')
|
|
->addOption('force', null, InputOption::VALUE_NONE, 'Auch eine Sicherung aus einer neueren Fassung einspielen.');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
$name = (string) $input->getArgument('name');
|
|
|
|
if ($input->isInteractive() && !$io->confirm(sprintf('Die Datenbank wird durch %s ersetzt. Fortfahren?', $name), false)) {
|
|
$io->writeln('Abgebrochen. Es wurde nichts verändert.');
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
// The site says "come back later" while the database is being replaced.
|
|
$this->updater->maintenance(true);
|
|
$result = $this->backup->restore($name, (bool) $input->getOption('force'));
|
|
foreach ($result['steps'] as $step) {
|
|
$io->writeln(sprintf('%s %s: %s', $step['ok'] ? ' ok ' : ' FEHLER', $step['step'], $step['detail']));
|
|
}
|
|
$this->updater->maintenance(false);
|
|
|
|
if (!$result['ok']) {
|
|
$io->error('Die Wiederherstellung ist nicht gelaufen. Die Installation ist unverändert.');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
$io->success('Wiederhergestellt. Prüfe jetzt die Website und räume danach den Cache: php bin/console cache:clear --env=prod');
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|