From: Dan Brown Date: Mon, 10 Mar 2025 21:23:22 +0000 (+0000) Subject: Added DownloadVendorCommand X-Git-Url: http://source.bookstackapp.com/system-cli/commitdiff_plain/6e6b8f0fdbc153b672dbbf6621bc56b310c4e3ea Added DownloadVendorCommand --- diff --git a/run.php b/run.php index 6fb523c..66b4721 100644 --- 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("WARNING: This CLI is in early alpha testing."); - $output->writeln("There's a high chance of running into bugs, and the CLI API is subject to change."); + $output->writeln("WARNING: This CLI is in alpha testing."); + $output->writeln("There's a high chance of issues, and the CLI API is subject to change."); $output->writeln(""); $app->run(null, $output); +} catch (CommandError $error) { + $output = (new ConsoleOutput())->getErrorOutput(); + $output->getFormatter()->setStyle('error', new OutputFormatterStyle('red')); + $output->writeln('' . $error->getMessage() . ''); + 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 index 0000000..2f8e927 --- /dev/null +++ b/src/Commands/DownloadVendorCommand.php @@ -0,0 +1,148 @@ +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("Checking app version..."); + $version = AppLocator::getVersion($appDir); + if (empty($version)) { + throw new CommandError("Could not determine instance BookStack version."); + } + $targetChecksum = $this->getTargetChecksum($appDir); + + $output->writeln("Downloading ZIP from files.bookstackapp.com..."); + $zip = $this->downloadVendorZip($version); + + $output->writeln("Validating downloaded ZIP..."); + $this->verifyZipChecksum($zip, $targetChecksum); + + $output->writeln("Deleting existing vendor/ directory..."); + try { + $this->deleteAppVendorFiles($appDir); + } catch (Exception $exception) { + unlink($zip); + throw $exception; + } + + $output->writeln("Extracting ZIP into BookStack instance..."); + $this->extractZip($zip, $appDir); + + $output->writeln("Cleaning up old app services..."); + $cleaned = $this->cleanupAppServices($appDir); + if (!$cleaned) { + $output->writeln("Failed to remove exising app services file"); + } + + $output->writeln("Successfully downloaded & extracted vendor files into BookStack instance!"); + + 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; + } +} diff --git a/src/Services/AppLocator.php b/src/Services/AppLocator.php index c972906..109a2a0 100644 --- a/src/Services/AppLocator.php +++ b/src/Services/AppLocator.php @@ -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__); diff --git a/src/Services/RequirementsValidator.php b/src/Services/RequirementsValidator.php index 734fe6c..c593bf0 100644 --- a/src/Services/RequirementsValidator.php +++ b/src/Services/RequirementsValidator.php @@ -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', ]; /** diff --git a/src/app.php b/src/app.php index 20c2b4c..a39c59f 100644 --- a/src/app.php +++ b/src/app.php @@ -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;