]> BookStack Code Mirror - system-cli/commitdiff
Progressed restore command a little
authorDan Brown <redacted>
Tue, 7 Mar 2023 12:05:04 +0000 (12:05 +0000)
committerDan Brown <redacted>
Thu, 9 Mar 2023 15:28:14 +0000 (15:28 +0000)
scripts/Commands/RestoreCommand.php
scripts/Services/BackupZip.php [new file with mode: 0644]
scripts/Services/InteractiveConsole.php [new file with mode: 0644]

index 1f830455ed8dc84c90832d2dfd17787d404b3126..c181ff84eb40c259812e9751a312d282a044e6e8 100644 (file)
@@ -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("<info>Warning!</info>");
+        $output->writeln("<info>- A restore operation will overwrite and remove files & content from an existing instance.</info>");
+        $output->writeln("<info>- Any existing tables within the configured database will be dropped.</info>");
+        $output->writeln("<info>- You should only restore into an instance of the same or newer BookStack version.</info>");
+        $output->writeln("<info>- This command won't handle, restore or address any server configuration.</info>");
+
         $appDir = AppLocator::require($input->getOption('app-directory'));
         $output->writeln("<info>Checking system requirements...</info>");
         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();
+
+        $output->writeln("\n<info>Contents found in the backup ZIP:</info>");
+        $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("<info>The checked elements will be restored into [{$appDir}].</info>");
+        $output->writeln("<info>Existing content may be overwritten.</info>");
+        $output->writeln("<info>Do you want to continue?</info>");
+
+        if (!$interactions->confirm("Do you want to continue?")) {
+            $output->writeln("<info>Stopping restore operation.</info>");
+            return Command::SUCCESS;
+        }
 
-        // TODO - Validate provided backup zip contents
-        //  - Display and prompt to user
+        $output->writeln("<info>Extracting ZIP into temporary directory...</info>");
+        $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 (file)
index 0000000..ae82f65
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace Cli\Services;
+
+use ZipArchive;
+
+class BackupZip
+{
+    protected ZipArchive $zip;
+    public function __construct(
+        protected string $filePath
+    ) {
+        $this->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 (file)
index 0000000..0cc4186
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Cli\Services;
+
+use Illuminate\Console\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+class InteractiveConsole
+{
+
+
+    public function __construct(
+        protected QuestionHelper $helper,
+        protected InputInterface $input,
+        protected OutputInterface $output,
+    )
+    {
+    }
+
+    public function confirm(string $text): bool
+    {
+        $question = new ConfirmationQuestion($text, false);
+        return $this->helper->ask($this->input, $this->output, $question);
+    }
+}
\ No newline at end of file