]> BookStack Code Mirror - system-cli/commitdiff
Added central way to resolve app path, improved ouput formatting
authorDan Brown <redacted>
Mon, 6 Mar 2023 17:35:23 +0000 (17:35 +0000)
committerDan Brown <redacted>
Thu, 9 Mar 2023 15:28:14 +0000 (15:28 +0000)
scripts/Commands/BackupCommand.php
scripts/Commands/UpdateCommand.php
scripts/Services/AppLocator.php [new file with mode: 0644]
scripts/Services/ProgramRunner.php
scripts/run

index cfef12ed7af31ed66f3ae131ae77130677ecdfbf..727e0200c3ef928e830ed8f9567743497833c022 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Cli\Commands;
 
+use Cli\Services\AppLocator;
 use Cli\Services\EnvironmentLoader;
 use Cli\Services\ProgramRunner;
 use RecursiveDirectoryIterator;
@@ -15,12 +16,6 @@ use ZipArchive;
 
 final class BackupCommand extends Command
 {
-    public function __construct(
-        protected string $appDir
-    ) {
-        parent::__construct();
-    }
-
     protected function configure(): void
     {
         $this->setName('backup');
@@ -36,6 +31,8 @@ final class BackupCommand extends Command
      */
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
+        $appDir = AppLocator::require($input->getOption('app-directory'));
+        $output->writeln("<info>Checking system requirements...</info>");
         $this->ensureRequiredExtensionInstalled();
 
         $handleDatabase = !$input->getOption('no-database');
@@ -43,7 +40,7 @@ final class BackupCommand extends Command
         $handleThemes = !$input->getOption('no-themes');
         $suggestedOutPath = $input->getArgument('backup-path');
 
-        $zipOutFile = $this->buildZipFilePath($suggestedOutPath);
+        $zipOutFile = $this->buildZipFilePath($suggestedOutPath, $appDir);
 
         // Create a new ZIP file
         $zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
@@ -52,24 +49,24 @@ final class BackupCommand extends Command
         $zip->open($zipTempFile, ZipArchive::CREATE);
 
         // Add default files (.env config file and this CLI)
-        $zip->addFile($this->appDir . DIRECTORY_SEPARATOR . '.env', '.env');
-        $zip->addFile($this->appDir . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'run', 'run');
+        $zip->addFile($appDir . DIRECTORY_SEPARATOR . '.env', '.env');
+        $zip->addFile($appDir . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'run', 'run');
 
         if ($handleDatabase) {
             $output->writeln("<info>Dumping the database via mysqldump...</info>");
-            $dumpTempFile = $this->createDatabaseDump();
+            $dumpTempFile = $this->createDatabaseDump($appDir);
             $output->writeln("<info>Adding database dump to backup archive...</info>");
             $zip->addFile($dumpTempFile, 'db.sql');
         }
 
         if ($handleUploads) {
             $output->writeln("<info>Adding BookStack upload folders to backup archive...</info>");
-            $this->addUploadFoldersToZip($zip);
+            $this->addUploadFoldersToZip($zip, $appDir);
         }
 
         if ($handleThemes) {
             $output->writeln("<info>Adding BookStack theme folders to backup archive...</info>");
-            $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$this->appDir, 'themes']), 'themes');
+            $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$appDir, 'themes']), 'themes');
         }
 
         // Close off our zip and move it to the required location
@@ -104,9 +101,9 @@ final class BackupCommand extends Command
      * a path to a folder, or a path to a file in relative or absolute form.
      * @throws CommandError
      */
-    protected function buildZipFilePath(string $suggestedOutPath): string
+    protected function buildZipFilePath(string $suggestedOutPath, string $appDir): string
     {
-        $zipDir = getcwd() ?: $this->appDir;
+        $zipDir = getcwd() ?: $appDir;
         $zipName = "bookstack-backup-" . date('Y-m-d-His') . '.zip';
 
         if ($suggestedOutPath) {
@@ -133,10 +130,10 @@ final class BackupCommand extends Command
      * Add app-relative upload folders to the provided zip archive.
      * Will recursively go through all directories to add all files.
      */
-    protected function addUploadFoldersToZip(ZipArchive $zip): void
+    protected function addUploadFoldersToZip(ZipArchive $zip, string $appDir): void
     {
-        $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$this->appDir, 'public', 'uploads']), 'public/uploads');
-        $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$this->appDir, 'storage', 'uploads']), 'storage/uploads');
+        $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$appDir, 'public', 'uploads']), 'public/uploads');
+        $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$appDir, 'storage', 'uploads']), 'storage/uploads');
     }
 
     /**
@@ -159,9 +156,9 @@ final class BackupCommand extends Command
      * Create a database dump and return the path to the dumped SQL output.
      * @throws CommandError
      */
