297 lines
11 KiB
Python
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
|