]> BookStack Code Mirror - system-cli/commitdiff
Added DownloadVendorCommand
authorDan Brown <redacted>
Mon, 10 Mar 2025 21:23:22 +0000 (21:23 +0000)
committerDan Brown <redacted>
Mon, 10 Mar 2025 21:23:22 +0000 (21:23 +0000)
run.php
src/Commands/DownloadVendorCommand.php [new file with mode: 0644]
src/Services/AppLocator.php
src/Services/RequirementsValidator.php
src/app.php

diff --git a/run.php b/run.php
index 6fb523c203c1d751a70b508ff554be54db3d76b6..66b47211952ba72e2ab9a10adccc002bbaf5b7d9 100644 (file)
--- a/run.php
+++ b/run.php
@@ -8,6 +8,7 @@ if (php_sapi_name() !== 'cli') {
 
 require __DIR__ . '/vendor/autoload.php';
 
+use Cli\Commands\CommandError;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Console\Output\ConsoleOutput;
 
@@ -24,11 +25,16 @@ $formatter->setStyle('error', new OutputFormatterStyle('red'));
 
 // Run the command and handle errors
 try {
-    $output->writeln("<warn>WARNING: This CLI is in early alpha testing.</warn>");
-    $output->writeln("<warn>There's a high chance of running into bugs, and the CLI API is subject to change.</warn>");
+    $output->writeln("<warn>WARNING: This CLI is in alpha testing.</warn>");
+    $output->writeln("<warn>There's a high chance of issues, and the CLI API is subject to change.</warn>");
     $output->writeln("");
 
     $app->run(null, $output);
+} catch (CommandError $error) {
+    $output = (new ConsoleOutput())->getErrorOutput();
+    $output->getFormatter()->setStyle('error', new OutputFormatterStyle('red'));
+    $output->writeln('<error>' . $error->getMessage() . '</error>');
+    exit(1);
 } catch (Exception $error) {
     $output = (new ConsoleOutput())->getErrorOutput();
     $output->getFormatter()->setStyle('error', new OutputFormatterStyle('red'));
diff --git a/src/Commands/DownloadVendorCommand.php b/src/Commands/DownloadVendorCommand.php
new file mode 100644 (file)
index 0000000..2f8e927
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+namespace Cli\Commands;
+
+use Cli\Services\AppLocator;
+use Exception;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use ZipArchive;
+
+class DownloadVendorCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setName('download-vendor');
+        $this->setDescription('Download and extract PHP vendor files in a BookStack instance.');
+        $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to download into', '');
+    }
+
+    /**
+     * @throws Exception
+     */
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $appDir = AppLocator::require($input->getOption('app-directory'));
+
+        $output->writeln("<info>Checking app version...</info>");
+        $version = AppLocator::getVersion($appDir);
+        if (empty($version)) {
+            throw new CommandError("Could not determine instance BookStack version.");
+        }
+        $targetChecksum = $this->getTargetChecksum($appDir);
+
+        $output->writeln("<info>Downloading ZIP from files.bookstackapp.com...</info>");
+        $zip = $this->downloadVendorZip($version);
+
+        $output->writeln("<info>Validating downloaded ZIP...</info>");
+        $this->verifyZipChecksum($zip, $targetChecksum);
+
+        $output->writeln("<info>Deleting existing vendor/ directory...</info>");
+        try {
+            $this->deleteAppVendorFiles($appDir);
+        } catch (Exception $exception) {
+            unlink($zip);
+            throw $exception;
+        }
+
+        $output->writeln("<info>Extracting ZIP into BookStack instance...</info>");
+        $this->extractZip($zip, $appDir);
+
+        $output->writeln("<info>Cleaning up old app services...</info>");
+        $cleaned = $this->cleanupAppServices($appDir);
+        if (!$cleaned) {
+            $output->writeln("<warning>Failed to remove exising app services file</warning>");
+        }
+
+        $output->writeln("<success>Successfully downloaded & extracted vendor files into BookStack instance!</success>");
+
+        return Command::SUCCESS;
+    }
+
+    protected function cleanupAppServices(string $appDir): bool
+    {
+        $servicesFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'services.php']);
+        if (file_exists($servicesFile)) {
+            return @unlink($servicesFile);
+        }
+
+        return true;
+    }
+
+    protected function extractZip(string $zipPath, string $appDir): void
+    {
+        $zip = new ZipArchive();
+        $opened = $zip->open($zipPath, ZipArchive::RDONLY);
+        $extracted = $zip->extractTo($appDir);
+        $closed = $zip->close();
+
+        unlink($zipPath);
+        if (!$opened || !$extracted || !$closed) {
+            throw new CommandError("Failed to extract ZIP files into {$appDir}");
+        }
+    }
+
+    protected function deleteAppVendorFiles(string $appDir): void
+    {
+        $targetDir = $appDir . DIRECTORY_SEPARATOR . 'vendor';
+        if (!is_dir($targetDir)) {
+            return;
+        }
+
+        $it = new RecursiveDirectoryIterator($targetDir, RecursiveDirectoryIterator::SKIP_DOTS);
+        $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
+        foreach($files as $file) {
+            if ($file->isDir()){
+                rmdir($file->getPathname());
+            } else {
+                unlink($file->getPathname());
+            }
+        }
+
+        $deleted = rmdir($targetDir);
+        if (!$deleted) {
+            throw new CommandError("Could not delete existing app vendor directory.");
+        }
+    }
+
+    protected function verifyZipChecksum(string $zipPath, string $targetChecksum): void
+    {
+        $zipChecksum = hash_file('sha256', $zipPath);
+        if ($zipChecksum !== $targetChecksum) {
+            unlink($zipPath);
+            throw new CommandError("Checksum of downloaded ZIP does not match the expected checksum.");
+        }
+    }
+
+    protected function downloadVendorZip(string $version): string
+    {
+        $tempFile = tempnam(sys_get_temp_dir(), 'bs-cli-vendor-zip');
+        $targetUrl = "https://files.bookstackapp.com/vendor/{$version}.zip";
+
+        file_put_contents($tempFile, fopen($targetUrl, 'rb'));
+
+        return $tempFile;
+    }
+
+    /**
+     * @throws CommandError
+     */
+    protected function getTargetChecksum(string $appDir): string
+    {
+        $checksumFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'dev', 'checksums', 'vendor']);
+        $checksum = '';
+        if (file_exists($checksumFile)) {
+            $checksum = trim(file_get_contents($checksumFile));
+        }
+
+        if (empty($checksum)) {
+            throw new CommandError("Could not find a vendor checksum for validation.");
+        }
+
+        return $checksum;
+    }
+}
index c97290606fa81c13d3926182c2f414997e202006..109a2a03af5578280a551c09b9bcaa8062ddd229 100644 (file)
@@ -33,6 +33,12 @@ class AppLocator
         return $dir;
     }
 
