diff --git a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php index a14ddca02..7a43f4c20 100644 --- a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php +++ b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php @@ -71,6 +71,8 @@ class SafeUpgradeService 'cache', 'user', ]; + /** @var callable|null */ + private $progressCallback = null; /** * @param array $options @@ -170,6 +172,7 @@ class SafeUpgradeService // Copy extracted package into staging area. Folder::rcopy($extractedPath, $packagePath, true); + $this->reportProgress('installing', 'Preparing staged package...', null); $this->carryOverRootDotfiles($packagePath); @@ -182,6 +185,7 @@ class SafeUpgradeService throw new RuntimeException('Staged package does not contain any files to promote.'); } + $this->reportProgress('snapshot', 'Creating backup snapshot...', null); $this->createBackupSnapshot($entries, $backupPath); $this->syncGitDirectory($this->rootPath, $backupPath); @@ -190,6 +194,8 @@ class SafeUpgradeService Folder::create(dirname($manifestPath)); file_put_contents($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT)); + $this->reportProgress('installing', 'Copying update files...', null); + try { $this->copyEntries($entries, $packagePath, $this->rootPath); } catch (Throwable $e) { @@ -198,6 +204,7 @@ class SafeUpgradeService throw new RuntimeException('Failed to promote staged Grav release.', 0, $e); } + $this->reportProgress('finalizing', 'Finalizing upgrade...', null); $this->syncGitDirectory($backupPath, $this->rootPath); $this->persistManifest($manifest); $this->pruneOldSnapshots(); @@ -274,6 +281,20 @@ class SafeUpgradeService } } + public function setProgressCallback(?callable $callback): self + { + $this->progressCallback = $callback; + + return $this; + } + + private function reportProgress(string $stage, string $message, ?int $percent = null, array $extra = []): void + { + if ($this->progressCallback) { + ($this->progressCallback)($stage, $message, $percent, $extra); + } + } + /** * Roll back to the most recent snapshot. * @@ -300,6 +321,7 @@ class SafeUpgradeService throw new RuntimeException('Rollback snapshot entries are missing from the manifest.'); } + $this->reportProgress('rollback', 'Restoring snapshot...', null); $this->copyEntries($entries, $backupPath, $this->rootPath); $this->syncGitDirectory($backupPath, $this->rootPath); $this->markRollback($manifest['id']); diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php index e98fb15da..f34c1870e 100644 --- a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -44,6 +44,8 @@ class SelfupgradeCommand extends GpmCommand private $tmp; /** @var Upgrader */ private $upgrader; + /** @var string|null */ + private $lastProgressMessage = null; /** @var string */ protected $all_yes; @@ -438,6 +440,7 @@ class SelfupgradeCommand extends GpmCommand private function upgrade(): bool { $io = $this->getIO(); + $this->lastProgressMessage = null; $this->upgradeGrav($this->file); @@ -496,14 +499,24 @@ class SelfupgradeCommand extends GpmCommand */ private function upgradeGrav(string $zip): void { + $io = $this->getIO(); + try { + $io->write("\x0D |- Extracting update... "); $folder = Installer::unZip($zip, $this->tmp . '/zip'); if ($folder === false) { throw new RuntimeException(Installer::lastErrorMsg()); } + $io->write("\x0D"); + $io->writeln(' |- Extracting update... ok '); $script = $folder . '/system/install.php'; if ((file_exists($script) && $install = include $script) && is_callable($install)) { + if (is_object($install) && method_exists($install, 'setProgressCallback')) { + $install->setProgressCallback(function (string $stage, string $message, ?int $percent = null, array $extra = []) { + $this->handleServiceProgress($stage, $message, $percent); + }); + } $install($zip); } else { throw new RuntimeException('Uploaded archive file is not a valid Grav update package'); @@ -513,6 +526,17 @@ class SelfupgradeCommand extends GpmCommand } } + private function handleServiceProgress(string $stage, string $message, ?int $percent = null, array $extra = []): void + { + if ($this->lastProgressMessage === $message) { + return; + } + + $this->lastProgressMessage = $message; + $io = $this->getIO(); + $io->writeln(sprintf(' |- %s', $message)); + } + private function ensureExecutablePermissions(): void { $executables = [ diff --git a/system/src/Grav/Installer/Install.php b/system/src/Grav/Installer/Install.php index e9c59b6e9..9de229c02 100644 --- a/system/src/Grav/Installer/Install.php +++ b/system/src/Grav/Installer/Install.php @@ -122,6 +122,8 @@ final class Install /** @var static */ private static $instance; + /** @var callable|null */ + private $progressCallback = null; /** * @return static @@ -187,6 +189,20 @@ ERR; $this->finalize(); } + public function setProgressCallback(?callable $callback): self + { + $this->progressCallback = $callback; + + return $this; + } + + private function relayProgress(string $stage, string $message, ?int $percent = null): void + { + if ($this->progressCallback) { + ($this->progressCallback)($stage, $message, $percent); + } + } + /** * NOTE: This method can only be called after $grav['plugins']->init(). * @@ -273,6 +289,11 @@ ERR; } $service = new SafeUpgradeService($options); + if ($this->progressCallback) { + $service->setProgressCallback(function (string $stage, string $message, ?int $percent = null, array $extra = []) { + $this->relayProgress($stage, $message, $percent); + }); + } $service->promote($this->location, $this->getVersion(), $this->ignores); Installer::setError(Installer::OK); } else { diff --git a/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php b/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php index 368a001ff..4873770c3 100644 --- a/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php +++ b/tests/unit/Grav/Common/Upgrade/SafeUpgradeServiceTest.php @@ -91,10 +91,9 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test public function testPromoteAndRollback(): void { - [$root, $staging, $manifestStore] = $this->prepareLiveEnvironment(); + [$root, $manifestStore] = $this->prepareLiveEnvironment(); $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $staging, 'manifest_store' => $manifestStore, ]); @@ -102,7 +101,7 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test $manifest = $service->promote($package, '1.8.0', ['backup', 'cache', 'images', 'logs', 'tmp', 'user']); self::assertFileExists($root . '/system/new.txt'); - self::assertFileDoesNotExist($root . '/ORIGINAL'); + self::assertFileExists($root . '/ORIGINAL'); $manifestFile = $manifestStore . '/' . $manifest['id'] . '.json'; self::assertFileExists($manifestFile); @@ -112,17 +111,14 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test self::assertFileExists($root . '/ORIGINAL'); self::assertFileDoesNotExist($root . '/system/new.txt'); - $snapshots = glob($staging . '/snapshot-*'); - self::assertNotEmpty($snapshots); - self::assertEmpty(glob($staging . '/stage-*')); + self::assertDirectoryExists($manifest['backup_path']); } public function testPrunesOldSnapshots(): void { - [$root, $staging, $manifestStore] = $this->prepareLiveEnvironment(); + [$root, $manifestStore] = $this->prepareLiveEnvironment(); $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $staging, 'manifest_store' => $manifestStore, ]); @@ -148,7 +144,6 @@ class SafeUpgradeServiceTest extends \Codeception\TestCase\Test $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $this->tmpDir . '/staging', ]); $method = new ReflectionMethod(SafeUpgradeService::class, 'detectPsrLogConflicts'); @@ -177,7 +172,6 @@ PHP; $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $this->tmpDir . '/staging', ]); $method = new ReflectionMethod(SafeUpgradeService::class, 'detectMonologConflicts'); @@ -198,7 +192,6 @@ PHP; $service = new SafeUpgradeService([ 'root' => $root, - 'staging_root' => $this->tmpDir . '/staging', ]); $service->clearRecoveryFlag(); @@ -206,12 +199,11 @@ PHP; } /** - * @return array{0:string,1:string,2:string} + * @return array{0:string,1:string} */ private function prepareLiveEnvironment(): array { $root = $this->tmpDir . '/root'; - $staging = $this->tmpDir . '/staging'; $manifestStore = $root . '/user/data/upgrades'; Folder::create($root . '/user/plugins/sample'); @@ -221,7 +213,7 @@ PHP; file_put_contents($root . '/user/plugins/sample/blueprints.yaml', "name: Sample Plugin\nversion: 1.0.0\n"); file_put_contents($root . '/user/plugins/sample/composer.json', json_encode(['require' => ['php' => '^8.0']], JSON_PRETTY_PRINT)); - return [$root, $staging, $manifestStore]; + return [$root, $manifestStore]; } /**