""" Automated Backup Scheduler Quality Recticel Application This module manages automatic backup execution based on the configured schedule. Uses APScheduler to run backups at specified times. """ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from datetime import datetime import logging # Configure logging logger = logging.getLogger(__name__) class BackupScheduler: """Manages automatic backup scheduling""" def __init__(self, app=None): """ Initialize the backup scheduler Args: app: Flask application instance """ self.scheduler = None self.app = app self.job_prefix = 'scheduled_backup' if app is not None: self.init_app(app) def init_app(self, app): """ Initialize scheduler with Flask app context Args: app: Flask application instance """ self.app = app # Create scheduler self.scheduler = BackgroundScheduler( daemon=True, timezone='Europe/Bucharest' # Adjust to your timezone ) # Load and apply schedule from configuration self.update_schedule() # Start scheduler self.scheduler.start() logger.info("Backup scheduler started") # Register shutdown handler import atexit atexit.register(lambda: self.scheduler.shutdown()) def execute_scheduled_backup(self, schedule_id, backup_type): """ Execute a backup based on the schedule configuration This method runs in the scheduler thread Args: schedule_id: Identifier for the schedule backup_type: Type of backup ('full' or 'data-only') """ try: from app.database_backup import DatabaseBackupManager with self.app.app_context(): backup_manager = DatabaseBackupManager() logger.info(f"Starting scheduled {backup_type} backup (schedule: {schedule_id})...") # Execute appropriate backup if backup_type == 'data-only': result = backup_manager.create_data_only_backup(backup_name='scheduled') else: result = backup_manager.create_backup(backup_name='scheduled') if result['success']: logger.info(f"✅ Scheduled backup completed: {result['filename']} ({result['size']})") # Clean up old backups based on retention policy schedule = backup_manager.get_backup_schedule() schedules = schedule.get('schedules', []) if isinstance(schedule, dict) and 'schedules' in schedule else [] # Find the schedule that triggered this backup current_schedule = next((s for s in schedules if s.get('id') == schedule_id), None) if current_schedule: retention_days = current_schedule.get('retention_days', 30) cleanup_result = backup_manager.cleanup_old_backups(retention_days) if cleanup_result['success'] and cleanup_result['deleted_count'] > 0: logger.info(f"🗑️ Cleaned up {cleanup_result['deleted_count']} old backup(s)") else: logger.error(f"❌ Scheduled backup failed: {result['message']}") except Exception as e: logger.error(f"❌ Error during scheduled backup: {e}", exc_info=True) def update_schedule(self): """ Reload schedule from configuration and update scheduler jobs Supports multiple schedules """ try: from app.database_backup import DatabaseBackupManager with self.app.app_context(): backup_manager = DatabaseBackupManager() schedule_config = backup_manager.get_backup_schedule() # Remove all existing backup jobs for job in self.scheduler.get_jobs(): if job.id.startswith(self.job_prefix): self.scheduler.remove_job(job.id) # Handle new multi-schedule format if isinstance(schedule_config, dict) and 'schedules' in schedule_config: schedules = schedule_config['schedules'] for schedule in schedules: if not schedule.get('enabled', False): continue schedule_id = schedule.get('id', 'default') time_str = schedule.get('time', '02:00') frequency = schedule.get('frequency', 'daily') backup_type = schedule.get('backup_type', 'full') # Parse time hour, minute = map(int, time_str.split(':')) # Create appropriate trigger if frequency == 'daily': trigger = CronTrigger(hour=hour, minute=minute) elif frequency == 'weekly': trigger = CronTrigger(day_of_week='sun', hour=hour, minute=minute) elif frequency == 'monthly': trigger = CronTrigger(day=1, hour=hour, minute=minute) else: logger.error(f"Unknown frequency: {frequency}") continue # Add job with unique ID job_id = f"{self.job_prefix}_{schedule_id}" self.scheduler.add_job( func=self.execute_scheduled_backup, trigger=trigger, args=[schedule_id, backup_type], id=job_id, name=f'Scheduled {backup_type} backup ({schedule_id})', replace_existing=True ) logger.info(f"✅ Schedule '{schedule_id}': {backup_type} backup {frequency} at {time_str}") # Handle legacy single-schedule format (backward compatibility) elif isinstance(schedule_config, dict) and schedule_config.get('enabled', False): time_str = schedule_config.get('time', '02:00') frequency = schedule_config.get('frequency', 'daily') backup_type = schedule_config.get('backup_type', 'full') hour, minute = map(int, time_str.split(':')) if frequency == 'daily': trigger = CronTrigger(hour=hour, minute=minute) elif frequency == 'weekly': trigger = CronTrigger(day_of_week='sun', hour=hour, minute=minute) elif frequency == 'monthly': trigger = CronTrigger(day=1, hour=hour, minute=minute) else: logger.error(f"Unknown frequency: {frequency}") return job_id = f"{self.job_prefix}_default" self.scheduler.add_job( func=self.execute_scheduled_backup, trigger=trigger, args=['default', backup_type], id=job_id, name=f'Scheduled {backup_type} backup', replace_existing=True ) logger.info(f"✅ Backup schedule configured: {backup_type} backup {frequency} at {time_str}") except Exception as e: logger.error(f"Error updating backup schedule: {e}", exc_info=True) def get_next_run_time(self, schedule_id='default'): """ Get the next scheduled run time for a specific schedule Args: schedule_id: Identifier for the schedule Returns: datetime or None: Next run time if job exists """ if not self.scheduler: return None job_id = f"{self.job_prefix}_{schedule_id}" job = self.scheduler.get_job(job_id) if job: return job.next_run_time return None def get_schedule_info(self): """ Get information about all schedules Returns: dict: Schedule information including next run times for all jobs """ try: from app.database_backup import DatabaseBackupManager with self.app.app_context(): backup_manager = DatabaseBackupManager() schedule_config = backup_manager.get_backup_schedule() # Get all backup jobs jobs_info = [] for job in self.scheduler.get_jobs(): if job.id.startswith(self.job_prefix): jobs_info.append({ 'id': job.id.replace(f"{self.job_prefix}_", ""), 'name': job.name, 'next_run_time': job.next_run_time.strftime('%Y-%m-%d %H:%M:%S') if job.next_run_time else None }) return { 'schedule': schedule_config, 'jobs': jobs_info, 'scheduler_running': self.scheduler.running if self.scheduler else False, 'total_jobs': len(jobs_info) } except Exception as e: logger.error(f"Error getting schedule info: {e}") return None def trigger_backup_now(self): """ Manually trigger a backup immediately (outside of schedule) Returns: dict: Result of backup operation """ try: logger.info("Manual backup trigger requested") self.execute_scheduled_backup() return { 'success': True, 'message': 'Backup triggered successfully' } except Exception as e: logger.error(f"Error triggering manual backup: {e}") return { 'success': False, 'message': f'Failed to trigger backup: {str(e)}' } # Global scheduler instance (initialized in __init__.py) backup_scheduler = None def init_backup_scheduler(app): """ Initialize the global backup scheduler instance Args: app: Flask application instance Returns: BackupScheduler: Initialized scheduler instance """ global backup_scheduler backup_scheduler = BackupScheduler(app) return backup_scheduler def get_backup_scheduler(): """ Get the global backup scheduler instance Returns: BackupScheduler or None: Scheduler instance if initialized """ return backup_scheduler