+    public static function getVersion(string $directory): string
+    {
+        $versionPath = $directory . DIRECTORY_SEPARATOR . 'version';
+        return trim(file_get_contents($versionPath));
+    }
+
     protected static function getCliDirectory(): string
     {
         $scriptDir = dirname(__DIR__);
index 734fe6c226849c322f4272159e940d5a088149dc..c593bf0b07df537d673f680a16b5b809f76da1c0 100644 (file)
@@ -6,7 +6,7 @@ use Exception;
 
 class RequirementsValidator
 {
-    protected static string $phpVersion = '8.0.2';
+    protected static string $phpVersion = '8.2.0';
     protected static array $extensions = [
         'curl',
         'dom',
@@ -21,6 +21,7 @@ class RequirementsValidator
         'simplexml',
         'tokenizer',
         'xml',
+        'zip',
     ];
 
     /**
index 20c2b4cd5fad157b733344e21848567041fa8c5a..a39c59f754a9bcf101c80c1ff875d88aaae964b2 100644 (file)
@@ -3,6 +3,7 @@ declare(strict_types=1);
 
 use Cli\Application;
 use Cli\Commands\BackupCommand;
+use Cli\Commands\DownloadVendorCommand;
 use Cli\Commands\InitCommand;
 use Cli\Commands\RestoreCommand;
 use Cli\Commands\UpdateCommand;
@@ -15,5 +16,6 @@ $app->add(new BackupCommand());
 $app->add(new UpdateCommand());
 $app->add(new InitCommand());
 $app->add(new RestoreCommand());
+$app->add(new DownloadVendorCommand());
 
 return $app;