中控备份支持上传 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,
|
create_backup as create_hub_backup,
|
||||||
list_backups as list_hub_backups,
|
list_backups as list_hub_backups,
|
||||||
restore_backup as restore_hub_backup,
|
restore_backup as restore_hub_backup,
|
||||||
|
save_uploaded_backup as save_hub_uploaded_backup,
|
||||||
start_backup_scheduler as start_hub_backup_scheduler,
|
start_backup_scheduler as start_hub_backup_scheduler,
|
||||||
)
|
)
|
||||||
from lib.bridge_client_lib import (
|
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)
|
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"])
|
@app.route("/api/settings/backup/instances", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_backup_instances():
|
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(
|
def maybe_run_auto_backup(
|
||||||
*,
|
*,
|
||||||
root_dir: str,
|
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() {
|
function boot() {
|
||||||
const createBtn = document.getElementById("hub-backup-create");
|
const createBtn = document.getElementById("hub-backup-create");
|
||||||
const hubRefresh = document.getElementById("hub-backup-refresh");
|
const hubRefresh = document.getElementById("hub-backup-refresh");
|
||||||
|
const uploadInput = document.getElementById("hub-backup-upload");
|
||||||
const clientRefresh = document.getElementById("client-backup-refresh");
|
const clientRefresh = document.getElementById("client-backup-refresh");
|
||||||
const sel = document.getElementById("client-backup-instance");
|
const sel = document.getElementById("client-backup-instance");
|
||||||
if (createBtn) createBtn.addEventListener("click", createHubBackup);
|
if (createBtn) createBtn.addEventListener("click", createHubBackup);
|
||||||
if (hubRefresh) hubRefresh.addEventListener("click", loadHubList);
|
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 (clientRefresh) clientRefresh.addEventListener("click", loadClientList);
|
||||||
if (sel) sel.addEventListener("change", loadClientList);
|
if (sel) sel.addEventListener("change", loadClientList);
|
||||||
const hubBody = document.getElementById("hub-backup-body");
|
const hubBody = document.getElementById("hub-backup-body");
|
||||||
|
|||||||
@@ -103,10 +103,14 @@
|
|||||||
<div class="backup-block">
|
<div class="backup-block">
|
||||||
<div class="backup-block-head">
|
<div class="backup-block-head">
|
||||||
<h3>中控备份</h3>
|
<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">
|
<div class="settings-actions">
|
||||||
<button type="button" class="settings-primary" id="hub-backup-create">立即备份</button>
|
<button type="button" class="settings-primary" id="hub-backup-create">立即备份</button>
|
||||||
<button type="button" class="btn-secondary" id="hub-backup-refresh">刷新</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>
|
<span class="muted" id="hub-backup-status" style="font-size:.8rem"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% 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 src="{{ url_for('static', filename='settings_update.js') }}?v=5"></script>
|
||||||
<script>
|
<script>
|
||||||
HubSettingsBackup.boot();
|
HubSettingsBackup.boot();
|
||||||
|
|||||||
Reference in New Issue
Block a user