""" Datenbank-Backup Service Unterstützt MariaDB, MySQL und PostgreSQL """ import subprocess import os import gzip import zipfile from pathlib import Path from datetime import datetime from sqlalchemy.orm import Session import logging from ..models.database import DbServer, Datenbank, BackupLog logger = logging.getLogger(__name__) def erstelle_db_backup(datenbank_id: int, db: Session) -> dict: """ Erstellt ein Backup einer Datenbank. Args: datenbank_id: ID der Datenbank-Konfiguration db: SQLAlchemy Session Returns: dict mit Backup-Informationen """ # Datenbank-Konfiguration laden datenbank = db.query(Datenbank).filter(Datenbank.id == datenbank_id).first() if not datenbank: raise Exception("Datenbank nicht gefunden") # Server-Konfiguration laden server = db.query(DbServer).filter(DbServer.id == datenbank.server_id).first() if not server: raise Exception("DB-Server nicht gefunden") # Backup-Verzeichnis erstellen backup_dir = Path(datenbank.backup_pfad) backup_dir.mkdir(parents=True, exist_ok=True) # Dateiname generieren timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = f"{datenbank.database}_{timestamp}" try: # Backup erstellen basierend auf DB-Typ if server.typ in ["mariadb", "mysql"]: sql_file = backup_mysql(server, datenbank.database, backup_dir, base_name) elif server.typ == "postgresql": sql_file = backup_postgresql(server, datenbank.database, backup_dir, base_name) else: raise Exception(f"Unbekannter Datenbanktyp: {server.typ}") # Format anwenden (Komprimierung) final_file = sql_file if datenbank.format == "sql.gz": final_file = komprimiere_gzip(sql_file) os.remove(sql_file) # Original löschen elif datenbank.format == "zip": final_file = komprimiere_zip(sql_file) os.remove(sql_file) # Original löschen # Dateigröße ermitteln file_size = os.path.getsize(final_file) # Backup in DB loggen log_entry = BackupLog( datenbank_id=datenbank.id, dateiname=os.path.basename(final_file), groesse_bytes=file_size, status="erfolg" ) db.add(log_entry) # Datenbank-Status aktualisieren datenbank.letztes_backup = datetime.now() datenbank.letzte_groesse_mb = round(file_size / 1024 / 1024) db.commit() # Alte Backups aufräumen bereinige_alte_backups(datenbank, db) logger.info(f"Backup erstellt: {final_file} ({file_size} bytes)") print(f"[BACKUP] ✓ Backup erstellt: {final_file}", flush=True) return { "erfolg": True, "datei": os.path.basename(final_file), "groesse_mb": round(file_size / 1024 / 1024, 2), "pfad": str(final_file) } except Exception as e: # Fehler loggen log_entry = BackupLog( datenbank_id=datenbank.id, dateiname=f"{base_name}_FEHLER", status="fehler", fehler=str(e) ) db.add(log_entry) db.commit() logger.error(f"Backup-Fehler: {e}") print(f"[BACKUP] ✗ Fehler: {e}", flush=True) raise Exception(f"Backup fehlgeschlagen: {e}") def backup_mysql(server: DbServer, database: str, backup_dir: Path, base_name: str) -> str: """ Erstellt ein MySQL/MariaDB Backup mit mysqldump. """ output_file = backup_dir / f"{base_name}.sql" cmd = [ "mysqldump", f"--host={server.host}", f"--port={server.port}", f"--user={server.user}", f"--password={server.passwort}", "--single-transaction", "--routines", "--triggers", "--events", database ] print(f"[BACKUP] Starte MySQL-Dump für {database}...", flush=True) with open(output_file, 'w') as f: result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, timeout=3600) if result.returncode != 0: error_msg = result.stderr.decode('utf-8', errors='ignore') raise Exception(f"mysqldump Fehler: {error_msg}") return str(output_file) def backup_postgresql(server: DbServer, database: str, backup_dir: Path, base_name: str) -> str: """ Erstellt ein PostgreSQL Backup mit pg_dump. """ output_file = backup_dir / f"{base_name}.sql" # Passwort über Umgebungsvariable env = os.environ.copy() env["PGPASSWORD"] = server.passwort cmd = [ "pg_dump", f"--host={server.host}", f"--port={server.port}", f"--username={server.user}", "--format=plain", "--no-owner", "--no-acl", database ] print(f"[BACKUP] Starte PostgreSQL-Dump für {database}...", flush=True) with open(output_file, 'w') as f: result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, env=env, timeout=3600) if result.returncode != 0: error_msg = result.stderr.decode('utf-8', errors='ignore') raise Exception(f"pg_dump Fehler: {error_msg}") return str(output_file) def komprimiere_gzip(input_file: str) -> str: """ Komprimiert eine Datei mit gzip. """ output_file = f"{input_file}.gz" with open(input_file, 'rb') as f_in: with gzip.open(output_file, 'wb') as f_out: f_out.writelines(f_in) return output_file def komprimiere_zip(input_file: str) -> str: """ Komprimiert eine Datei als ZIP. """ output_file = input_file.replace('.sql', '.zip') with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zf: zf.write(input_file, os.path.basename(input_file)) return output_file def bereinige_alte_backups(datenbank: Datenbank, db: Session): """ Löscht alte Backups basierend auf der Aufbewahrungsregel. """ backup_dir = Path(datenbank.backup_pfad) if not backup_dir.exists(): return # Alle Backup-Dateien für diese Datenbank finden pattern = f"{datenbank.database}_*" backups = sorted(backup_dir.glob(pattern), key=lambda x: x.stat().st_mtime, reverse=True) # Nur die neuesten behalten zu_loeschen = backups[datenbank.aufbewahrung:] for backup_file in zu_loeschen: try: backup_file.unlink() print(f"[BACKUP] Altes Backup gelöscht: {backup_file.name}", flush=True) # Auch aus der DB löschen db.query(BackupLog).filter(BackupLog.dateiname == backup_file.name).delete() except Exception as e: logger.warning(f"Konnte altes Backup nicht löschen: {e}") db.commit() def fuehre_alle_backups_aus(db: Session) -> dict: """ Führt Backups für alle aktiven Datenbanken aus. Returns: dict mit Zusammenfassung """ datenbanken = db.query(Datenbank).filter(Datenbank.aktiv == True).all() ergebnis = { "gesamt": len(datenbanken), "erfolgreich": 0, "fehlgeschlagen": 0, "details": [] } for datenbank in datenbanken: try: result = erstelle_db_backup(datenbank.id, db) ergebnis["erfolgreich"] += 1 ergebnis["details"].append({ "name": datenbank.name, "status": "erfolg", "datei": result.get("datei") }) except Exception as e: ergebnis["fehlgeschlagen"] += 1 ergebnis["details"].append({ "name": datenbank.name, "status": "fehler", "fehler": str(e) }) return ergebnis