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
自动备份
加载中...
恢复数据
恢复操作将覆盖当前数据,此操作不可逆。
"""
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)