diff options
Diffstat (limited to 'src/Services/VendorDownloader.php')
| -rw-r--r-- | src/Services/VendorDownloader.php | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/src/Services/VendorDownloader.php b/src/Services/VendorDownloader.php new file mode 100644 index 0000000..389baf2 --- /dev/null +++ b/src/Services/VendorDownloader.php @@ -0,0 +1,151 @@ +<?php declare(strict_types=1); + +namespace Cli\Services; + +use Cli\Commands\CommandError; +use Exception; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Symfony\Component\Console\Output\OutputInterface; +use ZipArchive; + +class VendorDownloader +{ + public function __construct( + protected string $appDir, + protected OutputInterface $output + ) {} + + public function download(): void + { + $this->output->writeln("<info>Checking app version...</info>"); + $version = AppLocator::getVersion($this->appDir); + if (empty($version)) { + throw new CommandError("Could not determine instance BookStack version."); + } + $targetChecksum = $this->getTargetChecksum(); + + $this->output->writeln("<info>Downloading ZIP from files.bookstackapp.com...</info>"); + $zip = $this->downloadVendorZip($version); + + $this->output->writeln("<info>Validating downloaded ZIP...</info>"); + $this->verifyZipChecksum($zip, $targetChecksum); + + $this->output->writeln("<info>Deleting existing vendor/ directory...</info>"); + try { + $this->deleteAppVendorFiles(); + } catch (Exception $exception) { + unlink($zip); + throw $exception; + } + + $this->output->writeln("<info>Extracting ZIP into BookStack instance...</info>"); + $this->extractZip($zip); + + $this->output->writeln("<info>Cleaning up old app services...</info>"); + $cleaned = $this->cleanupAppServices(); + if (!$cleaned) { + $this->output->writeln("<warning>Failed to remove exising app services file</warning>"); + } + + $this->output->writeln("<success>Successfully downloaded & extracted vendor files into BookStack instance!</success>"); + } + + protected function cleanupAppServices(): bool + { + $filesToClear = [ + implode(DIRECTORY_SEPARATOR, [$this->appDir, 'bootstrap', 'cache', 'services.php']), + implode(DIRECTORY_SEPARATOR, [$this->appDir, 'bootstrap', 'cache', 'packages.php']), + ]; + + $status = true; + + foreach ($filesToClear as $file) { + if (file_exists($file)) { + if (@unlink($file) === false) { + $status = false; + } + } + } + + return $status; + } + + protected function extractZip(string $zipPath): void + { + $zip = new ZipArchive(); + $opened = $zip->open($zipPath, ZipArchive::RDONLY); + $extracted = $zip->extractTo($this->appDir); + $closed = $zip->close(); + + unlink($zipPath); + if (!$opened || !$extracted || !$closed) { + throw new CommandError("Failed to extract ZIP files into {$this->appDir}"); + } + } + + protected function deleteAppVendorFiles(): void + { + $targetDir = $this->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"; + + $targetFile = @fopen($targetUrl, 'rb'); + if ($targetFile === false) { + throw new CommandError("Failed to download ZIP file from $targetUrl"); + } + + file_put_contents($tempFile, $targetFile); + + return $tempFile; + } + + /** + * @throws CommandError + */ + protected function getTargetChecksum(): string + { + $checksumFile = implode(DIRECTORY_SEPARATOR, [$this->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; + } +} |
