Add frontend backup upload and list-based restore with validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-02 16:03:18 +08:00
parent 481086eddc
commit 9379bc4f4f
7 changed files with 726 additions and 68 deletions
+134
View File
@@ -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';