]> BookStack Code Mirror - system-cli/blobdiff - scripts/Commands/RestoreCommand.php
Got restore command to a working state
[system-cli] / scripts / Commands / RestoreCommand.php
index c181ff84eb40c259812e9751a312d282a044e6e8..a7c8ac5ff618097d40dfb399b8bb3f48eb1d46ed 100644 (file)
@@ -7,7 +7,11 @@ use Cli\Services\ArtisanRunner;
 use Cli\Services\BackupZip;
 use Cli\Services\EnvironmentLoader;
 use Cli\Services\InteractiveConsole;
+use Cli\Services\MySqlRunner;
+use Cli\Services\ProgramRunner;
 use Cli\Services\RequirementsValidator;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
@@ -40,6 +44,7 @@ class RestoreCommand extends Command
         $appDir = AppLocator::require($input->getOption('app-directory'));
         $output->writeln("<info>Checking system requirements...</info>");
         RequirementsValidator::validate();
+        (new ProgramRunner('mysql', '/usr/bin/mysql'))->ensureFound();
 
         $zipPath = realpath($input->getArgument('backup-zip'));
         $zip = new BackupZip($zipPath);
@@ -60,7 +65,6 @@ class RestoreCommand extends Command
 
         $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>");
@@ -74,39 +78,127 @@ class RestoreCommand extends Command
         }
         $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
+        $envChanges = [];
+        if ($contents['env']['exists']) {
+            $output->writeln("<info>Restoring and merging .env file...</info>");
+            $envChanges = $this->restoreEnv($extractDir, $appDir, $output, $interactions);
+        }
 
-        // TODO - Restore database from backup
+        $folderLocations = ['themes', 'public/uploads', 'storage/uploads'];
+        foreach ($folderLocations as $folderSubPath) {
+            if ($contents[$folderSubPath]['exists']) {
+                $output->writeln("<info>Restoring {$folderSubPath} folder...</info>");
+                $this->restoreFolder($folderSubPath, $appDir, $extractDir);
+            }
+        }
 
-        $output->writeln("<info>Running database migrations...</info>");
         $artisan = (new ArtisanRunner($appDir));
-        $artisan->run(['migrate', '--force']);
+        if ($contents['db']['exists']) {
+            $output->writeln("<info>Restoring database from SQL dump...</info>");
+            $this->restoreDatabase($appDir, $extractDir);
+
+            $output->writeln("<info>Running database migrations...</info>");
+            $artisan->run(['migrate', '--force']);
+        }
 
-        // TODO - Update system URL (via BookStack artisan command) if
-        //   there's been a change from old backup env
+        if ($envChanges && $envChanges['old_url'] !== $envChanges['new_url']) {
+            $output->writeln("<info>App URL change made, Updating database with URL change...</info>");
+            $artisan->run([
+                'bookstack:update-url',
+                $envChanges['old_url'], $envChanges['new_url'],
+            ]);
+        }
 
         $output->writeln("<info>Clearing app caches...</info>");
         $artisan->run(['cache:clear']);
         $artisan->run(['config:clear']);
         $artisan->run(['view:clear']);
 
+        $output->writeln("<info>Cleaning up extract directory...</info>");
+        $this->deleteDirectoryAndContents($extractDir);
+
+        $output->writeln("<info>\nRestore operation complete!</info>");
+
         return Command::SUCCESS;
     }
 
-    protected function restoreEnv(string $extractDir, string $appDir, InteractiveConsole $interactions)
+    protected function restoreEnv(string $extractDir, string $appDir, OutputInterface $output, InteractiveConsole $interactions): array
     {
-        $extractEnv = EnvironmentLoader::load($extractDir);
-        $appEnv = EnvironmentLoader::load($appDir); // TODO - Probably pass in since we'll need the APP_URL later on.
+        $oldEnv = EnvironmentLoader::load($extractDir);
+        $currentEnv = EnvironmentLoader::load($appDir);
+        $envContents = file_get_contents($extractDir . DIRECTORY_SEPARATOR . '.env');
+        $appEnvPath = $appDir . DIRECTORY_SEPARATOR . '.env';
+
+        $mysqlCurrent = MySqlRunner::fromEnvOptions($currentEnv);
+        $mysqlOld = MySqlRunner::fromEnvOptions($oldEnv);
+        if (!$mysqlOld->testConnection()) {
+            $currentWorking = $mysqlCurrent->testConnection();
+            if (!$currentWorking) {
+                throw new CommandError("Could not find a working database configuration");
+            }
 
-        // 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.
+            // Copy across new env details to old env
+            $currentEnvContents = file_get_contents($appEnvPath);
+            $currentEnvDbLines = array_values(array_filter(explode("\n", $currentEnvContents), function (string $line) {
+                return str_starts_with($line, 'DB_');
+            }));
+            $oldEnvLines = array_values(array_filter(explode("\n", $currentEnvContents), function (string $line) {
+                return !str_starts_with($line, 'DB_');
+            }));
+            $envContents = implode("\n", [
+                '# Database credentials merged from existing .env file',
+                ...$currentEnvDbLines,
+                ...$oldEnvLines
+            ]);
+            copy($appEnvPath, $appEnvPath . '.backup');
+        }
+
+        $oldUrl = $oldEnv['APP_URL'] ?? '';
+        $newUrl = $currentEnv['APP_URL'] ?? '';
+        $returnData = [
+            'old_url' => $oldUrl,
+            'new_url' => $oldUrl,
+        ];
+
+        if ($oldUrl !== $newUrl) {
+            $output->writeln("Found different APP_URL values:");
+            $changedUrl = $interactions->choice('Which would you like to use?', array_filter([$oldUrl, $newUrl]));
+            $envContents = preg_replace('/^APP_URL=.*?$/', 'APP_URL="' . $changedUrl . '"', $envContents);
+            $returnData['new_url'] = $changedUrl;
+        }
+
+        file_put_contents($appDir . DIRECTORY_SEPARATOR . '.env', $envContents);
+
+        return $returnData;
+    }
+
+    protected function restoreFolder(string $folderSubPath, string $appDir, string $extractDir): void
+    {
+        $fullAppFolderPath = $appDir . DIRECTORY_SEPARATOR . $folderSubPath;
+        $this->deleteDirectoryAndContents($fullAppFolderPath);
+        rename($extractDir . DIRECTORY_SEPARATOR . $folderSubPath, $fullAppFolderPath);
+    }
+
+    protected function deleteDirectoryAndContents(string $dir)
+    {
+        $files = new RecursiveIteratorIterator(
+            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
+            RecursiveIteratorIterator::CHILD_FIRST
+        );
+
+        foreach ($files as $fileinfo) {
+            $path = $fileinfo->getRealPath();
+            $fileinfo->isDir() ? rmdir($path) : unlink($path);
+        }
+
+        rmdir($dir);
+    }
+
+    protected function restoreDatabase(string $appDir, string $extractDir): void
+    {
+        $dbDump = $extractDir . DIRECTORY_SEPARATOR . 'db.sql';
+        $currentEnv = EnvironmentLoader::load($appDir);
+        $mysql = MySqlRunner::fromEnvOptions($currentEnv);
+        $mysql->importSqlFile($dbDump);
     }
 }