Files
quality_app/py_app/app/backup_scheduler.py
2025-11-05 21:25:02 +02:00

297 lines
11 KiB
Python

"""
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