8ebe1a3c77
Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
3.7 KiB
Python
103 lines
3.7 KiB
Python
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
|
"""HTTP routes for backup module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from flask import jsonify, request, send_file
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def register(deps) -> None:
|
|
app = deps.app
|
|
login_required = deps.login_required
|
|
get_setting = deps.get_setting
|
|
|
|
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
|
|
def api_backup_list():
|
|
return jsonify(
|
|
{
|
|
"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):
|
|
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))
|
|
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())
|