"""备份与恢复服务。""" import logging import os import shutil import sqlite3 from datetime import datetime, timezone from models import Backup from database import db log = logging.getLogger("socks.backup") _BACKUP_DIR = None def set_backup_dir(path): global _BACKUP_DIR _BACKUP_DIR = os.path.abspath(path) os.makedirs(_BACKUP_DIR, exist_ok=True) def get_backup_dir(): return _BACKUP_DIR or os.path.join(os.path.dirname(__file__), "..", "backups") def create_backup(note=""): """创建数据库备份。""" global _BACKUP_DIR if not _BACKUP_DIR: _BACKUP_DIR = get_backup_dir() os.makedirs(_BACKUP_DIR, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"sm_backup_{ts}.db" dest = os.path.join(_BACKUP_DIR, filename) db_uri = db.engine.url.render_as_string(hide_password=False) if db_uri.startswith("sqlite:///"): src = db_uri.replace("sqlite:///", "", 1) if not src.startswith("/"): src = os.path.join(os.path.dirname(__file__), "..", src) shutil.copy2(src, dest) else: # 其他数据库类型需要不同处理 log.warning("不支持的数据库类型备份: %s", db_uri) return {"error": "不支持的数据库类型"} size = os.path.getsize(dest) with db.app.app_context(): b = Backup(filename=filename, path=dest, size=size, note=note) db.session.add(b) db.session.commit() log.info("备份完成: %s (%.1f MB)", filename, size / 1024 / 1024) return {"filename": filename, "path": dest, "size": size} def list_backups(): """列出所有备份。""" with db.app.app_context(): backups = db.session.query(Backup).order_by(Backup.created_at.desc()).all() return [{ "id": b.id, "filename": b.filename, "path": b.path, "size_mb": round(b.size / 1024 / 1024, 1), "created_at": b.created_at.isoformat(), "note": b.note, "exists": os.path.exists(b.path), } for b in backups] def restore_backup(backup_id): """从备份恢复。""" with db.app.app_context(): b = db.session.query(Backup).get(backup_id) if not b or not os.path.exists(b.path): return {"error": "备份文件不存在"} db_uri = db.engine.url.render_as_string(hide_password=False) if not db_uri.startswith("sqlite:///"): return {"error": "不支持的数据库类型"} src = b.path dest = db_uri.replace("sqlite:///", "", 1) if not dest.startswith("/"): dest = os.path.join(os.path.dirname(__file__), "..", dest) # 先备份当前数据库 if os.path.exists(dest): backup_current = f"{dest}.pre_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}" shutil.copy2(dest, backup_current) shutil.copy2(src, dest) log.info("从备份恢复: %s -> %s", b.filename, dest) return {"ok": True, "restored_from": b.filename} def cleanup_old_backups(keep=10): """清理旧备份,保留最近 N 份。""" with db.app.app_context(): backups = db.session.query(Backup).order_by(Backup.created_at.desc()).all() for b in backups[keep:]: if os.path.exists(b.path): os.remove(b.path) db.session.delete(b) log.info("删除旧备份: %s", b.filename) db.session.commit()