02ca76ea7c
Co-authored-by: Cursor <cursoragent@cursor.com>
241 lines
8.1 KiB
JavaScript
241 lines
8.1 KiB
JavaScript
(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);
|
|
}
|
|
}
|
|
|
|
async function uploadHubBackup(file) {
|
|
if (!file) return;
|
|
setText("hub-backup-status", "上传中…");
|
|
try {
|
|
const fd = new FormData();
|
|
fd.append("file", file, file.name);
|
|
const r = await fetch("/api/settings/backup/hub/upload", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
body: fd,
|
|
});
|
|
const j = await r.json();
|
|
if (!j.ok) throw new Error(j.msg || "上传失败");
|
|
setText("hub-backup-status", j.msg || ("已上传 " + (j.name || "")));
|
|
await loadHubList();
|
|
} catch (e) {
|
|
setText("hub-backup-status", e.message || "上传失败", true);
|
|
}
|
|
}
|
|
|
|
function boot() {
|
|
const createBtn = document.getElementById("hub-backup-create");
|
|
const hubRefresh = document.getElementById("hub-backup-refresh");
|
|
const uploadInput = document.getElementById("hub-backup-upload");
|
|
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 (uploadInput) {
|
|
uploadInput.addEventListener("change", function () {
|
|
const f = uploadInput.files && uploadInput.files[0];
|
|
uploadInput.value = "";
|
|
void uploadHubBackup(f);
|
|
});
|
|
}
|
|
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);
|