from modules.base_module import BaseModule from src.backup_manager import BackupManager, BackupScheduler import asyncio import logging from datetime import datetime import sys import os # Add parent directory to path for config import sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from config import ADMIN_USER class BackupCommands(BaseModule): """Module for database backup management commands.""" def __init__(self, bot, database): super().__init__(bot, database) self.backup_manager = BackupManager() self.scheduler = BackupScheduler(self.backup_manager) self.scheduler_task = None # Setup logging self.logger = logging.getLogger(__name__) # Start the scheduler self._start_scheduler() def _start_scheduler(self): """Start the backup scheduler task.""" if self.scheduler_task is None or self.scheduler_task.done(): self.scheduler_task = asyncio.create_task(self.scheduler.start_scheduler()) self.logger.info("Backup scheduler started") def get_commands(self): """Return list of available backup commands.""" return [ "backup", "restore", "backups", "backup_stats", "backup_cleanup" ] async def handle_command(self, channel, nickname, command, args): """Handle backup-related commands.""" # Check if user has admin privileges for backup commands if not await self._is_admin(nickname): self.send_message(channel, f"{nickname}: Backup commands require admin privileges.") return if command == "backup": await self.cmd_backup(channel, nickname, args) elif command == "restore": await self.cmd_restore(channel, nickname, args) elif command == "backups": await self.cmd_list_backups(channel, nickname) elif command == "backup_stats": await self.cmd_backup_stats(channel, nickname) elif command == "backup_cleanup": await self.cmd_backup_cleanup(channel, nickname) async def _is_admin(self, nickname): """Check if user has admin privileges.""" return nickname.lower() == ADMIN_USER.lower() async def cmd_backup(self, channel, nickname, args): """Create a manual backup.""" try: # Parse backup type from args backup_type = "manual" compress = True if args: if "uncompressed" in args: compress = False if "daily" in args: backup_type = "daily" elif "weekly" in args: backup_type = "weekly" elif "monthly" in args: backup_type = "monthly" self.send_message(channel, f"{nickname}: Creating {backup_type} backup...") result = await self.backup_manager.create_backup(backup_type, compress) if result["success"]: compression_text = "compressed" if compress else "uncompressed" self.send_message(channel, f"✅ {nickname}: Backup created successfully! " f"File: {result['backup_filename']} " f"({result['size_mb']:.1f}MB, {compression_text})" ) else: self.send_message(channel, f"❌ {nickname}: Backup failed: {result['error']}") except Exception as e: self.send_message(channel, f"❌ {nickname}: Error creating backup: {str(e)}") async def cmd_restore(self, channel, nickname, args): """Restore database from backup.""" try: if not args: self.send_message(channel, f"{nickname}: Usage: !restore ") return backup_filename = args[0] # Confirmation check self.send_message(channel, f"⚠️ {nickname}: This will restore the database from {backup_filename}. " f"Current database will be backed up first. Type '!restore {backup_filename} confirm' to proceed." ) if len(args) < 2 or args[1] != "confirm": return self.send_message(channel, f"{nickname}: Restoring database from {backup_filename}...") result = await self.backup_manager.restore_backup(backup_filename) if result["success"]: self.send_message(channel, f"✅ {nickname}: Database restored successfully! " f"Current database backed up as: {result['current_backup']} " f"Verified {result['tables_verified']} tables." ) # Restart bot to reload data self.send_message(channel, f"{nickname}: ⚠️ Bot restart recommended to reload data.") else: self.send_message(channel, f"❌ {nickname}: Restore failed: {result['error']}") except Exception as e: self.send_message(channel, f"❌ {nickname}: Error restoring backup: {str(e)}") async def cmd_list_backups(self, channel, nickname): """List available backups.""" try: backups = await self.backup_manager.list_backups() if not backups: self.send_message(channel, f"{nickname}: No backups found.") return self.send_message(channel, f"{nickname}: Available backups:") # Show up to 10 most recent backups for backup in backups[:10]: age = datetime.now() - backup["created_at"] age_str = self._format_age(age) compression = "📦" if backup["compressed"] else "📄" type_emoji = {"daily": "🌅", "weekly": "📅", "monthly": "🗓️", "manual": "🔧"}.get(backup["type"], "📋") self.send_message(channel, f" {type_emoji}{compression} {backup['filename']} " f"({backup['size_mb']:.1f}MB, {age_str} ago)" ) if len(backups) > 10: self.send_message(channel, f" ... and {len(backups) - 10} more backups") except Exception as e: self.send_message(channel, f"❌ {nickname}: Error listing backups: {str(e)}") async def cmd_backup_stats(self, channel, nickname): """Show backup statistics.""" try: stats = await self.backup_manager.get_backup_stats() if not stats["success"]: self.send_message(channel, f"❌ {nickname}: Error getting stats: {stats['error']}") return if stats["total_backups"] == 0: self.send_message(channel, f"{nickname}: No backups found.") return self.send_message(channel, f"{nickname}: Backup Statistics:") self.send_message(channel, f" 📊 Total backups: {stats['total_backups']}") self.send_message(channel, f" 💾 Total size: {stats['total_size_mb']:.1f}MB") if stats["oldest_backup"]: oldest_age = datetime.now() - stats["oldest_backup"] newest_age = datetime.now() - stats["newest_backup"] self.send_message(channel, f" 📅 Oldest: {self._format_age(oldest_age)} ago") self.send_message(channel, f" 🆕 Newest: {self._format_age(newest_age)} ago") # Show breakdown by type for backup_type, type_stats in stats["by_type"].items(): type_emoji = {"daily": "🌅", "weekly": "📅", "monthly": "🗓️", "manual": "🔧"}.get(backup_type, "📋") self.send_message(channel, f" {type_emoji} {backup_type.title()}: {type_stats['count']} backups " f"({type_stats['size_mb']:.1f}MB)" ) except Exception as e: self.send_message(channel, f"❌ {nickname}: Error getting backup stats: {str(e)}") async def cmd_backup_cleanup(self, channel, nickname): """Clean up old backups based on retention policy.""" try: self.send_message(channel, f"{nickname}: Cleaning up old backups...") result = await self.backup_manager.cleanup_old_backups() if result["success"]: if result["cleaned_count"] > 0: self.send_message(channel, f"✅ {nickname}: Cleaned up {result['cleaned_count']} old backups. " f"{result['remaining_backups']} backups remaining." ) else: self.send_message(channel, f"{nickname}: No old backups to clean up.") else: self.send_message(channel, f"❌ {nickname}: Cleanup failed: {result['error']}") except Exception as e: self.send_message(channel, f"❌ {nickname}: Error during cleanup: {str(e)}") def _format_age(self, age): """Format a timedelta as human-readable age.""" if age.days > 0: return f"{age.days}d {age.seconds // 3600}h" elif age.seconds > 3600: return f"{age.seconds // 3600}h {(age.seconds % 3600) // 60}m" elif age.seconds > 60: return f"{age.seconds // 60}m" else: return f"{age.seconds}s" async def get_backup_status(self): """Get current backup system status for monitoring.""" try: stats = await self.backup_manager.get_backup_stats() return { "scheduler_running": self.scheduler.running, "total_backups": stats.get("total_backups", 0), "total_size_mb": stats.get("total_size_mb", 0), "last_backup": stats.get("newest_backup"), "backup_types": stats.get("by_type", {}) } except Exception as e: return {"error": str(e)} async def shutdown(self): """Shutdown the backup system gracefully.""" if self.scheduler: self.scheduler.stop_scheduler() if self.scheduler_task and not self.scheduler_task.done(): self.scheduler_task.cancel() try: await self.scheduler_task except asyncio.CancelledError: pass self.logger.info("Backup system shutdown complete")