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
68 $servicesFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'bootstrap', 'cache', 'services.php']);
69 if (file_exists($servicesFile)) {
70 return @unlink($servicesFile);
76 protected function extractZip(string $zipPath, string $appDir): void
78 $zip = new ZipArchive();
79 $opened = $zip->open($zipPath, ZipArchive::RDONLY);
80 $extracted = $zip->extractTo($appDir);
81 $closed = $zip->close();
84 if (!$opened || !$extracted || !$closed) {
85 throw new CommandError("Failed to extract ZIP files into {$appDir}");
89 protected function deleteAppVendorFiles(string $appDir): void
91 $targetDir = $appDir . DIRECTORY_SEPARATOR . 'vendor';
92 if (!is_dir($targetDir)) {
96 $it = new RecursiveDirectoryIterator($targetDir, RecursiveDirectoryIterator::SKIP_DOTS);
97 $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
98 foreach($files as $file) {
100 rmdir($file->getPathname());
102 unlink($file->getPathname());
106 $deleted = rmdir($targetDir);
108 throw new CommandError("Could not delete existing app vendor directory.");
112 protected function verifyZipChecksum(string $zipPath, string $targetChecksum): void
114 $zipChecksum = hash_file('sha256', $zipPath);
115 if ($zipChecksum !== $targetChecksum) {
117 throw new CommandError("Checksum of downloaded ZIP does not match the expected checksum.");
121 protected function downloadVendorZip(string $version): string
123 $tempFile = tempnam(sys_get_temp_dir(), 'bs-cli-vendor-zip');
124 $targetUrl = "https://files.bookstackapp.com/vendor/{$version}.zip";
126 $targetFile = @fopen($targetUrl, 'rb');
127 if ($targetFile === false) {
128 throw new CommandError("Failed to download ZIP file from $targetUrl");
131 file_put_contents($tempFile, $targetFile);
137 * @throws CommandError
139 protected function getTargetChecksum(string $appDir): string
141 $checksumFile = implode(DIRECTORY_SEPARATOR, [$appDir, 'dev', 'checksums', 'vendor']);
143 if (file_exists($checksumFile)) {
144 $checksum = trim(file_get_contents($checksumFile));
147 if (empty($checksum)) {
148 throw new CommandError("Could not find a vendor checksum for validation.");