backup.py 4.9 KB

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