"""每日 02:00 数据库备份 job —— 包装 backend/scripts/pg_backup.sh。 独立脚本可手动/CI 直跑;系统调度经此包装,保证与脚本同一份备份逻辑与轮转策略。 Windows 下须显式定位 Git Bash(PATH 里的 bash 可能是 WSL shim,mount/PATH 均不匹配), 脚本路径统一转 MSYS /d/... 格式传给子进程。 """ import asyncio import logging import shutil from pathlib import Path logger = logging.getLogger(__name__) # backend/app/utils/backup_job.py → parents[2] = backend/ _SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "pg_backup.sh" _GIT_BASH_CANDIDATES = ( Path(r"C:\Program Files\Git\bin\bash.exe"), Path(r"C:\Program Files\Git\usr\bin\bash.exe"), ) def _find_git_bash() -> str | None: for cand in _GIT_BASH_CANDIDATES: if cand.exists(): return str(cand) return shutil.which("bash") def _msys_path(p: Path) -> str: """D:/dpb/... → /d/dpb/...(Git Bash 挂载约定)。""" s = p.as_posix() if len(s) >= 2 and s[1] == ":": return f"/{s[0].lower()}{s[2:]}" return s async def run_daily_backup() -> bool: """每日备份:调用 pg_backup.sh(独立脚本,pg_dump 自定义格式 + N 天轮转)。""" script = _SCRIPT if not script.exists(): alt = Path.cwd() / "scripts" / "pg_backup.sh" script = alt if alt.exists() else script if not script.exists(): logger.error(f"备份脚本不存在: {script}") return False git_bash = _find_git_bash() if not git_bash: logger.error("未找到 Git Bash(C:/Program Files/Git/bin/bash.exe)") return False proc = await asyncio.create_subprocess_exec( git_bash, _msys_path(script), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if proc.returncode == 0: logger.info(f"每日备份完成: {stdout.decode().strip()}") return True logger.error(f"每日备份失败(rc={proc.returncode}): {stderr.decode()[:500]}") return False