系统设置增加备份恢复:中控本机备份,并可下载各客户端备份。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
(function (global) {
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function fmtSize(n) {
|
||||
const v = Number(n) || 0;
|
||||
if (v < 1024) return v + " B";
|
||||
if (v < 1024 * 1024) return (v / 1024).toFixed(1) + " KB";
|
||||
return (v / (1024 * 1024)).toFixed(2) + " MB";
|
||||
}
|
||||
|
||||
function setText(id, msg, isErr) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = msg || "";
|
||||
el.style.color = isErr ? "var(--bad)" : "";
|
||||
}
|
||||
|
||||
function renderRows(tbodyId, items, downloadHrefFn, restoreAttr) {
|
||||
const body = document.getElementById(tbodyId);
|
||||
if (!body) return;
|
||||
if (!items || !items.length) {
|
||||
body.innerHTML = '<tr><td colspan="4" class="muted">暂无备份</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = items
|
||||
.map(function (it) {
|
||||
const name = it.name || "";
|
||||
const ops =
|
||||
'<a class="btn btn-sm" href="' +
|
||||
downloadHrefFn(name) +
|
||||
'">下载</a>' +
|
||||
(restoreAttr
|
||||
? ' <button type="button" class="btn btn-sm btn-secondary" data-restore-name="' +
|
||||
encodeURIComponent(name) +
|
||||
'">恢复</button>'
|
||||
: "");
|
||||
return (
|
||||
"<tr>" +
|
||||
"<td><code>" +
|
||||
escapeHtml(name) +
|
||||
"</code></td>" +
|
||||
"<td>" +
|
||||
escapeHtml(it.mtime_bj || "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(fmtSize(it.size)) +
|
||||
"</td>" +
|
||||
"<td style=\"white-space:nowrap\">" +
|
||||
ops +
|
||||
"</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function loadHubList() {
|
||||
const body = document.getElementById("hub-backup-body");
|
||||
if (body) body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/hub/list", { credentials: "same-origin" });
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "加载失败");
|
||||
const dir = document.getElementById("hub-backup-dir");
|
||||
if (dir && j.backup_dir) dir.textContent = j.backup_dir;
|
||||
renderRows(
|
||||
"hub-backup-body",
|
||||
j.items || [],
|
||||
function (name) {
|
||||
return "/api/settings/backup/hub/download/" + encodeURIComponent(name);
|
||||
},
|
||||
true
|
||||
);
|
||||
} catch (e) {
|
||||
if (body) {
|
||||
body.innerHTML =
|
||||
'<tr><td colspan="4" class="muted">' + escapeHtml(e.message || "加载失败") + "</td></tr>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createHubBackup() {
|
||||
setText("hub-backup-status", "备份中…");
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/hub/create", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "备份失败");
|
||||
setText("hub-backup-status", "已生成 " + (j.name || ""));
|
||||
await loadHubList();
|
||||
} catch (e) {
|
||||
setText("hub-backup-status", e.message || "备份失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreHub(name) {
|
||||
if (!name) return;
|
||||
if (!confirm("确定恢复该中控备份?\n将覆盖 .env 与 hub.db,建议恢复后重启。")) return;
|
||||
setText("hub-backup-status", "恢复中…");
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/hub/restore", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: name }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "恢复失败");
|
||||
setText("hub-backup-status", j.msg || "恢复完成");
|
||||
} catch (e) {
|
||||
setText("hub-backup-status", e.message || "恢复失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInstances() {
|
||||
const sel = document.getElementById("client-backup-instance");
|
||||
if (!sel) return;
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/instances", { credentials: "same-origin" });
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "加载实例失败");
|
||||
const items = j.items || [];
|
||||
sel.innerHTML =
|
||||
'<option value="">选择客户端</option>' +
|
||||
items
|
||||
.map(function (it) {
|
||||
return (
|
||||
'<option value="' +
|
||||
escapeHtml(String(it.id)) +
|
||||
'">' +
|
||||
escapeHtml(it.name || "#" + it.id) +
|
||||
(it.enabled ? "" : " (停用)") +
|
||||
"</option>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
} catch (e) {
|
||||
setText("client-backup-status", e.message || "加载实例失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClientList() {
|
||||
const sel = document.getElementById("client-backup-instance");
|
||||
const body = document.getElementById("client-backup-body");
|
||||
const iid = sel && sel.value;
|
||||
if (!iid) {
|
||||
if (body) body.innerHTML = '<tr><td colspan="4" class="muted">请选择客户端</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (body) body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
|
||||
setText("client-backup-status", "");
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/client/" + encodeURIComponent(iid) + "/list", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "加载失败");
|
||||
renderRows(
|
||||
"client-backup-body",
|
||||
j.items || [],
|
||||
function (name) {
|
||||
return (
|
||||
"/api/settings/backup/client/" +
|
||||
encodeURIComponent(iid) +
|
||||
"/download/" +
|
||||
encodeURIComponent(name)
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
} catch (e) {
|
||||
if (body) {
|
||||
body.innerHTML =
|
||||
'<tr><td colspan="4" class="muted">' + escapeHtml(e.message || "加载失败") + "</td></tr>";
|
||||
}
|
||||
setText("client-backup-status", e.message || "加载失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
function boot() {
|
||||
const createBtn = document.getElementById("hub-backup-create");
|
||||
const hubRefresh = document.getElementById("hub-backup-refresh");
|
||||
const clientRefresh = document.getElementById("client-backup-refresh");
|
||||
const sel = document.getElementById("client-backup-instance");
|
||||
if (createBtn) createBtn.addEventListener("click", createHubBackup);
|
||||
if (hubRefresh) hubRefresh.addEventListener("click", loadHubList);
|
||||
if (clientRefresh) clientRefresh.addEventListener("click", loadClientList);
|
||||
if (sel) sel.addEventListener("change", loadClientList);
|
||||
const hubBody = document.getElementById("hub-backup-body");
|
||||
if (hubBody) {
|
||||
hubBody.addEventListener("click", function (ev) {
|
||||
const btn = ev.target.closest("[data-restore-name]");
|
||||
if (!btn) return;
|
||||
void restoreHub(decodeURIComponent(btn.getAttribute("data-restore-name") || ""));
|
||||
});
|
||||
}
|
||||
void loadHubList();
|
||||
void loadInstances();
|
||||
}
|
||||
|
||||
global.HubSettingsBackup = { boot: boot };
|
||||
})(window);
|
||||
Reference in New Issue
Block a user