558bd8153f
架构: - engine/server.py: asyncio SOCKS5 服务器引擎(含隧道/认证/限速) - engine/instances.py: 多实例管理器(独立事件循环/热启动/热停止) - services/user_service.py: 用户认证/流量统计/IP白名单/防爆破 - services/stats_service.py: 实时监控/流量趋势/连接追踪 - services/backup_service.py: 数据库备份与恢复 - api/v1.py: 完整 RESTful API - web/routes.py: Web 管理面板(暗色 Bootstrap 5 主题) 功能覆盖: ✅ 多实例 SOCKS5 管理(创建/启动/停止/重启/热加载) ✅ 用户认证(无认证/账号密码/流量上限/限速/并发限制) ✅ IP 白名单/黑名单/防爆破 ✅ 实时仪表盘(CPU/内存/网络/活跃连接) ✅ 流量统计(30天趋势图/用户排名/实例统计) ✅ 活跃连接追踪 ✅ 审计日志(事件/用户/IP 筛选) ✅ 数据库备份与恢复 ✅ 管理员密码保护 ✅ 16 项功能测试全部通过
109 lines
3.4 KiB
Python
109 lines
3.4 KiB
Python
"""备份与恢复服务。"""
|
|
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()
|