diff --git a/scripts/Commands/RestoreCommand.php b/scripts/Commands/RestoreCommand.php index 1f830455e..c181ff84e 100644 --- a/scripts/Commands/RestoreCommand.php +++ b/scripts/Commands/RestoreCommand.php @@ -4,6 +4,9 @@ namespace Cli\Commands; use Cli\Services\AppLocator; use Cli\Services\ArtisanRunner; +use Cli\Services\BackupZip; +use Cli\Services\EnvironmentLoader; +use Cli\Services\InteractiveConsole; use Cli\Services\RequirementsValidator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -26,16 +29,52 @@ class RestoreCommand extends Command */ 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(); - // TODO - Warn that potentially dangerous, - // warn for same/forward versions only, - // warn this won't handle server-level stuff + $zipPath = realpath($input->getArgument('backup-zip')); + $zip = new BackupZip($zipPath); + $contents = $zip->getContentsOverview(); - // TODO - Validate provided backup zip contents - // - Display and prompt to user + $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 @@ -60,4 +99,14 @@ class RestoreCommand extends Command 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. + } } diff --git a/scripts/Services/BackupZip.php b/scripts/Services/BackupZip.php new file mode 100644 index 000000000..ae82f6503 --- /dev/null +++ b/scripts/Services/BackupZip.php @@ -0,0 +1,54 @@ +zip = new ZipArchive(); + $status = $this->zip->open($this->filePath); + + if (!file_exists($this->filePath) || $status !== true) { + throw new \Exception("Could not open file [{$this->filePath}] as ZIP"); + } + } + + public function getContentsOverview(): array + { + return [ + 'env' => [ + 'desc' => '.env Config File', + 'exists' => boolval($this->zip->statName('.env')), + ], + 'themes' => [ + 'desc' => 'Themes Folder', + 'exists' => boolval($this->zip->statName('themes')), + ], + 'public-uploads' => [ + 'desc' => 'Public File Uploads', + 'exists' => boolval($this->zip->statName('public/uploads')), + ], + 'storage-uploads' => [ + 'desc' => 'Private File Uploads', + 'exists' => boolval($this->zip->statName('storage/uploads')), + ], + 'db' => [ + 'desc' => 'Database Dump', + 'exists' => boolval($this->zip->statName('db.sql')), + ], + ]; + } + + public function extractInto(string $directoryPath): void + { + $result = $this->zip->extractTo($directoryPath); + if (!$result) { + throw new \Exception("Failed extraction of ZIP into [{$directoryPath}]."); + } + } +} diff --git a/scripts/Services/InteractiveConsole.php b/scripts/Services/InteractiveConsole.php new file mode 100644 index 000000000..0cc4186fe --- /dev/null +++ b/scripts/Services/InteractiveConsole.php @@ -0,0 +1,27 @@ +helper->ask($this->input, $this->output, $question); + } +} \ No newline at end of file