系统设置增加备份恢复:中控本机备份,并可下载各客户端备份。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,11 +11,13 @@ from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from flask import (
|
||||
Flask,
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
@@ -27,8 +29,19 @@ if str(ROOT) not in sys.path:
|
||||
ENV_PATH = ROOT / ".env"
|
||||
load_dotenv(ENV_PATH)
|
||||
|
||||
from lib.backup_lib import (
|
||||
backup_dir as hub_backup_dir,
|
||||
backup_keep_days as hub_backup_keep_days,
|
||||
backup_path as hub_backup_path,
|
||||
create_backup as create_hub_backup,
|
||||
list_backups as list_hub_backups,
|
||||
restore_backup as restore_hub_backup,
|
||||
start_backup_scheduler as start_hub_backup_scheduler,
|
||||
)
|
||||
from lib.bridge_client_lib import (
|
||||
dispatch_cond_order_to_ids,
|
||||
fetch_backup_file,
|
||||
fetch_backup_list,
|
||||
fetch_cond_orders,
|
||||
post_close_full,
|
||||
post_cond_cancel,
|
||||
@@ -36,7 +49,7 @@ from lib.bridge_client_lib import (
|
||||
pull_all_enabled,
|
||||
)
|
||||
from lib.cash_sample_lib import record_cash_samples_from_overview
|
||||
from lib.db_lib import init_db
|
||||
from lib.db_lib import db_path as hub_db_path, init_db
|
||||
from lib.env_lib import apply_env_updates, env_get
|
||||
from lib.health_watch_lib import inspect_overview_rows
|
||||
from lib.instances_lib import (
|
||||
@@ -309,6 +322,116 @@ def settings_page():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/hub/list", methods=["GET"])
|
||||
@login_required
|
||||
def api_hub_backup_list():
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"items": list_hub_backups(),
|
||||
"backup_dir": hub_backup_dir(),
|
||||
"keep_days": hub_backup_keep_days(),
|
||||
"auto_hour_bj": 3,
|
||||
"includes": [".env", "database"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _resolved_hub_db_path() -> str:
|
||||
p = hub_db_path()
|
||||
if os.path.isabs(p):
|
||||
return p
|
||||
return str(ROOT / p)
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/hub/create", methods=["POST"])
|
||||
@login_required
|
||||
def api_hub_backup_create():
|
||||
try:
|
||||
result = create_hub_backup(
|
||||
root_dir=str(ROOT),
|
||||
env_path=str(ENV_PATH),
|
||||
db_path=_resolved_hub_db_path(),
|
||||
label="manual",
|
||||
)
|
||||
return jsonify(result)
|
||||
except Exception as exc:
|
||||
return jsonify({"ok": False, "msg": str(exc) or "备份失败"}), 500
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/hub/download/<name>", methods=["GET"])
|
||||
@login_required
|
||||
def api_hub_backup_download(name: str):
|
||||
path = hub_backup_path(name)
|
||||
if not path or not os.path.isfile(path):
|
||||
return jsonify({"ok": False, "msg": "备份不存在"}), 404
|
||||
return send_file(
|
||||
path,
|
||||
mimetype="application/gzip",
|
||||
as_attachment=True,
|
||||
download_name=os.path.basename(path),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/hub/restore", methods=["POST"])
|
||||
@login_required
|
||||
def api_hub_backup_restore():
|
||||
data = request.get_json(silent=True) or {}
|
||||
name = str(data.get("name") or "").strip()
|
||||
if not name:
|
||||
return jsonify({"ok": False, "msg": "缺少备份文件名"}), 400
|
||||
result = restore_hub_backup(
|
||||
name,
|
||||
root_dir=str(ROOT),
|
||||
env_path=str(ENV_PATH),
|
||||
db_path=_resolved_hub_db_path(),
|
||||
)
|
||||
return jsonify(result), (200 if result.get("ok") else 400)
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/instances", methods=["GET"])
|
||||
@login_required
|
||||
def api_backup_instances():
|
||||
items = [
|
||||
{
|
||||
"id": i["id"],
|
||||
"name": i.get("name") or f"#{i['id']}",
|
||||
"enabled": bool(i.get("enabled")),
|
||||
}
|
||||
for i in list_instances(enabled_only=False)
|
||||
]
|
||||
return jsonify({"ok": True, "items": items})
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/client/<int:iid>/list", methods=["GET"])
|
||||
@login_required
|
||||
def api_client_backup_list(iid: int):
|
||||
inst = get_instance(iid)
|
||||
if not inst:
|
||||
return jsonify({"ok": False, "msg": "实例不存在", "items": []}), 404
|
||||
result = fetch_backup_list(inst)
|
||||
code = 200 if result.get("ok") else 502
|
||||
return jsonify(result), code
|
||||
|
||||
|
||||
@app.route("/api/settings/backup/client/<int:iid>/download/<name>", methods=["GET"])
|
||||
@login_required
|
||||
def api_client_backup_download(iid: int, name: str):
|
||||
inst = get_instance(iid)
|
||||
if not inst:
|
||||
return jsonify({"ok": False, "msg": "实例不存在"}), 404
|
||||
ok, content, meta = fetch_backup_file(inst, name)
|
||||
if not ok or content is None:
|
||||
return jsonify({"ok": False, "msg": meta or "下载失败"}), 502
|
||||
return Response(
|
||||
content,
|
||||
mimetype="application/gzip",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{os.path.basename(meta)}"'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/desk")
|
||||
@login_required
|
||||
def desk():
|
||||
@@ -369,6 +492,16 @@ def start_health_watch() -> None:
|
||||
|
||||
start_health_watch()
|
||||
|
||||
try:
|
||||
start_hub_backup_scheduler(
|
||||
root_dir=str(ROOT),
|
||||
env_path=str(ENV_PATH),
|
||||
db_path_fn=_resolved_hub_db_path,
|
||||
hour=3,
|
||||
)
|
||||
except Exception as _hub_bak_err:
|
||||
print(f"[backup] scheduler: {_hub_bak_err}")
|
||||
|
||||
|
||||
@app.route("/api/instances/<int:iid>/open_url")
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user