mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2025-10-27 08:16:41 +01:00
604 lines
18 KiB
PHP
604 lines
18 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @package Grav\Plugin\Admin
|
||
|
|
*
|
||
|
|
* Handles Safe Upgrade orchestration for the Admin plugin.
|
||
|
|
*
|
||
|
|
* This class mirrors the behaviour offered by the CLI `bin/gpm self-upgrade`
|
||
|
|
* command while exposing a task-oriented API suitable for AJAX interactions.
|
||
|
|
*
|
||
|
|
* IMPORTANT: Keep this implementation aligned with
|
||
|
|
* `Grav\Console\Gpm\SelfupgradeCommand` whenever logic changes there.
|
||
|
|
*/
|
||
|
|
|
||
|
|
namespace Grav\Plugin\Admin;
|
||
|
|
|
||
|
|
use Grav\Common\Filesystem\Folder;
|
||
|
|
use Grav\Common\GPM\Installer;
|
||
|
|
use Grav\Common\GPM\Upgrader;
|
||
|
|
use Grav\Common\Grav;
|
||
|
|
use Grav\Common\HTTP\Response;
|
||
|
|
use Grav\Common\Recovery\RecoveryManager;
|
||
|
|
use Grav\Common\Upgrade\SafeUpgradeService;
|
||
|
|
use Grav\Installer\Install;
|
||
|
|
use RuntimeException;
|
||
|
|
use Throwable;
|
||
|
|
use ZipArchive;
|
||
|
|
use function class_exists;
|
||
|
|
use function dirname;
|
||
|
|
use function file_exists;
|
||
|
|
use function file_get_contents;
|
||
|
|
use function file_put_contents;
|
||
|
|
use function glob;
|
||
|
|
use function is_array;
|
||
|
|
use function is_dir;
|
||
|
|
use function is_file;
|
||
|
|
use function json_decode;
|
||
|
|
use function json_encode;
|
||
|
|
use function max;
|
||
|
|
use function rsort;
|
||
|
|
use function sprintf;
|
||
|
|
use function strftime;
|
||
|
|
use function strtotime;
|
||
|
|
use function time;
|
||
|
|
use function uniqid;
|
||
|
|
use const GRAV_ROOT;
|
||
|
|
use const GRAV_SCHEMA;
|
||
|
|
|
||
|
|
class SafeUpgradeManager
|
||
|
|
{
|
||
|
|
private const PROGRESS_FILENAME = 'safe-upgrade-progress.json';
|
||
|
|
|
||
|
|
/** @var Grav */
|
||
|
|
private $grav;
|
||
|
|
/** @var Upgrader|null */
|
||
|
|
private $upgrader;
|
||
|
|
/** @var SafeUpgradeService|null */
|
||
|
|
private $safeUpgrade;
|
||
|
|
/** @var RecoveryManager */
|
||
|
|
private $recovery;
|
||
|
|
/** @var string */
|
||
|
|
private $progressPath;
|
||
|
|
/** @var string|null */
|
||
|
|
private $tmp;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SafeUpgradeManager constructor.
|
||
|
|
*
|
||
|
|
* @param Grav|null $grav
|
||
|
|
*/
|
||
|
|
public function __construct(?Grav $grav = null)
|
||
|
|
{
|
||
|
|
$this->grav = $grav ?? Grav::instance();
|
||
|
|
$this->recovery = $this->grav['recovery'];
|
||
|
|
|
||
|
|
$locator = $this->grav['locator'];
|
||
|
|
$progressDir = $locator->findResource('user://data/upgrades', true, true);
|
||
|
|
$this->progressPath = $progressDir . '/' . self::PROGRESS_FILENAME;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Execute preflight checks and return upgrade readiness data.
|
||
|
|
*
|
||
|
|
* @param bool $force
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public function preflight(bool $force = false): array
|
||
|
|
{
|
||
|
|
$this->resetProgress();
|
||
|
|
|
||
|
|
if (!class_exists(ZipArchive::class)) {
|
||
|
|
return [
|
||
|
|
'status' => 'error',
|
||
|
|
'message' => 'php-zip extension needs to be enabled.',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$this->upgrader = new Upgrader($force);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return [
|
||
|
|
'status' => 'error',
|
||
|
|
'message' => $e->getMessage(),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$local = $this->upgrader->getLocalVersion();
|
||
|
|
$remote = $this->upgrader->getRemoteVersion();
|
||
|
|
$releaseDate = $this->upgrader->getReleaseDate();
|
||
|
|
$assets = $this->upgrader->getAssets();
|
||
|
|
$package = $assets['grav-update'] ?? null;
|
||
|
|
|
||
|
|
$payload = [
|
||
|
|
'status' => 'ready',
|
||
|
|
'version' => [
|
||
|
|
'local' => $local,
|
||
|
|
'remote' => $remote,
|
||
|
|
'release_date' => $releaseDate ? strftime('%c', strtotime($releaseDate)) : null,
|
||
|
|
'package_size' => $package['size'] ?? null,
|
||
|
|
],
|
||
|
|
'upgrade_available' => $this->upgrader->isUpgradable(),
|
||
|
|
'requirements' => [
|
||
|
|
'meets' => $this->upgrader->meetsRequirements(),
|
||
|
|
'minimum_php' => $this->upgrader->minPHPVersion(),
|
||
|
|
],
|
||
|
|
'symlinked' => false,
|
||
|
|
'safe_upgrade' => [
|
||
|
|
'enabled' => $this->isSafeUpgradeEnabled(),
|
||
|
|
'staging_ready' => true,
|
||
|
|
'error' => null,
|
||
|
|
],
|
||
|
|
'preflight' => [
|
||
|
|
'warnings' => [],
|
||
|
|
'plugins_pending' => [],
|
||
|
|
'psr_log_conflicts' => [],
|
||
|
|
'monolog_conflicts' => [],
|
||
|
|
],
|
||
|
|
];
|
||
|
|
|
||
|
|
Installer::isValidDestination(GRAV_ROOT . '/system');
|
||
|
|
$payload['symlinked'] = Installer::IS_LINK === Installer::lastErrorCode();
|
||
|
|
|
||
|
|
try {
|
||
|
|
$safeUpgrade = $this->getSafeUpgradeService();
|
||
|
|
$payload['preflight'] = $safeUpgrade->preflight();
|
||
|
|
} catch (RuntimeException $e) {
|
||
|
|
$payload['safe_upgrade']['staging_ready'] = false;
|
||
|
|
$payload['safe_upgrade']['error'] = $e->getMessage();
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
$payload['safe_upgrade']['staging_ready'] = false;
|
||
|
|
$payload['safe_upgrade']['error'] = $e->getMessage();
|
||
|
|
}
|
||
|
|
|
||
|
|
return $payload;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Run the safe upgrade lifecycle.
|
||
|
|
*
|
||
|
|
* @param array $options
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public function run(array $options = []): array
|
||
|
|
{
|
||
|
|
$force = (bool)($options['force'] ?? false);
|
||
|
|
$timeout = (int)($options['timeout'] ?? 30);
|
||
|
|
$overwrite = (bool)($options['overwrite'] ?? false);
|
||
|
|
$decisions = is_array($options['decisions'] ?? null) ? $options['decisions'] : [];
|
||
|
|
|
||
|
|
$this->setProgress('initializing', 'Preparing safe upgrade...', null);
|
||
|
|
|
||
|
|
if (!class_exists(ZipArchive::class)) {
|
||
|
|
return $this->errorResult('php-zip extension needs to be enabled.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$this->upgrader = new Upgrader($force);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->errorResult($e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
$safeUpgradeEnabled = $this->isSafeUpgradeEnabled();
|
||
|
|
if (!$safeUpgradeEnabled) {
|
||
|
|
return $this->errorResult('Safe upgrade is disabled in configuration.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$remoteVersion = $this->upgrader->getRemoteVersion();
|
||
|
|
$localVersion = $this->upgrader->getLocalVersion();
|
||
|
|
|
||
|
|
if (!$this->upgrader->meetsRequirements()) {
|
||
|
|
$minPhp = $this->upgrader->minPHPVersion();
|
||
|
|
$message = sprintf(
|
||
|
|
'Grav requires PHP %s, current PHP version is %s.',
|
||
|
|
$minPhp,
|
||
|
|
PHP_VERSION
|
||
|
|
);
|
||
|
|
|
||
|
|
return $this->errorResult($message, [
|
||
|
|
'minimum_php' => $minPhp,
|
||
|
|
'current_php' => PHP_VERSION,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!$overwrite && !$this->upgrader->isUpgradable()) {
|
||
|
|
$result = $this->runFinalizeIfNeeded($localVersion);
|
||
|
|
if ($result) {
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'status' => 'noop',
|
||
|
|
'version' => $localVersion,
|
||
|
|
'message' => 'Grav is already up to date.',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
Installer::isValidDestination(GRAV_ROOT . '/system');
|
||
|
|
if (Installer::IS_LINK === Installer::lastErrorCode()) {
|
||
|
|
return $this->errorResult('Grav installation is symlinked, cannot perform upgrade.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$safeUpgrade = $this->getSafeUpgradeService();
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return $this->errorResult($e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
$preflight = $safeUpgrade->preflight();
|
||
|
|
if (!empty($preflight['plugins_pending'])) {
|
||
|
|
return $this->errorResult('Plugins and/or themes require updates before upgrading Grav.', [
|
||
|
|
'plugins_pending' => $preflight['plugins_pending'],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$conflictError = $this->handleConflictDecisions($preflight, $decisions);
|
||
|
|
if ($conflictError !== null) {
|
||
|
|
return $conflictError;
|
||
|
|
}
|
||
|
|
|
||
|
|
$assets = $this->upgrader->getAssets();
|
||
|
|
$package = $assets['grav-update'] ?? null;
|
||
|
|
if (!$package) {
|
||
|
|
return $this->errorResult('Unable to locate Grav update package information.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->recovery->markUpgradeWindow('core-upgrade', [
|
||
|
|
'scope' => 'core',
|
||
|
|
'target_version' => $remoteVersion,
|
||
|
|
]);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$file = $this->download($package, $timeout);
|
||
|
|
$this->setProgress('installing', 'Installing update...', null);
|
||
|
|
$this->performInstall($file);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
$this->setProgress('error', $e->getMessage(), null);
|
||
|
|
|
||
|
|
return $this->errorResult($e->getMessage());
|
||
|
|
} finally {
|
||
|
|
if ($this->tmp && is_dir($this->tmp)) {
|
||
|
|
Folder::delete($this->tmp);
|
||
|
|
}
|
||
|
|
$this->tmp = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->setProgress('finalizing', 'Finalizing upgrade...', 100);
|
||
|
|
$safeUpgrade->clearRecoveryFlag();
|
||
|
|
$this->recovery->closeUpgradeWindow();
|
||
|
|
|
||
|
|
$manifest = $this->resolveLatestManifest();
|
||
|
|
|
||
|
|
$this->setProgress('complete', 'Upgrade complete.', 100, [
|
||
|
|
'target_version' => $remoteVersion,
|
||
|
|
'manifest' => $manifest,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'status' => 'success',
|
||
|
|
'version' => $remoteVersion,
|
||
|
|
'manifest' => $manifest,
|
||
|
|
'previous_version' => $localVersion,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Retrieve current progress payload.
|
||
|
|
*
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public function getProgress(): array
|
||
|
|
{
|
||
|
|
if (!is_file($this->progressPath)) {
|
||
|
|
return [
|
||
|
|
'stage' => 'idle',
|
||
|
|
'message' => '',
|
||
|
|
'percent' => null,
|
||
|
|
'timestamp' => time(),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$decoded = json_decode((string)file_get_contents($this->progressPath), true);
|
||
|
|
if (!is_array($decoded)) {
|
||
|
|
return [
|
||
|
|
'stage' => 'idle',
|
||
|
|
'message' => '',
|
||
|
|
'percent' => null,
|
||
|
|
'timestamp' => time(),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return $decoded;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset progress file to idle state.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function resetProgress(): void
|
||
|
|
{
|
||
|
|
$this->setProgress('idle', '', null);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return SafeUpgradeService
|
||
|
|
*/
|
||
|
|
protected function getSafeUpgradeService(): SafeUpgradeService
|
||
|
|
{
|
||
|
|
if ($this->safeUpgrade instanceof SafeUpgradeService) {
|
||
|
|
return $this->safeUpgrade;
|
||
|
|
}
|
||
|
|
|
||
|
|
$config = null;
|
||
|
|
try {
|
||
|
|
$config = $this->grav['config'] ?? null;
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
$config = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$stagingRoot = $config ? $config->get('system.updates.staging_root') : null;
|
||
|
|
|
||
|
|
$this->safeUpgrade = new SafeUpgradeService([
|
||
|
|
'staging_root' => $stagingRoot,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->safeUpgrade;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
protected function isSafeUpgradeEnabled(): bool
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$config = $this->grav['config'] ?? null;
|
||
|
|
if ($config === null) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (bool)$config->get('system.updates.safe_upgrade', true);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array $preflight
|
||
|
|
* @param array $decisions
|
||
|
|
* @return array|null
|
||
|
|
*/
|
||
|
|
protected function handleConflictDecisions(array $preflight, array $decisions): ?array
|
||
|
|
{
|
||
|
|
$psrConflicts = $preflight['psr_log_conflicts'] ?? [];
|
||
|
|
$monologConflicts = $preflight['monolog_conflicts'] ?? [];
|
||
|
|
|
||
|
|
if ($psrConflicts) {
|
||
|
|
$decision = $decisions['psr_log'] ?? null;
|
||
|
|
$error = $this->applyConflictDecision(
|
||
|
|
$decision,
|
||
|
|
$psrConflicts,
|
||
|
|
'Disabled before upgrade because of psr/log conflict'
|
||
|
|
);
|
||
|
|
if ($error !== null) {
|
||
|
|
return $error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($monologConflicts) {
|
||
|
|
$decision = $decisions['monolog'] ?? null;
|
||
|
|
$error = $this->applyConflictDecision(
|
||
|
|
$decision,
|
||
|
|
$monologConflicts,
|
||
|
|
'Disabled before upgrade because of Monolog API conflict'
|
||
|
|
);
|
||
|
|
if ($error !== null) {
|
||
|
|
return $error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param string|null $decision
|
||
|
|
* @param array $conflicts
|
||
|
|
* @param string $disableNote
|
||
|
|
* @return array|null
|
||
|
|
*/
|
||
|
|
protected function applyConflictDecision(?string $decision, array $conflicts, string $disableNote): ?array
|
||
|
|
{
|
||
|
|
if (!$conflicts) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$choice = $decision ?: 'abort';
|
||
|
|
if ($choice === 'abort') {
|
||
|
|
return $this->errorResult('Upgrade aborted due to unresolved conflicts.', [
|
||
|
|
'conflicts' => $conflicts,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($choice === 'disable') {
|
||
|
|
foreach (array_keys($conflicts) as $slug) {
|
||
|
|
$this->recovery->disablePlugin($slug, ['message' => $disableNote]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($choice === 'continue') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->errorResult('Unknown conflict decision provided.', [
|
||
|
|
'conflicts' => $conflicts,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array $package
|
||
|
|
* @param int $timeout
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
protected function download(array $package, int $timeout): string
|
||
|
|
{
|
||
|
|
$tmpDir = $this->grav['locator']->findResource('tmp://', true, true);
|
||
|
|
$this->tmp = $tmpDir . '/grav-update-' . uniqid('', false);
|
||
|
|
|
||
|
|
Folder::create($this->tmp);
|
||
|
|
|
||
|
|
$this->setProgress('downloading', 'Downloading update...', 0, [
|
||
|
|
'package_size' => $package['size'] ?? null,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$options = [
|
||
|
|
'timeout' => max(0, $timeout),
|
||
|
|
];
|
||
|
|
|
||
|
|
$progressCallback = function (array $progress): void {
|
||
|
|
$this->setProgress('downloading', 'Downloading update...', $progress['percent'], [
|
||
|
|
'transferred' => $progress['transferred'],
|
||
|
|
'filesize' => $progress['filesize'],
|
||
|
|
]);
|
||
|
|
};
|
||
|
|
|
||
|
|
$output = Response::get($package['download'], $options, $progressCallback);
|
||
|
|
|
||
|
|
$this->setProgress('downloading', 'Download complete.', 100);
|
||
|
|
|
||
|
|
$target = $this->tmp . '/' . $package['name'];
|
||
|
|
file_put_contents($target, $output);
|
||
|
|
|
||
|
|
return $target;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param string $zip
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function performInstall(string $zip): void
|
||
|
|
{
|
||
|
|
$folder = Installer::unZip($zip, $this->tmp . '/zip');
|
||
|
|
if ($folder === false) {
|
||
|
|
throw new RuntimeException(Installer::lastErrorMsg());
|
||
|
|
}
|
||
|
|
|
||
|
|
$script = $folder . '/system/install.php';
|
||
|
|
if (!file_exists($script)) {
|
||
|
|
throw new RuntimeException('Downloaded archive is not a valid Grav package.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$install = include $script;
|
||
|
|
if (!is_callable($install)) {
|
||
|
|
throw new RuntimeException('Unable to bootstrap installer from downloaded package.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$install($zip);
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
throw new RuntimeException($e->getMessage(), 0, $e);
|
||
|
|
}
|
||
|
|
|
||
|
|
$errorCode = Installer::lastErrorCode();
|
||
|
|
if ($errorCode) {
|
||
|
|
throw new RuntimeException(Installer::lastErrorMsg());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Attempt to run finalize scripts if Grav is already up to date but schema mismatched.
|
||
|
|
*
|
||
|
|
* @param string $localVersion
|
||
|
|
* @return array|null
|
||
|
|
*/
|
||
|
|
protected function runFinalizeIfNeeded(string $localVersion): ?array
|
||
|
|
{
|
||
|
|
$config = $this->grav['config'];
|
||
|
|
$schema = $config->get('versions.core.grav.schema');
|
||
|
|
|
||
|
|
if ($schema !== GRAV_SCHEMA && version_compare((string)$schema, GRAV_SCHEMA, '<')) {
|
||
|
|
$this->setProgress('finalizing', 'Running post-install scripts...', null);
|
||
|
|
Install::instance()->finalize();
|
||
|
|
$this->setProgress('complete', 'Post-install scripts executed.', 100, [
|
||
|
|
'target_version' => $localVersion,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'status' => 'finalized',
|
||
|
|
'version' => $localVersion,
|
||
|
|
'message' => 'Post-install scripts completed.',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch most recent safe upgrade manifest if available.
|
||
|
|
*
|
||
|
|
* @return array|null
|
||
|
|
*/
|
||
|
|
protected function resolveLatestManifest(): ?array
|
||
|
|
{
|
||
|
|
$store = $this->grav['locator']->findResource('user://data/upgrades', false);
|
||
|
|
if (!$store || !is_dir($store)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$files = glob($store . '/*.json') ?: [];
|
||
|
|
if (!$files) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
rsort($files);
|
||
|
|
|
||
|
|
$latest = $files[0];
|
||
|
|
$decoded = json_decode(file_get_contents($latest), true);
|
||
|
|
|
||
|
|
return is_array($decoded) ? $decoded : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Persist progress payload.
|
||
|
|
*
|
||
|
|
* @param string $stage
|
||
|
|
* @param string $message
|
||
|
|
* @param int|null $percent
|
||
|
|
* @param array $extra
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function setProgress(string $stage, string $message, ?int $percent = null, array $extra = []): void
|
||
|
|
{
|
||
|
|
$payload = [
|
||
|
|
'stage' => $stage,
|
||
|
|
'message' => $message,
|
||
|
|
'percent' => $percent,
|
||
|
|
'timestamp' => time(),
|
||
|
|
] + $extra;
|
||
|
|
|
||
|
|
try {
|
||
|
|
Folder::create(dirname($this->progressPath));
|
||
|
|
file_put_contents($this->progressPath, json_encode($payload, JSON_PRETTY_PRINT));
|
||
|
|
} catch (Throwable $e) {
|
||
|
|
// ignore write failures
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Helper for building an error result payload.
|
||
|
|
*
|
||
|
|
* @param string $message
|
||
|
|
* @param array $extra
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
protected function errorResult(string $message, array $extra = []): array
|
||
|
|
{
|
||
|
|
$this->setProgress('error', $message, null, $extra);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'status' => 'error',
|
||
|
|
'message' => $message,
|
||
|
|
] + $extra;
|
||
|
|
}
|
||
|
|
}
|