From: Dan Brown Date: Mon, 6 Mar 2023 14:55:41 +0000 (+0000) Subject: Added update command X-Git-Url: http://source.bookstackapp.com/system-cli/commitdiff_plain/a69f2bc809fa3ea207cc138e0443dfb310b2a763 Added update command Extracted some common parts to their own service files --- diff --git a/scripts/Commands/InitCommand.php b/scripts/Commands/InitCommand.php index 6021d48..720dcd1 100644 --- a/scripts/Commands/InitCommand.php +++ b/scripts/Commands/InitCommand.php @@ -2,8 +2,10 @@ namespace Cli\Commands; +use Cli\Services\ComposerLocator; use Cli\Services\EnvironmentLoader; use Cli\Services\ProgramRunner; +use Cli\Services\RequirementsValidator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -24,7 +26,7 @@ class InitCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln("Checking system requirements..."); - $this->ensureRequirementsMet(); + RequirementsValidator::validate(); $suggestedOutPath = $input->getArgument('target-directory'); @@ -36,12 +38,11 @@ class InitCommand extends Command $this->cloneBookStackViaGit($installDir); $output->writeln("Checking composer exists..."); - $composer = $this->getComposerProgram($installDir); - try { - $composer->ensureFound(); - } catch (\Exception $exception) { + $composerLocator = new ComposerLocator($installDir); + $composer = $composerLocator->getProgram(); + if (!$composer->isFound()) { $output->writeln("Composer does not exist, downloading a local copy..."); - $this->downloadComposerToInstall($installDir); + $composerLocator->download(); } $output->writeln("Installing application dependencies using composer..."); @@ -65,74 +66,6 @@ class InitCommand extends Command return Command::SUCCESS; } - /** - * Ensure the required PHP extensions are installed for this command. - * @throws CommandError - */ - protected function ensureRequirementsMet(): void - { - $errors = []; - - if (version_compare(PHP_VERSION, '8.0.2') < 0) { - $errors[] = "PHP >= 8.0.2 is required to install BookStack."; - } - - $requiredExtensions = ['bcmath', 'curl', 'gd', 'iconv', 'libxml', 'mbstring', 'mysqlnd', 'xml']; - foreach ($requiredExtensions as $extension) { - if (!extension_loaded($extension)) { - $errors[] = "The \"{$extension}\" PHP extension is required by not active."; - } - } - - try { - (new ProgramRunner('git', '/usr/bin/git'))->ensureFound(); - (new ProgramRunner('php', '/usr/bin/php'))->ensureFound(); - } catch (\Exception $exception) { - $errors[] = $exception->getMessage(); - } - - if (count($errors) > 0) { - throw new CommandError("Requirements failed with following errors:\n" . implode("\n", $errors)); - } - } - - protected function downloadComposerToInstall(string $installDir): void - { - $setupPath = $installDir . DIRECTORY_SEPARATOR . 'composer-setup.php'; - $signature = file_get_contents('https://composer.github.io/installer.sig'); - copy('https://getcomposer.org/installer', $setupPath); - $checksum = hash_file('sha384', $setupPath); - - if ($signature !== $checksum) { - unlink($setupPath); - throw new CommandError("Could not install composer, checksum validation failed."); - } - - $status = (new ProgramRunner('php', '/usr/bin/php')) - ->runWithoutOutputCallbacks([ - $setupPath, '--quiet', - "--install-dir={$installDir}", - "--filename=composer", - ]); - - unlink($setupPath); - - if ($status !== 0) { - throw new CommandError("Could not install composer, composer-setup script run failed."); - } - } - - /** - * Get the composer program. - */ - protected function getComposerProgram(string $installDir): ProgramRunner - { - return (new ProgramRunner('composer', '/usr/local/bin/composer')) - ->withTimeout(300) - ->withIdleTimeout(15) - ->withAdditionalPathLocation($installDir); - } - protected function generateAppKey(string $installDir): void { $errors = (new ProgramRunner('php', '/usr/bin/php')) diff --git a/scripts/Commands/UpdateCommand.php b/scripts/Commands/UpdateCommand.php new file mode 100644 index 0000000..573b04c --- /dev/null +++ b/scripts/Commands/UpdateCommand.php @@ -0,0 +1,112 @@ +setName('update'); + $this->setDescription('Update an existing BookStack instance.'); + } + + /** + * @throws CommandError + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln("Checking system requirements..."); + RequirementsValidator::validate(); + + $output->writeln("Checking composer exists..."); + $composerLocator = new ComposerLocator($this->appDir); + $composer = $composerLocator->getProgram(); + if (!$composer->isFound()) { + $output->writeln("Composer does not exist, downloading a local copy..."); + $composerLocator->download(); + } + + $output->writeln("Fetching latest code via Git..."); + $this->updateCodeUsingGit(); + + $output->writeln("Installing PHP dependencies via composer..."); + $this->installComposerDependencies($composer); + + $output->writeln("Running database migrations..."); + $this->runArtisanCommand(['migrate', '--force']); + + $output->writeln("Clearing app caches..."); + $this->runArtisanCommand(['cache:clear']); + $this->runArtisanCommand(['config:clear']); + $this->runArtisanCommand(['view:clear']); + + return Command::SUCCESS; + } + + /** + * @throws CommandError + */ + protected function updateCodeUsingGit(): void + { + $errors = (new ProgramRunner('git', '/usr/bin/git')) + ->withTimeout(240) + ->withIdleTimeout(15) + ->runCapturingStdErr([ + '-C', $this->appDir, + 'pull', '-q', 'origin', 'release', + ]); + + if ($errors) { + throw new CommandError("Failed git pull with errors:\n" . $errors); + } + } + + /** + * @throws CommandError + */ + protected function installComposerDependencies(ProgramRunner $composer): void + { + $errors = $composer->runCapturingStdErr([ + 'install', + '--no-dev', '-n', '-q', '--no-progress', + '-d', $this->appDir, + ]); + + if ($errors) { + throw new CommandError("Failed composer install with errors:\n" . $errors); + } + } + + protected function runArtisanCommand(array $commandArgs): void + { + $errors = (new ProgramRunner('php', '/usr/bin/php')) + ->withTimeout(60) + ->withIdleTimeout(5) + ->withEnvironment(EnvironmentLoader::load($this->appDir)) + ->runCapturingAllOutput([ + $this->appDir . DIRECTORY_SEPARATOR . 'artisan', + '-n', '-q', + ...$commandArgs + ]); + + if ($errors) { + $cmdString = implode(' ', $commandArgs); + throw new CommandError("Failed 'php artisan {$cmdString}' with errors:\n" . $errors); + } + } +} diff --git a/scripts/Services/ComposerLocator.php b/scripts/Services/ComposerLocator.php new file mode 100644 index 0000000..9b8e596 --- /dev/null +++ b/scripts/Services/ComposerLocator.php @@ -0,0 +1,50 @@ +withTimeout(300) + ->withIdleTimeout(15) + ->withAdditionalPathLocation($this->appDir); + } + + /** + * @throws Exception + */ + public function download(): void + { + $setupPath = $this->appDir . DIRECTORY_SEPARATOR . 'composer-setup.php'; + $signature = file_get_contents('https://composer.github.io/installer.sig'); + copy('https://getcomposer.org/installer', $setupPath); + $checksum = hash_file('sha384', $setupPath); + + if ($signature !== $checksum) { + unlink($setupPath); + throw new Exception("Could not install composer, checksum validation failed."); + } + + $status = (new ProgramRunner('php', '/usr/bin/php')) + ->runWithoutOutputCallbacks([ + $setupPath, '--quiet', + "--install-dir={$this->appDir}", + "--filename=composer", + ]); + + unlink($setupPath); + + if ($status !== 0) { + throw new Exception("Could not install composer, composer-setup script run failed."); + } + } +} diff --git a/scripts/Services/ProgramRunner.php b/scripts/Services/ProgramRunner.php index acc51b9..ee7492a 100644 --- a/scripts/Services/ProgramRunner.php +++ b/scripts/Services/ProgramRunner.php @@ -88,6 +88,16 @@ class ProgramRunner $this->resolveProgramPath(); } + public function isFound(): bool + { + try { + $this->ensureFound(); + return true; + } catch (\Exception $exception) { + return false; + } + } + protected function startProcess(array $args): Process { $programPath = $this->resolveProgramPath(); diff --git a/scripts/Services/RequirementsValidator.php b/scripts/Services/RequirementsValidator.php new file mode 100644 index 0000000..489490f --- /dev/null +++ b/scripts/Services/RequirementsValidator.php @@ -0,0 +1,39 @@ += 8.0.2 is required to install BookStack."; + } + + $requiredExtensions = ['bcmath', 'curl', 'gd', 'iconv', 'libxml', 'mbstring', 'mysqlnd', 'xml']; + foreach ($requiredExtensions as $extension) { + if (!extension_loaded($extension)) { + $errors[] = "The \"{$extension}\" PHP extension is required by not active."; + } + } + + try { + (new ProgramRunner('git', '/usr/bin/git'))->ensureFound(); + (new ProgramRunner('php', '/usr/bin/php'))->ensureFound(); + } catch (Exception $exception) { + $errors[] = $exception->getMessage(); + } + + if (count($errors) > 0) { + throw new Exception("Requirements failed with following errors:\n" . implode("\n", $errors)); + } + } +} \ No newline at end of file diff --git a/scripts/run b/scripts/run index 31bebbf..ca3ea56 100644 --- a/scripts/run +++ b/scripts/run @@ -10,6 +10,7 @@ require __DIR__ . '/vendor/autoload.php'; use Symfony\Component\Console\Application; use Cli\Commands\BackupCommand; use Cli\Commands\InitCommand; +use Cli\Commands\UpdateCommand; // Get the directory of the CLI "entrypoint", adjusted to be the real // location where running via a phar. @@ -17,12 +18,15 @@ $scriptDir = __DIR__; if (str_starts_with($scriptDir, 'phar://')) { $scriptDir = dirname(Phar::running(false)); } +// TODO - Add smarter strategy for locating install +// (working directory or directory of running script or maybe passed option?) $bsDir = dirname($scriptDir); // Setup our CLI $app = new Application('bookstack-system'); $app->add(new BackupCommand($bsDir)); +$app->add(new UpdateCommand($bsDir)); $app->add(new InitCommand()); try {