2025-10-15 14:20:30 -06:00
|
|
|
#!/usr/bin/env php
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Grav Snapshot Restore Utility
|
|
|
|
|
*
|
|
|
|
|
* Lightweight CLI that can list and apply safe-upgrade snapshots without
|
|
|
|
|
* bootstrapping the full Grav application (or any plugins).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
$root = dirname(__DIR__);
|
2025-10-15 20:14:15 -06:00
|
|
|
|
2025-10-15 14:20:30 -06:00
|
|
|
define('GRAV_CLI', true);
|
2025-10-15 20:14:15 -06:00
|
|
|
define('GRAV_REQUEST_TIME', microtime(true));
|
|
|
|
|
|
|
|
|
|
if (!file_exists($root . '/vendor/autoload.php')) {
|
|
|
|
|
fwrite(STDERR, "Unable to locate vendor/autoload.php. Run composer install first.\n");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$autoload = require $root . '/vendor/autoload.php';
|
2025-10-15 14:20:30 -06:00
|
|
|
|
2025-10-15 20:14:15 -06:00
|
|
|
if (!file_exists($root . '/index.php')) {
|
|
|
|
|
fwrite(STDERR, "FATAL: Must be run from Grav root directory.\n");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
2025-10-15 14:20:30 -06:00
|
|
|
|
2025-10-16 08:09:47 -06:00
|
|
|
use Grav\Common\Recovery\RecoveryManager;
|
2025-10-15 14:20:30 -06:00
|
|
|
use Grav\Common\Upgrade\SafeUpgradeService;
|
|
|
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
|
|
|
|
|
|
const RESTORE_USAGE = <<<USAGE
|
|
|
|
|
Grav Restore Utility
|
|
|
|
|
|
|
|
|
|
Usage:
|
2025-10-16 08:09:47 -06:00
|
|
|
bin/restore list [--staging-root=/absolute/path]
|
2025-10-15 14:20:30 -06:00
|
|
|
Lists all available snapshots (most recent first).
|
|
|
|
|
|
2025-10-16 08:09:47 -06:00
|
|
|
bin/restore apply <snapshot-id> [--staging-root=/absolute/path]
|
2025-10-15 14:20:30 -06:00
|
|
|
Restores the specified snapshot created by safe-upgrade.
|
|
|
|
|
|
2025-10-16 08:09:47 -06:00
|
|
|
bin/restore recovery [status|clear]
|
|
|
|
|
Shows the recovery flag context or clears it.
|
|
|
|
|
|
2025-10-15 14:20:30 -06:00
|
|
|
Options:
|
|
|
|
|
--staging-root Overrides the staging directory (defaults to configured value).
|
|
|
|
|
|
|
|
|
|
Examples:
|
2025-10-16 08:09:47 -06:00
|
|
|
bin/restore list
|
|
|
|
|
bin/restore apply stage-68eff31cc4104
|
|
|
|
|
bin/restore apply stage-68eff31cc4104 --staging-root=/var/grav-backups
|
|
|
|
|
bin/restore recovery status
|
|
|
|
|
bin/restore recovery clear
|
2025-10-15 14:20:30 -06:00
|
|
|
USAGE;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array $args
|
|
|
|
|
* @return array{command:string,arguments:array,options:array}
|
|
|
|
|
*/
|
|
|
|
|
function parseArguments(array $args): array
|
|
|
|
|
{
|
|
|
|
|
array_shift($args); // remove script name
|
|
|
|
|
|
|
|
|
|
$command = $args[0] ?? 'help';
|
|
|
|
|
$arguments = [];
|
|
|
|
|
$options = [];
|
|
|
|
|
|
|
|
|
|
foreach (array_slice($args, 1) as $arg) {
|
|
|
|
|
if (substr($arg, 0, 2) === '--') {
|
|
|
|
|
echo "Unknown option: {$arg}\n";
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$arguments[] = $arg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'command' => $command,
|
|
|
|
|
'arguments' => $arguments,
|
|
|
|
|
'options' => $options,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
/**
|
|
|
|
|
* @param array $options
|
|
|
|
|
* @return SafeUpgradeService
|
|
|
|
|
*/
|
|
|
|
|
function createUpgradeService(array $options): SafeUpgradeService
|
|
|
|
|
{
|
|
|
|
|
$options['root'] = GRAV_ROOT;
|
|
|
|
|
|
|
|
|
|
return new SafeUpgradeService($options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-16 11:56:40 -06:00
|
|
|
* @return list<array{id:string,source_version:?string,target_version:?string,created_at:int}>
|
2025-10-15 14:20:30 -06:00
|
|
|
*/
|
|
|
|
|
function loadSnapshots(): array
|
|
|
|
|
{
|
|
|
|
|
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
|
|
|
|
|
if (!is_dir($manifestDir)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$files = glob($manifestDir . '/*.json') ?: [];
|
|
|
|
|
rsort($files);
|
|
|
|
|
|
|
|
|
|
$snapshots = [];
|
|
|
|
|
foreach ($files as $file) {
|
|
|
|
|
$decoded = json_decode(file_get_contents($file) ?: '', true);
|
|
|
|
|
if (!is_array($decoded) || empty($decoded['id'])) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$snapshots[] = [
|
|
|
|
|
'id' => $decoded['id'],
|
2025-10-16 11:56:40 -06:00
|
|
|
'source_version' => $decoded['source_version'] ?? null,
|
2025-10-15 14:20:30 -06:00
|
|
|
'target_version' => $decoded['target_version'] ?? null,
|
|
|
|
|
'created_at' => $decoded['created_at'] ?? 0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $snapshots;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$cli = parseArguments($argv);
|
|
|
|
|
$command = $cli['command'];
|
|
|
|
|
$arguments = $cli['arguments'];
|
|
|
|
|
$options = $cli['options'];
|
|
|
|
|
|
|
|
|
|
switch ($command) {
|
|
|
|
|
case 'list':
|
|
|
|
|
$snapshots = loadSnapshots();
|
|
|
|
|
if (!$snapshots) {
|
|
|
|
|
echo "No snapshots found. Run bin/gpm self-upgrade (with safe upgrade enabled) to create one.\n";
|
|
|
|
|
exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "Available snapshots:\n";
|
|
|
|
|
foreach ($snapshots as $snapshot) {
|
|
|
|
|
$time = $snapshot['created_at'] ? date('c', (int)$snapshot['created_at']) : 'unknown';
|
2025-10-16 11:56:40 -06:00
|
|
|
$restoreVersion = $snapshot['source_version'] ?? $snapshot['target_version'] ?? 'unknown';
|
|
|
|
|
echo sprintf(" - %s (restore to Grav %s, %s)\n", $snapshot['id'], $restoreVersion, $time);
|
2025-10-15 14:20:30 -06:00
|
|
|
}
|
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
|
|
case 'apply':
|
|
|
|
|
$snapshotId = $arguments[0] ?? null;
|
|
|
|
|
if (!$snapshotId) {
|
|
|
|
|
echo "Missing snapshot id.\n\n" . RESTORE_USAGE . "\n";
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$service = createUpgradeService($options);
|
|
|
|
|
$manifest = $service->rollback($snapshotId);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
fwrite(STDERR, "Restore failed: " . $e->getMessage() . "\n");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$manifest) {
|
|
|
|
|
fwrite(STDERR, "Snapshot {$snapshotId} not found.\n");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 11:56:40 -06:00
|
|
|
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
|
2025-10-15 14:20:30 -06:00
|
|
|
echo "Restored snapshot {$snapshotId} (Grav {$version}).\n";
|
|
|
|
|
exit(0);
|
|
|
|
|
|
2025-10-16 08:09:47 -06:00
|
|
|
case 'recovery':
|
|
|
|
|
$action = strtolower($arguments[0] ?? 'status');
|
|
|
|
|
$manager = new RecoveryManager(GRAV_ROOT);
|
|
|
|
|
|
|
|
|
|
switch ($action) {
|
|
|
|
|
case 'clear':
|
|
|
|
|
if ($manager->isActive()) {
|
|
|
|
|
$manager->clear();
|
|
|
|
|
echo "Recovery flag cleared.\n";
|
|
|
|
|
} else {
|
|
|
|
|
echo "Recovery mode is not active.\n";
|
|
|
|
|
}
|
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
|
|
case 'status':
|
|
|
|
|
if (!$manager->isActive()) {
|
|
|
|
|
echo "Recovery mode is not active.\n";
|
|
|
|
|
exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$context = $manager->getContext();
|
|
|
|
|
if (!$context) {
|
|
|
|
|
echo "Recovery flag present but context could not be parsed.\n";
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$created = isset($context['created_at']) ? date('c', (int)$context['created_at']) : 'unknown';
|
|
|
|
|
$token = $context['token'] ?? '(missing)';
|
|
|
|
|
$message = $context['message'] ?? '(no message)';
|
|
|
|
|
$plugin = $context['plugin'] ?? '(none detected)';
|
|
|
|
|
$file = $context['file'] ?? '(unknown file)';
|
|
|
|
|
$line = $context['line'] ?? '(unknown line)';
|
|
|
|
|
|
|
|
|
|
echo "Recovery flag context:\n";
|
|
|
|
|
echo " Token: {$token}\n";
|
|
|
|
|
echo " Message: {$message}\n";
|
|
|
|
|
echo " Plugin: {$plugin}\n";
|
|
|
|
|
echo " File: {$file}\n";
|
|
|
|
|
echo " Line: {$line}\n";
|
|
|
|
|
echo " Created: {$created}\n";
|
2025-10-16 09:08:53 -06:00
|
|
|
|
|
|
|
|
$window = $manager->getUpgradeWindow();
|
|
|
|
|
if ($window) {
|
|
|
|
|
$expires = isset($window['expires_at']) ? date('c', (int)$window['expires_at']) : 'unknown';
|
|
|
|
|
$reason = $window['reason'] ?? '(unknown)';
|
|
|
|
|
echo " Window: active ({$reason}, expires {$expires})\n";
|
|
|
|
|
} else {
|
|
|
|
|
echo " Window: inactive\n";
|
|
|
|
|
}
|
2025-10-16 08:09:47 -06:00
|
|
|
exit(0);
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
echo "Unknown recovery action: {$action}\n\n" . RESTORE_USAGE . "\n";
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 14:20:30 -06:00
|
|
|
case 'help':
|
|
|
|
|
default:
|
|
|
|
|
echo RESTORE_USAGE . "\n";
|
|
|
|
|
exit($command === 'help' ? 0 : 1);
|
|
|
|
|
}
|