]> BookStack Code Mirror - system-cli/commitdiff
Added update command
authorDan Brown <redacted>
Mon, 6 Mar 2023 14:55:41 +0000 (14:55 +0000)
committerDan Brown <redacted>
Thu, 9 Mar 2023 15:28:14 +0000 (15:28 +0000)
Extracted some common parts to their own service files

scripts/Commands/InitCommand.php
scripts/Commands/UpdateCommand.php [new file with mode: 0644]
scripts/Services/ComposerLocator.php [new file with mode: 0644]
scripts/Services/ProgramRunner.php
scripts/Services/RequirementsValidator.php [new file with mode: 0644]
scripts/run

index 6021d482d94eccfb726a66b4c58e745f0083128e..720dcd1190f296323546a40e2078b077009ecf7d 100644 (file)
@@ -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("<info>Checking system requirements...</info>");
-        $this->ensureRequirementsMet();
+        RequirementsValidator::validate();
 
         $suggestedOutPath = $input->getArgument('target-directory');
 
@@ -36,12 +38,11 @@ class InitCommand extends Command
         $this->cloneBookStackViaGit($installDir);
 
         $output->writeln("<info>Checking composer exists...</info>");
-        $composer = $this->getComposerProgram($installDir);
-        try {
-            $composer->ensureFound();
-        } catch (\Exception $exception) {
+        $composerLocator = new ComposerLocator($installDir);
+        $composer = $composerLocator->getProgram();
+        if (!$composer->isFound()) {
             $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>");
@@ -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 (file)
index 0000000..573b04c
--- /dev/null
@@ -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);
+        }
+    }
+}
diff --git a/scripts/Services/ComposerLocator.php b/scripts/Services/ComposerLocator.php
new file mode 100644 (file)
index 0000000..9b8e596
--- /dev/null
@@ -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.");
+        }
+    }
+}
index acc51b959b66206102d62e0e021a50640dd438af..ee7492a88205ff7304923b545996d471136c54c4 100644 (file)
@@ -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 (file)
index 0000000..489490f
--- /dev/null
@@ -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));
+        }
+    }
+}
\ No newline at end of file
index 31bebbf13fc312a3931b264e6c471ef29523ee40..ca3ea5644a9d7276d2ce4a496e7aab0df547d4fe 100644 (file)
@@ -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 {