Add frontend backup upload and list-based restore with validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+68
-44
@@ -3,55 +3,31 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
import logging
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
session,
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
from flask import jsonify, request, send_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register(deps) -> None:
|
||||
app = deps.app
|
||||
login_required = deps.login_required
|
||||
require_nav = deps.require_nav
|
||||
get_db = deps.get_db
|
||||
get_setting = deps.get_setting
|
||||
set_setting = deps.set_setting
|
||||
fetch_price = deps.fetch_price
|
||||
send_wechat_msg = deps.send_wechat_msg
|
||||
touch_stats_cache = deps.touch_stats_cache
|
||||
get_stats_data = deps.get_stats_data
|
||||
build_market_quote_payload = deps.build_market_quote_payload
|
||||
today_str = deps.today_str
|
||||
expire_old_plans = deps.expire_old_plans
|
||||
TZ = deps.tz
|
||||
DB_PATH = deps.db_path
|
||||
UPLOAD_DIR = deps.upload_dir
|
||||
OPEN_TYPES = deps.open_types
|
||||
EXIT_TRIGGERS = deps.exit_triggers
|
||||
BEHAVIOR_TAGS = deps.behavior_tags
|
||||
KLINE_PERIODS = deps.kline_periods
|
||||
KLINE_CUTOFFS = deps.kline_cutoffs
|
||||
calc_holding_duration = deps.calc_holding_duration
|
||||
holding_to_minutes = deps.holding_to_minutes
|
||||
classify_close_result = deps.classify_close_result
|
||||
calc_rr_ratio = deps.calc_rr_ratio
|
||||
calc_theoretical_pnl = deps.calc_theoretical_pnl
|
||||
parse_review_date_filter = deps.parse_review_date_filter
|
||||
_trading_mode = deps.trading_mode
|
||||
_ua_is_phone = deps.ua_is_phone
|
||||
_static_asset_v = deps.static_asset_v
|
||||
|
||||
from modules.backup.db_backup import list_backups, resolve_backup_file
|
||||
from modules.backup.db_backup import (
|
||||
RESTORE_CONFIRM_TOKEN,
|
||||
backup_dir,
|
||||
backup_in_progress,
|
||||
get_backup_last_at,
|
||||
get_restore_status,
|
||||
inspect_backup_archive,
|
||||
list_backups,
|
||||
resolve_backup_file,
|
||||
restore_in_progress,
|
||||
save_uploaded_backup,
|
||||
schedule_restore,
|
||||
)
|
||||
|
||||
@app.route("/api/backup/list")
|
||||
@login_required
|
||||
@@ -61,18 +37,66 @@ def register(deps) -> None:
|
||||
"dir": str(backup_dir()),
|
||||
"last_at": get_backup_last_at(get_setting),
|
||||
"running": backup_in_progress(),
|
||||
"restore": get_restore_status(),
|
||||
"items": list_backups(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/backup/download/<filename>")
|
||||
@login_required
|
||||
def api_backup_download(filename):
|
||||
from flask import send_file
|
||||
|
||||
try:
|
||||
path = resolve_backup_file(filename)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
return jsonify({"error": str(exc)}), 404
|
||||
return send_file(path, as_attachment=True, download_name=path.name)
|
||||
|
||||
@app.route("/api/backup/upload", methods=["POST"])
|
||||
@login_required
|
||||
def api_backup_upload():
|
||||
if backup_in_progress():
|
||||
return jsonify({"error": "备份进行中,请稍后再试"}), 409
|
||||
if restore_in_progress():
|
||||
return jsonify({"error": "恢复进行中,请稍后再试"}), 409
|
||||
upload = request.files.get("file")
|
||||
if not upload or not upload.filename:
|
||||
return jsonify({"error": "请选择备份文件"}), 400
|
||||
if not upload.filename.lower().endswith(".tar.gz"):
|
||||
return jsonify({"error": "仅支持 .tar.gz 备份包"}), 400
|
||||
try:
|
||||
name, info = save_uploaded_backup(upload.stream, upload.filename)
|
||||
return jsonify({"ok": True, "name": name, "info": info})
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
except Exception:
|
||||
logger.exception("backup upload failed")
|
||||
return jsonify({"error": "上传失败,请检查备份包是否完整"}), 500
|
||||
|
||||
@app.route("/api/backup/info/<filename>")
|
||||
@login_required
|
||||
def api_backup_info(filename):
|
||||
try:
|
||||
path = resolve_backup_file(filename)
|
||||
return jsonify(inspect_backup_archive(path, check_backend=True))
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
return jsonify({"error": str(exc)}), 404
|
||||
|
||||
@app.route("/api/backup/restore", methods=["POST"])
|
||||
@login_required
|
||||
def api_backup_restore():
|
||||
data = request.get_json(silent=True) or {}
|
||||
filename = (data.get("filename") or request.form.get("filename") or "").strip()
|
||||
confirm = (data.get("confirm") or request.form.get("confirm") or "").strip()
|
||||
if confirm != RESTORE_CONFIRM_TOKEN:
|
||||
return jsonify({"error": "请确认恢复操作"}), 400
|
||||
if not filename:
|
||||
return jsonify({"error": "缺少备份文件名"}), 400
|
||||
ok, msg = schedule_restore(filename)
|
||||
if ok:
|
||||
return jsonify({"ok": True, "message": msg}), 202
|
||||
return jsonify({"error": msg}), 409
|
||||
|
||||
@app.route("/api/backup/restore/status")
|
||||
@login_required
|
||||
def api_backup_restore_status():
|
||||
return jsonify(get_restore_status())
|
||||
|
||||
Reference in New Issue
Block a user