-    protected function createDatabaseDump(): string
+    protected function createDatabaseDump(string $appDir): string
     {
-        $envOptions = EnvironmentLoader::loadMergedWithCurrentEnv($this->appDir);
+        $envOptions = EnvironmentLoader::loadMergedWithCurrentEnv($appDir);
         $dbOptions = [
             'host' => ($envOptions['DB_HOST'] ?? ''),
             'username' => ($envOptions['DB_USERNAME'] ?? ''),
index 573b04cfe1f61151ee286e410e31306f4b760b0a..e9dfa60301120c961a4c404ea71338454f4a9399 100644 (file)
@@ -2,27 +2,23 @@
 
 namespace Cli\Commands;
 
+use Cli\Services\AppLocator;
 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\Input\InputOption;
 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.');
+        $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to update', '');
     }
 
     /**
@@ -30,11 +26,12 @@ class UpdateCommand extends Command
      */
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
+        $appDir = AppLocator::require($input->getOption('app-directory'));
         $output->writeln("<info>Checking system requirements...</info>");
         RequirementsValidator::validate();
 
         $output->writeln("<info>Checking composer exists...</info>");
-        $composerLocator = new ComposerLocator($this->appDir);
+        $composerLocator = new ComposerLocator($appDir);
         $composer = $composerLocator->getProgram();
         if (!$composer->isFound()) {
             $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
@@ -42,18 +39,18 @@ class UpdateCommand extends Command
         }
 
         $output->writeln("<info>Fetching latest code via Git...</info>");
-        $this->updateCodeUsingGit();
+        $this->updateCodeUsingGit($appDir);
 
         $output->writeln("<info>Installing PHP dependencies via composer...</info>");
-        $this->installComposerDependencies($composer);
+        $this->installComposerDependencies($composer, $appDir);
 
         $output->writeln("<info>Running database migrations...</info>");
-        $this->runArtisanCommand(['migrate', '--force']);
+        $this->runArtisanCommand(['migrate', '--force'], $appDir);
 
         $output->writeln("<info>Clearing app caches...</info>");
-        $this->runArtisanCommand(['cache:clear']);
-        $this->runArtisanCommand(['config:clear']);
-        $this->runArtisanCommand(['view:clear']);
+        $this->runArtisanCommand(['cache:clear'], $appDir);
+        $this->runArtisanCommand(['config:clear'], $appDir);
+        $this->runArtisanCommand(['view:clear'], $appDir);
 
         return Command::SUCCESS;
     }
@@ -61,13 +58,13 @@ class UpdateCommand extends Command
     /**
      * @throws CommandError
      */
