中控备份支持上传 tar.gz 后再恢复,便于换机迁移。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,6 +36,7 @@ from lib.backup_lib import (
|
||||
create_backup as create_hub_backup,
|
||||
list_backups as list_hub_backups,
|
||||
restore_backup as restore_hub_backup,
|
||||
save_uploaded_backup as save_hub_uploaded_backup,
|
||||
start_backup_scheduler as start_hub_backup_scheduler,
|
||||
)
|
||||
from lib.bridge_client_lib import (
|
||||
@@ -397,6 +398,16 @@ def api_hub_backup_restore():
|
||||
return jsonify(result), (200 if result.get("ok") else 400)
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/hub/upload", methods=["POST"])
|
||||
@login_required
|
||||
def api_hub_backup_upload():
|
||||
fs = request.files.get("file")
|
||||
if fs is None or not getattr(fs, "filename", None):
|
||||
return jsonify({"ok": False, "msg": "请选择备份文件"}), 400
|
||||
result = save_hub_uploaded_backup(fs)
|
||||
return jsonify(result), (200 if result.get("ok") else 400)
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/instances", methods=["GET"])
|
||||
@login_required
|
||||
def api_backup_instances():
|
||||
|
||||
@@ -175,6 +175,62 @@ def restore_backup(name: str, *, root_dir: str, env_path: str, db_path: str) ->
|
||||
}
|
||||
|
||||
|
||||
_MAX_UPLOAD_BYTES = 32 * 1024 * 1024
|
||||
|
||||
|
||||
def save_uploaded_backup(file_storage: Any) -> dict[str, Any]:
|
||||
"""把本机下载的中控备份包写入备份目录,不自动恢复."""
|
||||
filename = os.path.basename(str(getattr(file_storage, "filename", "") or "").strip())
|
||||
safe = _safe_backup_name(filename)
|
||||
if not safe:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "文件名须为 crypto_okx_hub_backup_YYYYMMDD_HHMMSS.tar.gz",
|
||||
}
|
||||
dest = os.path.join(backup_dir(), safe)
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".tar.gz", prefix="hub_up_")
|
||||
os.close(tmp_fd)
|
||||
try:
|
||||
file_storage.save(tmp_path)
|
||||
total = os.path.getsize(tmp_path)
|
||||
if total <= 0:
|
||||
os.remove(tmp_path)
|
||||
return {"ok": False, "msg": "上传文件为空"}
|
||||
if total > _MAX_UPLOAD_BYTES:
|
||||
os.remove(tmp_path)
|
||||
return {"ok": False, "msg": "备份文件超过 32MB"}
|
||||
with tarfile.open(tmp_path, "r:gz") as tar:
|
||||
names = tar.getnames()
|
||||
has_env = any(n.replace("\\", "/").rstrip("/") in (".env", "./.env") for n in names)
|
||||
has_db = any(
|
||||
n.replace("\\", "/").startswith("database/") and n.replace("\\", "/") != "database/"
|
||||
for n in names
|
||||
)
|
||||
if not has_env and not has_db:
|
||||
os.remove(tmp_path)
|
||||
return {"ok": False, "msg": "不是中控备份包(缺少 .env / database)"}
|
||||
shutil.move(tmp_path, dest)
|
||||
except tarfile.TarError:
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
return {"ok": False, "msg": "无法读取 tar.gz"}
|
||||
except Exception as exc:
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
return {"ok": False, "msg": str(exc) or "上传失败"}
|
||||
st = os.stat(dest)
|
||||
return {
|
||||
"ok": True,
|
||||
"name": safe,
|
||||
"size": int(st.st_size),
|
||||
"msg": "已上传,请在列表中点恢复",
|
||||
}
|
||||
|
||||
|
||||
def maybe_run_auto_backup(
|
||||
*,
|
||||
root_dir: str,
|
||||
|
||||
@@ -187,13 +187,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -103,10 +103,14 @@
|
||||
<div class="backup-block">
|
||||
<div class="backup-block-head">
|
||||
<h3>中控备份</h3>
|
||||
<p class="muted field-hint">目录 <code id="hub-backup-dir">/root/crypto_okx_hub_backups</code> · 含 <code>.env</code> 与 <code>hub.db</code></p>
|
||||
<p class="muted field-hint">目录 <code id="hub-backup-dir">/root/crypto_okx_hub_backups</code> · 含 <code>.env</code> 与 <code>hub.db</code>。换机时先上传家里下载的包,再点恢复。</p>
|
||||
<div class="settings-actions">
|
||||
<button type="button" class="settings-primary" id="hub-backup-create">立即备份</button>
|
||||
<button type="button" class="btn-secondary" id="hub-backup-refresh">刷新</button>
|
||||
<label class="btn-secondary" style="cursor:pointer;display:inline-block">
|
||||
上传备份
|
||||
<input type="file" id="hub-backup-upload" accept=".gz,.tar.gz,application/gzip" hidden>
|
||||
</label>
|
||||
<span class="muted" id="hub-backup-status" style="font-size:.8rem"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,7 +188,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='settings_backup.js') }}?v=2"></script>
|
||||
<script src="{{ url_for('static', filename='settings_backup.js') }}?v=3"></script>
|
||||
<script src="{{ url_for('static', filename='settings_update.js') }}?v=5"></script>
|
||||
<script>
|
||||
HubSettingsBackup.boot();
|
||||
|
||||
Reference in New Issue
Block a user