Petbot/modules/backup_commands.py
megaproxy 86902c6b83 Clean up IRC command architecture and eliminate redundancy
Major cleanup of the modular command system:

**Legacy Code Removal:**
- Removed all legacy command handlers from src/bot.py (74 lines)
- Eliminated outdated command implementations superseded by modular system
- Maintained backward compatibility while cleaning up codebase

**Duplicate Command Consolidation:**
- Removed duplicate status/uptime/ping commands from admin.py
- Kept comprehensive implementations in connection_monitor.py
- Eliminated 3 redundant commands and ~70 lines of duplicate code

**Admin System Standardization:**
- Updated backup_commands.py to use central ADMIN_USER from config.py
- Updated connection_monitor.py to use central ADMIN_USER from config.py
- Removed hardcoded admin user lists across modules
- Ensured consistent admin privilege checking system-wide

**Benefits:**
- Cleaner, more maintainable command structure
- No duplicate functionality across modules
- Consistent admin configuration management
- Reduced codebase size while maintaining all functionality
- Better separation of concerns in modular architecture

This cleanup addresses technical debt identified in the command audit
and prepares the codebase for future development.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 17:00:06 +00:00

259 lines
No EOL
11 KiB
Python

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