""" DB 자동 백업 유틸리티. 전략: - 앱 시작 시 1일 1회만 백업 (last_backup_date 설정 키로 가드) - 사용자 폴더(`~/.clockout_backups/`)에 회전 보관 (기본 7개) - SQLite의 안전한 백업 API(sqlite3.Connection.backup) 사용 — 락 안전 """ from __future__ import annotations import os import sqlite3 from datetime import date from pathlib import Path from typing import Optional from core.settings_keys import LAST_BACKUP_DATE DEFAULT_BACKUP_DIR = Path.home() / '.clockout_backups' DEFAULT_KEEP = 7 def backup_db_if_needed(db, source_path: str = "database.db", backup_dir: Optional[Path] = None, keep: int = DEFAULT_KEEP) -> Optional[Path]: """오늘 첫 실행이면 백업 1개 만들고 오래된 것 회전. Args: db: Database 인스턴스 (set_setting/get_setting 사용) source_path: 원본 DB 파일 경로 backup_dir: 백업 저장 디렉토리 (기본 ~/.clockout_backups) keep: 보관할 백업 개수 Returns: 생성된 백업 파일 경로, 또는 이미 오늘 백업했으면 None """ today = date.today().isoformat() if db.get_setting(LAST_BACKUP_DATE, '') == today: return None src = Path(source_path) if not src.exists(): return None target_dir = backup_dir or DEFAULT_BACKUP_DIR target_dir.mkdir(parents=True, exist_ok=True) target = target_dir / f"database-{today}.db" # SQLite 백업 API: WAL/락 환경에서도 안전한 일관성 있는 복사 src_conn = sqlite3.connect(str(src)) try: dest_conn = sqlite3.connect(str(target)) try: src_conn.backup(dest_conn) finally: dest_conn.close() finally: src_conn.close() db.set_setting(LAST_BACKUP_DATE, today) _rotate(target_dir, keep) return target def _rotate(directory: Path, keep: int) -> None: """오래된 백업 제거 — 최신 keep개만 유지""" files = sorted( (p for p in directory.glob('database-*.db') if p.is_file()), key=lambda p: p.stat().st_mtime, reverse=True, ) for old in files[keep:]: try: old.unlink() except OSError as e: # 회전 실패 시 로그만 — 다음 실행에 재시도. 누적 시 디스크 압박 가능. from utils.debug_log import dlog dlog(f"backup rotation failed for {old}: {e}")