mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Added update command
Extracted some common parts to their own service files
This commit is contained in:
parent
0be1bf7499
commit
3b6de8872a
@ -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'))
|
||||||
|
112
scripts/Commands/UpdateCommand.php
Normal file
112
scripts/Commands/UpdateCommand.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
scripts/Services/ComposerLocator.php
Normal file
50
scripts/Services/ComposerLocator.php
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
39
scripts/Services/RequirementsValidator.php
Normal file
39
scripts/Services/RequirementsValidator.php
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user