# cogs/backup.py import discord from discord.ext import commands, tasks from discord import app_commands import asyncio import os import datetime import zipfile import logging from config import DB_DSN, BACKUP_CHANNEL_ID logger = logging.getLogger(__name__) class BackupCog(commands.Cog): def __init__(self, bot): self.bot = bot self.auto_backup.start() def cog_unload(self): self.auto_backup.cancel() @tasks.loop(hours=12) async def auto_backup(self): """Automatically runs every 12 hours.""" await self.bot.wait_until_ready() await self._execute_backup("🕒 Automated 12-Hour Backup") async def _execute_backup(self, title_prefix: str, interaction: discord.Interaction = None): """The core logic for dumping, zipping, and uploading the database.""" # Ensure we have a valid destination channel = self.bot.get_channel(BACKUP_CHANNEL_ID) if not channel: error_msg = "❌ `BACKUP_CHANNEL_ID` is missing or invalid in your .env file! Cannot upload database backup." logger.error(error_msg) if interaction: await interaction.followup.send(error_msg, ephemeral=True) return now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") sql_filename = f"db_dump_{now}.sql" zip_filename = f"Gacha_Backup_{now}.zip" logger.info(f"Starting database backup: {sql_filename}") try: # 1. Run the native PostgreSQL dump command securely process = await asyncio.create_subprocess_exec( 'pg_dump', '--dbname', DB_DSN, '-f', sql_filename, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode != 0: error_msg = f"❌ pg_dump failed: {stderr.decode()}" logger.error(error_msg) if interaction: await interaction.followup.send(error_msg, ephemeral=True) return # 2. Compress it heavily so we don't hit Discord's 25MB upload limit def zip_file(): with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: zipf.write(sql_filename) await asyncio.to_thread(zip_file) # 3. Format and Upload to Discord file = discord.File(fp=zip_filename, filename=zip_filename) embed = discord.Embed( title=f"📦 {title_prefix}", description=f"Secure database snapshot generated at ``.", color=discord.Color.green() ) embed.set_footer(text="To restore: Extract the .zip and run `psql -d gacha_bot_db -f ` in Termux.") await channel.send(embed=embed, file=file) logger.info("✅ Backup successfully uploaded to Discord Cloud.") if interaction: await interaction.followup.send("✅ Backup successfully forced and uploaded to the private vault channel!", ephemeral=True) except Exception as e: logger.error(f"Backup Error: {e}", exc_info=True) if interaction: await interaction.followup.send("❌ A critical error occurred while generating the backup.", ephemeral=True) finally: # 4. TRASH THE LOCAL FILES: Prevents your Android storage from filling up! if os.path.exists(sql_filename): os.remove(sql_filename) if os.path.exists(zip_filename): os.remove(zip_filename) # Allow admins to manually force a backup at any time @app_commands.command(name="force_backup", description="[ADMIN] Force an immediate database backup to the cloud vault") @app_commands.default_permissions(administrator=True) async def force_backup(self, interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) await self._execute_backup("⚙️ Manual Admin Backup", interaction) async def setup(bot): await bot.add_cog(BackupCog(bot))