Progressed restore command a little

This commit is contained in:
Dan Brown 2023-03-07 12:05:04 +00:00
parent 2894ce275c
commit 74b5fadf60
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
3 changed files with 135 additions and 5 deletions

View file

@ -4,6 +4,9 @@ namespace Cli\Commands;
use Cli\Services\AppLocator; use Cli\Services\AppLocator;
use Cli\Services\ArtisanRunner; use Cli\Services\ArtisanRunner;
use Cli\Services\BackupZip;
use Cli\Services\EnvironmentLoader;
use Cli\Services\InteractiveConsole;
use Cli\Services\RequirementsValidator; use Cli\Services\RequirementsValidator;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -26,16 +29,52 @@ class RestoreCommand extends Command
*/ */
protected function execute(InputInterface $input, OutputInterface $output): int 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')); $appDir = AppLocator::require($input->getOption('app-directory'));
$output->writeln("<info>Checking system requirements...</info>"); $output->writeln("<info>Checking system requirements...</info>");
RequirementsValidator::validate(); RequirementsValidator::validate();
// TODO - Warn that potentially dangerous, $zipPath = realpath($input->getArgument('backup-zip'));
// warn for same/forward versions only, $zip = new BackupZip($zipPath);
// warn this won't handle server-level stuff $contents = $zip->getContentsOverview();
// TODO - Validate provided backup zip contents $output->writeln("\n<info>Contents found in the backup ZIP:</info>");
// - Display and prompt to user $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;
}
$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 // TODO - Environment handling
// - Restore of old .env // - Restore of old .env
@ -60,4 +99,14 @@ class RestoreCommand extends Command
return Command::SUCCESS; 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.
}
} }

View file

@ -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}].");
}
}
}

View file

@ -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);
}
}