Files
intotheeast-com-content/plugins/admin/themes/grav/app/tools/restore.js
T

565 lines
18 KiB
JavaScript

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}`,
snapshot: `${base}/${task}safeUpgradeSnapshot/${nonce}`,
status: `${base}/${task}safeUpgradeStatus/${nonce}`,
};
const NICETIME_PERIODS_SHORT = [
'NICETIME.SEC',
'NICETIME.MIN',
'NICETIME.HR',
'NICETIME.DAY',
'NICETIME.WK',
'NICETIME.MO',
'NICETIME.YR',
'NICETIME.DEC'
];
const NICETIME_PERIODS_LONG = [
'NICETIME.SECOND',
'NICETIME.MINUTE',
'NICETIME.HOUR',
'NICETIME.DAY',
'NICETIME.WEEK',
'NICETIME.MONTH',
'NICETIME.YEAR',
'NICETIME.DECADE'
];
const NICETIME_LENGTHS = [60, 60, 24, 7, 4.35, 12, 10];
const FAST_UPDATE_THRESHOLD_SECONDS = 60;
const FAST_REFRESH_INTERVAL_MS = 1000;
const SLOW_REFRESH_INTERVAL_MS = 60000;
const NICETIME_TRANSLATION_ROOTS = ['GRAV_CORE', 'GRAV'];
const NICETIME_BASE_FALLBACKS = {
SECOND: 'second',
MINUTE: 'minute',
HOUR: 'hour',
DAY: 'day',
WEEK: 'week',
MONTH: 'month',
YEAR: 'year',
DECADE: 'decade',
SEC: 'sec',
MIN: 'min',
HR: 'hr',
WK: 'wk',
MO: 'mo',
YR: 'yr',
DEC: 'dec'
};
const NICETIME_PLURAL_FALLBACKS = {
SECOND: 'seconds',
MINUTE: 'minutes',
HOUR: 'hours',
DAY: 'days',
WEEK: 'weeks',
MONTH: 'months',
YEAR: 'years',
DECADE: 'decades',
SEC: 'secs',
MIN: 'mins',
HR: 'hrs',
WK: 'wks',
MO: 'mos',
YR: 'yrs',
DEC: 'decs'
};
const getTranslationKey = (key) => {
for (const root of NICETIME_TRANSLATION_ROOTS) {
const catalog = translations[root];
if (catalog && Object.prototype.hasOwnProperty.call(catalog, key)) {
const value = catalog[key];
if (typeof value === 'string' && value.trim() !== '') {
return value;
}
}
}
return undefined;
};
const nicetimeHasKey = (key) => typeof getTranslationKey(key) === 'string';
const nicetimeTranslate = (key, fallback) => {
const value = getTranslationKey(key);
if (typeof value === 'string' && value.length) {
return value;
}
return fallback;
};
const getFallbackForPeriodKey = (key) => {
const normalized = key.replace(/^GRAV\./, '');
const period = normalized.replace(/^NICETIME\./, '');
const plural = /_PLURAL/.test(period);
const baseKey = period.replace(/_PLURAL(_MORE_THAN_TWO)?$/, '');
const base = NICETIME_BASE_FALLBACKS[baseKey] || baseKey.toLowerCase();
if (!plural) {
return base;
}
const pluralKey = NICETIME_PLURAL_FALLBACKS[baseKey];
if (pluralKey) {
return pluralKey;
}
if (base.endsWith('y')) {
return `${base.slice(0, -1)}ies`;
}
if (base.endsWith('s')) {
return `${base}es`;
}
return `${base}s`;
};
const parseTimestampValue = (input) => {
if (input instanceof Date) {
return Math.floor(input.getTime() / 1000);
}
if (typeof input === 'number' && Number.isFinite(input)) {
return input > 1e12 ? Math.floor(input / 1000) : Math.floor(input);
}
if (typeof input === 'string') {
const trimmed = input.trim();
if (!trimmed) {
return null;
}
const numeric = Number(trimmed);
if (!Number.isNaN(numeric) && trimmed === String(numeric)) {
return numeric > 1e12 ? Math.floor(numeric / 1000) : Math.floor(numeric);
}
const parsed = Date.parse(trimmed);
if (!Number.isNaN(parsed)) {
return Math.floor(parsed / 1000);
}
}
return null;
};
const computeNicetime = (input, { longStrings = false, showTense = false } = {}) => {
if (input === null || input === undefined || input === '') {
return nicetimeTranslate('NICETIME.NO_DATE_PROVIDED', 'No date provided');
}
const unixDate = parseTimestampValue(input);
if (unixDate === null) {
return nicetimeTranslate('NICETIME.BAD_DATE', 'Bad date');
}
const now = Math.floor(Date.now() / 1000);
const periods = (longStrings ? NICETIME_PERIODS_LONG : NICETIME_PERIODS_SHORT).slice();
let difference;
let tense;
if (now > unixDate) {
difference = now - unixDate;
tense = nicetimeTranslate('NICETIME.AGO', 'ago');
} else if (now === unixDate) {
difference = 0;
tense = nicetimeTranslate('NICETIME.JUST_NOW', 'just now');
} else {
difference = unixDate - now;
tense = nicetimeTranslate('NICETIME.FROM_NOW', 'from now');
}
if (now === unixDate) {
return tense;
}
let index = 0;
while (index < NICETIME_LENGTHS.length - 1 && difference >= NICETIME_LENGTHS[index]) {
difference /= NICETIME_LENGTHS[index];
index += 1;
}
difference = Math.round(difference);
let periodKey = periods[index];
if (difference !== 1) {
periodKey += '_PLURAL';
const moreThanTwoKey = `${periodKey}_MORE_THAN_TWO`;
if (difference > 2 && nicetimeHasKey(moreThanTwoKey)) {
periodKey = moreThanTwoKey;
}
}
const labelFallback = periodKey.split('.').pop().toLowerCase();
const fallbackLabel = getFallbackForPeriodKey(periodKey) || labelFallback;
const periodLabel = nicetimeTranslate(periodKey, fallbackLabel);
const timeString = `${difference} ${periodLabel}`;
return showTense ? `${timeString} ${tense}` : timeString;
};
const parseBoolAttribute = (element, attributeName, defaultValue = false) => {
const rawValue = element.getAttribute(attributeName);
if (rawValue === null) {
return defaultValue;
}
const normalized = rawValue.trim().toLowerCase();
if (normalized === '') {
return true;
}
return ['1', 'true', 'yes', 'on'].includes(normalized);
};
const initialiseNicetimeUpdater = () => {
const selector = '[data-nicetime-timestamp]';
if (!document.querySelector(selector)) {
return null;
}
const update = () => {
const nowSeconds = Math.floor(Date.now() / 1000);
let youngestAge = Infinity;
document.querySelectorAll(selector).forEach((element) => {
const timestamp = element.getAttribute('data-nicetime-timestamp');
const longStrings = parseBoolAttribute(element, 'data-nicetime-long', false);
const showTense = parseBoolAttribute(element, 'data-nicetime-tense', false);
const unixTimestamp = parseTimestampValue(timestamp);
if (unixTimestamp !== null) {
const age = Math.max(0, nowSeconds - unixTimestamp);
if (age < youngestAge) {
youngestAge = age;
}
}
const updated = computeNicetime(timestamp, { longStrings, showTense });
if (updated && element.textContent !== updated) {
element.textContent = updated;
}
});
return youngestAge;
};
let timerId = null;
const scheduleNext = (lastAge) => {
const useFastInterval = Number.isFinite(lastAge) && lastAge < FAST_UPDATE_THRESHOLD_SECONDS;
const delay = useFastInterval ? FAST_REFRESH_INTERVAL_MS : SLOW_REFRESH_INTERVAL_MS;
timerId = window.setTimeout(() => {
const nextAge = update();
scheduleNext(nextAge);
}, delay);
};
const destroy = () => {
if (timerId !== null) {
window.clearTimeout(timerId);
timerId = null;
}
};
const initialAge = update();
scheduleNext(initialAge);
window.addEventListener('beforeunload', destroy, { once: true });
return { update, destroy };
};
class RestoreManager {
constructor() {
this.job = null;
this.pollTimer = null;
this.pollFailures = 0;
$(document).on('click', '[data-restore-snapshot]', (event) => {
event.preventDefault();
const button = $(event.currentTarget);
if (this.job) {
return;
}
this.startRestore(button);
});
$(document).on('click', '[data-create-snapshot]', (event) => {
event.preventDefault();
const button = $(event.currentTarget);
if (this.job) {
return;
}
this.startSnapshot(button);
});
}
startSnapshot(button) {
let label = null;
if (typeof window !== 'undefined' && window.prompt) {
const promptMessage = translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_PROMPT || 'Enter an optional snapshot label';
const input = window.prompt(promptMessage, '');
if (input === null) {
return;
}
label = input.trim();
if (label === '') {
label = null;
}
}
button.prop('disabled', true).addClass('is-loading');
const body = {};
if (label) {
body.label = label;
}
request(urls.snapshot, { method: 'post', body }, (response) => {
button.prop('disabled', false).removeClass('is-loading');
if (!response) {
toastr.error(translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_FAILED || 'Snapshot creation failed.');
return;
}
if (response.status === 'error') {
toastr.error(response.message || translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_FAILED || 'Snapshot creation 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_SNAPSHOT_FAILED || 'Snapshot creation failed.';
toastr.error(message);
return;
}
this.job = {
id: jobId,
operation: 'snapshot',
snapshot: null,
label
};
this.pollFailures = 0;
const descriptor = label || jobId;
const runningMessage = translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_RUNNING
? translations.PLUGIN_ADMIN.RESTORE_GRAV_SNAPSHOT_RUNNING.replace('%s', descriptor)
: 'Creating snapshot...';
toastr.info(runningMessage);
this.schedulePoll();
});
}
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,
operation: 'restore',
};
this.pollFailures = 0;
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;
let handled = false;
request(`${urls.status}?job=${encodeURIComponent(jobId)}`, { silentErrors: true }, (response) => {
handled = true;
this.pollFailures = 0;
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;
const operation = progress.operation || this.job.operation || null;
if (!this.job.snapshot && progress.snapshot) {
this.job.snapshot = progress.snapshot;
} else if (!this.job.snapshot && job.result && job.result.snapshot) {
this.job.snapshot = job.result.snapshot;
}
if (!this.job.label && progress.label) {
this.job.label = progress.label;
} else if (!this.job.label && job.result && job.result.label) {
this.job.label = job.result.label;
}
if (stage === 'error' || status === 'error') {
const message = job.error || progress.message || (operation === 'snapshot'
? translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_FAILED || 'Snapshot creation failed.'
: translations.PLUGIN_ADMIN?.RESTORE_GRAV_FAILED || 'Snapshot restore failed.');
toastr.error(message);
this.job = null;
this.clearPoll();
return;
}
if (stage === 'complete' || status === 'success') {
if (operation === 'snapshot') {
const snapshotId = progress.snapshot || (job.result && job.result.snapshot) || this.job.snapshot || '';
const labelValue = progress.label || (job.result && job.result.label) || this.job.label || '';
let displayName = labelValue || snapshotId || (translations.PLUGIN_ADMIN?.RESTORE_GRAV_TABLE_SNAPSHOT || 'snapshot');
if (labelValue && snapshotId && labelValue !== snapshotId) {
displayName = `${labelValue} (${snapshotId})`;
}
const successMessage = translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_SUCCESS
? translations.PLUGIN_ADMIN.RESTORE_GRAV_SNAPSHOT_SUCCESS.replace('%s', displayName)
: (snapshotId ? `Snapshot ${displayName} created.` : 'Snapshot created.');
toastr.success(successMessage);
this.job = null;
this.clearPoll();
setTimeout(() => window.location.reload(), 1500);
return;
}
const snapshotId = progress.snapshot || this.job.snapshot || '';
const labelValue = progress.label || (job.result && job.result.label) || this.job.label || '';
let snapshotDisplay = snapshotId || labelValue;
if (labelValue && snapshotId && labelValue !== snapshotId) {
snapshotDisplay = `${labelValue} (${snapshotId})`;
} else if (!snapshotDisplay) {
snapshotDisplay = translations.PLUGIN_ADMIN?.RESTORE_GRAV_TABLE_SNAPSHOT || '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', snapshotDisplay).replace('%2$s', version);
} else if (translations.PLUGIN_ADMIN?.RESTORE_GRAV_SUCCESS_SIMPLE) {
successMessage = translations.PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS_SIMPLE.replace('%s', snapshotDisplay);
} else {
successMessage = version ? `Snapshot ${snapshotDisplay} restored (Grav ${version}).` : `Snapshot ${snapshotDisplay} restored.`;
}
toastr.success(successMessage);
this.job = null;
this.clearPoll();
setTimeout(() => window.location.reload(), 1500);
return;
}
this.schedulePoll();
}).then(() => {
if (!handled) {
this.handleSilentFailure();
}
});
}
handleSilentFailure() {
if (!this.job) {
return;
}
this.pollFailures += 1;
const operation = this.job.operation || 'restore';
const snapshot = this.job.snapshot || '';
if (this.pollFailures >= 3) {
let message;
if (operation === 'snapshot') {
message = translations.PLUGIN_ADMIN?.RESTORE_GRAV_SNAPSHOT_FALLBACK || 'Snapshot creation may have completed. Reloading...';
} else {
message = snapshot
? `Snapshot ${snapshot} restore is completing. Reloading...`
: 'Snapshot restore is completing. Reloading...';
}
toastr.info(message);
this.job = null;
this.clearPoll();
setTimeout(() => window.location.reload(), 1500);
return;
}
const delay = Math.min(5000, 1200 * this.pollFailures);
this.schedulePoll(delay);
}
}
// Initialize restore manager when tools view loads.
$(document).ready(() => {
initialiseNicetimeUpdater();
new RestoreManager();
});