""" Sync Module - Integration with Automation Daemon Manages sync operations, scheduling, and database integration """ from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional, Callable from loguru import logger from core.database import Database from modules.sync.sync_engine import DirectorySync class SyncModule: """ Sync module for unified daemon Integrates DirectorySync with database and scheduling system """ def __init__(self, db: Database): """ Initialize sync module Args: db: Database instance for CRUD operations """ self.db = db logger.info("SyncModule initialized") def execute_sync( self, profile_id: int, schedule_id: Optional[int] = None, progress_callback: Optional[Callable] = None ) -> Dict[str, Any]: """ Execute a sync operation Args: profile_id: Sync profile ID schedule_id: Optional schedule ID (when triggered by scheduler) progress_callback: Optional callback for progress updates Returns: Dict with operation results (success, operation_id, stats, error) """ # Load profile profile = self.db.get_sync_profile(profile_id) if not profile: error_msg = f"Sync profile {profile_id} not found" logger.error(error_msg) return {"success": False, "error": error_msg} if not profile['enabled']: error_msg = f"Sync profile {profile_id} is disabled" logger.warning(error_msg) return {"success": False, "error": error_msg} # Check if there's already a running sync for this profile if self.db.has_running_sync_operation(profile_id): error_msg = f"Sync profile {profile_id} is already running - skipping to prevent overlap" logger.warning(error_msg) return {"success": False, "error": error_msg, "skipped": True} # Create operation record start_time = datetime.now() operation_id = self.db.create_sync_operation( profile_id=profile_id, status='running', start_time=start_time ) logger.info(f"Starting sync operation {operation_id} for profile {profile_id}: {profile['name']}") try: # Parse exclude patterns from profile exclude_patterns = [] if profile.get('exclude_patterns'): exclude_patterns = [p.strip() for p in profile['exclude_patterns'].split('\n') if p.strip()] # Get protect_excluded flag (default False for clean mirror behavior) protect_excluded = bool(profile.get('protect_excluded', False)) # Create sync engine sync_engine = DirectorySync( source=profile['source_path'], destination=profile['destination_path'], exclude_patterns=exclude_patterns, protect_excluded=protect_excluded ) # Execute sync stats = sync_engine.sync(progress_callback=progress_callback) # Check for errors if stats.get('errors'): # Partial success - some files had errors status = 'completed_with_errors' error_message = f"{len(stats['errors'])} error(s) occurred" logger.warning(f"Sync operation {operation_id} completed with errors") else: status = 'completed' error_message = None logger.info(f"Sync operation {operation_id} completed successfully") # Update operation with results end_time = datetime.now() self.db.update_sync_operation( operation_id=operation_id, status=status, end_time=end_time, files_scanned=stats['files_scanned'], files_copied=stats['files_copied'], files_deleted=stats['files_deleted'], bytes_total=stats['bytes_total'], error_message=error_message ) return { "success": True, "operation_id": operation_id, "profile_id": profile_id, "stats": { "files_scanned": stats['files_scanned'], "files_copied": stats['files_copied'], "files_deleted": stats['files_deleted'], "bytes_total": stats['bytes_total'], "duration_seconds": (end_time - start_time).total_seconds() } } except Exception as e: # Update operation with error error_msg = str(e) logger.error(f"Sync operation {operation_id} failed: {error_msg}") self.db.update_sync_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 schedule_item(self, schedule_id: int, target_id: int) -> Dict[str, Any]: """ Execute scheduled sync operation Called by daemon when a schedule triggers Args: schedule_id: Schedule ID that triggered target_id: Sync profile ID (from schedule.target_id) Returns: Dict with execution results """ logger.info(f"Schedule {schedule_id} triggered for sync profile {target_id}") # Execute sync result = self.execute_sync(profile_id=target_id, schedule_id=schedule_id) return result def cleanup(self): """Cleanup module resources""" logger.info("SyncModule cleanup completed")