dedicated endpoint for upgrade

This commit is contained in:
Andy Miller
2025-10-16 23:04:49 -06:00
parent 4a2b386b51
commit 51551b332f
4 changed files with 243 additions and 7 deletions

83
safe-upgrade-status.php Normal file
View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
$root = dirname(__DIR__, 3);
$jobsDir = $root . '/user/data/upgrades/jobs';
$fallbackProgress = $root . '/user/data/upgrades/safe-upgrade-progress.json';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
$jobId = isset($_GET['job']) ? (string)$_GET['job'] : '';
if ($jobId !== '' && !preg_match('/^job-[A-Za-z0-9\\-]+$/', $jobId)) {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => 'Invalid job identifier.',
]);
exit;
}
$readJson = static function (string $path): ?array {
if (!is_file($path)) {
return null;
}
$decoded = json_decode((string)file_get_contents($path), true);
return is_array($decoded) ? $decoded : null;
};
$progress = null;
$manifest = null;
if ($jobId !== '') {
$jobPath = $jobsDir . '/' . $jobId;
$progress = $readJson($jobPath . '/progress.json');
$manifest = $readJson($jobPath . '/manifest.json');
if (!$progress && !$manifest && !is_dir($jobPath)) {
$progress = $readJson($fallbackProgress) ?: [
'stage' => 'idle',
'message' => '',
'percent' => null,
'timestamp' => time(),
];
echo json_encode([
'status' => 'success',
'message' => 'Safe upgrade job not found.',
'data' => [
'job' => null,
'progress' => $progress,
],
]);
exit;
}
}
if ($progress === null) {
$progress = $readJson($fallbackProgress) ?: [
'stage' => 'idle',
'message' => '',
'percent' => null,
'timestamp' => time(),
];
}
if ($jobId !== '' && is_array($progress) && !isset($progress['job_id'])) {
$progress['job_id'] = $jobId;
}
echo json_encode([
'status' => 'success',
'data' => [
'job' => $manifest ?: null,
'progress' => $progress,
],
]);
exit;

View File

