Add frontend backup upload and list-based restore with validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -116,6 +116,140 @@
|
||||
});
|
||||
loadCtpFoldState();
|
||||
|
||||
function initBackupPanel() {
|
||||
var uploadBtn = document.getElementById('backup-upload-btn');
|
||||
var uploadInput = document.getElementById('backup-upload-file');
|
||||
var statusEl = document.getElementById('backup-restore-status');
|
||||
var pollTimer = null;
|
||||
|
||||
function setRestoreStatus(data) {
|
||||
if (!statusEl || !data) return;
|
||||
var state = data.state || 'idle';
|
||||
statusEl.hidden = state === 'idle';
|
||||
statusEl.classList.remove('is-running', 'is-error', 'is-done');
|
||||
if (state === 'pending' || state === 'running') {
|
||||
statusEl.classList.add('is-running');
|
||||
statusEl.textContent = data.message || '恢复进行中…';
|
||||
} else if (state === 'done') {
|
||||
statusEl.classList.add('is-done');
|
||||
statusEl.textContent = data.message || '恢复完成';
|
||||
} else if (state === 'error') {
|
||||
statusEl.classList.add('is-error');
|
||||
statusEl.textContent = '恢复失败:' + (data.message || '未知错误');
|
||||
} else {
|
||||
statusEl.textContent = data.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
function setBusy(busy) {
|
||||
document.querySelectorAll('[data-backup-restore], #backup-upload-btn').forEach(function (btn) {
|
||||
btn.disabled = !!busy;
|
||||
});
|
||||
}
|
||||
|
||||
function pollRestoreStatus() {
|
||||
fetch('/api/backup/restore/status', { credentials: 'same-origin' })
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
setRestoreStatus(data);
|
||||
var active = data.state === 'pending' || data.state === 'running';
|
||||
setBusy(active);
|
||||
if (active) {
|
||||
if (!pollTimer) {
|
||||
pollTimer = window.setInterval(pollRestoreStatus, 2500);
|
||||
}
|
||||
} else if (pollTimer) {
|
||||
window.clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
if (data.state === 'done') {
|
||||
window.setTimeout(function () { window.location.reload(); }, 1200);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
if (uploadBtn && uploadInput && !uploadBtn.dataset.settingsBound) {
|
||||
uploadBtn.dataset.settingsBound = '1';
|
||||
uploadBtn.addEventListener('click', function () {
|
||||
var file = uploadInput.files && uploadInput.files[0];
|
||||
if (!file) {
|
||||
window.alert('请先选择 .tar.gz 备份文件');
|
||||
return;
|
||||
}
|
||||
if (!/\.tar\.gz$/i.test(file.name)) {
|
||||
window.alert('仅支持 .tar.gz 格式');
|
||||
return;
|
||||
}
|
||||
var form = new FormData();
|
||||
form.append('file', file);
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = '上传中…';
|
||||
fetch('/api/backup/upload', { method: 'POST', body: form, credentials: 'same-origin' })
|
||||
.then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
|
||||
.then(function (result) {
|
||||
if (!result.ok) {
|
||||
throw new Error((result.body && result.body.error) || '上传失败');
|
||||
}
|
||||
window.alert('上传成功:' + (result.body.name || file.name));
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(function (err) {
|
||||
window.alert(err.message || '上传失败');
|
||||
})
|
||||
.finally(function () {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = '上传并校验';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-backup-restore]').forEach(function (btn) {
|
||||
if (btn.dataset.settingsBound) return;
|
||||
btn.dataset.settingsBound = '1';
|
||||
btn.addEventListener('click', function () {
|
||||
var name = btn.getAttribute('data-backup-restore') || '';
|
||||
if (!name) return;
|
||||
var ok = window.confirm(
|
||||
'确定要恢复备份「' + name + '」吗?\n\n'
|
||||
+ '将停止服务并覆盖当前数据库、uploads 与 .env,完成后自动重启。\n'
|
||||
+ '此操作不可撤销,请确认已做好当前数据备份。'
|
||||
);
|
||||
if (!ok) return;
|
||||
var typed = window.prompt('请输入 RESTORE 确认恢复:');
|
||||
if (typed !== 'RESTORE') {
|
||||
if (typed !== null) window.alert('确认文字不正确,已取消');
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
fetch('/api/backup/restore', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: name, confirm: 'RESTORE' })
|
||||
})
|
||||
.then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
|
||||
.then(function (result) {
|
||||
if (!result.ok) {
|
||||
throw new Error((result.body && result.body.error) || '恢复启动失败');
|
||||
}
|
||||
setRestoreStatus({ state: 'pending', message: result.body.message || '恢复已开始…' });
|
||||
setBusy(true);
|
||||
pollRestoreStatus();
|
||||
})
|
||||
.catch(function (err) {
|
||||
window.alert(err.message || '恢复启动失败');
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (statusEl && !statusEl.hidden) {
|
||||
pollRestoreStatus();
|
||||
}
|
||||
}
|
||||
initBackupPanel();
|
||||
|
||||
var ctpForm = document.getElementById('ctp-settings-form');
|
||||
if (ctpForm && !ctpForm.dataset.settingsBound) {
|
||||
ctpForm.dataset.settingsBound = '1';
|
||||
|
||||
Reference in New Issue
Block a user