Progressed restore command a little
This commit is contained in:
parent
2894ce275c
commit
74b5fadf60
3 changed files with 135 additions and 5 deletions
|
@ -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();
|
||||
|
||||
// TODO - Validate provided backup zip contents
|
||||
// - Display and prompt to user
|
||||
$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;
|
||||
}
|
||||
|
||||
$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.
|
||||
}
|
||||
}
|
||||
|
|
54
scripts/Services/BackupZip.php
Normal file
54
scripts/Services/BackupZip.php
Normal 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}].");
|
||||
}
|
||||
}
|
||||
}
|
27
scripts/Services/InteractiveConsole.php
Normal file
27
scripts/Services/InteractiveConsole.php
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue