Fixed the scan error and backup problems
This commit is contained in:
296
py_app/app/backup_scheduler.py
Normal file
296
py_app/app/backup_scheduler.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user