3 namespace Cli\Commands;
5 use Cli\Services\AppLocator;
6 use Cli\Services\ArtisanRunner;
7 use Cli\Services\BackupZip;
8 use Cli\Services\EnvironmentLoader;
9 use Cli\Services\InteractiveConsole;
10 use Cli\Services\RequirementsValidator;
11 use Symfony\Component\Console\Command\Command;
12 use Symfony\Component\Console\Input\InputArgument;
13 use Symfony\Component\Console\Input\InputInterface;
14 use Symfony\Component\Console\Input\InputOption;
15 use Symfony\Component\Console\Output\OutputInterface;
17 class RestoreCommand extends Command
19 protected function configure(): void
21 $this->setName('restore');
22 $this->addArgument('backup-zip', InputArgument::REQUIRED, 'Path to the ZIP file containing your backup.');
23 $this->setDescription('Restore data and files from a backup ZIP file.');
24 $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to restore into', '');
28 * @throws CommandError
30 protected function execute(InputInterface $input, OutputInterface $output): int
32 $interactions = new InteractiveConsole($this->getHelper('question'), $input, $output);
34 $output->writeln("<info>Warning!</info>");
35 $output->writeln("<info>- A restore operation will overwrite and remove files & content from an existing instance.</info>");
36 $output->writeln("<info>- Any existing tables within the configured database will be dropped.</info>");
37 $output->writeln("<info>- You should only restore into an instance of the same or newer BookStack version.</info>");
38 $output->writeln("<info>- This command won't handle, restore or address any server configuration.</info>");
40 $appDir = AppLocator::require($input->getOption('app-directory'));
41 $output->writeln("<info>Checking system requirements...</info>");
42 RequirementsValidator::validate();
44 $zipPath = realpath($input->getArgument('backup-zip'));
45 $zip = new BackupZip($zipPath);
46 $contents = $zip->getContentsOverview();
48 $output->writeln("\n<info>Contents found in the backup ZIP:</info>");
50 foreach ($contents as $info) {
51 $output->writeln(($info['exists'] ? '✔ ' : '❌ ') . $info['desc']);
52 if ($info['exists']) {
58 throw new CommandError("Provided ZIP backup [{$zipPath}] does not have any expected restore-able content.");
61 $output->writeln("<info>The checked elements will be restored into [{$appDir}].</info>");
62 $output->writeln("<info>Existing content may be overwritten.</info>");
63 $output->writeln("<info>Do you want to continue?</info>");
65 if (!$interactions->confirm("Do you want to continue?")) {
66 $output->writeln("<info>Stopping restore operation.</info>");
67 return Command::SUCCESS;
70 $output->writeln("<info>Extracting ZIP into temporary directory...</info>");
71 $extractDir = $appDir . DIRECTORY_SEPARATOR . 'restore-temp-' . time();
72 if (!mkdir($extractDir)) {
73 throw new CommandError("Could not create temporary extraction directory at [{$extractDir}].");
75 $zip->extractInto($extractDir);
77 // TODO - Cleanup temp extract dir
79 // TODO - Environment handling
80 // - Restore of old .env
81 // - Prompt for correct DB details (Test before serving?)
82 // - Prompt for correct URL (Allow entry of new?)
84 // TODO - Restore folders from backup
86 // TODO - Restore database from backup
88 $output->writeln("<info>Running database migrations...</info>");
89 $artisan = (new ArtisanRunner($appDir));
90 $artisan->run(['migrate', '--force']);
92 // TODO - Update system URL (via BookStack artisan command) if
93 // there's been a change from old backup env
95 $output->writeln("<info>Clearing app caches...</info>");
96 $artisan->run(['cache:clear']);
97 $artisan->run(['config:clear']);
98 $artisan->run(['view:clear']);
100 return Command::SUCCESS;
103 protected function restoreEnv(string $extractDir, string $appDir, InteractiveConsole $interactions)
105 $extractEnv = EnvironmentLoader::load($extractDir);
106 $appEnv = EnvironmentLoader::load($appDir); // TODO - Probably pass in since we'll need the APP_URL later on.
108 // TODO - Create mysql runner to take variables to a programrunner instance.
109 // Then test each, backup existing env, then replace env with old then overwrite
110 // db options if the new app env options are the valid ones.