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
|
|
|
|
|
|
|
|
use Grav\Common\Upgrade\SafeUpgradeService;
|
|
|
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
|
|
|
|
|
|
const RESTORE_USAGE = <<<USAGE
|
|
|
|
|
Grav Restore Utility
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
bin/grav-restore list [--staging-root=/absolute/path]
|
|
|
|
|
Lists all available snapshots (most recent first).
|
|
|
|
|
|
|
|
|
|
bin/grav-restore apply <snapshot-id> [--staging-root=/absolute/path]
|
|
|
|
|
Restores the specified snapshot created by safe-upgrade.
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
--staging-root Overrides the staging directory (defaults to configured value).
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
bin/grav-restore list
|
|
|
|
|
bin/grav-restore apply stage-68eff31cc4104
|
|
|
|
|
bin/grav-restore apply stage-68eff31cc4104 --staging-root=/var/grav-backups
|
|
|
|
|
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 (strncmp($arg, '--staging-root=', 15) === 0) {
|
|
|
|
|
$options['staging_root'] = substr($arg, 15);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (substr($arg, 0, 2) === '--') {
|
|
|
|
|
echo "Unknown option: {$arg}\n";
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$arguments[] = $arg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'command' => $command,
|
|
|
|
|
'arguments' => $arguments,
|
|
|
|
|
'options' => $options,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
function readConfiguredStagingRoot(): ?string
|
|
|
|
|
{
|
|
|
|
|
$configFiles = [
|
|
|
|
|
GRAV_ROOT . '/user/config/system.yaml',
|
|
|
|
|
GRAV_ROOT . '/system/config/system.yaml'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($configFiles as $file) {
|
|
|
|
|
if (!is_file($file)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$data = Yaml::parseFile($file);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_array($data)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$current = $data['system']['updates']['staging_root'] ?? null;
|
|
|
|
|
if (null !== $current && $current !== '') {
|
|
|
|
|
return $current;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array $options
|
|
|
|
|
* @return SafeUpgradeService
|
|
|
|
|
*/
|
|
|
|
|
function createUpgradeService(array $options): SafeUpgradeService
|
|
|
|
|
{
|
|
|
|
|
$config = readConfiguredStagingRoot();
|
|
|
|
|
if ($config !== null && empty($options['staging_root'])) {
|
|
|
|
|
$options['staging_root'] = $config;
|
|
|
|
|
} elseif (isset($options['staging_root']) && $options['staging_root'] === '') {
|
|
|
|
|
unset($options['staging_root']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$options['root'] = GRAV_ROOT;
|
|
|
|
|
|
|
|
|
|
return new SafeUpgradeService($options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return list<array{id: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'],
|
|
|
|
|
'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';
|
|
|
|
|
$version = $snapshot['target_version'] ?? 'unknown';
|
|
|
|
|
echo sprintf(" - %s (Grav %s, %s)\n", $snapshot['id'], $version, $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['target_version'] ?? 'unknown';
|
|
|
|
|
echo "Restored snapshot {$snapshotId} (Grav {$version}).\n";
|
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
|
|
case 'help':
|
|
|
|
|
default:
|
|
|
|
|
echo RESTORE_USAGE . "\n";
|
|
|
|
|
exit($command === 'help' ? 0 : 1);
|
|
|
|
|
}
|