1 <?php declare(strict_types=1);
3 namespace Cli\Commands;
5 use Cli\Services\AppLocator;
7 use RecursiveDirectoryIterator;
8 use RecursiveIteratorIterator;
9 use Symfony\Component\Console\Command\Command;
10 use Symfony\Component\Console\Input\InputInterface;
11 use Symfony\Component\Console\Input\InputOption;
12 use Symfony\Component\Console\Output\OutputInterface;
15 class DownloadVendorCommand extends Command
17 protected function configure(): void
19 $this->setName('download-vendor');
20 $this->setDescription('Download and extract PHP vendor files in a BookStack instance.');
21 $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to download into', '');
27 protected function execute(InputInterface $input, OutputInterface $output): int
29 $appDir = AppLocator::require($input->getOption('app-directory'));
31 $output->writeln("<info>Checking app version...</info>");
32 $version = AppLocator::getVersion($appDir);
33 if (empty($version)) {
34 throw new CommandError("Could not determine instance BookStack version.");
36 $targetChecksum = $this->getTargetChecksum($appDir);
38 $output->writeln("<info>Downloading ZIP from files.bookstackapp.com...</info>");
39 $zip = $this->downloadVendorZip($version);
41 $output->writeln("<info>Validating downloaded ZIP...</info>");
42 $this->verifyZipChecksum($zip, $targetChecksum);
44 $output->writeln("<info>Deleting existing vendor/ directory...</info>");
46 $this->deleteAppVendorFiles($appDir);
47 } catch (Exception $exception) {
52 $output->writeln("<info>Extracting ZIP into BookStack instance...</info>");
53 $this->extractZip($zip, $appDir);
55 $output->writeln("<info>Cleaning up old app services...</info>");
56 $cleaned = $this->cleanupAppServices($appDir);
58 $output->writeln("<warning>Failed to remove exising app services file</warning>");
61 $output->writeln("<success>Successfully downloaded & extracted vendor files into BookStack instance!</success>");
63 return Command::SUCCESS;
66 protected function cleanupAppServices(string $appDir): bool
69 implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'services.php']),
70 implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'packages.php']),
75 foreach ($filesToClear as $file) {
76 if (file_exists($file)) {
77 if (@unlink($file) === false) {
86 protected function extractZip(string $zipPath, string $appDir): void
88 $zip = new ZipArchive();
89 $opened = $zip->open($zipPath, ZipArchive::RDONLY);
90 $extracted = $zip->extractTo($appDir);
91 $closed = $zip->close();
94 if (!$opened || !$extracted || !$closed) {
95 throw new CommandError("Failed to extract ZIP files into {$appDir}");
99 protected function deleteAppVendorFiles(string $appDir): void
101 $targetDir = $appDir . DIRECTORY_SEPARATOR . 'vendor';
102 if (!is_dir($targetDir)) {
106 $it = new RecursiveDirectoryIterator($targetDir, RecursiveDirectoryIterator::SKIP_DOTS);
107 $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
108 foreach($files as $file) {
110 rmdir($file->getPathname());
112 unlink($file->getPathname());
116 $deleted = rmdir($targetDir);
118 throw new CommandError("Could not delete existing app vendor directory.");
122 protected function verifyZipChecksum(string $zipPath, string $targetChecksum): void
124 $zipChecksum = hash_file('sha256', $zipPath);
125 if ($zipChecksum !== $targetChecksum) {
127 throw new CommandError("Checksum of downloaded ZIP does not match the expected checksum.");
131 protected function downloadVendorZip(string $version): string
133 $tempFile = tempnam(sys_get_temp_dir(), 'bs-cli-vendor-zip');
134 $targetUrl = "https://files.bookstackapp.com/vendor/{$version}.zip";
136 $targetFile = @fopen($targetUrl, 'rb');
137 if ($targetFile === false) {
138 throw new CommandError("Failed to download ZIP file from $targetUrl");
141 file_put_contents($tempFile, $targetFile);
147 * @throws CommandError
149 protected function getTargetChecksum(string $appDir): string
151 $checksumFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'dev', 'checksums', 'vendor']);
153 if (file_exists($checksumFile)) {
154 $checksum = trim(file_get_contents($checksumFile));
157 if (empty($checksum)) {
158 throw new CommandError("Could not find a vendor checksum for validation.");