""" Scheduler-Service für automatische Ausführung von Aufgaben """ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from datetime import datetime import logging import os import stat from pathlib import Path from typing import Optional, Dict, List from zoneinfo import ZoneInfo from ..models.database import SessionLocal, Zeitplan, Postfach, QuellOrdner from ..modules.mail_fetcher import MailFetcher from ..modules.sorter import Sorter from ..config import INBOX_DIR logger = logging.getLogger(__name__) def check_folder_permissions(pfad: str, name: str = "") -> Dict: """ Prüft Ordner-Berechtigungen und gibt detaillierte Infos zurück. Gibt Debug-Infos auf stdout aus für Container-Logs. """ result = { "pfad": pfad, "name": name, "existiert": False, "lesbar": False, "schreibbar": False, "ist_ordner": False, "dateien_anzahl": 0, "fehler": None } prefix = f"[DEBUG] [{name}]" if name else "[DEBUG]" try: p = Path(pfad) result["existiert"] = p.exists() if not p.exists(): print(f"{prefix} ❌ Pfad existiert NICHT: {pfad}", flush=True) result["fehler"] = "Pfad existiert nicht" return result result["ist_ordner"] = p.is_dir() if not p.is_dir(): print(f"{prefix} ❌ Pfad ist KEIN Ordner: {pfad}", flush=True) result["fehler"] = "Pfad ist kein Ordner" return result # Berechtigungen prüfen result["lesbar"] = os.access(pfad, os.R_OK) result["schreibbar"] = os.access(pfad, os.W_OK) # Dateien zählen try: dateien = list(p.glob("*")) result["dateien_anzahl"] = len([f for f in dateien if f.is_file()]) except PermissionError: result["dateien_anzahl"] = -1 # Stat-Infos try: st = p.stat() mode = stat.filemode(st.st_mode) uid = st.st_uid gid = st.st_gid except Exception: mode = "?" uid = "?" gid = "?" # Status-Symbol if result["lesbar"] and result["schreibbar"]: status = "✅" elif result["lesbar"]: status = "⚠️ NUR LESBAR" else: status = "❌ KEIN ZUGRIFF" print(f"{prefix} {status} {pfad}", flush=True) print(f"{prefix} Rechte: {mode} | UID: {uid} | GID: {gid} | Dateien: {result['dateien_anzahl']}", flush=True) if not result["schreibbar"]: result["fehler"] = "Keine Schreibrechte" except Exception as e: print(f"{prefix} ❌ FEHLER beim Prüfen: {pfad} - {e}", flush=True) result["fehler"] = str(e) return result def check_all_folders_on_startup(): """Prüft alle konfigurierten Ordner beim Start""" print("", flush=True) print("=" * 60, flush=True) print("[DEBUG] === ORDNER-BERECHTIGUNGEN PRÜFEN ===", flush=True) print("=" * 60, flush=True) # Prozess-Info print(f"[DEBUG] Prozess läuft als UID: {os.getuid()}, GID: {os.getgid()}", flush=True) # Umgebungsvariablen für PUID/PGID (Unraid-Style) puid = os.environ.get("PUID", "nicht gesetzt") pgid = os.environ.get("PGID", "nicht gesetzt") print(f"[DEBUG] Umgebung: PUID={puid}, PGID={pgid}", flush=True) # Aktueller User try: import pwd import grp user_info = pwd.getpwuid(os.getuid()) group_info = grp.getgrgid(os.getgid()) print(f"[DEBUG] User: {user_info.pw_name}, Gruppe: {group_info.gr_name}", flush=True) except Exception: print("[DEBUG] User/Gruppe konnte nicht ermittelt werden", flush=True) db = SessionLocal() try: # Quellordner prüfen quell_ordner = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all() print(f"[DEBUG] Gefunden: {len(quell_ordner)} aktive Quellordner", flush=True) print("", flush=True) for qo in quell_ordner: print(f"[DEBUG] --- {qo.name} ---", flush=True) # Quellpfad prüfen check_folder_permissions(qo.pfad, f"{qo.name}/Quelle") # Zielpfad prüfen check_folder_permissions(qo.ziel_ordner, f"{qo.name}/Ziel") print("", flush=True) # Postfächer Zielordner prüfen postfaecher = db.query(Postfach).filter(Postfach.aktiv == True).all() print(f"[DEBUG] Gefunden: {len(postfaecher)} aktive Postfächer", flush=True) for pf in postfaecher: if pf.ziel_ordner: check_folder_permissions(pf.ziel_ordner, f"Postfach/{pf.name}") print("=" * 60, flush=True) print("", flush=True) except Exception as e: print(f"[DEBUG] ❌ Fehler bei Ordner-Prüfung: {e}", flush=True) finally: db.close() # Globaler Scheduler scheduler: Optional[BackgroundScheduler] = None # Timezone für Scheduler (robust gegen ungültige TZ-Variablen) def get_timezone(): """Ermittelt eine gültige Timezone""" tz_env = os.environ.get("TZ", "Europe/Berlin") # Prüfen ob gültiger Timezone-String (nicht ${...} oder ähnlich) if tz_env.startswith("$") or "/" not in tz_env: tz_env = "Europe/Berlin" try: return ZoneInfo(tz_env) except Exception: return ZoneInfo("Europe/Berlin") def get_scheduler() -> BackgroundScheduler: """Gibt den globalen Scheduler zurück""" global scheduler if scheduler is None: scheduler = BackgroundScheduler(timezone=get_timezone()) return scheduler def init_scheduler(): """Initialisiert den Scheduler beim App-Start""" global scheduler scheduler = BackgroundScheduler(timezone=get_timezone()) # Ordner-Berechtigungen beim Start prüfen check_all_folders_on_startup() # Zeitpläne aus DB laden und Jobs erstellen sync_zeitplaene() scheduler.start() logger.info("Scheduler gestartet") # Überfällige Zeitpläne beim Start ausführen (asynchron nach 5 Sekunden) import threading def delayed_overdue_check(): import time time.sleep(5) # Warte bis App vollständig gestartet execute_overdue_zeitplaene() thread = threading.Thread(target=delayed_overdue_check, daemon=True) thread.start() def execute_overdue_zeitplaene(): """Führt alle überfälligen Zeitpläne aus""" from datetime import datetime print("Prüfe überfällige Zeitpläne...", flush=True) db = SessionLocal() try: zeitplaene = db.query(Zeitplan).filter(Zeitplan.aktiv == True).all() now = datetime.utcnow() print(f"Gefunden: {len(zeitplaene)} aktive Zeitpläne, aktuelle Zeit (UTC): {now}", flush=True) for zp in zeitplaene: print(f" Zeitplan '{zp.name}': nächste={zp.naechste_ausfuehrung}, letzte={zp.letzte_ausfuehrung}", flush=True) # Prüfen ob überfällig (naechste_ausfuehrung liegt in der Vergangenheit) if zp.naechste_ausfuehrung and zp.naechste_ausfuehrung < now: print(f" -> Führe überfälligen Zeitplan aus: {zp.name}", flush=True) try: execute_zeitplan(zp.id) except Exception as e: print(f" -> Fehler bei überfälligem Zeitplan {zp.name}: {e}", flush=True) # Oder: Noch nie ausgeführt elif zp.letzte_ausfuehrung is None: print(f" -> Führe noch nie ausgeführten Zeitplan aus: {zp.name}", flush=True) try: execute_zeitplan(zp.id) except Exception as e: print(f" -> Fehler bei Zeitplan {zp.name}: {e}", flush=True) else: print(f" -> Nicht überfällig", flush=True) except Exception as e: print(f"Fehler bei Zeitplan-Prüfung: {e}", flush=True) finally: db.close() def shutdown_scheduler(): """Beendet den Scheduler""" global scheduler if scheduler: scheduler.shutdown() logger.info("Scheduler beendet") def sync_zeitplaene(): """Synchronisiert Zeitpläne aus der DB mit dem Scheduler""" global scheduler if not scheduler: return # Alle bestehenden Jobs entfernen scheduler.remove_all_jobs() db = SessionLocal() try: zeitplaene = db.query(Zeitplan).filter(Zeitplan.aktiv == True).all() for zp in zeitplaene: add_job_for_zeitplan(zp) logger.info(f"Job hinzugefügt: {zp.name} ({zp.intervall})") finally: db.close() def add_job_for_zeitplan(zp: Zeitplan): """Fügt einen Job für einen Zeitplan hinzu""" global scheduler if not scheduler: return job_id = f"zeitplan_{zp.id}" # CronTrigger basierend auf Intervall erstellen trigger = create_trigger(zp) if not trigger: return # Job hinzufügen scheduler.add_job( func=execute_zeitplan, trigger=trigger, args=[zp.id], id=job_id, name=zp.name, replace_existing=True ) # Nächste Ausführungszeit berechnen und speichern job = scheduler.get_job(job_id) if job: try: # APScheduler 3.x: next_run_time als Attribut # APScheduler 4.x: get_next_fire_time() oder scheduled_fire_time next_time = getattr(job, 'next_run_time', None) if next_time is None and hasattr(job, 'get_next_fire_time'): next_time = job.get_next_fire_time() if next_time: db = SessionLocal() try: zeitplan = db.query(Zeitplan).filter(Zeitplan.id == zp.id).first() if zeitplan: zeitplan.naechste_ausfuehrung = next_time db.commit() finally: db.close() except Exception as e: logger.warning(f"Konnte nächste Ausführungszeit nicht ermitteln: {e}") def create_trigger(zp: Zeitplan) -> Optional[CronTrigger]: """Erstellt einen CronTrigger basierend auf dem Zeitplan""" try: tz = get_timezone() if zp.intervall == "stündlich": return CronTrigger(minute=zp.minute or 0, timezone=tz) elif zp.intervall == "täglich": return CronTrigger(hour=zp.stunde or 6, minute=zp.minute or 0, timezone=tz) elif zp.intervall == "wöchentlich": return CronTrigger( day_of_week=zp.wochentag or 0, hour=zp.stunde or 6, minute=zp.minute or 0, timezone=tz ) elif zp.intervall == "monatlich": return CronTrigger( day=zp.monatstag or 1, hour=zp.stunde or 6, minute=zp.minute or 0, timezone=tz ) return None except Exception as e: logger.error(f"Fehler beim Erstellen des Triggers: {e}") return None def execute_zeitplan(zeitplan_id: int): """Führt einen Zeitplan aus""" db = SessionLocal() try: zeitplan = db.query(Zeitplan).filter(Zeitplan.id == zeitplan_id).first() if not zeitplan: return logger.info(f"Starte Zeitplan: {zeitplan.name}") try: if zeitplan.typ == "mail_abruf": result = execute_mail_abruf(db, zeitplan) elif zeitplan.typ == "grobsortierung": result = execute_grobsortierung(db, zeitplan) elif zeitplan.typ == "sortierregeln": result = execute_sortierregeln(db, zeitplan) elif zeitplan.typ == "sortierung": # Legacy: alte "sortierung" wird wie "grobsortierung" behandelt result = execute_grobsortierung(db, zeitplan) elif zeitplan.typ == "db_backup": result = execute_db_backup(db, zeitplan) else: result = {"erfolg": False, "meldung": f"Unbekannter Typ: {zeitplan.typ}"} # Status aktualisieren zeitplan.letzte_ausfuehrung = datetime.utcnow() zeitplan.letzter_status = "erfolg" if result.get("erfolg") else "fehler" zeitplan.letzte_meldung = result.get("meldung", "")[:500] # Nächste Ausführung berechnen job = scheduler.get_job(f"zeitplan_{zeitplan_id}") if job: try: next_time = getattr(job, 'next_run_time', None) if next_time is None and hasattr(job, 'get_next_fire_time'): next_time = job.get_next_fire_time() if next_time: zeitplan.naechste_ausfuehrung = next_time except Exception: pass db.commit() logger.info(f"Zeitplan abgeschlossen: {zeitplan.name} - {zeitplan.letzter_status}") except Exception as e: zeitplan.letzte_ausfuehrung = datetime.utcnow() zeitplan.letzter_status = "fehler" zeitplan.letzte_meldung = str(e)[:500] db.commit() logger.error(f"Fehler bei Zeitplan {zeitplan.name}: {e}") finally: db.close() def execute_mail_abruf(db, zeitplan: Zeitplan) -> Dict: """Führt Mail-Abruf aus""" from ..models.database import VerarbeiteteMail # Postfächer bestimmen if zeitplan.postfach_id: postfaecher = db.query(Postfach).filter( Postfach.id == zeitplan.postfach_id, Postfach.aktiv == True ).all() else: postfaecher = db.query(Postfach).filter(Postfach.aktiv == True).all() if not postfaecher: return {"erfolg": True, "meldung": "Keine aktiven Postfächer gefunden"} gesamt_dateien = 0 fehler = [] for postfach in postfaecher: try: # Bereits verarbeitete Message-IDs laden bereits_verarbeitet = set( m.message_id for m in db.query(VerarbeiteteMail) .filter(VerarbeiteteMail.postfach_id == postfach.id) .all() ) config = { "imap_server": postfach.imap_server, "imap_port": postfach.imap_port, "email": postfach.email, "passwort": postfach.passwort, "ordner": postfach.ordner, "erlaubte_typen": postfach.erlaubte_typen or [".pdf"], "max_groesse_mb": postfach.max_groesse_mb or 25, "min_groesse_kb": postfach.min_groesse_kb or 10 } fetcher = MailFetcher(config) if not fetcher.connect(): fehler.append(f"{postfach.name}: Verbindung fehlgeschlagen") continue from pathlib import Path ziel = Path(postfach.ziel_ordner) if postfach.ziel_ordner else INBOX_DIR ergebnisse = fetcher.fetch_attachments( ziel_ordner=ziel, nur_ungelesen=postfach.nur_ungelesen, markiere_gelesen=True, alle_ordner=postfach.alle_ordner, bereits_verarbeitet=bereits_verarbeitet ) # Verarbeitete Mails speichern for ergebnis in ergebnisse: if ergebnis.get("message_id"): db.add(VerarbeiteteMail( postfach_id=postfach.id, message_id=ergebnis["message_id"], ordner=ergebnis.get("ordner"), betreff=ergebnis.get("betreff", "")[:500], absender=ergebnis.get("absender", "")[:255], anzahl_attachments=1 )) # Postfach-Status aktualisieren try: postfach.letzter_abruf = datetime.utcnow() postfach.letzte_anzahl = len(ergebnisse) db.commit() except Exception as commit_error: logger.warning(f"Postfach-Status Update fehlgeschlagen: {commit_error}") db.rollback() gesamt_dateien += len(ergebnisse) fetcher.disconnect() except Exception as e: db.rollback() fehler.append(f"{postfach.name}: {str(e)[:100]}") if fehler: return { "erfolg": len(fehler) < len(postfaecher), "meldung": f"{gesamt_dateien} Dateien geholt. Fehler: {'; '.join(fehler)}" } return {"erfolg": True, "meldung": f"{gesamt_dateien} Dateien aus {len(postfaecher)} Postfächern geholt"} def execute_grobsortierung(db, zeitplan: Zeitplan) -> Dict: """Führt Grobsortierung aus (QuellOrdner verarbeiten)""" from ..models.database import SortierRegel, VerarbeiteteDatei from ..modules.pdf_processor import PDFProcessor from pathlib import Path print("", flush=True) print("[GROBSORTIERUNG] === START ===", flush=True) print(f"[GROBSORTIERUNG] Zeitplan: {zeitplan.name} (ID: {zeitplan.id})", flush=True) # QuellOrdner bestimmen if zeitplan.quell_ordner_id: quell_ordner = db.query(QuellOrdner).filter( QuellOrdner.id == zeitplan.quell_ordner_id, QuellOrdner.aktiv == True ).all() else: quell_ordner = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all() print(f"[GROBSORTIERUNG] Gefunden: {len(quell_ordner)} aktive Quellordner", flush=True) if not quell_ordner: print("[GROBSORTIERUNG] ⚠️ Keine aktiven Quellordner - Abbruch", flush=True) return {"erfolg": True, "meldung": "Keine aktiven Quellordner gefunden"} # Regeln laden regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all() print(f"[GROBSORTIERUNG] Gefunden: {len(regeln)} aktive Regeln", flush=True) # Prüfen ob mindestens ein Ordner direkt_verschieben aktiviert hat hat_direkt_verschieben = any(getattr(qo, 'direkt_verschieben', False) for qo in quell_ordner) if not regeln and not hat_direkt_verschieben: print("[GROBSORTIERUNG] ⚠️ Keine aktiven Regeln und kein direkt_verschieben - Abbruch", flush=True) return {"erfolg": False, "meldung": "Keine aktiven Regeln definiert"} if not regeln: print("[GROBSORTIERUNG] ℹ️ Keine Regeln, aber direkt_verschieben aktiv - fahre fort", flush=True) # Regeln in Dict-Format regeln_dicts = [{ "id": r.id, "name": r.name, "prioritaet": r.prioritaet, "muster": r.muster, "extraktion": r.extraktion, "schema": r.schema, "unterordner": r.unterordner } for r in regeln] sorter = Sorter(regeln_dicts) pdf_processor = PDFProcessor() gesamt_sortiert = 0 gesamt_fehler = 0 gesamt_ohne_regel = 0 fehler_meldungen = [] for qo in quell_ordner: ordner_sortiert = 0 # Zähler pro Ordner print("", flush=True) print(f"[GROBSORTIERUNG] --- Verarbeite: {qo.name} ---", flush=True) print(f"[GROBSORTIERUNG] Quelle: {qo.pfad}", flush=True) print(f"[GROBSORTIERUNG] Ziel: {qo.ziel_ordner}", flush=True) print(f"[GROBSORTIERUNG] Einstellungen: direkt_verschieben={qo.direkt_verschieben}, zugferd={qo.zugferd_behandlung}", flush=True) try: pfad = Path(qo.pfad) # Debug: Ordner-Checks quelle_check = check_folder_permissions(str(pfad), f"{qo.name}/Quelle") ziel_check = check_folder_permissions(qo.ziel_ordner, f"{qo.name}/Ziel") if not pfad.exists(): print(f"[GROBSORTIERUNG] ❌ Quellpfad existiert nicht - überspringe", flush=True) fehler_meldungen.append(f"{qo.name}: Quellpfad existiert nicht") continue if not quelle_check["lesbar"]: print(f"[GROBSORTIERUNG] ❌ Quellpfad nicht lesbar - überspringe", flush=True) fehler_meldungen.append(f"{qo.name}: Keine Leserechte auf Quelle") continue if not ziel_check["schreibbar"]: print(f"[GROBSORTIERUNG] ❌ Zielpfad nicht beschreibbar - überspringe", flush=True) fehler_meldungen.append(f"{qo.name}: Keine Schreibrechte auf Ziel") continue ziel_basis = Path(qo.ziel_ordner) # Dateien sammeln pattern = "**/*" if qo.rekursiv else "*" erlaubte = [t.lower() for t in (qo.dateitypen or [".pdf"])] print(f"[GROBSORTIERUNG] Suche nach: {erlaubte} (rekursiv={qo.rekursiv})", flush=True) dateien = [f for f in pfad.glob(pattern) if f.is_file() and f.suffix.lower() in erlaubte] print(f"[GROBSORTIERUNG] ✓ Gefunden: {len(dateien)} Dateien", flush=True) if len(dateien) == 0: print(f"[GROBSORTIERUNG] Keine passenden Dateien im Ordner", flush=True) for datei in dateien: try: ist_pdf = datei.suffix.lower() == ".pdf" text = "" if ist_pdf: pdf_result = pdf_processor.verarbeite(str(datei)) if pdf_result.get("fehler"): raise Exception(pdf_result["fehler"]) text = pdf_result.get("text", "") # ZUGFeRD-Behandlung basierend auf Einstellung # Optionen: "separieren", "regel", "normal", "ignorieren" zugferd_behandlung = getattr(qo, 'zugferd_behandlung', 'normal') or 'normal' ist_zugferd = pdf_result.get("ist_zugferd", False) if zugferd_behandlung == "separieren": # NUR ZUGFeRD-PDFs verarbeiten if not ist_zugferd: # Keine ZUGFeRD-PDF -> überspringen continue # ZUGFeRD-PDF direkt in Zielordner verschieben ziel_basis.mkdir(parents=True, exist_ok=True) neuer_pfad = ziel_basis / datei.name counter = 1 while neuer_pfad.exists(): neuer_pfad = ziel_basis / f"{datei.stem}_{counter}{datei.suffix}" counter += 1 datei.rename(neuer_pfad) db.add(VerarbeiteteDatei( original_pfad=str(datei), original_name=datei.name, neuer_pfad=str(neuer_pfad), neuer_name=neuer_pfad.name, ist_zugferd=True, status="zugferd" )) gesamt_sortiert += 1 ordner_sortiert += 1 continue elif zugferd_behandlung == "ignorieren": # ZUGFeRD-PDFs überspringen, nur normale verarbeiten if ist_zugferd: continue # Weiter mit Regelprüfung für normale PDFs # Bei "regel" oder "normal": Alle PDFs durch Regeln prüfen # Direkt verschieben (ohne Regelprüfung)? direkt_verschieben = getattr(qo, 'direkt_verschieben', False) if direkt_verschieben: # Datei direkt in Zielordner verschieben print(f"[GROBSORTIERUNG] → Verschiebe direkt: {datei.name}", flush=True) try: ziel_basis.mkdir(parents=True, exist_ok=True) except PermissionError as pe: print(f"[GROBSORTIERUNG] ❌ Kann Zielordner nicht erstellen: {pe}", flush=True) raise neuer_pfad = ziel_basis / datei.name counter = 1 while neuer_pfad.exists(): neuer_pfad = ziel_basis / f"{datei.stem}_{counter}{datei.suffix}" counter += 1 try: datei.rename(neuer_pfad) print(f"[GROBSORTIERUNG] ✓ Verschoben nach: {neuer_pfad}", flush=True) except PermissionError as pe: print(f"[GROBSORTIERUNG] ❌ Keine Berechtigung zum Verschieben: {pe}", flush=True) raise except Exception as me: print(f"[GROBSORTIERUNG] ❌ Fehler beim Verschieben: {me}", flush=True) raise db.add(VerarbeiteteDatei( original_pfad=str(datei), original_name=datei.name, neuer_pfad=str(neuer_pfad), neuer_name=neuer_pfad.name, status="direkt" )) gesamt_sortiert += 1 ordner_sortiert += 1 continue # Regel finden doc_info = {"text": text, "original_name": datei.name, "absender": "", "dateityp": datei.suffix.lower()} regel = sorter.finde_passende_regel(doc_info) if not regel: gesamt_ohne_regel += 1 db.add(VerarbeiteteDatei( original_pfad=str(datei), original_name=datei.name, status="keine_regel", fehler="Keine passende Regel gefunden" )) continue # Felder extrahieren und verschieben extrahiert = sorter.extrahiere_felder(regel, doc_info) schema = regel.get("schema", "{datum} - Dokument.pdf") if schema.endswith(".pdf"): schema = schema[:-4] + datei.suffix neuer_name = sorter.generiere_dateinamen({"schema": schema, **regel}, extrahiert) ziel = ziel_basis if regel.get("unterordner"): ziel = ziel / regel["unterordner"] ziel.mkdir(parents=True, exist_ok=True) sorter.verschiebe_datei(str(datei), str(ziel), neuer_name) gesamt_sortiert += 1 ordner_sortiert += 1 except Exception as e: gesamt_fehler += 1 print(f"[GROBSORTIERUNG] ❌ FEHLER bei {datei.name}: {e}", flush=True) logger.error(f"Fehler bei Datei {datei}: {e}") db.add(VerarbeiteteDatei( original_pfad=str(datei), original_name=datei.name, status="fehler", fehler=str(e)[:500] )) # Ordner-Status aktualisieren (wie bei Postfächern) qo.letzte_verarbeitung = datetime.utcnow() qo.letzte_anzahl = ordner_sortiert print(f"[GROBSORTIERUNG] ✓ {qo.name} abgeschlossen: {ordner_sortiert} Dateien verschoben", flush=True) except Exception as e: print(f"[GROBSORTIERUNG] ❌ FEHLER bei Ordner {qo.name}: {e}", flush=True) fehler_meldungen.append(f"{qo.name}: {str(e)[:100]}") db.commit() meldung = f"{gesamt_sortiert} Dateien sortiert" if gesamt_ohne_regel > 0: meldung += f", {gesamt_ohne_regel} ohne passende Regel" if gesamt_fehler > 0: meldung += f", {gesamt_fehler} Fehler" if fehler_meldungen: meldung += f" ({'; '.join(fehler_meldungen)})" print("", flush=True) print(f"[GROBSORTIERUNG] === ENDE === {meldung}", flush=True) print("", flush=True) # Erfolg wenn keine echten Fehler (ohne_regel zählt nicht als Fehler) return {"erfolg": gesamt_fehler == 0 and not fehler_meldungen, "meldung": meldung} def execute_sortierregeln(db, zeitplan: Zeitplan) -> Dict: """Führt nur Sortierregeln aus (freie Ordner von Regeln)""" from ..models.database import SortierRegel, VerarbeiteteDatei from ..modules.pdf_processor import PDFProcessor from pathlib import Path # Regeln laden (optional spezifische Regel) if zeitplan.regel_id: regeln = db.query(SortierRegel).filter( SortierRegel.id == zeitplan.regel_id, SortierRegel.aktiv == True ).all() else: regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).all() if not regeln: return {"erfolg": True, "meldung": "Keine aktiven Regeln gefunden"} pdf_processor = PDFProcessor() gesamt_sortiert = 0 gesamt_fehler = 0 fehler_meldungen = [] for regel in regeln: freie_ordner = regel.freie_ordner if regel.freie_ordner else [] if not freie_ordner: continue regel_dict = { "id": regel.id, "name": regel.name, "prioritaet": regel.prioritaet, "muster": regel.muster, "extraktion": regel.extraktion, "schema": regel.schema, "unterordner": regel.unterordner, "ziel_ordner": getattr(regel, 'ziel_ordner', None), "nur_umbenennen": getattr(regel, 'nur_umbenennen', False) } regel_sorter = Sorter([regel_dict]) for freier_ordner_pfad in freie_ordner: freier_pfad = Path(freier_ordner_pfad) if not freier_pfad.exists() or not freier_pfad.is_dir(): continue # Dateien sammeln dateien = [f for f in freier_pfad.glob("**/*") if f.is_file() and f.suffix.lower() == ".pdf"] for datei in dateien: try: ist_pdf = datei.suffix.lower() == ".pdf" text = "" ist_zugferd = False if ist_pdf: pdf_result = pdf_processor.verarbeite(str(datei)) if pdf_result.get("fehler"): raise Exception(pdf_result["fehler"]) text = pdf_result.get("text", "") ist_zugferd = pdf_result.get("ist_zugferd", False) doc_info = { "text": text, "original_name": datei.name, "absender": "", "dateityp": datei.suffix.lower() } # Prüfe ob Regel passt passend = regel_sorter.finde_passende_regel(doc_info) if not passend: continue # Felder extrahieren extrahiert = regel_sorter.extrahiere_felder(passend, doc_info) # Dateiname generieren schema = passend.get("schema", "{datum} - Dokument.pdf") if schema.endswith(".pdf"): schema = schema[:-4] + datei.suffix neuer_name = regel_sorter.generiere_dateinamen( {"schema": schema, **passend}, extrahiert ) # Zielordner bestimmen if passend.get("nur_umbenennen"): # Nur umbenennen - Datei bleibt im aktuellen Ordner ziel = datei.parent elif passend.get("ziel_ordner"): # Regel hat eigenen Zielordner ziel = Path(passend["ziel_ordner"]) if passend.get("unterordner"): ziel = ziel / passend["unterordner"] else: # Kein Zielordner - bleibt im freien Ordner ziel = freier_pfad if passend.get("unterordner"): ziel = ziel / passend["unterordner"] ziel.mkdir(parents=True, exist_ok=True) # Verschieben/Umbenennen regel_sorter.verschiebe_datei(str(datei), str(ziel), neuer_name) gesamt_sortiert += 1 db.add(VerarbeiteteDatei( original_pfad=str(datei), original_name=datei.name, neuer_pfad=str(ziel / neuer_name), neuer_name=neuer_name, ist_zugferd=ist_zugferd, status="sortiert", extrahierte_daten=extrahiert )) except Exception as e: gesamt_fehler += 1 logger.error(f"Fehler bei Datei {datei}: {e}") db.commit() meldung = f"{gesamt_sortiert} Dateien mit Regeln sortiert" if gesamt_fehler > 0: meldung += f", {gesamt_fehler} Fehler" if fehler_meldungen: meldung += f" ({'; '.join(fehler_meldungen)})" return {"erfolg": gesamt_fehler == 0, "meldung": meldung} def execute_db_backup(db, zeitplan: Zeitplan) -> Dict: """Führt Datenbank-Backup aus""" from ..models.database import Datenbank from .backup_service import erstelle_db_backup print(f"[SCHEDULER] Starte DB-Backup: {zeitplan.name}", flush=True) # Datenbanken laden (optional spezifische Datenbank) if zeitplan.datenbank_id: datenbanken = db.query(Datenbank).filter( Datenbank.id == zeitplan.datenbank_id, Datenbank.aktiv == True ).all() else: datenbanken = db.query(Datenbank).filter(Datenbank.aktiv == True).all() if not datenbanken: return {"erfolg": True, "meldung": "Keine aktiven Datenbanken gefunden"} gesamt_erfolg = 0 gesamt_fehler = 0 fehler_meldungen = [] for datenbank in datenbanken: try: print(f"[SCHEDULER] Backup für: {datenbank.name}", flush=True) result = erstelle_db_backup(datenbank.id, db) gesamt_erfolg += 1 except Exception as e: gesamt_fehler += 1 fehler_meldungen.append(f"{datenbank.name}: {str(e)}") logger.error(f"Backup-Fehler für {datenbank.name}: {e}") meldung = f"{gesamt_erfolg} Backups erstellt" if gesamt_fehler > 0: meldung += f", {gesamt_fehler} Fehler" if fehler_meldungen: meldung += f" ({'; '.join(fehler_meldungen[:3])})" return {"erfolg": gesamt_fehler == 0, "meldung": meldung} def get_scheduler_status() -> Dict: """Gibt den Status aller Zeitpläne zurück""" global scheduler db = SessionLocal() try: zeitplaene = db.query(Zeitplan).all() result = [] for zp in zeitplaene: job = scheduler.get_job(f"zeitplan_{zp.id}") if scheduler else None result.append({ "id": zp.id, "name": zp.name, "typ": zp.typ, "intervall": zp.intervall, "aktiv": zp.aktiv, "letzte_ausfuehrung": zp.letzte_ausfuehrung.isoformat() if zp.letzte_ausfuehrung else None, "naechste_ausfuehrung": zp.naechste_ausfuehrung.isoformat() if zp.naechste_ausfuehrung else None, "letzter_status": zp.letzter_status, "letzte_meldung": zp.letzte_meldung, "job_aktiv": job is not None }) return { "scheduler_laeuft": scheduler is not None and scheduler.running if scheduler else False, "zeitplaene": result } finally: db.close() def trigger_zeitplan_manuell(zeitplan_id: int) -> Dict: """Löst einen Zeitplan manuell aus""" db = SessionLocal() try: zeitplan = db.query(Zeitplan).filter(Zeitplan.id == zeitplan_id).first() if not zeitplan: return {"erfolg": False, "meldung": "Zeitplan nicht gefunden"} # Synchron ausführen execute_zeitplan(zeitplan_id) return {"erfolg": True, "meldung": f"Zeitplan '{zeitplan.name}' wurde ausgeführt"} finally: db.close()