-    protected function updateCodeUsingGit(): void
+    protected function updateCodeUsingGit(string $appDir): void
     {
         $errors = (new ProgramRunner('git', '/usr/bin/git'))
             ->withTimeout(240)
             ->withIdleTimeout(15)
             ->runCapturingStdErr([
-                '-C', $this->appDir,
+                '-C', $appDir,
                 'pull', '-q', 'origin', 'release',
             ]);
 
@@ -79,12 +76,12 @@ class UpdateCommand extends Command
     /**
      * @throws CommandError
      */
-    protected function installComposerDependencies(ProgramRunner $composer): void
+    protected function installComposerDependencies(ProgramRunner $composer, string $appDir): void
     {
         $errors = $composer->runCapturingStdErr([
             'install',
             '--no-dev', '-n', '-q', '--no-progress',
-            '-d', $this->appDir,
+            '-d', $appDir,
         ]);
 
         if ($errors) {
@@ -92,14 +89,14 @@ class UpdateCommand extends Command
         }
     }
 
-    protected function runArtisanCommand(array $commandArgs): void
+    protected function runArtisanCommand(array $commandArgs, string $appDir): void
     {
         $errors = (new ProgramRunner('php', '/usr/bin/php'))
             ->withTimeout(60)
             ->withIdleTimeout(5)
-            ->withEnvironment(EnvironmentLoader::load($this->appDir))
+            ->withEnvironment(EnvironmentLoader::load($appDir))
             ->runCapturingAllOutput([
-                $this->appDir . DIRECTORY_SEPARATOR . 'artisan',
+                $appDir . DIRECTORY_SEPARATOR . 'artisan',
                 '-n', '-q',
                 ...$commandArgs
             ]);
diff --git a/scripts/Services/AppLocator.php b/scripts/Services/AppLocator.php
new file mode 100644 (file)
index 0000000..676fc11
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+namespace Cli\Services;
+
+use Phar;
+
+class AppLocator
+{
+    public static function search(string $directory = ''): string
+    {
+        $directoriesToSearch = $directory ? [$directory] : [
+            getcwd(),
+            static::getCliDirectory(),
+        ];
+
+        foreach ($directoriesToSearch as $directory) {
+            if ($directory && static::isProbablyAppDirectory($directory)) {
+                return $directory;
+            }
+        }
+
+        return '';
+    }
+
+    public static function require(string $directory = ''): string
+    {
+        $dir = static::search($directory);
+
+        if (!$dir) {
+            throw new \Exception('Could not find a valid BookStack installation');
+        }
+
+        return $dir;
+    }
+
+    protected static function getCliDirectory(): string
+    {
+        $scriptDir = dirname(__DIR__);
+        if (str_starts_with($scriptDir, 'phar://')) {
+            $scriptDir = dirname(Phar::running(false));
+        }
+
+        return dirname($scriptDir);
+    }
+
+    protected static function isProbablyAppDirectory(string $directory): bool
+    {
+        return file_exists($directory . DIRECTORY_SEPARATOR . 'version')
+            && file_exists($directory . DIRECTORY_SEPARATOR . 'package.json');
+    }
+}
index ee7492a88205ff7304923b545996d471136c54c4..eb3bda7e9abf1453874f7d7c1aa37191d1e4dad8 100644 (file)
@@ -46,7 +46,7 @@ class ProgramRunner
     {
         $output = '';
         $callable = function ($data) use (&$output) {
-            $output .= $data . "\n";
+            $output .= $data;
         };
 
         $this->runWithoutOutputCallbacks($args, $callable, $callable);
@@ -57,7 +57,7 @@ class ProgramRunner
     {
         $err = '';
         $this->runWithoutOutputCallbacks($args, fn() => '', function ($data) use (&$err) {
-            $err .= $data . "\n";
+            $err .= $data;
         });
         return $err;
     }
index ca3ea5644a9d7276d2ce4a496e7aab0df547d4fe..aee6902d2ba24f3b247ce145acf660991a6c3b56 100644 (file)
@@ -11,28 +11,23 @@ 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.
-$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);
+use Symfony\Component\Console\Formatter\OutputFormatterStyle;
+use Symfony\Component\Console\Output\ConsoleOutput;
 
 // Setup our CLI
 $app = new Application('bookstack-system');
+$app->setCatchExceptions(false);
 
-$app->add(new BackupCommand($bsDir));
-$app->add(new UpdateCommand($bsDir));
+$app->add(new BackupCommand());
+$app->add(new UpdateCommand());
 $app->add(new InitCommand());
 
 try {
     $app->run();
 } catch (Exception $error) {
-    fwrite(STDERR, "An error occurred when attempting to run a command:\n");
-    fwrite(STDERR, $error->getMessage() . "\n");
+    $output = (new ConsoleOutput())->getErrorOutput();
+    $output->getFormatter()->setStyle('error', new OutputFormatterStyle('red'));
+    $output->writeln("<error>\nAn error occurred when attempting to run a command:\n</error>");
+    $output->writeln($error->getMessage());
     exit(1);
 }