| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- """备份 + WebDAV 推送模块"""
- import shutil
- import time
- from datetime import datetime
- from pathlib import Path
- from app.database import get_connection
- def full_backup(config) -> Path:
- """全量备份:bank.db + 全部 docx 文件"""
- import shutil
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
- backup_dir = config.backups_dir / ts
- backup_dir.mkdir(parents=True, exist_ok=True)
- # bank.db
- if config.db_path.exists():
- shutil.copy2(config.db_path, backup_dir / "bank.db")
- # 全部 docx
- if config.problems_dir.exists():
- dst_dir = backup_dir / "problems"
- dst_dir.mkdir(parents=True, exist_ok=True)
- for docx in config.problems_dir.glob("*.docx"):
- shutil.copy2(docx, dst_dir / docx.name)
- # 推送到 WebDAV
- if config.webdav_enabled:
- try:
- _push_to_webdav(backup_dir, config)
- except Exception as e:
- print(f"⚠ WebDAV 推送失败: {e}")
- _cleanup_old_backups(config)
- print(f"✓ 全量备份: {backup_dir}")
- return backup_dir
- def manual_backup(config, remote: bool = False):
- """手动备份"""
- backup_dir = _create_backup(config)
- print(f"✓ 本地备份: {backup_dir}")
- if remote and config.webdav_enabled:
- _push_to_webdav(backup_dir, config)
- print("✓ WebDAV 推送完成")
- def auto_backup(config, changed_files: list[str] | None = None):
- """自动备份(操作前调用)"""
- backup_dir = _create_backup(config, changed_files)
- # 清理旧备份
- _cleanup_old_backups(config)
- # 如果配置了 WebDAV,尝试推送
- if config.webdav_enabled:
- try:
- _push_to_webdav(backup_dir, config)
- except Exception as e:
- print(f"⚠ WebDAV 推送失败: {e}")
- def _create_backup(config, changed_files: list[str] | None = None) -> Path:
- """创建备份快照"""
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
- backup_dir = config.backups_dir / ts
- backup_dir.mkdir(parents=True, exist_ok=True)
- # 备份数据库
- if config.db_path.exists():
- shutil.copy2(config.db_path, backup_dir / "bank.db")
- # 备份被修改的 docx
- if changed_files:
- for f in changed_files:
- src = Path(f) if Path(f).is_absolute() else config.data_dir / f
- if src.exists():
- dst = backup_dir / src.parent.name / src.name
- dst.parent.mkdir(parents=True, exist_ok=True)
- shutil.copy2(src, dst)
- return backup_dir
- def _cleanup_old_backups(config):
- """保留最近 N 个备份"""
- backups = sorted(config.backups_dir.glob("*"), key=lambda p: p.stat().st_mtime)
- while len(backups) > config.max_backups:
- oldest = backups.pop(0)
- if oldest.is_dir():
- shutil.rmtree(oldest)
- else:
- oldest.unlink()
- def _push_to_webdav(backup_dir: Path, config):
- """推送备份到 WebDAV 服务器"""
- # 使用标准库实现简单的 WebDAV PUT 上传
- import urllib.request
- import base64
- remote_base = config.webdav_url.rstrip('/')
- remote_path = config.webdav_remote_path.rstrip('/')
- auth = base64.b64encode(f"{config.webdav_username}:{config.webdav_password}".encode()).decode()
- for file_path in backup_dir.rglob("*"):
- if file_path.is_file():
- rel_path = file_path.relative_to(backup_dir)
- url = f"{remote_base}{remote_path}/{backup_dir.name}/{rel_path}"
- # 创建目录(MKCOL)
- parent_url = '/'.join(url.split('/')[:-1])
- _webdav_mkcol(parent_url, auth)
- # 上传文件
- with open(file_path, 'rb') as f:
- data = f.read()
- req = urllib.request.Request(url, data=data, method='PUT')
- req.add_header('Authorization', f'Basic {auth}')
- req.add_header('Content-Type', 'application/octet-stream')
- try:
- urllib.request.urlopen(req, timeout=30)
- except Exception as e:
- print(f"⚠ 上传失败 {rel_path}: {e}")
- def _webdav_mkcol(url: str, auth: str):
- """递归创建 WebDAV 目录"""
- import urllib.request
- parts = url.split('/')
- # 从根开始逐级创建
- for i in range(3, len(parts) + 1):
- partial = '/'.join(parts[:i])
- req = urllib.request.Request(partial, method='MKCOL')
- req.add_header('Authorization', f'Basic {auth}')
- try:
- urllib.request.urlopen(req, timeout=10)
- except Exception:
- pass # 目录可能已存在
- def get_backup_history(config) -> list[dict]:
- """获取备份历史"""
- backups = []
- for d in sorted(config.backups_dir.glob("*"), key=lambda p: p.stat().st_mtime, reverse=True):
- if d.is_dir():
- files = list(d.rglob("*"))
- backups.append({
- "name": d.name,
- "time": datetime.fromtimestamp(d.stat().st_mtime).isoformat(),
- "files": len(files),
- })
- return backups
|