@@ -53,6 +53,9 @@ export default class SafeUpgrade {
this.isPolling = false;
this.active = false;
this.jobId = null;
this.statusFailures = 0;
this.directStatusUrl = this.resolveDirectStatusUrl();
this.preferDirectStatus = !!this.directStatusUrl;
this.registerEvents();
}
@@ -115,6 +118,8 @@ export default class SafeUpgrade {
open() {
this.active = true;
this.decisions = {};
this.statusFailures = 0;
this.preferDirectStatus = !!this.directStatusUrl;
this.renderLoading();
this.modal.open();
this.fetchPreflight();
@@ -405,6 +410,8 @@ export default class SafeUpgrade {
if (data.progress) {
this.renderProgress(data.progress);
}
this.statusFailures = 0;
this.preferDirectStatus = !!this.directStatusUrl;
this.beginPolling(1200);
} else {
this.renderResult(data);
@@ -413,6 +420,49 @@ export default class SafeUpgrade {
});
}
resolveDirectStatusUrl() {
const scriptPath = '/user/plugins/admin/safe-upgrade-status.php';
const join = (base, path) => {
if (!base) {
return path;
}
const trimmed = base.endsWith('/') ? base.slice(0, -1) : base;
return `${trimmed}${path}`;
};
const normalize = (url) => url.replace(/([^:]\/)\/+/g, '$1');
const candidates = [
config.base_url_simple || '',
(config.base_url_relative || '').replace(/\/admin\/?$/, ''),
''
];
for (const base of candidates) {
if (typeof base !== 'string') {
continue;
}
const candidate = normalize(join(base, scriptPath));
if (candidate) {
return candidate;
}
}
return scriptPath;
}
resolveStatusEndpoint() {
const useDirect = this.directStatusUrl && this.preferDirectStatus;
let url = useDirect ? this.directStatusUrl : this.urls.status;
if (this.jobId) {
url += (url.includes('?') ? '&' : '?') + `job=${encodeURIComponent(this.jobId)}`;
}
return {
url,
direct: useDirect
};
}
beginPolling(delay = 1200) {
if (this.isPolling) {
return;
@@ -448,14 +498,26 @@ export default class SafeUpgrade {
let jobComplete = false;
let jobFailed = false;
let shouldReload = false;
let handled = false;
console.debug('[SafeUpgrade] poll status');
const statusUrl = this.jobId ? `${this.urls.status}?job=${encodeURIComponent(this.jobId)}` : this.urls.status;
const endpoint = this.resolveStatusEndpoint();
const statusUrl = endpoint.url;
const usingDirect = endpoint.direct;
const requestOptions = { silentErrors: true };
this.statusRequest = request(statusUrl, (response) => {
this.statusRequest = request(statusUrl, requestOptions, (response) => {
console.debug('[SafeUpgrade] status response', response);
if (!response) {
this.statusFailures += 1;
return;
}
handled = true;
this.statusFailures = 0;
if (response.status === 'error') {
if (!silent) {
this.renderProgress({
@@ -513,7 +575,16 @@ export default class SafeUpgrade {
return;
}
if (jobFailed) {
if (!handled) {
if (usingDirect && this.statusFailures >= 3) {
this.preferDirectStatus = false;
this.statusFailures = 0;
this.schedulePoll();
} else {
const delay = Math.min(5000, 1200 * Math.max(1, this.statusFailures));
this.schedulePoll(delay);
}
} else if (jobFailed) {
this.stopPolling();
this.jobId = null;
} else if (jobComplete || nextStage === 'complete') {

View File

@@ -8,6 +8,11 @@ let request = function(url, options = {}, callback = () => true) {
options = {};
}
const silentErrors = !!options.silentErrors;
if (options.silentErrors) {
delete options.silentErrors;
}
if (options.method && options.method === 'post') {
let data = new FormData();
@@ -34,7 +39,16 @@ let request = function(url, options = {}, callback = () => true) {
.then(parseJSON)
.then(userFeedback)
.then((response) => callback(response, raw))
.catch(userFeedbackError);
.catch((error) => {
if (silentErrors) {
console.debug('[Request] silent failure', url, error);
return undefined;
}
userFeedbackError(error);
return undefined;
});
};
export default request;

View File

@@ -4609,6 +4609,9 @@ var SafeUpgrade = /*#__PURE__*/function () {
this.isPolling = false;
this.active = false;
this.jobId = null;
this.statusFailures = 0;
this.directStatusUrl = this.resolveDirectStatusUrl();
this.preferDirectStatus = !!this.directStatusUrl;
this.registerEvents();
}
return safe_upgrade_createClass(SafeUpgrade, [{
@@ -4671,6 +4674,8 @@ var SafeUpgrade = /*#__PURE__*/function () {
value: function open() {
this.active = true;
this.decisions = {};
this.statusFailures = 0;
this.preferDirectStatus = !!this.directStatusUrl;
this.renderLoading();
this.modal.open();
this.fetchPreflight();
@@ -4875,6 +4880,8 @@ var SafeUpgrade = /*#__PURE__*/function () {
if (data.progress) {
_this4.renderProgress(data.progress);
}
_this4.statusFailures = 0;
_this4.preferDirectStatus = !!_this4.directStatusUrl;
_this4.beginPolling(1200);
} else {
_this4.renderResult(data);
@@ -4882,6 +4889,46 @@ var SafeUpgrade = /*#__PURE__*/function () {
}
});
}
}, {
key: "resolveDirectStatusUrl",
value: function resolveDirectStatusUrl() {
var scriptPath = '/user/plugins/admin/safe-upgrade-status.php';
var join = function join(base, path) {
if (!base) {
return path;
}
var trimmed = base.endsWith('/') ? base.slice(0, -1) : base;
return "".concat(trimmed).concat(path);
};
var normalize = function normalize(url) {
return url.replace(/([^:]\/)\/+/g, '$1');
};
var candidates = [external_GravAdmin_namespaceObject.config.base_url_simple || '', (external_GravAdmin_namespaceObject.config.base_url_relative || '').replace(/\/admin\/?$/, ''), ''];
for (var i = 0; i < candidates.length; i += 1) {
var base = candidates[i];
if (typeof base !== 'string') {
continue;
}
var candidate = normalize(join(base, scriptPath));
if (candidate) {
return candidate;
}
}
return scriptPath;
}
}, {
key: "resolveStatusEndpoint",
value: function resolveStatusEndpoint() {
var useDirect = this.directStatusUrl && this.preferDirectStatus;
var url = useDirect ? this.directStatusUrl : this.urls.status;
if (this.jobId) {
url += (url.indexOf('?') !== -1 ? '&' : '?') + "job=".concat(encodeURIComponent(this.jobId));
}
return {
url: url,
direct: useDirect
};
}
}, {
key: "beginPolling",
value: function beginPolling() {
@@ -4926,10 +4973,22 @@ var SafeUpgrade = /*#__PURE__*/function () {
var jobComplete = false;
var jobFailed = false;
var shouldReload = false;
var handled = false;
console.debug('[SafeUpgrade] poll status');
var statusUrl = this.jobId ? "".concat(this.urls.status, "?job=").concat(encodeURIComponent(this.jobId)) : this.urls.status;
this.statusRequest = utils_request(statusUrl, function (response) {
var endpoint = this.resolveStatusEndpoint();
var statusUrl = endpoint.url;
var usingDirect = endpoint.direct;
var requestOptions = {
silentErrors: true
};
this.statusRequest = utils_request(statusUrl, requestOptions, function (response) {
console.debug('[SafeUpgrade] status response', response);
if (!response) {
_this6.statusFailures += 1;
return;
}
handled = true;
_this6.statusFailures = 0;
if (response.status === 'error') {
if (!silent) {
_this6.renderProgress({
@@ -4980,7 +5039,16 @@ var SafeUpgrade = /*#__PURE__*/function () {
if (!_this6.isPolling) {
return;
}
if (jobFailed) {
if (!handled) {
if (usingDirect && _this6.statusFailures >= 3) {
_this6.preferDirectStatus = false;
_this6.statusFailures = 0;
_this6.schedulePoll();
} else {
var delay = Math.min(5000, 1200 * Math.max(1, _this6.statusFailures));
_this6.schedulePoll(delay);
}
} else if (jobFailed) {
_this6.stopPolling();
_this6.jobId = null;
} else if (jobComplete || nextStage === 'complete') {