mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2025-10-30 01:36:27 +01:00
bg process for restore
This commit is contained in:
@@ -943,6 +943,11 @@ class AdminController extends AdminBaseController
|
|||||||
public function taskSafeUpgradeRestore()
|
public function taskSafeUpgradeRestore()
|
||||||
{
|
{
|
||||||
if (!$this->authorizeTask('install grav', ['admin.super'])) {
|
if (!$this->authorizeTask('install grav', ['admin.super'])) {
|
||||||
|
$this->sendJsonResponse([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
|
||||||
|
]);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,28 +955,28 @@ class AdminController extends AdminBaseController
|
|||||||
$snapshotId = isset($post['snapshot']) ? (string)$post['snapshot'] : '';
|
$snapshotId = isset($post['snapshot']) ? (string)$post['snapshot'] : '';
|
||||||
|
|
||||||
if ($snapshotId === '') {
|
if ($snapshotId === '') {
|
||||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error');
|
$this->sendJsonResponse([
|
||||||
$this->setRedirect('/tools/restore-grav');
|
'status' => 'error',
|
||||||
|
'message' => $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID')
|
||||||
|
]);
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$manager = $this->getSafeUpgradeManager();
|
$manager = $this->getSafeUpgradeManager();
|
||||||
$result = $manager->restoreSnapshot($snapshotId);
|
$result = $manager->queueRestore($snapshotId);
|
||||||
|
$status = $result['status'] ?? 'error';
|
||||||
|
|
||||||
if (($result['status'] ?? 'error') === 'success') {
|
$response = [
|
||||||
$manifest = $result['manifest'] ?? [];
|
'status' => $status === 'error' ? 'error' : 'success',
|
||||||
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
|
'data' => $result,
|
||||||
$this->admin->setMessage(
|
];
|
||||||
sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS'), $snapshotId, $version),
|
|
||||||
'info'
|
if (!empty($result['message'])) {
|
||||||
);
|
$response['message'] = $result['message'];
|
||||||
} else {
|
|
||||||
$message = $result['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_FAILED');
|
|
||||||
$this->admin->setMessage($message, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setRedirect('/tools/restore-grav');
|
$this->sendJsonResponse($response);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,30 @@ class SafeUpgradeManager
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function queueRestore(string $snapshotId): array
|
||||||
|
{
|
||||||
|
$snapshotId = trim($snapshotId);
|
||||||
|
if ($snapshotId === '') {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Snapshot identifier is required.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifestPath = GRAV_ROOT . '/user/data/upgrades/' . $snapshotId . '.json';
|
||||||
|
if (!is_file($manifestPath)) {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => sprintf('Snapshot %s not found.', $snapshotId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->queue([
|
||||||
|
'operation' => 'restore',
|
||||||
|
'snapshot_id' => $snapshotId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, string> $snapshotIds
|
* @param array<int, string> $snapshotIds
|
||||||
* @return array<int, array{id:string,status:string,message:?string}>
|
* @return array<int, array{id:string,status:string,message:?string}>
|
||||||
@@ -458,6 +482,10 @@ class SafeUpgradeManager
|
|||||||
|
|
||||||
public function queue(array $options = []): array
|
public function queue(array $options = []): array
|
||||||
{
|
{
|
||||||
|
$operation = $options['operation'] ?? 'upgrade';
|
||||||
|
$options['operation'] = $operation;
|
||||||
|
|
||||||
|
$this->resetProgress();
|
||||||
$jobId = $this->generateJobId();
|
$jobId = $this->generateJobId();
|
||||||
$this->setJobId($jobId);
|
$this->setJobId($jobId);
|
||||||
|
|
||||||
@@ -487,7 +515,8 @@ class SafeUpgradeManager
|
|||||||
|
|
||||||
$this->log(sprintf('Queued safe upgrade job %s', $jobId));
|
$this->log(sprintf('Queued safe upgrade job %s', $jobId));
|
||||||
|
|
||||||
$this->setProgress('queued', 'Waiting for upgrade worker...', 0, ['job_id' => $jobId, 'status' => 'queued']);
|
$queueMessage = $operation === 'restore' ? 'Waiting for restore worker...' : 'Waiting for upgrade worker...';
|
||||||
|
$this->setProgress('queued', $queueMessage, 0, ['job_id' => $jobId, 'status' => 'queued', 'operation' => $operation]);
|
||||||
|
|
||||||
if (!function_exists('proc_open')) {
|
if (!function_exists('proc_open')) {
|
||||||
$message = 'proc_open() is disabled on this server; unable to run safe upgrade worker.';
|
$message = 'proc_open() is disabled on this server; unable to run safe upgrade worker.';
|
||||||
@@ -495,12 +524,13 @@ class SafeUpgradeManager
|
|||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'error' => $message,
|
'error' => $message,
|
||||||
]);
|
]);
|
||||||
$this->setProgress('error', $message, null, ['job_id' => $jobId]);
|
$this->setProgress('error', $message, null, ['job_id' => $jobId, 'operation' => $operation]);
|
||||||
$this->clearJobContext();
|
$this->clearJobContext();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
|
'operation' => $operation,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,12 +584,13 @@ class SafeUpgradeManager
|
|||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'error' => $message,
|
'error' => $message,
|
||||||
]);
|
]);
|
||||||
$this->setProgress('error', $message, null, ['job_id' => $jobId]);
|
$this->setProgress('error', $message, null, ['job_id' => $jobId, 'operation' => $operation]);
|
||||||
$this->clearJobContext();
|
$this->clearJobContext();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
|
'operation' => $operation,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +608,7 @@ class SafeUpgradeManager
|
|||||||
'progress' => $this->getProgress(),
|
'progress' => $this->getProgress(),
|
||||||
'job' => $this->readManifest(),
|
'job' => $this->readManifest(),
|
||||||
'context' => $this->buildStatusContext(),
|
'context' => $this->buildStatusContext(),
|
||||||
|
'operation' => $operation,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,6 +845,57 @@ class SafeUpgradeManager
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function runRestore(array $options): array
|
||||||
|
{
|
||||||
|
$snapshotId = isset($options['snapshot_id']) ? (string)$options['snapshot_id'] : '';
|
||||||
|
if ($snapshotId === '') {
|
||||||
|
return $this->errorResult('Snapshot identifier is required.', ['operation' => 'restore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setProgress('restoring', sprintf('Restoring snapshot %s...', $snapshotId), null, [
|
||||||
|
'operation' => 'restore',
|
||||||
|
'snapshot' => $snapshotId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->restoreSnapshot($snapshotId);
|
||||||
|
if (($result['status'] ?? 'error') !== 'success') {
|
||||||
|
$message = $result['message'] ?? 'Snapshot restore failed.';
|
||||||
|
|
||||||
|
return $this->errorResult($message, [
|
||||||
|
'operation' => 'restore',
|
||||||
|
'snapshot' => $snapshotId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = $result['manifest'] ?? [];
|
||||||
|
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? null;
|
||||||
|
|
||||||
|
$this->setProgress('complete', sprintf('Snapshot %s restored.', $snapshotId), 100, [
|
||||||
|
'operation' => 'restore',
|
||||||
|
'snapshot' => $snapshotId,
|
||||||
|
'version' => $version,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->jobManifestPath) {
|
||||||
|
$this->updateJob([
|
||||||
|
'result' => [
|
||||||
|
'status' => 'success',
|
||||||
|
'snapshot' => $snapshotId,
|
||||||
|
'version' => $version,
|
||||||
|
'manifest' => $manifest,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'snapshot' => $snapshotId,
|
||||||
|
'version' => $version,
|
||||||
|
'manifest' => $manifest,
|
||||||
|
'context' => $this->buildStatusContext(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve current progress payload.
|
* Retrieve current progress payload.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -852,6 +852,9 @@ PLUGIN_ADMIN:
|
|||||||
RESTORE_GRAV_NONE: "No safe upgrade snapshots are currently available."
|
RESTORE_GRAV_NONE: "No safe upgrade snapshots are currently available."
|
||||||
RESTORE_GRAV_INVALID: "Select at least one snapshot before continuing."
|
RESTORE_GRAV_INVALID: "Select at least one snapshot before continuing."
|
||||||
RESTORE_GRAV_SUCCESS: "Snapshot %s restored (Grav %s)."
|
RESTORE_GRAV_SUCCESS: "Snapshot %s restored (Grav %s)."
|
||||||
|
RESTORE_GRAV_SUCCESS_MESSAGE: "Snapshot %1$s restored (Grav %2$s)."
|
||||||
|
RESTORE_GRAV_SUCCESS_SIMPLE: "Snapshot %s restored."
|
||||||
|
RESTORE_GRAV_RUNNING: "Restoring snapshot %s..."
|
||||||
RESTORE_GRAV_FAILED: "Unable to restore the selected snapshot."
|
RESTORE_GRAV_FAILED: "Unable to restore the selected snapshot."
|
||||||
RESTORE_GRAV_DELETE_SUCCESS: "%d snapshot(s) deleted."
|
RESTORE_GRAV_DELETE_SUCCESS: "%d snapshot(s) deleted."
|
||||||
RESTORE_GRAV_DELETE_FAILED: "Failed to delete one or more snapshots."
|
RESTORE_GRAV_DELETE_FAILED: "Failed to delete one or more snapshots."
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
import './logs';
|
import './logs';
|
||||||
|
import './restore';
|
||||||
|
|||||||
139
themes/grav/app/tools/restore.js
Normal file
139
themes/grav/app/tools/restore.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
import { config, translations } from 'grav-config';
|
||||||
|
import request from '../utils/request';
|
||||||
|
import toastr from '../utils/toastr';
|
||||||
|
|
||||||
|
const paramSep = config.param_sep;
|
||||||
|
const task = `task${paramSep}`;
|
||||||
|
const nonce = `admin-nonce${paramSep}${config.admin_nonce}`;
|
||||||
|
const base = `${config.base_url_relative}/update.json`;
|
||||||
|
|
||||||
|
const urls = {
|
||||||
|
restore: `${base}/${task}safeUpgradeRestore/${nonce}`,
|
||||||
|
status: `${base}/${task}safeUpgradeStatus/${nonce}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
class RestoreManager {
|
||||||
|
constructor() {
|
||||||
|
this.job = null;
|
||||||
|
this.pollTimer = null;
|
||||||
|
|
||||||
|
$(document).on('click', '[data-restore-snapshot]', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const button = $(event.currentTarget);
|
||||||
|
if (this.job) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.startRestore(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startRestore(button) {
|
||||||
|
const snapshot = button.data('restore-snapshot');
|
||||||
|
if (!snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.prop('disabled', true).addClass('is-loading');
|
||||||
|
|
||||||
|
const body = { snapshot };
|
||||||
|
request(urls.restore, { method: 'post', body }, (response) => {
|
||||||
|
button.prop('disabled', false).removeClass('is-loading');
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
toastr.error(translations.PLUGIN_ADMIN?.RESTORE_GRAV_FAILED || 'Snapshot restore failed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 'error') {
|
||||||
|
toastr.error(response.message || translations.PLUGIN_ADMIN?.RESTORE_GRAV_FAILED || 'Snapshot restore failed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data || {};
|
||||||
|
const jobId = data.job_id || (data.job && data.job.id);
|
||||||
|
if (!jobId) {
|
||||||
|
const message = response.message || translations.PLUGIN_ADMIN?.RESTORE_GRAV_FAILED || 'Snapshot restore failed.';
|
||||||
|
toastr.error(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.job = {
|
||||||
|
id: jobId,
|
||||||
|
snapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const runningMessage = translations.PLUGIN_ADMIN?.RESTORE_GRAV_RUNNING
|
||||||
|
? translations.PLUGIN_ADMIN.RESTORE_GRAV_RUNNING.replace('%s', snapshot)
|
||||||
|
: `Restoring snapshot ${snapshot}...`;
|
||||||
|
toastr.info(runningMessage);
|
||||||
|
this.schedulePoll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePoll(delay = 1200) {
|
||||||
|
this.clearPoll();
|
||||||
|
this.pollTimer = setTimeout(() => this.pollStatus(), delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPoll() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearTimeout(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pollStatus() {
|
||||||
|
if (!this.job) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobId = this.job.id;
|
||||||
|
request(`${urls.status}?job=${encodeURIComponent(jobId)}`, { silentErrors: true }, (response) => {
|
||||||
|
if (!response || response.status !== 'success') {
|
||||||
|
this.schedulePoll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data || {};
|
||||||
|
const job = data.job || {};
|
||||||
|
const progress = data.progress || {};
|
||||||
|
|
||||||
|
const stage = progress.stage || null;
|
||||||
|
const status = job.status || progress.status || null;
|
||||||
|
|
||||||
|
if (stage === 'error' || status === 'error') {
|
||||||
|
const message = job.error || progress.message || translations.PLUGIN_ADMIN?.RESTORE_GRAV_FAILED || 'Snapshot restore failed.';
|
||||||
|
toastr.error(message);
|
||||||
|
this.job = null;
|
||||||
|
this.clearPoll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stage === 'complete' || status === 'success') {
|
||||||
|
const snapshot = progress.snapshot || this.job.snapshot;
|
||||||
|
const version = (job.result && job.result.version) || progress.version || '';
|
||||||
|
let successMessage;
|
||||||
|
if (translations.PLUGIN_ADMIN?.RESTORE_GRAV_SUCCESS_MESSAGE && version) {
|
||||||
|
successMessage = translations.PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS_MESSAGE.replace('%1$s', snapshot).replace('%2$s', version);
|
||||||
|
} else if (translations.PLUGIN_ADMIN?.RESTORE_GRAV_SUCCESS_SIMPLE) {
|
||||||
|
successMessage = translations.PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS_SIMPLE.replace('%s', snapshot);
|
||||||
|
} else {
|
||||||
|
successMessage = version ? `Snapshot ${snapshot} restored (Grav ${version}).` : `Snapshot ${snapshot} restored.`;
|
||||||
|
}
|
||||||
|
toastr.success(successMessage);
|
||||||
|
this.job = null;
|
||||||
|
this.clearPoll();
|
||||||
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedulePoll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize restore manager when tools view loads.
|
||||||
|
$(document).ready(() => {
|
||||||
|
new RestoreManager();
|
||||||
|
});
|
||||||
4866
themes/grav/js/admin.min.js
vendored
4866
themes/grav/js/admin.min.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
|||||||
{% do assets.addInlineCss(' \
|
{% do assets.addInlineCss(' \
|
||||||
.restore-grav-content {\n padding-bottom: 2rem;\n}\n\n.restore-grav-intro {\n margin-left: 1.5rem;\n margin-bottom: 1.5rem;\n}\n\n.restore-grav-table {\n margin-bottom: 1.5rem;\n}\n\n.restore-grav-table code {\n white-space: nowrap;\n}\n\n.restore-grav-content .button-bar {\n margin-bottom: 1rem;\n}\n') %}
|
.restore-grav-content {\n padding-bottom: 2rem;\n}\n\n.restore-grav-intro {\n margin-left: 1.5rem;\n margin-bottom: 1.5rem;\n}\n\n.restore-grav-table {\n margin-bottom: 1.5rem;\n}\n\n.restore-grav-table code {\n white-space: nowrap;\n}\n\n.restore-grav-content .button-bar {\n margin-bottom: 1rem;\n}\n') %}
|
||||||
|
|
||||||
|
{% do assets.addInlineCss(' \
|
||||||
|
.restore-grav-content {\n padding-bottom: 2rem;\n}\n\n.restore-grav-intro {\n margin-left: 1.5rem;\n margin-bottom: 1.5rem;\n}\n\n.restore-grav-table {\n margin-bottom: 1.5rem;\n}\n\n.restore-grav-table code {\n white-space: nowrap;\n}\n\n.restore-grav-content .button-bar {\n margin-bottom: 1rem;\n}\n') %}
|
||||||
|
|
||||||
<h1>{{ "PLUGIN_ADMIN.RESTORE_GRAV"|t }}</h1>
|
<h1>{{ "PLUGIN_ADMIN.RESTORE_GRAV"|t }}</h1>
|
||||||
|
|
||||||
<div class="restore-grav-content">
|
<div class="restore-grav-content">
|
||||||
@@ -44,12 +47,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
<form action="{{ admin_route('/tools/restore-grav') }}" method="post" class="inline-form">
|
<button type="button" class="button" data-restore-snapshot="{{ snapshot.id }}">{{ "PLUGIN_ADMIN.RESTORE_GRAV_RESTORE_BUTTON"|t }}</button>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user