""" Backup Module - Integration with Automation Daemon Manages backup operations, scheduling, and database integration """ from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional, Callable import json from loguru import logger from core.database import Database from modules.backup.backup_engine import TimeMachineBackup class BackupModule: """ Backup module for unified daemon Integrates TimeMachineBackup with database and scheduling system """ def __init__(self, db: Database): """ Initialize backup module Args: db: Database instance for CRUD operations """ self.db = db self.backup_engines: Dict[int, TimeMachineBackup] = {} logger.info("BackupModule initialized") def get_engine(self, profile_id: int) -> Optional[TimeMachineBackup]: """ Get or create backup engine for a profile Args: profile_id: Backup profile ID Returns: TimeMachineBackup instance or None if profile not found """ # Check cache if profile_id in self.backup_engines: return self.backup_engines[profile_id] # Load profile from database profile = self.db.get_backup_profile(profile_id) if not profile: logger.error(f"Profile {profile_id} not found") return None # Create engine try: engine = TimeMachineBackup(backup_root=profile['backup_root']) self.backup_engines[profile_id] = engine logger.info(f"Created backup engine for profile {profile_id}: {profile['name']}") return engine except Exception as e: logger.error(f"Failed to create backup engine for profile {profile_id}: {e}") return None def execute_backup( self, profile_id: int, backup_name: Optional[str] = None, progress_callback: Optional[Callable] = None ) -> Dict[str, Any]: """ Execute a backup operation Args: profile_id: Backup profile ID backup_name: Optional custom backup name (default: auto-generated timestamp) progress_callback: Optional callback for progress updates Returns: Dict with operation results (id, status, stats) """ # Load profile profile = self.db.get_backup_profile(profile_id) if not profile: error_msg = f"Profile {profile_id} not found" logger.error(error_msg) return {"success": False, "error": error_msg} if not profile['enabled']: error_msg = f"Profile {profile_id} is disabled" logger.warning(error_msg) return {"success": False, "error": error_msg} # Get or create engine engine = self.get_engine(profile_id) if not engine: error_msg = f"Failed to create backup engine for profile {profile_id}" logger.error(error_msg) return {"success": False, "error": error_msg} # Create operation record start_time = datetime.now() operation_id = self.db.create_backup_operation( profile_id=profile_id, operation_type='backup', backup_name=backup_name or f"backup-{start_time.strftime('%Y-%m-%d-%H%M%S')}", status='running', start_time=start_time ) logger.info(f"Starting backup operation {operation_id} for profile {profile_id}: {profile['name']}") try: # Parse sources sources = json.loads(profile['sources']) if isinstance(profile['sources'], str) else profile['sources'] # Parse exclude patterns exclude_patterns = None if profile.get('exclude_patterns'): exclude_patterns = json.loads(profile['exclude_patterns']) if isinstance(profile['exclude_patterns'], str) else profile['exclude_patterns'] # Execute backup engine.create_backup( source_dirs=sources, backup_name=backup_name, exclude=exclude_patterns, progress=progress_callback, progress_precount=True # Enable progress counting for GUI ) # Get backup info from engine metadata backup_info = engine.metadata['backups'].get( backup_name or f"backup-{start_time.strftime('%Y-%m-%d-%H%M%S')}" ) if not backup_info: raise Exception("Backup completed but metadata not found") # Update operation with results end_time = datetime.now() self.db.update_backup_operation( operation_id=operation_id, status='completed', end_time=end_time, files_total=backup_info.get('files_linked', 0) + backup_info.get('files_copied', 0), files_linked=backup_info.get('files_linked', 0), files_copied=backup_info.get('files_copied', 0), files_in_store=backup_info.get('files_in_store', 0), total_size_bytes=backup_info.get('total_size', 0) ) logger.info(f"Backup operation {operation_id} completed successfully") return { "success": True, "operation_id": operation_id, "profile_id": profile_id, "backup_name": backup_name, "stats": { "files_total": backup_info.get('files_linked', 0) + backup_info.get('files_copied', 0), "files_linked": backup_info.get('files_linked', 0), "files_copied": backup_info.get('files_copied', 0), "files_in_store": backup_info.get('files_in_store', 0), "total_size_bytes": backup_info.get('total_size', 0), "duration_seconds": (end_time - start_time).total_seconds() } } except Exception as e: # Update operation with error error_msg = str(e) logger.error(f"Backup operation {operation_id} failed: {error_msg}") self.db.update_backup_operation( operation_id=operation_id, status='failed', end_time=datetime.now(), error_message=error_msg ) return { "success": False, "operation_id": operation_id, "profile_id": profile_id, "error": error_msg } def execute_prune(self, profile_id: int, keep: Optional[int] = None, older_than_days: Optional[int] = None) -> Dict[str, Any]: """ Execute backup pruning operation Args: profile_id: Backup profile ID keep: Keep only N most recent backups older_than_days: Remove backups older than N days Returns: Dict with operation results """ profile = self.db.get_backup_profile(profile_id) if not profile: error_msg = f"Profile {profile_id} not found" logger.error(error_msg) return {"success": False, "error": error_msg} engine = self.get_engine(profile_id) if not engine: error_msg = f"Failed to create backup engine for profile {profile_id}" logger.error(error_msg) return {"success": False, "error": error_msg} # Create operation record start_time = datetime.now() operation_id = self.db.create_backup_operation( profile_id=profile_id, operation_type='prune', status='running', start_time=start_time ) logger.info(f"Starting prune operation {operation_id} for profile {profile_id}") try: # Execute prune engine.prune_backups(keep=keep, older_than_days=older_than_days) # Update operation self.db.update_backup_operation( operation_id=operation_id, status='completed', end_time=datetime.now() ) logger.info(f"Prune operation {operation_id} completed successfully") return { "success": True, "operation_id": operation_id, "profile_id": profile_id } except Exception as e: error_msg = str(e) logger.error(f"Prune operation {operation_id} failed: {error_msg}") self.db.update_backup_operation( operation_id=operation_id, status='failed', end_time=datetime.now(), error_message=error_msg ) return { "success": False, "operation_id": operation_id, "profile_id": profile_id, "error": error_msg } def execute_gc(self, profile_id: int) -> Dict[str, Any]: """ Execute garbage collection on backup store Args: profile_id: Backup profile ID Returns: Dict with operation results """ profile = self.db.get_backup_profile(profile_id) if not profile: error_msg = f"Profile {profile_id} not found" logger.error(error_msg) return {"success": False, "error": error_msg} engine = self.get_engine(profile_id) if not engine: error_msg = f"Failed to create backup engine for profile {profile_id}" logger.error(error_msg) return {"success": False, "error": error_msg} # Create operation record start_time = datetime.now() operation_id = self.db.create_backup_operation( profile_id=profile_id, operation_type='gc', status='running', start_time=start_time ) logger.info(f"Starting GC operation {operation_id} for profile {profile_id}") try: # Execute garbage collection engine.gc_store() # Update operation self.db.update_backup_operation( operation_id=operation_id, status='completed', end_time=datetime.now() ) logger.info(f"GC operation {operation_id} completed successfully") return { "success": True, "operation_id": operation_id, "profile_id": profile_id } except Exception as e: error_msg = str(e) logger.error(f"GC operation {operation_id} failed: {error_msg}") self.db.update_backup_operation( operation_id=operation_id, status='failed', end_time=datetime.now(), error_message=error_msg ) return { "success": False, "operation_id": operation_id, "profile_id": profile_id, "error": error_msg } def execute_restore( self, profile_id: int, backup_name: str, restore_type: str, destination_path: str, source_path: Optional[str] = None ) -> Dict[str, Any]: """ Execute backup restore operation Args: profile_id: Backup profile ID backup_name: Name of backup to restore restore_type: 'full', 'file', or 'directory' destination_path: Where to restore files source_path: Source file/directory within backup (for file/directory restore) Returns: Dict with operation results """ profile = self.db.get_backup_profile(profile_id) if not profile: error_msg = f"Profile {profile_id} not found" logger.error(error_msg) return {"success": False, "error": error_msg} engine = self.get_engine(profile_id) if not engine: error_msg = f"Failed to create backup engine for profile {profile_id}" logger.error(error_msg) return {"success": False, "error": error_msg} # Create restore record start_time = datetime.now() restore_id = self.db.create_backup_restore( profile_id=profile_id, backup_name=backup_name, restore_type=restore_type, source_path=source_path, destination_path=destination_path, status='running', start_time=start_time ) logger.info(f"Starting restore operation {restore_id} for profile {profile_id}") try: # Execute restore based on type if restore_type == 'full': engine.restore_backup(backup_name, restore_path=destination_path) elif restore_type == 'file': if not source_path: raise ValueError("source_path required for file restore") engine.restore_file(backup_name, source_path, restore_to=destination_path) elif restore_type == 'directory': if not source_path: raise ValueError("source_path required for directory restore") engine.restore_directory(backup_name, source_path, restore_to=destination_path) else: raise ValueError(f"Invalid restore_type: {restore_type}") # Update restore record self.db.update_backup_restore( restore_id=restore_id, status='completed', end_time=datetime.now() ) logger.info(f"Restore operation {restore_id} completed successfully") return { "success": True, "restore_id": restore_id, "profile_id": profile_id, "backup_name": backup_name } except Exception as e: error_msg = str(e) logger.error(f"Restore operation {restore_id} failed: {error_msg}") self.db.update_backup_restore( restore_id=restore_id, status='failed', end_time=datetime.now(), error_message=error_msg ) return { "success": False, "restore_id": restore_id, "profile_id": profile_id, "error": error_msg } def schedule_item(self, schedule_id: int, target_id: int) -> Dict[str, Any]: """ Execute scheduled backup operation Called by daemon when a schedule triggers Args: schedule_id: Schedule ID that triggered target_id: Backup profile ID (from schedule.target_id) Returns: Dict with execution results """ logger.info(f"Schedule {schedule_id} triggered for backup profile {target_id}") # Execute backup result = self.execute_backup(profile_id=target_id) return result def cleanup(self): """Cleanup module resources""" self.backup_engines.clear() logger.info("BackupModule cleanup completed")