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