summaryrefslogtreecommitdiff
path: root/src/Services/VendorDownloader.php
diff options
context:
space:
mode:
Diffstat (limited to 'src/Services/VendorDownloader.php')
-rw-r--r--src/Services/VendorDownloader.php151
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;
+ }
+}