backup.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. """备份 + WebDAV 推送模块"""
  2. import shutil
  3. import time
  4. from datetime import datetime
  5. from pathlib import Path
  6. from app.database import get_connection
  7. def manual_backup(config, remote: bool = False):
  8. """手动备份"""
  9. backup_dir = _create_backup(config)
  10. print(f"✓ 本地备份: {backup_dir}")
  11. if remote and config.webdav_enabled:
  12. _push_to_webdav(backup_dir, config)
  13. print("✓ WebDAV 推送完成")
  14. def auto_backup(config, changed_files: list[str] | None = None):
  15. """自动备份(操作前调用)"""
  16. backup_dir = _create_backup(config, changed_files)
  17. # 清理旧备份
  18. _cleanup_old_backups(config)
  19. # 如果配置了 WebDAV,尝试推送
  20. if config.webdav_enabled:
  21. try:
  22. _push_to_webdav(backup_dir, config)
  23. except Exception as e:
  24. print(f"⚠ WebDAV 推送失败: {e}")
  25. def _create_backup(config, changed_files: list[str] | None = None) -> Path:
  26. """创建备份快照"""
  27. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  28. backup_dir = config.backups_dir / ts
  29. backup_dir.mkdir(parents=True, exist_ok=True)
  30. # 备份数据库
  31. if config.db_path.exists():
  32. shutil.copy2(config.db_path, backup_dir / "bank.db")
  33. # 备份被修改的 docx
  34. if changed_files:
  35. for f in changed_files:
  36. src = Path(f) if Path(f).is_absolute() else config.data_dir / f
  37. if src.exists():
  38. dst = backup_dir / src.parent.name / src.name
  39. dst.parent.mkdir(parents=True, exist_ok=True)
  40. shutil.copy2(src, dst)
  41. return backup_dir
  42. def _cleanup_old_backups(config):
  43. """保留最近 N 个备份"""
  44. backups = sorted(config.backups_dir.glob("*"), key=lambda p: p.stat().st_mtime)
  45. while len(backups) > config.max_backups:
  46. oldest = backups.pop(0)
  47. if oldest.is_dir():
  48. shutil.rmtree(oldest)
  49. else:
  50. oldest.unlink()
  51. def _push_to_webdav(backup_dir: Path, config):
  52. """推送备份到 WebDAV 服务器"""
  53. # 使用标准库实现简单的 WebDAV PUT 上传
  54. import urllib.request
  55. import base64
  56. remote_base = config.webdav_url.rstrip('/')
  57. remote_path = config.webdav_remote_path.rstrip('/')
  58. auth = base64.b64encode(f"{config.webdav_username}:{config.webdav_password}".encode()).decode()
  59. for file_path in backup_dir.rglob("*"):
  60. if file_path.is_file():
  61. rel_path = file_path.relative_to(backup_dir)
  62. url = f"{remote_base}{remote_path}/{backup_dir.name}/{rel_path}"
  63. # 创建目录(MKCOL)
  64. parent_url = '/'.join(url.split('/')[:-1])
  65. _webdav_mkcol(parent_url, auth)
  66. # 上传文件
  67. with open(file_path, 'rb') as f:
  68. data = f.read()
  69. req = urllib.request.Request(url, data=data, method='PUT')
  70. req.add_header('Authorization', f'Basic {auth}')
  71. req.add_header('Content-Type', 'application/octet-stream')
  72. try:
  73. urllib.request.urlopen(req, timeout=30)
  74. except Exception as e:
  75. print(f"⚠ 上传失败 {rel_path}: {e}")
  76. def _webdav_mkcol(url: str, auth: str):
  77. """递归创建 WebDAV 目录"""
  78. import urllib.request
  79. parts = url.split('/')
  80. # 从根开始逐级创建
  81. for i in range(3, len(parts) + 1):
  82. partial = '/'.join(parts[:i])
  83. req = urllib.request.Request(partial, method='MKCOL')
  84. req.add_header('Authorization', f'Basic {auth}')
  85. try:
  86. urllib.request.urlopen(req, timeout=10)
  87. except Exception:
  88. pass # 目录可能已存在
  89. def get_backup_history(config) -> list[dict]:
  90. """获取备份历史"""
  91. backups = []
  92. for d in sorted(config.backups_dir.glob("*"), key=lambda p: p.stat().st_mtime, reverse=True):
  93. if d.is_dir():
  94. files = list(d.rglob("*"))
  95. backups.append({
  96. "name": d.name,
  97. "time": datetime.fromtimestamp(d.stat().st_mtime).isoformat(),
  98. "files": len(files),
  99. })
  100. return backups