setName('restore'); $this->addArgument('backup-zip', InputArgument::REQUIRED, 'Path to the ZIP file containing your backup.'); $this->setDescription('Restore data and files from a backup ZIP file.'); $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to restore into', ''); } /** * @throws CommandError */ protected function execute(InputInterface $input, OutputInterface $output): int { $interactions = new InteractiveConsole($this->getHelper('question'), $input, $output); $output->writeln("Warning!"); $output->writeln("- A restore operation will overwrite and remove files & content from an existing instance."); $output->writeln("- Any existing tables within the configured database will be dropped."); $output->writeln("- You should only restore into an instance of the same or newer BookStack version."); $output->writeln("- This command won't handle, restore or address any server configuration."); $appDir = AppLocator::require($input->getOption('app-directory')); $output->writeln("Checking system requirements..."); RequirementsValidator::validate(); $zipPath = realpath($input->getArgument('backup-zip')); $zip = new BackupZip($zipPath); $contents = $zip->getContentsOverview(); $output->writeln("\nContents found in the backup ZIP:"); $hasContent = false; foreach ($contents as $info) { $output->writeln(($info['exists'] ? '✔ ' : '❌ ') . $info['desc']); if ($info['exists']) { $hasContent = true; } } if (!$hasContent) { throw new CommandError("Provided ZIP backup [{$zipPath}] does not have any expected restore-able content."); } $output->writeln("The checked elements will be restored into [{$appDir}]."); $output->writeln("Existing content may be overwritten."); $output->writeln("Do you want to continue?"); if (!$interactions->confirm("Do you want to continue?")) { $output->writeln("Stopping restore operation."); return Command::SUCCESS; } $output->writeln("Extracting ZIP into temporary directory..."); $extractDir = $appDir . DIRECTORY_SEPARATOR . 'restore-temp-' . time(); if (!mkdir($extractDir)) { throw new CommandError("Could not create temporary extraction directory at [{$extractDir}]."); } $zip->extractInto($extractDir); // TODO - Cleanup temp extract dir // TODO - Environment handling // - Restore of old .env // - Prompt for correct DB details (Test before serving?) // - Prompt for correct URL (Allow entry of new?) // TODO - Restore folders from backup // TODO - Restore database from backup $output->writeln("Running database migrations..."); $artisan = (new ArtisanRunner($appDir)); $artisan->run(['migrate', '--force']); // TODO - Update system URL (via BookStack artisan command) if // there's been a change from old backup env $output->writeln("Clearing app caches..."); $artisan->run(['cache:clear']); $artisan->run(['config:clear']); $artisan->run(['view:clear']); return Command::SUCCESS; } protected function restoreEnv(string $extractDir, string $appDir, InteractiveConsole $interactions) { $extractEnv = EnvironmentLoader::load($extractDir); $appEnv = EnvironmentLoader::load($appDir); // TODO - Probably pass in since we'll need the APP_URL later on. // TODO - Create mysql runner to take variables to a programrunner instance. // Then test each, backup existing env, then replace env with old then overwrite // db options if the new app env options are the valid ones. } }