import os import tarfile import datetime import threading import time import json import shutil from flask import Flask, render_template_string, request, jsonify from huggingface_hub import HfApi app = Flask(__name__) SOURCE_MOUNT = "/mnt/data" BACKUP_DATASET = "sjfs/memos-backup-data" CONFIG_FILE = "/tmp/auto_backup_config.json" auto_interval_seconds = 86400 _last_backup_time = None _auto_stop_event = threading.Event() _auto_thread = None def load_config(): global auto_interval_seconds try: with open(CONFIG_FILE, 'r') as f: config = json.load(f) auto_interval_seconds = config.get('interval_seconds', 86400) except (FileNotFoundError, json.JSONDecodeError, OSError): pass def save_config(): try: with open(CONFIG_FILE, 'w') as f: json.dump({'interval_seconds': auto_interval_seconds}, f) except OSError: pass HTML_TEMPLATE = """ Memos Backup

Memos Backup

{{ source_path }}目标 {{ backup_dataset }}
手动备份
自动备份
加载中...
备份记录
加载中...
恢复数据
恢复操作将覆盖当前数据,此操作不可逆。
""" def get_backup_files(): try: api = HfApi() repo_info = api.repo_info(repo_id=BACKUP_DATASET, repo_type="dataset") siblings = getattr(repo_info, 'siblings', []) backups = [] for f in siblings: if f.rfilename.startswith("backup_") and f.rfilename.endswith(".tar.gz"): backups.append({ "name": f.rfilename, "date": f.rfilename.replace("backup_", "").replace(".tar.gz", ""), "size": getattr(f, 'size', 0) or 0 }) backups.sort(key=lambda x: x["name"], reverse=True) return backups except Exception as e: print(f"获取备份列表失败: {e}") return [] def create_backup(): global _last_backup_time timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_name = f"backup_{timestamp}.tar.gz" backup_path = f"/tmp/{backup_name}" try: with tarfile.open(backup_path, "w:gz") as tar: tar.add(SOURCE_MOUNT, arcname="memos_data") api = HfApi() api.upload_file( path_or_fileobj=backup_path, path_in_repo=backup_name, repo_id=BACKUP_DATASET, repo_type="dataset" ) os.remove(backup_path) _last_backup_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") return {"success": True, "backup_name": backup_name} except Exception as e: return {"success": False, "error": str(e)} def safe_extract_tar(tar, path): for member in tar.getmembers(): if member.name.startswith('/') or '..' in member.name.split('/'): raise ValueError(f"不安全的路径: {member.name}") member_path = os.path.join(path, member.name) if not os.path.abspath(member_path).startswith(os.path.abspath(path) + os.sep): raise ValueError(f"不安全的路径: {member.name}") tar.extractall(path) @app.route('/') def index(): interval = auto_interval_seconds if interval % 86400 == 0: interval_value = interval // 86400 interval_unit = 'day' elif interval % 3600 == 0: interval_value = interval // 3600 interval_unit = 'hour' else: interval_value = interval // 60 interval_unit = 'minute' return render_template_string( HTML_TEMPLATE, source_path=SOURCE_MOUNT, backup_dataset=BACKUP_DATASET, auto_interval_value=interval_value, auto_interval_unit=interval_unit, last_backup_time=_last_backup_time or '尚未备份' ) @app.route('/api/backups') def list_backups(): return jsonify(get_backup_files()) @app.route('/api/backup', methods=['POST']) def do_backup(): return jsonify(create_backup()) @app.route('/api/restore', methods=['POST']) def do_restore(): data = request.json backup_name = data.get('backup_name') if not backup_name: return jsonify({"success": False, "error": "未指定备份文件"}) try: api = HfApi() backup_path = f"/tmp/{backup_name}" api.hf_hub_download( repo_id=BACKUP_DATASET, filename=backup_name, local_dir="/tmp", repo_type="dataset" ) with tarfile.open(backup_path, "r:gz") as tar: safe_extract_tar(tar, "/tmp/restore") for item in os.listdir("/tmp/restore/memos_data"): src = f"/tmp/restore/memos_data/{item}" dst = f"{SOURCE_MOUNT}/{item}" if os.path.isdir(src): if os.path.exists(dst): shutil.rmtree(dst) shutil.copytree(src, dst) else: shutil.copy2(src, dst) os.remove(backup_path) shutil.rmtree("/tmp/restore", ignore_errors=True) return jsonify({"success": True, "message": f"已从 {backup_name} 恢复"}) except Exception as e: return jsonify({"success": False, "error": str(e)}) @app.route('/api/set_interval', methods=['POST']) def set_interval(): global auto_interval_seconds data = request.json new_interval = data.get('interval_seconds') if not new_interval or new_interval < 60: return jsonify({"success": False, "error": "间隔不能小于 60 秒"}) auto_interval_seconds = new_interval save_config() restart_auto_backup() return jsonify({"success": True, "message": f"自动备份间隔已设为 {new_interval // 60} 分钟"}) @app.route('/api/auto_status') def auto_status(): next_time = datetime.datetime.now() + datetime.timedelta(seconds=auto_interval_seconds) return jsonify({ "enabled": True, "interval_seconds": auto_interval_seconds, "next_backup": next_time.strftime("%Y-%m-%d %H:%M:%S"), "last_backup": _last_backup_time }) def auto_backup_worker(): while not _auto_stop_event.is_set(): if _auto_stop_event.wait(timeout=auto_interval_seconds): break with app.app_context(): result = create_backup() print(f"[{datetime.datetime.now()}] 自动备份: {result}") def start_auto_backup_thread(): global _auto_thread _auto_stop_event.clear() _auto_thread = threading.Thread(target=auto_backup_worker, daemon=True) _auto_thread.start() def restart_auto_backup(): _auto_stop_event.set() if _auto_thread and _auto_thread.is_alive(): _auto_thread.join(timeout=5) start_auto_backup_thread() load_config() start_auto_backup_thread() if __name__ == '__main__': app.run(host='0.0.0.0', port=7860)