Added update command

Extracted some common parts to their own service files
This commit is contained in:
Dan Brown 2023-03-06 14:55:41 +00:00
parent 0be1bf7499
commit 3b6de8872a
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 222 additions and 74 deletions

View File

@ -2,8 +2,10 @@
namespace Cli\Commands; namespace Cli\Commands;
use Cli\Services\ComposerLocator;
use Cli\Services\EnvironmentLoader; use Cli\Services\EnvironmentLoader;
use Cli\Services\ProgramRunner; use Cli\Services\ProgramRunner;
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;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -24,7 +26,7 @@ class InitCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$output->writeln("<info>Checking system requirements...</info>"); $output->writeln("<info>Checking system requirements...</info>");
$this->ensureRequirementsMet(); RequirementsValidator::validate();
$suggestedOutPath = $input->getArgument('target-directory'); $suggestedOutPath = $input->getArgument('target-directory');
@ -36,12 +38,11 @@ class InitCommand extends Command
$this->cloneBookStackViaGit($installDir); $this->cloneBookStackViaGit($installDir);
$output->writeln("<info>Checking composer exists...</info>"); $output->writeln("<info>Checking composer exists...</info>");
$composer = $this->getComposerProgram($installDir); $composerLocator = new ComposerLocator($installDir);
try { $composer = $composerLocator->getProgram();
$composer->ensureFound(); if (!$composer->isFound()) {
} catch (\Exception $exception) {
$output->writeln("<info>Composer does not exist, downloading a local copy...</info>"); $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
$this->downloadComposerToInstall($installDir); $composerLocator->download();
} }
$output->writeln("<info>Installing application dependencies using composer...</info>"); $output->writeln("<info>Installing application dependencies using composer...</info>");
@ -65,74 +66,6 @@ class InitCommand extends Command
return Command::SUCCESS; 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 protected function generateAppKey(string $installDir): void
{ {
$errors = (new ProgramRunner('php', '/usr/bin/php')) $errors = (new ProgramRunner('php', '/usr/bin/php'))

View File

@ -0,0 +1,112 @@
<?php
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\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UpdateCommand extends Command
{
public function __construct(
protected string $appDir
) {
parent::__construct();
}
protected function configure(): void
{
$this->setName('update');
$this->setDescription('Update an existing BookStack instance.');
}
/**
* @throws CommandError
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln("<info>Checking system requirements...</info>");
RequirementsValidator::validate();
$output->writeln("<info>Checking composer exists...</info>");
$composerLocator = new ComposerLocator($this->appDir);
$composer = $composerLocator->getProgram();
if (!$composer->isFound()) {
$output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
$composerLocator->download();
}
$output->writeln("<info>Fetching latest code via Git...</info>");
$this->updateCodeUsingGit();
$output->writeln("<info>Installing PHP dependencies via composer...</info>");
$this->installComposerDependencies($composer);
$output->writeln("<info>Running database migrations...</info>");
$this->runArtisanCommand(['migrate', '--force']);
$output->writeln("<info>Clearing app caches...</info>");
$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);
}
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Cli\Services;
use Exception;
class ComposerLocator
{
public function __construct(
protected string $appDir
) {
}
public function getProgram(): ProgramRunner
{
return (new ProgramRunner('composer', '/usr/local/bin/composer'))
->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.");
}
}
}

View File

@ -88,6 +88,16 @@ class ProgramRunner
$this->resolveProgramPath(); $this->resolveProgramPath();
} }
public function isFound(): bool
{
try {
$this->ensureFound();
return true;
} catch (\Exception $exception) {
return false;
}
}
protected function startProcess(array $args): Process protected function startProcess(array $args): Process
{ {
$programPath = $this->resolveProgramPath(); $programPath = $this->resolveProgramPath();

View File

@ -0,0 +1,39 @@
<?php
namespace Cli\Services;
use Exception;
class RequirementsValidator
{
/**
* Ensure the required PHP extensions are installed for this command.
* @throws Exception
*/
public static function validate(): 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 Exception("Requirements failed with following errors:\n" . implode("\n", $errors));
}
}
}

View File

@ -10,6 +10,7 @@ require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Cli\Commands\BackupCommand; use Cli\Commands\BackupCommand;
use Cli\Commands\InitCommand; use Cli\Commands\InitCommand;
use Cli\Commands\UpdateCommand;
// Get the directory of the CLI "entrypoint", adjusted to be the real // Get the directory of the CLI "entrypoint", adjusted to be the real
// location where running via a phar. // location where running via a phar.
@ -17,12 +18,15 @@ $scriptDir = __DIR__;
if (str_starts_with($scriptDir, 'phar://')) { if (str_starts_with($scriptDir, 'phar://')) {
$scriptDir = dirname(Phar::running(false)); $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); $bsDir = dirname($scriptDir);
// Setup our CLI // Setup our CLI
$app = new Application('bookstack-system'); $app = new Application('bookstack-system');
$app->add(new BackupCommand($bsDir)); $app->add(new BackupCommand($bsDir));
$app->add(new UpdateCommand($bsDir));
$app->add(new InitCommand()); $app->add(new InitCommand());
try { try {