restore tool - but not curretly working

This commit is contained in:
Andy Miller
2025-10-18 12:04:25 -06:00
parent 7bb6044e05
commit 796c61e66d
9 changed files with 393 additions and 9 deletions

View File

@@ -32,6 +32,7 @@ use Grav\Plugin\Admin\Popularity;
use Grav\Plugin\Admin\Router;
use Grav\Plugin\Admin\Themes;
use Grav\Plugin\Admin\AdminController;
use Grav\Plugin\Admin\SafeUpgradeManager;
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
use Grav\Plugin\Admin\WhiteLabel;
use Grav\Plugin\Form\Form;
@@ -383,6 +384,21 @@ class AdminPlugin extends Plugin
'reports' => [['admin.super'], 'PLUGIN_ADMIN.REPORTS'],
'direct-install' => [['admin.super'], 'PLUGIN_ADMIN.DIRECT_INSTALL'],
]);
try {
$manifestFiles = glob(GRAV_ROOT . '/user/data/upgrades/*.json') ?: [];
if (!$manifestFiles) {
$manager = new SafeUpgradeManager(Grav::instance());
$manifestFiles = $manager->hasSnapshots() ? [true] : [];
}
if ($manifestFiles) {
$event['tools']['restore-grav'] = [['admin.super'], 'PLUGIN_ADMIN.RESTORE_GRAV'];
}
} catch (\Throwable $e) {
// ignore availability errors, snapshots tool will simply stay hidden
}
}
/**

View File

@@ -341,6 +341,31 @@ class Admin
return array_unique($perms);
}
/**
* @return array<int, array{id:string, source_version:?string, target_version:?string, created_at:int, created_at_iso:?string, backup_path:?string, package_path:?string}>
*/
public function safeUpgradeSnapshots(): array
{
try {
$manager = new SafeUpgradeManager();
return $manager->listSnapshots();
} catch (\Throwable $e) {
return [];
}
}
public function safeUpgradeHasSnapshots(): bool
{
try {
$manager = new SafeUpgradeManager();
return $manager->hasSnapshots();
} catch (\Throwable $e) {
return false;
}
}
/**
* Return the languages available in the site
*

View File

@@ -933,6 +933,102 @@ class AdminController extends AdminBaseController
return true;
}
/**
* Restore a safe-upgrade snapshot via Tools.
*
* Route: POST /tools/restore-grav?task:safeUpgradeRestore
*
* @return bool
*/
public function taskSafeUpgradeRestore()
{
if (!$this->authorizeTask('install grav', ['admin.super'])) {
return false;
}
$post = $this->getPost($_POST ?? []);
$snapshotId = isset($post['snapshot']) ? (string)$post['snapshot'] : '';
if ($snapshotId === '') {
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error');
$this->setRedirect('/tools/restore-grav');
return false;
}
$manager = $this->getSafeUpgradeManager();
$result = $manager->restoreSnapshot($snapshotId);
if (($result['status'] ?? 'error') === 'success') {
$manifest = $result['manifest'] ?? [];
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
$this->admin->setMessage(
sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS'), $snapshotId, $version),
'info'
);
} else {
$message = $result['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_FAILED');
$this->admin->setMessage($message, 'error');
}
$this->setRedirect('/tools/restore-grav');
return true;
}
/**
* Delete one or more safe-upgrade snapshots via Tools.
*
* Route: POST /tools/restore-grav?task:safeUpgradeDelete
*
* @return bool
*/
public function taskSafeUpgradeDelete()
{
if (!$this->authorizeTask('install grav', ['admin.super'])) {
return false;
}
$post = $this->getPost($_POST ?? []);
$snapshots = $post['snapshots'] ?? [];
if (is_string($snapshots)) {
$snapshots = [$snapshots];
}
if (empty($snapshots)) {
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error');
$this->setRedirect('/tools/restore-grav');
return false;
}
$manager = $this->getSafeUpgradeManager();
$results = $manager->deleteSnapshots($snapshots);
$success = array_filter($results, static function ($item) {
return ($item['status'] ?? 'error') === 'success';
});
$failed = array_filter($results, static function ($item) {
return ($item['status'] ?? 'error') !== 'success';
});
if ($success) {
$this->admin->setMessage(
sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_DELETE_SUCCESS'), count($success)),
'info'
);
}
foreach ($failed as $entry) {
$message = $entry['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_DELETE_FAILED');
$this->admin->setMessage($message, 'error');
}
$this->setRedirect('/tools/restore-grav');
return true;
}
/**
* Handles uninstalling plugins and themes
*

View File

@@ -120,6 +120,164 @@ class SafeUpgradeManager
$this->setJobId(null);
}
/**
* @return array<int, array{id: string, source_version:?string, target_version:?string, created_at:int, created_at_iso:?string, backup_path:?string, package_path:?string}>
*/
public function listSnapshots(): 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;
}
$createdAt = isset($decoded['created_at']) ? (int)$decoded['created_at'] : 0;
$snapshots[] = [
'id' => (string)$decoded['id'],
'source_version' => $decoded['source_version'] ?? null,
'target_version' => $decoded['target_version'] ?? null,
'created_at' => $createdAt,
'created_at_iso' => $createdAt > 0 ? date('c', $createdAt) : null,
'backup_path' => $decoded['backup_path'] ?? null,
'package_path' => $decoded['package_path'] ?? null,
];
}
return $snapshots;
}
public function hasSnapshots(): bool
{
return !empty($this->listSnapshots());
}
/**
* @param string $snapshotId
* @return array{status:string,message:?string,manifest:array|null}
*/
public function restoreSnapshot(string $snapshotId): array
{
try {
$safeUpgrade = $this->getSafeUpgradeService();
$manifest = $safeUpgrade->rollback($snapshotId);
} catch (RuntimeException $e) {
return [
'status' => 'error',
'message' => $e->getMessage(),
'manifest' => null,
];
} catch (Throwable $e) {
return [
'status' => 'error',
'message' => $e->getMessage(),
'manifest' => null,
];
}
if (!$manifest) {
return [
'status' => 'error',
'message' => sprintf('Snapshot %s not found.', $snapshotId),
'manifest' => null,
];
}
return [
'status' => 'success',
'message' => null,
'manifest' => $manifest,
];
}
/**
* @param array<int, string> $snapshotIds
* @return array<int, array{id:string,status:string,message:?string}>
*/
public function deleteSnapshots(array $snapshotIds): array
{
$ids = array_values(array_unique(array_filter(array_map('strval', $snapshotIds))));
$results = [];
foreach ($ids as $id) {
$results[] = $this->deleteSnapshot($id);
}
return $results;
}
/**
* @param string $snapshotId
* @return array{id:string,status:string,message:?string}
*/
protected function deleteSnapshot(string $snapshotId): array
{
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
$manifestPath = $manifestDir . '/' . $snapshotId . '.json';
if (!is_file($manifestPath)) {
return [
'id' => $snapshotId,
'status' => 'error',
'message' => sprintf('Snapshot %s not found.', $snapshotId),
];
}
$manifest = json_decode(file_get_contents($manifestPath) ?: '', true);
if (!is_array($manifest)) {
return [
'id' => $snapshotId,
'status' => 'error',
'message' => sprintf('Snapshot %s manifest is corrupted.', $snapshotId),
];
}
$errors = [];
foreach (['package_path', 'backup_path'] as $key) {
$path = isset($manifest[$key]) ? (string)$manifest[$key] : '';
if ($path === '' || !file_exists($path)) {
continue;
}
try {
if (is_dir($path)) {
Folder::delete($path);
} else {
@unlink($path);
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if (!@unlink($manifestPath)) {
$errors[] = sprintf('Unable to delete manifest file %s.', $manifestPath);
}
if ($errors) {
return [
'id' => $snapshotId,
'status' => 'error',
'message' => implode(' ', $errors),
];
}
return [
'id' => $snapshotId,
'status' => 'success',
'message' => sprintf('Snapshot %s removed.', $snapshotId),
];
}
protected function getJobDir(string $jobId): string
{
return $this->jobsDir . '/' . $jobId;

View File

@@ -540,8 +540,8 @@ PLUGIN_ADMIN:
SAFE_UPGRADE_STAGE_COMPLETE: "Upgrade complete"
SAFE_UPGRADE_STAGE_ERROR: "Upgrade encountered an error"
SAFE_UPGRADE_RESULT_SUCCESS: "Grav upgraded to v%s"
SAFE_UPGRADE_RESULT_MANIFEST: "Snapshot reference: %s"
SAFE_UPGRADE_RESULT_ROLLBACK: "Rollback snapshot stored at: %s"
SAFE_UPGRADE_RESULT_MANIFEST: "Snapshot reference: <code>%s</code>"
SAFE_UPGRADE_RESULT_HINT: "Restore snapshots from Tools → Restore Grav."
SAFE_UPGRADE_RESULT_NOOP: "Grav is already up to date."
SAFE_UPGRADE_RESULT_FAILURE: "Safe upgrade failed"
OF_THIS: "of this"
@@ -840,6 +840,21 @@ PLUGIN_ADMIN:
TOOLS_DIRECT_INSTALL_URL_TITLE: "Install Package via Remote URL Reference"
TOOLS_DIRECT_INSTALL_URL_DESC: "Alternatively, you can also reference a full URL to the package ZIP file and install it via this remote URL."
TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Upload and install"
RESTORE_GRAV: "Restore Grav"
RESTORE_GRAV_DESC: "Select a snapshot created by Safe Upgrade to restore your site or remove snapshots you no longer need."
RESTORE_GRAV_TABLE_SNAPSHOT: "Snapshot"
RESTORE_GRAV_TABLE_VERSION: "Version"
RESTORE_GRAV_TABLE_CREATED: "Created"
RESTORE_GRAV_TABLE_LOCATION: "Snapshot path"
RESTORE_GRAV_TABLE_ACTIONS: "Actions"
RESTORE_GRAV_RESTORE_BUTTON: "Restore"
RESTORE_GRAV_DELETE_SELECTED: "Delete Selected"
RESTORE_GRAV_NONE: "No safe upgrade snapshots are currently available."
RESTORE_GRAV_INVALID: "Select at least one snapshot before continuing."
RESTORE_GRAV_SUCCESS: "Snapshot %s restored (Grav %s)."
RESTORE_GRAV_FAILED: "Unable to restore the selected snapshot."
RESTORE_GRAV_DELETE_SUCCESS: "%d snapshot(s) deleted."
RESTORE_GRAV_DELETE_FAILED: "Failed to delete one or more snapshots."
ROUTE_OVERRIDES: "Route Overrides"
ROUTE_DEFAULT: "Default Route"
ROUTE_CANONICAL: "Canonical Route"

View File

@@ -859,14 +859,13 @@ export default class SafeUpgrade {
if (status === 'success' || status === 'finalized') {
const manifest = result.manifest || {};
const target = result.version || manifest.target_version || '';
const backup = manifest.backup_path || '';
const identifier = manifest.id || '';
this.steps.result.html(`
<div class="safe-upgrade-result success">
<h3>${r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s')}</h3>
${identifier ? `<p>${r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s')}</p>` : ''}
${backup ? `<p>${r('SAFE_UPGRADE_RESULT_ROLLBACK', backup, 'Rollback snapshot stored at: %s')}</p>` : ''}
${identifier ? `<p>${r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: <code>%s</code>')}</p>` : ''}
<p>${t('SAFE_UPGRADE_RESULT_HINT', 'Restore snapshots from Tools → Restore Grav.')}</p>
</div>
`);

View File

@@ -5258,9 +5258,8 @@ var SafeUpgrade = /*#__PURE__*/function () {
if (status === 'success' || status === 'finalized') {
var manifest = result.manifest || {};
var target = result.version || manifest.target_version || '';
var backup = manifest.backup_path || '';
var identifier = manifest.id || '';
this.steps.result.html("\n <div class=\"safe-upgrade-result success\">\n <h3>".concat(r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s'), "</h3>\n ").concat(identifier ? "<p>".concat(r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s'), "</p>") : '', "\n ").concat(backup ? "<p>".concat(r('SAFE_UPGRADE_RESULT_ROLLBACK', backup, 'Rollback snapshot stored at: %s'), "</p>") : '', "\n </div>\n "));
this.steps.result.html("\n <div class=\"safe-upgrade-result success\">\n <h3>".concat(r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s'), "</h3>\n ").concat(identifier ? "<p>".concat(r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: <code>%s</code>'), "</p>") : '', "\n <p>").concat(t('SAFE_UPGRADE_RESULT_HINT', 'Restore snapshots from Tools → Restore Grav.'), "</p>\n </div>\n "));
this.switchStep('result');
external_jQuery_default()('[data-gpm-grav]').remove();
if (target) {

View File

@@ -0,0 +1,74 @@
<h1>{{ "PLUGIN_ADMIN.RESTORE_GRAV"|t }}</h1>
<div class="restore-grav-content">
{% set snapshots = admin.safeUpgradeSnapshots() %}
{% if snapshots %}
<p class="restore-grav-intro">
{{ "PLUGIN_ADMIN.RESTORE_GRAV_DESC"|t }}
</p>
<form id="snapshot-delete-form" action="{{ admin_route('/tools/restore-grav') }}" method="post" class="snapshot-delete-form">
<input type="hidden" name="task" value="safeUpgradeDelete">
{{ nonce_field('admin-form', 'admin-nonce')|raw }}
</form>
<table class="restore-grav-table">
<thead>
<tr>
<th class="checkbox-cell"></th>
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_SNAPSHOT"|t }}</th>
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_VERSION"|t }}</th>
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_CREATED"|t }}</th>
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_LOCATION"|t }}</th>
<th class="actions-cell">{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_ACTIONS"|t }}</th>
</tr>
</thead>
<tbody>
{% for snapshot in snapshots %}
{% set version = snapshot.source_version ?: snapshot.target_version %}
<tr>
<td class="checkbox-cell">
<input type="checkbox" name="snapshots[]" value="{{ snapshot.id }}" form="snapshot-delete-form" />
</td>
<td><code>{{ snapshot.id }}</code></td>
<td>{{ version ?: "PLUGIN_ADMIN.UNKNOWN"|t }}</td>
<td>
{% if snapshot.created_at %}
{{ snapshot.created_at|date('Y-m-d H:i:s') }}
<span class="hint">{{ snapshot.created_at|nicetime(false, false) }}</span>
{% else %}
{{ "PLUGIN_ADMIN.UNKNOWN"|t }}
{% endif %}
</td>
<td>
{% if snapshot.backup_path %}
<code class="snapshot-path">{{ snapshot.backup_path }}</code>
{% else %}
{{ "PLUGIN_ADMIN.UNKNOWN"|t }}
{% endif %}
</td>
<td class="actions-cell">
<form action="{{ admin_route('/tools/restore-grav') }}" method="post" class="inline-form">
<input type="hidden" name="task" value="safeUpgradeRestore">
<input type="hidden" name="snapshot" value="{{ snapshot.id }}">
{{ nonce_field('admin-form', 'admin-nonce')|raw }}
<button type="submit" class="button">{{ "PLUGIN_ADMIN.RESTORE_GRAV_RESTORE_BUTTON"|t }}</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="button-bar">
<button type="submit" form="snapshot-delete-form" class="button danger" name="task" value="safeUpgradeDelete">
{{ "PLUGIN_ADMIN.RESTORE_GRAV_DELETE_SELECTED"|t }}
</button>
</div>
{% else %}
<div class="notice secondary">
<p>{{ "PLUGIN_ADMIN.RESTORE_GRAV_NONE"|t }}</p>
</div>
{% endif %}
</div>

View File

@@ -2,8 +2,11 @@
{% set tools_slug = uri.basename %}
{% if tools_slug == 'tools' %}{% set tools_slug = 'backups' %}{% endif %}
{% set title = "PLUGIN_ADMIN.TOOLS"|t ~ ": " ~ ("PLUGIN_ADMIN." ~ tools_slug|underscorize|upper)|t %}
{% set tools = admin.tools() %}
{% if tools[tools_slug] is not defined %}
{% set tools_slug = tools|keys|first %}
{% endif %}
{% set title = "PLUGIN_ADMIN.TOOLS"|t ~ ": " ~ ("PLUGIN_ADMIN." ~ tools_slug|underscorize|upper)|t %}
{% set titlebar -%}
{% include 'partials/tools-' ~ tools_slug ~ '-titlebar.html.twig' ignore missing %}
@@ -47,4 +50,3 @@
<h1>Unauthorized</h1>
{% endif %}
{% endblock %}