Files
Grav/bin/restore

235 lines
6.8 KiB
Plaintext
Raw Normal View History

#!/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__);
define('GRAV_CLI', true);
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';
if (!file_exists($root . '/index.php')) {
fwrite(STDERR, "FATAL: Must be run from Grav root directory.\n");
exit(1);
}
use Grav\Common\Recovery\RecoveryManager;
use Grav\Common\Upgrade\SafeUpgradeService;
use Symfony\Component\Yaml\Yaml;
const RESTORE_USAGE = <<<USAGE
Grav Restore Utility
Usage:
bin/restore list [--staging-root=/absolute/path]
Lists all available snapshots (most recent first).
bin/restore apply <snapshot-id> [--staging-root=/absolute/path]
Restores the specified snapshot created by safe-upgrade.
bin/restore recovery [status|clear]
Shows the recovery flag context or clears it.
Options:
--staging-root Overrides the staging directory (defaults to configured value).
Examples:
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
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);
}
/**
* @return list<array{id:string,source_version:?string,target_version:?string,created_at:int}>
*/
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'],
'source_version' => $decoded['source_version'] ?? null,
'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';
$restoreVersion = $snapshot['source_version'] ?? $snapshot['target_version'] ?? 'unknown';
echo sprintf(" - %s (restore to Grav %s, %s)\n", $snapshot['id'], $restoreVersion, $time);
}
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);
}
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
echo "Restored snapshot {$snapshotId} (Grav {$version}).\n";
exit(0);
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";
$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";
}
exit(0);
default:
echo "Unknown recovery action: {$action}\n\n" . RESTORE_USAGE . "\n";
exit(1);
}
case 'help':
default:
echo RESTORE_USAGE . "\n";
exit($command === 'help' ? 0 : 1);
}