docker.dateiverwaltung/Source/backend/app/routes/api.py
2026-02-10 19:15:45 +01:00

3058 lines
111 KiB
Python
Executable file

"""
API Routes - Getrennte Bereiche: Mail-Abruf und Datei-Sortierung
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from pathlib import Path
import json
import asyncio
import tempfile
import re
from ..models.database import get_db, Postfach, QuellOrdner, SortierRegel, VerarbeiteteDatei, VerarbeiteteMail, Zeitplan, OrdnerRegel, DbServer, Datenbank, BackupLog
from ..modules.mail_fetcher import MailFetcher
from ..modules.pdf_processor import PDFProcessor
from ..modules.sorter import Sorter
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["api"])
# ============ Pydantic Models ============
class PostfachCreate(BaseModel):
name: str
imap_server: str
imap_port: int = 993
email: str
passwort: str
ordner: str = "INBOX"
alle_ordner: bool = False # Alle IMAP-Ordner durchsuchen
nur_ungelesen: bool = False # Nur ungelesene Mails (False = alle)
ziel_ordner: str
erlaubte_typen: List[str] = [".pdf"]
max_groesse_mb: int = 25
min_groesse_kb: int = 10 # Mindestgröße (gegen kleine Icons)
ab_datum: Optional[datetime] = None # Nur Mails ab diesem Datum
# Größenfilter pro Dateityp: {".pdf": {"min_kb": 10, "max_mb": 25}}
groessen_filter: Optional[dict] = None
class PostfachUpdate(BaseModel):
"""Für Updates - Passwort ist optional"""
name: str
imap_server: str
imap_port: int = 993
email: str
passwort: Optional[str] = None # Optional beim Update
ordner: str = "INBOX"
alle_ordner: bool = False
nur_ungelesen: bool = False
ziel_ordner: str
erlaubte_typen: List[str] = [".pdf"]
max_groesse_mb: int = 25
min_groesse_kb: int = 10
ab_datum: Optional[datetime] = None
groessen_filter: Optional[dict] = None
class PostfachResponse(BaseModel):
id: int
name: str
imap_server: str
email: str
ordner: str
alle_ordner: bool
nur_ungelesen: bool
ziel_ordner: str
erlaubte_typen: List[str]
max_groesse_mb: int
min_groesse_kb: int
ab_datum: Optional[datetime]
groessen_filter: Optional[dict]
aktiv: bool
letzter_abruf: Optional[datetime]
letzte_anzahl: int
class Config:
from_attributes = True
class OrdnerCreate(BaseModel):
name: str
pfad: str
ziel_ordner: str
rekursiv: bool = True
dateitypen: List[str] = [".pdf", ".jpg", ".jpeg", ".png", ".tiff"]
zugferd_behandlung: str = "separieren" # separieren, regel, normal, ignorieren
signiert_behandlung: str = "normal" # normal, separieren, regel, ignorieren
direkt_verschieben: bool = False # Ohne Regelprüfung verschieben
ocr_aktivieren: bool = True # OCR für gescannte PDFs
original_sichern: Optional[str] = None # Ordner für Original-Backup
class OrdnerResponse(BaseModel):
id: int
name: str
pfad: str
ziel_ordner: str
rekursiv: bool
dateitypen: List[str]
direkt_verschieben: bool = False
zugferd_behandlung: str
signiert_behandlung: str = "normal"
ocr_aktivieren: bool = True
original_sichern: Optional[str] = None
aktiv: bool
letzte_verarbeitung: Optional[datetime] = None
letzte_anzahl: int = 0
class Config:
from_attributes = True
class RegelCreate(BaseModel):
name: str
prioritaet: int = 100
muster: dict = {}
extraktion: dict = {}
schema: str = "{datum} - Dokument.pdf"
unterordner: Optional[str] = None
ist_fallback: bool = False
ziel_ordner: Optional[str] = None # Ziel-Ordner für diese Regel
nur_umbenennen: bool = False # Nur umbenennen, nicht verschieben
class RegelResponse(BaseModel):
id: int
name: str
prioritaet: int
aktiv: bool
muster: dict
extraktion: dict
ist_fallback: bool = False
schema: str
unterordner: Optional[str]
freie_ordner: Optional[List[str]] = []
ziel_ordner: Optional[str] = None
nur_umbenennen: bool = False
ordner_ids: List[int] = [] # IDs der zugeordneten Ordner
class Config:
from_attributes = True
class RegelTestRequest(BaseModel):
regel: dict
text: str
# ============ Verzeichnis-Browser ============
@router.get("/browse")
def browse_directory(path: str = "/"):
"""Listet Verzeichnisse für File-Browser"""
import os
# Sicherheit: Nur bestimmte Basispfade erlauben
allowed_bases = ["/srv", "/home", "/mnt", "/media", "/data", "/tmp"]
path = os.path.abspath(path)
# Prüfen ob Pfad erlaubt
is_allowed = any(path.startswith(base) for base in allowed_bases) or path == "/"
if not is_allowed:
return {"error": "Pfad nicht erlaubt", "entries": []}
if not os.path.exists(path):
return {"error": "Pfad existiert nicht", "entries": []}
if not os.path.isdir(path):
return {"error": "Kein Verzeichnis", "entries": []}
try:
entries = []
for entry in sorted(os.listdir(path)):
full_path = os.path.join(path, entry)
if os.path.isdir(full_path):
entries.append({
"name": entry,
"path": full_path,
"type": "directory"
})
return {
"current": path,
"parent": os.path.dirname(path) if path != "/" else None,
"entries": entries
}
except PermissionError:
return {"error": "Zugriff verweigert", "entries": []}
# ============ BEREICH 1: Postfächer ============
@router.get("/postfaecher", response_model=List[PostfachResponse])
def liste_postfaecher(db: Session = Depends(get_db)):
return db.query(Postfach).all()
@router.post("/postfaecher", response_model=PostfachResponse)
def erstelle_postfach(data: PostfachCreate, db: Session = Depends(get_db)):
postfach = Postfach(**data.dict())
db.add(postfach)
db.commit()
db.refresh(postfach)
return postfach
@router.put("/postfaecher/{id}", response_model=PostfachResponse)
def aktualisiere_postfach(id: int, data: PostfachUpdate, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
update_data = data.dict()
# Passwort nur aktualisieren wenn nicht leer
if not update_data.get("passwort"):
del update_data["passwort"]
for key, value in update_data.items():
setattr(postfach, key, value)
db.commit()
db.refresh(postfach)
return postfach
@router.delete("/postfaecher/{id}")
def loesche_postfach(id: int, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(postfach)
db.commit()
return {"message": "Gelöscht"}
@router.post("/postfaecher/{id}/test")
def teste_postfach(id: int, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
fetcher = MailFetcher({
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner
})
return fetcher.test_connection()
@router.get("/postfaecher/{id}/abrufen/stream")
def rufe_postfach_ab_stream(id: int, db: Session = Depends(get_db)):
"""Streaming-Endpoint für Mail-Abruf mit Live-Updates"""
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
# Daten kopieren für Generator (Session ist nach return nicht mehr verfügbar)
pf_data = {
"id": postfach.id,
"name": postfach.name,
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"alle_ordner": postfach.alle_ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb,
"min_groesse_kb": postfach.min_groesse_kb or 10,
"ziel_ordner": postfach.ziel_ordner,
"ab_datum": postfach.ab_datum.isoformat() if postfach.ab_datum else None,
"groessen_filter": postfach.groessen_filter or {}
}
# Bereits verarbeitete Message-IDs laden
bereits_verarbeitet = set(
row.message_id for row in
db.query(VerarbeiteteMail.message_id)
.filter(VerarbeiteteMail.postfach_id == id)
.all()
)
def event_generator():
from ..models.database import SessionLocal
def send_event(data):
return f"data: {json.dumps(data)}\n\n"
yield send_event({"type": "start", "postfach": pf_data["name"], "bereits_verarbeitet": len(bereits_verarbeitet)})
# Zielordner erstellen
ziel = Path(pf_data["ziel_ordner"])
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": pf_data["imap_server"],
"imap_port": pf_data["imap_port"],
"email": pf_data["email"],
"passwort": pf_data["passwort"],
"ordner": pf_data["ordner"],
"erlaubte_typen": pf_data["erlaubte_typen"],
"max_groesse_mb": pf_data["max_groesse_mb"],
"min_groesse_kb": pf_data["min_groesse_kb"],
"ab_datum": pf_data["ab_datum"],
"groessen_filter": pf_data["groessen_filter"]
})
attachments = []
try:
# Generator für streaming
for event in fetcher.fetch_attachments_generator(
ziel,
nur_ungelesen=False,
alle_ordner=pf_data["alle_ordner"],
bereits_verarbeitet=bereits_verarbeitet
):
yield send_event(event)
if event.get("type") == "datei":
attachments.append(event)
# DB-Session für Speicherung
session = SessionLocal()
try:
verarbeitete_msg_ids = set()
for att in attachments:
msg_id = att.get("message_id")
if msg_id and msg_id not in verarbeitete_msg_ids:
verarbeitete_msg_ids.add(msg_id)
session.add(VerarbeiteteMail(
postfach_id=pf_data["id"],
message_id=msg_id,
ordner=att.get("ordner", ""),
betreff=att.get("betreff", "")[:500] if att.get("betreff") else None,
absender=att.get("absender", "")[:255] if att.get("absender") else None,
anzahl_attachments=1
))
# Postfach aktualisieren
pf = session.query(Postfach).filter(Postfach.id == pf_data["id"]).first()
if pf:
pf.letzter_abruf = datetime.utcnow()
pf.letzte_anzahl = len(attachments)
session.commit()
finally:
session.close()
yield send_event({"type": "fertig", "anzahl": len(attachments)})
except Exception as e:
yield send_event({"type": "fehler", "nachricht": str(e)})
finally:
fetcher.disconnect()
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
@router.post("/postfaecher/{id}/abrufen")
def rufe_postfach_ab(id: int, db: Session = Depends(get_db)):
postfach = db.query(Postfach).filter(Postfach.id == id).first()
if not postfach:
raise HTTPException(status_code=404, detail="Nicht gefunden")
# Bereits verarbeitete Message-IDs laden
bereits_verarbeitet = set(
row.message_id for row in
db.query(VerarbeiteteMail.message_id)
.filter(VerarbeiteteMail.postfach_id == id)
.all()
)
# Zielordner erstellen
ziel = Path(postfach.ziel_ordner)
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb,
"min_groesse_kb": postfach.min_groesse_kb or 10
})
try:
attachments = fetcher.fetch_attachments(
ziel,
nur_ungelesen=False, # Alle Mails durchsuchen
alle_ordner=postfach.alle_ordner,
bereits_verarbeitet=bereits_verarbeitet
)
# Verarbeitete Mails in DB speichern
verarbeitete_msg_ids = set()
for att in attachments:
msg_id = att.get("message_id")
if msg_id and msg_id not in verarbeitete_msg_ids:
verarbeitete_msg_ids.add(msg_id)
db.add(VerarbeiteteMail(
postfach_id=id,
message_id=msg_id,
ordner=att.get("ordner", ""),
betreff=att.get("betreff", "")[:500] if att.get("betreff") else None,
absender=att.get("absender", "")[:255] if att.get("absender") else None,
anzahl_attachments=1
))
postfach.letzter_abruf = datetime.utcnow()
postfach.letzte_anzahl = len(attachments)
db.commit()
return {
"ergebnisse": [{
"postfach": postfach.name,
"anzahl": len(attachments),
"dateien": [a["original_name"] for a in attachments],
"bereits_verarbeitet": len(bereits_verarbeitet)
}]
}
except Exception as e:
return {
"ergebnisse": [{
"postfach": postfach.name,
"fehler": str(e)
}]
}
finally:
fetcher.disconnect()
@router.post("/postfaecher/abrufen-alle")
def rufe_alle_postfaecher_ab(db: Session = Depends(get_db)):
postfaecher = db.query(Postfach).filter(Postfach.aktiv == True).all()
ergebnisse = []
for postfach in postfaecher:
ziel = Path(postfach.ziel_ordner)
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb,
"min_groesse_kb": postfach.min_groesse_kb or 10
})
try:
attachments = fetcher.fetch_attachments(ziel)
postfach.letzter_abruf = datetime.utcnow()
postfach.letzte_anzahl = len(attachments)
ergebnisse.append({
"postfach": postfach.name,
"anzahl": len(attachments),
"dateien": [a["original_name"] for a in attachments]
})
except Exception as e:
ergebnisse.append({
"postfach": postfach.name,
"fehler": str(e)
})
finally:
fetcher.disconnect()
db.commit()
return {"ergebnisse": ergebnisse}
@router.get("/postfaecher/abrufen-alle/stream")
def rufe_alle_postfaecher_ab_stream(db: Session = Depends(get_db)):
"""Streaming-Endpoint für Abruf aller Postfächer mit Live-Updates"""
postfaecher = db.query(Postfach).filter(Postfach.aktiv == True).all()
# Daten kopieren für Generator
pf_data_list = []
for postfach in postfaecher:
bereits_verarbeitet = set(
row.message_id for row in
db.query(VerarbeiteteMail.message_id)
.filter(VerarbeiteteMail.postfach_id == postfach.id)
.all()
)
pf_data_list.append({
"id": postfach.id,
"name": postfach.name,
"imap_server": postfach.imap_server,
"imap_port": postfach.imap_port,
"email": postfach.email,
"passwort": postfach.passwort,
"ordner": postfach.ordner,
"alle_ordner": postfach.alle_ordner,
"erlaubte_typen": postfach.erlaubte_typen,
"max_groesse_mb": postfach.max_groesse_mb,
"min_groesse_kb": postfach.min_groesse_kb or 10,
"ziel_ordner": postfach.ziel_ordner,
"ab_datum": postfach.ab_datum.isoformat() if postfach.ab_datum else None,
"groessen_filter": postfach.groessen_filter or {},
"bereits_verarbeitet": bereits_verarbeitet
})
def event_generator():
from ..models.database import SessionLocal
def send_event(data):
return f"data: {json.dumps(data)}\n\n"
yield send_event({"type": "init", "anzahl_postfaecher": len(pf_data_list)})
for pf_data in pf_data_list:
yield send_event({"type": "postfach_start", "name": pf_data["name"], "bereits_verarbeitet": len(pf_data["bereits_verarbeitet"])})
ziel = Path(pf_data["ziel_ordner"])
ziel.mkdir(parents=True, exist_ok=True)
fetcher = MailFetcher({
"imap_server": pf_data["imap_server"],
"imap_port": pf_data["imap_port"],
"email": pf_data["email"],
"passwort": pf_data["passwort"],
"ordner": pf_data["ordner"],
"erlaubte_typen": pf_data["erlaubte_typen"],
"max_groesse_mb": pf_data["max_groesse_mb"],
"min_groesse_kb": pf_data["min_groesse_kb"],
"ab_datum": pf_data["ab_datum"],
"groessen_filter": pf_data["groessen_filter"]
})
attachments = []
try:
for event in fetcher.fetch_attachments_generator(
ziel,
nur_ungelesen=False,
alle_ordner=pf_data["alle_ordner"],
bereits_verarbeitet=pf_data["bereits_verarbeitet"]
):
# Postfach-Name zum Event hinzufügen
event["postfach"] = pf_data["name"]
yield send_event(event)
if event.get("type") == "datei":
attachments.append(event)
# Verarbeitete Mails speichern
session = SessionLocal()
try:
verarbeitete_msg_ids = set()
for att in attachments:
msg_id = att.get("message_id")
if msg_id and msg_id not in verarbeitete_msg_ids:
verarbeitete_msg_ids.add(msg_id)
session.add(VerarbeiteteMail(
postfach_id=pf_data["id"],
message_id=msg_id,
ordner=att.get("ordner"),
betreff=att.get("betreff", "")[:500],
absender=att.get("absender", "")[:255],
anzahl_attachments=1
))
# Postfach Status aktualisieren
postfach_obj = session.query(Postfach).filter(Postfach.id == pf_data["id"]).first()
if postfach_obj:
postfach_obj.letzter_abruf = datetime.utcnow()
postfach_obj.letzte_anzahl = len(attachments)
session.commit()
finally:
session.close()
yield send_event({"type": "postfach_done", "name": pf_data["name"], "anzahl": len(attachments)})
except Exception as e:
yield send_event({"type": "postfach_error", "name": pf_data["name"], "fehler": str(e)})
finally:
fetcher.disconnect()
yield send_event({"type": "done"})
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
# ============ BEREICH 2: Quell-Ordner ============
@router.get("/ordner", response_model=List[OrdnerResponse])
def liste_ordner(db: Session = Depends(get_db)):
return db.query(QuellOrdner).all()
@router.post("/ordner", response_model=OrdnerResponse)
def erstelle_ordner(data: OrdnerCreate, db: Session = Depends(get_db)):
ordner = QuellOrdner(**data.dict())
db.add(ordner)
db.commit()
db.refresh(ordner)
return ordner
@router.put("/ordner/{id}", response_model=OrdnerResponse)
def aktualisiere_ordner(id: int, data: OrdnerCreate, db: Session = Depends(get_db)):
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Nicht gefunden")
for key, value in data.dict().items():
setattr(ordner, key, value)
db.commit()
db.refresh(ordner)
return ordner
@router.delete("/ordner/{id}")
def loesche_ordner(id: int, db: Session = Depends(get_db)):
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(ordner)
db.commit()
return {"message": "Gelöscht"}
@router.post("/ordner/{id}/aktivieren")
def aktiviere_ordner(id: int, db: Session = Depends(get_db)):
"""Aktiviert/Deaktiviert einen Quellordner"""
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Ordner nicht gefunden")
ordner.aktiv = not ordner.aktiv
db.commit()
return {"message": f"Ordner {'aktiviert' if ordner.aktiv else 'deaktiviert'}", "aktiv": ordner.aktiv}
@router.post("/ordner/{id}/kopieren")
def kopiere_ordner(id: int, db: Session = Depends(get_db)):
"""Kopiert eine Grobsortierung (QuellOrdner)"""
original = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not original:
raise HTTPException(status_code=404, detail="Ordner nicht gefunden")
# Kopie erstellen
kopie = QuellOrdner(
name=f"{original.name} (Kopie)",
pfad=original.pfad,
ziel_ordner=original.ziel_ordner,
dateitypen=original.dateitypen.copy() if original.dateitypen else [".pdf"],
rekursiv=original.rekursiv,
aktiv=False, # Kopie erstmal deaktiviert
zugferd_behandlung=original.zugferd_behandlung,
signiert_behandlung=original.signiert_behandlung,
ocr_aktivieren=original.ocr_aktivieren,
original_sichern=original.original_sichern
)
db.add(kopie)
db.commit()
db.refresh(kopie)
return {"id": kopie.id, "name": kopie.name, "message": "Grobsortierung kopiert"}
@router.get("/ordner/{id}/scannen")
def scanne_ordner(id: int, db: Session = Depends(get_db)):
"""Zeigt Vorschau der Dateien im Ordner (ohne Verarbeitung)"""
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Nicht gefunden")
pfad = Path(ordner.pfad)
if not pfad.exists():
return {"anzahl": 0, "fehler": "Ordner existiert nicht"}
# Dateien sammeln (rekursiv oder nicht)
dateien = []
pattern = "**/*" if ordner.rekursiv else "*"
for f in pfad.glob(pattern):
if f.is_file() and f.suffix.lower() in [t.lower() for t in ordner.dateitypen]:
dateien.append(f)
return {"anzahl": len(dateien), "dateien": [str(f.relative_to(pfad)) for f in dateien[:30]]}
@router.post("/ordner/{id}/verarbeiten")
def verarbeite_ordner(id: int, db: Session = Depends(get_db)):
"""Verarbeitet alle Dateien eines spezifischen Ordners"""
quell_ordner = db.query(QuellOrdner).filter(QuellOrdner.id == id).first()
if not quell_ordner:
raise HTTPException(status_code=404, detail="Nicht gefunden")
pfad = Path(quell_ordner.pfad)
if not pfad.exists():
return {"fehler": "Ordner existiert nicht", "verarbeitet": []}
regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all()
# Prüfen ob direkt_verschieben aktiv ist
hat_direkt_verschieben = getattr(quell_ordner, 'direkt_verschieben', False)
if not regeln and not hat_direkt_verschieben:
return {"fehler": "Keine Regeln definiert", "verarbeitet": []}
# Regeln in Dict-Format
regeln_dicts = []
for r in regeln:
regeln_dicts.append({
"id": r.id,
"name": r.name,
"prioritaet": r.prioritaet,
"muster": r.muster,
"extraktion": r.extraktion,
"schema": r.schema,
"unterordner": r.unterordner
})
sorter = Sorter(regeln_dicts)
pdf_processor = PDFProcessor()
ergebnis = {
"gesamt": 0,
"sortiert": 0,
"zugferd": 0,
"keine_regel": 0,
"fehler": 0,
"verarbeitet": []
}
ziel_basis = Path(quell_ordner.ziel_ordner)
dateien = sammle_dateien(quell_ordner)
for datei in dateien:
ergebnis["gesamt"] += 1
try:
rel_pfad = str(datei.relative_to(pfad))
except:
rel_pfad = datei.name
datei_info = {"original": rel_pfad}
try:
ist_pdf = datei.suffix.lower() == ".pdf"
text = ""
ist_zugferd = False
ocr_gemacht = False
# Nur PDFs durch den PDF-Processor
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)
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
# ZUGFeRD behandeln basierend auf Einstellung
zugferd_modus = getattr(quell_ordner, 'zugferd_behandlung', 'separieren') or 'separieren'
if ist_zugferd:
if zugferd_modus == "ignorieren":
# ZUGFeRD überspringen
datei_info["status"] = "zugferd_ignoriert"
ergebnis["zugferd"] += 1
ergebnis["verarbeitet"].append(datei_info)
continue
elif zugferd_modus == "separieren":
# Original sichern falls konfiguriert (VOR dem Verschieben)
original_sichern = getattr(quell_ordner, 'original_sichern', None)
if original_sichern:
import shutil
backup_dir = Path(original_sichern)
backup_dir.mkdir(parents=True, exist_ok=True)
backup_pfad = backup_dir / datei.name
counter = 1
while backup_pfad.exists():
backup_pfad = backup_dir / f"{datei.stem}_{counter}{datei.suffix}"
counter += 1
shutil.copy2(str(datei), str(backup_pfad))
datei_info["original_gesichert"] = str(backup_pfad)
logger.info(f"ZUGFeRD Original gesichert: {backup_pfad}")
# Direkt in Zielordner verschieben (ohne Umbenennung)
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)
ergebnis["zugferd"] += 1
datei_info["zugferd"] = True
datei_info["neuer_name"] = neuer_pfad.name
datei_info["status"] = "zugferd_verschoben"
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"
))
ergebnis["verarbeitet"].append(datei_info)
continue
# Bei "normal" oder "regel": fortfahren mit Regel-Matching
else:
# Keine ZUGFeRD-PDF
if zugferd_modus == "separieren":
# Bei "separieren" NUR ZUGFeRD verarbeiten, normale PDFs ignorieren
datei_info["status"] = "kein_zugferd_ignoriert"
ergebnis["verarbeitet"].append(datei_info)
continue
# Direkt verschieben ohne Regel?
if hat_direkt_verschieben:
# Einfach in Zielordner verschieben ohne Umbenennung
ziel_basis.mkdir(parents=True, exist_ok=True)
# Original sichern falls konfiguriert
original_sichern = getattr(quell_ordner, 'original_sichern', None)
if original_sichern:
import shutil
backup_dir = Path(original_sichern)
backup_dir.mkdir(parents=True, exist_ok=True)
backup_pfad = backup_dir / datei.name
counter = 1
while backup_pfad.exists():
backup_pfad = backup_dir / f"{datei.stem}_{counter}{datei.suffix}"
counter += 1
shutil.copy2(str(datei), str(backup_pfad))
datei_info["original_gesichert"] = str(backup_pfad)
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)
ergebnis["sortiert"] += 1
datei_info["neuer_name"] = neuer_pfad.name
datei_info["status"] = "direkt_verschoben"
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=str(neuer_pfad),
neuer_name=neuer_pfad.name,
ist_zugferd=ist_zugferd,
ocr_durchgefuehrt=ocr_gemacht,
status="direkt_verschoben"
))
ergebnis["verarbeitet"].append(datei_info)
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:
datei_info["status"] = "keine_regel"
ergebnis["keine_regel"] += 1
ergebnis["verarbeitet"].append(datei_info)
continue
# Felder extrahieren
extrahiert = sorter.extrahiere_felder(regel, doc_info)
# Dateiendung beibehalten
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)
# Zielordner
ziel = ziel_basis
if regel.get("unterordner"):
ziel = ziel / regel["unterordner"]
ziel.mkdir(parents=True, exist_ok=True)
# Original sichern falls konfiguriert (VOR dem Verschieben)
original_sichern = getattr(quell_ordner, 'original_sichern', None)
if original_sichern:
import shutil
backup_dir = Path(original_sichern)
backup_dir.mkdir(parents=True, exist_ok=True)
backup_pfad = backup_dir / datei.name
counter = 1
while backup_pfad.exists():
backup_pfad = backup_dir / f"{datei.stem}_{counter}{datei.suffix}"
counter += 1
shutil.copy2(str(datei), str(backup_pfad))
datei_info["original_gesichert"] = str(backup_pfad)
logger.info(f"Original gesichert: {backup_pfad}")
# Verschieben
neuer_pfad = sorter.verschiebe_datei(str(datei), str(ziel), neuer_name)
ergebnis["sortiert"] += 1
datei_info["neuer_name"] = neuer_name
datei_info["regel"] = regel.get("name")
if ist_zugferd:
datei_info["zugferd"] = True
ergebnis["zugferd"] += 1
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=neuer_pfad,
neuer_name=neuer_name,
ist_zugferd=ist_zugferd,
ocr_durchgefuehrt=ocr_gemacht,
status="sortiert",
extrahierte_daten=extrahiert
))
except Exception as e:
ergebnis["fehler"] += 1
datei_info["fehler"] = str(e)
ergebnis["verarbeitet"].append(datei_info)
db.commit()
return ergebnis
@router.get("/grobsortierung/stream")
async def grobsortierung_stream(db: Session = Depends(get_db)):
"""Streaming-Endpoint für Grobsortierung aller aktiven Ordner mit Live-Updates"""
from ..models.database import SessionLocal
# Aktive Ordner vorab laden
ordner_liste = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all()
ordner_daten = [{
"id": o.id, "name": o.name, "pfad": o.pfad, "ziel_ordner": o.ziel_ordner,
"dateitypen": o.dateitypen, "rekursiv": o.rekursiv,
"direkt_verschieben": getattr(o, 'direkt_verschieben', False),
"zugferd_behandlung": getattr(o, 'zugferd_behandlung', 'separieren'),
"ocr_aktivieren": getattr(o, 'ocr_aktivieren', True),
"original_sichern": getattr(o, 'original_sichern', None)
} for o in ordner_liste]
# Regeln vorab laden
regeln = db.query(SortierRegel).filter(SortierRegel.aktiv == True).order_by(SortierRegel.prioritaet).all()
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]
async def event_generator():
def send_event(data):
return f"data: {json.dumps(data)}\n\n"
if not ordner_daten:
yield send_event({"type": "fehler", "nachricht": "Keine aktiven Ordner konfiguriert"})
return
pdf_processor = PDFProcessor()
sorter = Sorter(regeln_dicts) if regeln_dicts else None
gesamt_stats = {"gesamt": 0, "sortiert": 0, "zugferd": 0, "fehler": 0}
# Dateien zählen
total_dateien = 0
for o in ordner_daten:
pfad = Path(o["pfad"])
if pfad.exists():
dateien = sammle_dateien_aus_pfad(str(pfad), o["dateitypen"], o["rekursiv"])
total_dateien += len(dateien)
yield send_event({"type": "start", "ordner_count": len(ordner_daten), "gesamt": total_dateien})
await asyncio.sleep(0)
session = SessionLocal()
try:
for quell_ordner in ordner_daten:
pfad = Path(quell_ordner["pfad"])
if not pfad.exists():
yield send_event({"type": "ordner_fehler", "ordner": quell_ordner["name"], "fehler": "Ordner existiert nicht"})
await asyncio.sleep(0)
continue
ziel_basis = Path(quell_ordner["ziel_ordner"])
dateien = sammle_dateien_aus_pfad(str(pfad), quell_ordner["dateitypen"], quell_ordner["rekursiv"])
yield send_event({
"type": "ordner",
"ordner": quell_ordner["name"],
"dateien": len(dateien)
})
await asyncio.sleep(0)
for datei in dateien:
gesamt_stats["gesamt"] += 1
datei_info = {"original": datei.name, "ordner": quell_ordner["name"]}
try:
ist_pdf = datei.suffix.lower() == ".pdf"
text = ""
ist_zugferd = False
ocr_gemacht = False
# PDF verarbeiten
if ist_pdf:
pdf_result = pdf_processor.verarbeite(
str(datei),
ocr_erlaubt=quell_ordner.get("ocr_aktivieren", True),
original_backup_pfad=quell_ordner.get("original_sichern")
)
if pdf_result.get("fehler"):
raise Exception(pdf_result["fehler"])
text = pdf_result.get("text", "")
ist_zugferd = pdf_result.get("ist_zugferd", False)
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
# Direkt verschieben?
if quell_ordner["direkt_verschieben"]:
ziel_basis.mkdir(parents=True, exist_ok=True)
# Original sichern
if quell_ordner["original_sichern"]:
import shutil
backup_dir = Path(quell_ordner["original_sichern"])
backup_dir.mkdir(parents=True, exist_ok=True)
backup_pfad = backup_dir / datei.name
counter = 1
while backup_pfad.exists():
backup_pfad = backup_dir / f"{datei.stem}_{counter}{datei.suffix}"
counter += 1
shutil.copy2(str(datei), str(backup_pfad))
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)
gesamt_stats["sortiert"] += 1
datei_info["neuer_name"] = neuer_pfad.name
datei_info["status"] = "verschoben"
session.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=str(neuer_pfad),
neuer_name=neuer_pfad.name,
ist_zugferd=ist_zugferd,
ocr_durchgefuehrt=ocr_gemacht,
status="direkt_verschoben"
))
yield send_event({"type": "datei_fertig", **datei_info})
await asyncio.sleep(0)
continue
# Mit Regeln sortieren
if sorter:
doc_info = {
"text": text,
"original_name": datei.name,
"absender": "",
"dateityp": datei.suffix.lower()
}
regel = sorter.finde_passende_regel(doc_info)
if regel:
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)
neuer_pfad = sorter.verschiebe_datei(str(datei), str(ziel), neuer_name)
gesamt_stats["sortiert"] += 1
datei_info["neuer_name"] = neuer_name
datei_info["regel"] = regel.get("name")
session.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=neuer_pfad,
neuer_name=neuer_name,
ist_zugferd=ist_zugferd,
ocr_durchgefuehrt=ocr_gemacht,
status="sortiert",
extrahierte_daten=extrahiert
))
yield send_event({"type": "datei_fertig", **datei_info})
await asyncio.sleep(0)
continue
# Keine Regel gefunden
datei_info["status"] = "keine_regel"
yield send_event({"type": "datei_keine_regel", **datei_info})
await asyncio.sleep(0)
except Exception as e:
gesamt_stats["fehler"] += 1
datei_info["fehler"] = str(e)
yield send_event({"type": "datei_fehler", **datei_info})
await asyncio.sleep(0)
session.commit()
finally:
session.close()
yield send_event({"type": "fertig", **gesamt_stats})
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
# ============ Regeln ============
@router.get("/regeln")
def liste_regeln(db: Session = Depends(get_db)):
"""Gibt alle Regeln mit ihren zugeordneten Ordner-IDs zurück"""
regeln = db.query(SortierRegel).order_by(SortierRegel.prioritaet).all()
# Alle Ordner-Zuweisungen auf einmal laden
alle_zuweisungen = db.query(OrdnerRegel).all()
regel_ordner_map = {}
for z in alle_zuweisungen:
if z.regel_id not in regel_ordner_map:
regel_ordner_map[z.regel_id] = []
regel_ordner_map[z.regel_id].append(z.ordner_id)
# Regeln mit Ordner-IDs anreichern
result = []
for r in regeln:
regel_dict = {
"id": r.id,
"name": r.name,
"prioritaet": r.prioritaet,
"aktiv": r.aktiv,
"muster": r.muster or {},
"extraktion": r.extraktion or {},
"ist_fallback": r.ist_fallback,
"schema": r.schema,
"unterordner": r.unterordner,
"freie_ordner": r.freie_ordner or [],
"ziel_ordner": r.ziel_ordner,
"nur_umbenennen": r.nur_umbenennen,
"ordner_ids": regel_ordner_map.get(r.id, [])
}
result.append(regel_dict)
return result
@router.post("/regeln", response_model=RegelResponse)
def erstelle_regel(data: RegelCreate, db: Session = Depends(get_db)):
regel = SortierRegel(**data.dict())
db.add(regel)
db.commit()
db.refresh(regel)
return regel
@router.put("/regeln/{id}", response_model=RegelResponse)
def aktualisiere_regel(id: int, data: RegelCreate, db: Session = Depends(get_db)):
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Nicht gefunden")
for key, value in data.dict().items():
setattr(regel, key, value)
db.commit()
db.refresh(regel)
return regel
@router.delete("/regeln/{id}")
def loesche_regel(id: int, db: Session = Depends(get_db)):
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(regel)
db.commit()
return {"message": "Gelöscht"}
@router.post("/regeln/{id}/aktivieren")
def aktiviere_regel(id: int, db: Session = Depends(get_db)):
"""Aktiviert/Deaktiviert eine Sortierregel"""
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
regel.aktiv = not regel.aktiv
db.commit()
return {"message": f"Regel {'aktiviert' if regel.aktiv else 'deaktiviert'}", "aktiv": regel.aktiv}
@router.post("/regeln/{id}/kopieren")
def kopiere_regel(id: int, db: Session = Depends(get_db)):
"""Kopiert eine Sortierregel"""
original = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not original:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
# Kopie erstellen
kopie = SortierRegel(
name=f"{original.name} (Kopie)",
prioritaet=original.prioritaet,
aktiv=False, # Kopie erstmal deaktiviert
muster=original.muster.copy() if original.muster else {},
extraktion=original.extraktion.copy() if original.extraktion else {},
schema=original.schema,
unterordner=original.unterordner,
ist_fallback=original.ist_fallback,
freie_ordner=original.freie_ordner.copy() if original.freie_ordner else [],
ziel_ordner=original.ziel_ordner,
nur_umbenennen=original.nur_umbenennen
)
db.add(kopie)
db.commit()
db.refresh(kopie)
return {"id": kopie.id, "name": kopie.name, "message": "Regel kopiert"}
@router.post("/regeln/test")
def teste_regel(data: RegelTestRequest):
regel = data.regel
regel["aktiv"] = True
regel["prioritaet"] = 1
sorter = Sorter([regel])
doc_info = {"text": data.text, "original_name": "test.pdf", "absender": ""}
passend = sorter.finde_passende_regel(doc_info)
if passend:
extrahiert = sorter.extrahiere_felder(passend, doc_info)
dateiname = sorter.generiere_dateinamen(passend, extrahiert)
return {"passt": True, "extrahiert": extrahiert, "dateiname": dateiname}
return {"passt": False}
@router.get("/regeln/export")
def export_regeln(db: Session = Depends(get_db)):
"""Exportiert alle Regeln als JSON"""
regeln = db.query(SortierRegel).order_by(SortierRegel.prioritaet).all()
export_data = []
for r in regeln:
export_data.append({
"name": r.name,
"prioritaet": r.prioritaet,
"muster": r.muster,
"extraktion": r.extraktion,
"schema": r.schema,
"unterordner": r.unterordner,
"ziel_ordner": r.ziel_ordner,
"nur_umbenennen": r.nur_umbenennen,
"ist_fallback": r.ist_fallback,
"aktiv": r.aktiv
})
return {"regeln": export_data, "anzahl": len(export_data)}
@router.get("/regeln/{id}/export")
def export_einzelne_regel(id: int, db: Session = Depends(get_db)):
"""Exportiert eine einzelne Regel als JSON"""
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
export_data = {
"name": regel.name,
"prioritaet": regel.prioritaet,
"muster": regel.muster,
"extraktion": regel.extraktion,
"schema": regel.schema,
"unterordner": regel.unterordner,
"ziel_ordner": regel.ziel_ordner,
"nur_umbenennen": regel.nur_umbenennen,
"ist_fallback": regel.ist_fallback,
"aktiv": regel.aktiv
}
return {"regeln": [export_data], "anzahl": 1, "regel_name": regel.name}
class RegelnImportRequest(BaseModel):
regeln: List[dict]
modus: str = "hinzufuegen" # "hinzufuegen", "ersetzen", "aktualisieren"
@router.post("/regeln/import")
def import_regeln(data: RegelnImportRequest, db: Session = Depends(get_db)):
"""Importiert Regeln aus JSON"""
importiert = 0
aktualisiert = 0
uebersprungen = 0
if data.modus == "ersetzen":
# Alle bestehenden Regeln löschen
db.query(OrdnerRegel).delete()
db.query(SortierRegel).delete()
for regel_data in data.regeln:
name = regel_data.get("name")
if not name:
uebersprungen += 1
continue
# Prüfen ob Regel mit diesem Namen existiert
existiert = db.query(SortierRegel).filter(SortierRegel.name == name).first()
if existiert and data.modus == "hinzufuegen":
uebersprungen += 1
continue
if existiert and data.modus == "aktualisieren":
# Bestehende Regel aktualisieren
existiert.prioritaet = regel_data.get("prioritaet", 100)
existiert.muster = regel_data.get("muster", {})
existiert.extraktion = regel_data.get("extraktion", {})
existiert.schema = regel_data.get("schema", "{datum} - Dokument.pdf")
existiert.unterordner = regel_data.get("unterordner")
existiert.ziel_ordner = regel_data.get("ziel_ordner")
existiert.nur_umbenennen = regel_data.get("nur_umbenennen", False)
existiert.ist_fallback = regel_data.get("ist_fallback", False)
existiert.aktiv = regel_data.get("aktiv", True)
aktualisiert += 1
else:
# Neue Regel erstellen
neue_regel = SortierRegel(
name=name,
prioritaet=regel_data.get("prioritaet", 100),
muster=regel_data.get("muster", {}),
extraktion=regel_data.get("extraktion", {}),
schema=regel_data.get("schema", "{datum} - Dokument.pdf"),
unterordner=regel_data.get("unterordner"),
ziel_ordner=regel_data.get("ziel_ordner"),
nur_umbenennen=regel_data.get("nur_umbenennen", False),
ist_fallback=regel_data.get("ist_fallback", False),
aktiv=regel_data.get("aktiv", True)
)
db.add(neue_regel)
importiert += 1
db.commit()
return {
"importiert": importiert,
"aktualisiert": aktualisiert,
"uebersprungen": uebersprungen,
"gesamt": len(data.regeln)
}
# ============ Ordner-Regel-Zuweisungen ============
@router.get("/ordner/{ordner_id}/regeln")
def get_ordner_regeln(ordner_id: int, db: Session = Depends(get_db)):
"""Gibt alle Regeln zurück die diesem Ordner zugewiesen sind"""
zuweisungen = db.query(OrdnerRegel).filter(OrdnerRegel.ordner_id == ordner_id).all()
regel_ids = [z.regel_id for z in zuweisungen]
return {"regel_ids": regel_ids}
@router.post("/ordner/{ordner_id}/regeln/{regel_id}")
def ordner_regel_zuweisen(ordner_id: int, regel_id: int, db: Session = Depends(get_db)):
"""Weist eine Regel einem Ordner zu"""
# Prüfe ob Ordner und Regel existieren
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == ordner_id).first()
regel = db.query(SortierRegel).filter(SortierRegel.id == regel_id).first()
if not ordner:
raise HTTPException(status_code=404, detail="Ordner nicht gefunden")
if not regel:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
# Prüfe ob Zuweisung bereits existiert
existiert = db.query(OrdnerRegel).filter(
OrdnerRegel.ordner_id == ordner_id,
OrdnerRegel.regel_id == regel_id
).first()
if existiert:
return {"message": "Bereits zugewiesen"}
# Neue Zuweisung erstellen
zuweisung = OrdnerRegel(ordner_id=ordner_id, regel_id=regel_id)
db.add(zuweisung)
db.commit()
return {"message": "Zugewiesen"}
@router.delete("/ordner/{ordner_id}/regeln/{regel_id}")
def ordner_regel_entfernen(ordner_id: int, regel_id: int, db: Session = Depends(get_db)):
"""Entfernt eine Regel-Zuweisung von einem Ordner"""
zuweisung = db.query(OrdnerRegel).filter(
OrdnerRegel.ordner_id == ordner_id,
OrdnerRegel.regel_id == regel_id
).first()
if not zuweisung:
raise HTTPException(status_code=404, detail="Zuweisung nicht gefunden")
db.delete(zuweisung)
db.commit()
return {"message": "Entfernt"}
@router.get("/regeln/{regel_id}/ordner")
def get_regel_ordner(regel_id: int, db: Session = Depends(get_db)):
"""Gibt alle Ordner zurück denen diese Regel zugewiesen ist"""
# Zugewiesene Quell-Ordner (über OrdnerRegel)
zuweisungen = db.query(OrdnerRegel).filter(OrdnerRegel.regel_id == regel_id).all()
ordner_ids = [z.ordner_id for z in zuweisungen]
# Freie Ordner aus der Regel selbst
regel = db.query(SortierRegel).filter(SortierRegel.id == regel_id).first()
freie_ordner = regel.freie_ordner if regel and regel.freie_ordner else []
return {"ordner_ids": ordner_ids, "freie_ordner": freie_ordner}
class OrdnerZuweisungRequest(BaseModel):
ordner_ids: List[int] = []
freie_ordner: List[str] = []
@router.put("/regeln/{regel_id}/ordner")
def set_regel_ordner(regel_id: int, data: OrdnerZuweisungRequest, db: Session = Depends(get_db)):
"""Setzt alle Ordner-Zuweisungen für eine Regel (ersetzt bestehende)"""
# Prüfe ob Regel existiert
regel = db.query(SortierRegel).filter(SortierRegel.id == regel_id).first()
if not regel:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
# Lösche alle bestehenden Zuweisungen
db.query(OrdnerRegel).filter(OrdnerRegel.regel_id == regel_id).delete()
# Erstelle neue Zuweisungen
for ordner_id in data.ordner_ids:
ordner = db.query(QuellOrdner).filter(QuellOrdner.id == ordner_id).first()
if ordner:
zuweisung = OrdnerRegel(ordner_id=ordner_id, regel_id=regel_id)
db.add(zuweisung)
# Freie Ordner in der Regel speichern
regel.freie_ordner = data.freie_ordner
db.commit()
return {"message": f"{len(data.ordner_ids)} Ordner + {len(data.freie_ordner)} freie Ordner zugewiesen"}
# ============ Sortierung ============
def sammle_dateien(ordner: QuellOrdner) -> list:
"""Sammelt alle Dateien aus einem Ordner (rekursiv oder nicht)"""
pfad = Path(ordner.pfad)
if not pfad.exists():
return []
dateien = []
pattern = "**/*" if ordner.rekursiv else "*"
erlaubte = [t.lower() for t in (ordner.dateitypen or [".pdf"])]
# Wenn ZUGFeRD oder Signiert aktiviert, PDFs auch sammeln (für Erkennung)
zugferd_aktiv = getattr(ordner, 'zugferd_behandlung', 'normal') == 'separieren'
signiert_aktiv = getattr(ordner, 'signiert_behandlung', 'normal') == 'separieren'
pdf_fuer_erkennung = (zugferd_aktiv or signiert_aktiv) and ".pdf" not in erlaubte
for f in pfad.glob(pattern):
if f.is_file():
suffix = f.suffix.lower()
if suffix in erlaubte:
dateien.append(f)
elif pdf_fuer_erkennung and suffix == ".pdf":
# PDF nur für ZUGFeRD/Signiert-Erkennung hinzufügen
dateien.append(f)
return dateien
def sammle_dateien_aus_pfad(pfad_str: str, erlaubte_typen: List[str] = None, rekursiv: bool = True) -> list:
"""Sammelt alle Dateien aus einem freien Ordner-Pfad"""
pfad = Path(pfad_str)
if not pfad.exists() or not pfad.is_dir():
return []
dateien = []
pattern = "**/*" if rekursiv else "*"
erlaubte = [t.lower() for t in (erlaubte_typen or [".pdf"])]
for f in pfad.glob(pattern):
if f.is_file() and f.suffix.lower() in erlaubte:
dateien.append(f)
return dateien
@router.post("/sortierung/starten")
def starte_sortierung(db: Session = Depends(get_db)):
ordner_liste = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all()
if not ordner_liste:
return {"fehler": "Keine Quell-Ordner konfiguriert", "verarbeitet": []}
pdf_processor = PDFProcessor()
ergebnis = {
"gesamt": 0,
"sortiert": 0,
"zugferd": 0,
"fehler": 0,
"verarbeitet": []
}
# Fallback-Regeln laden (gelten für alle Ordner wenn keine andere Regel passt)
fallback_regeln = db.query(SortierRegel).filter(
SortierRegel.aktiv == True,
SortierRegel.ist_fallback == True
).order_by(SortierRegel.prioritaet).all()
fallback_dicts = [{
"id": r.id, "name": r.name, "prioritaet": r.prioritaet,
"muster": r.muster, "extraktion": r.extraktion,
"schema": r.schema, "unterordner": r.unterordner,
"ziel_ordner": getattr(r, 'ziel_ordner', None),
"nur_umbenennen": getattr(r, 'nur_umbenennen', False)
} for r in fallback_regeln]
for quell_ordner in ordner_liste:
# Sortierregeln suchen im ZIEL-Ordner (wo Dateien nach Grobsortierung liegen)
pfad = Path(quell_ordner.ziel_ordner)
if not pfad.exists():
continue
dateien = sammle_dateien_aus_pfad(str(pfad), [".pdf"], rekursiv=True)
# Lade nur Regeln die diesem Ordner zugewiesen sind
zuweisungen = db.query(OrdnerRegel).filter(OrdnerRegel.ordner_id == quell_ordner.id).all()
zugewiesene_regel_ids = [z.regel_id for z in zuweisungen]
if zugewiesene_regel_ids:
regeln = db.query(SortierRegel).filter(
SortierRegel.id.in_(zugewiesene_regel_ids),
SortierRegel.aktiv == True,
SortierRegel.ist_fallback == False # Fallbacks separat behandeln
).order_by(SortierRegel.prioritaet).all()
else:
regeln = []
# 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,
"ziel_ordner": getattr(r, 'ziel_ordner', None),
"nur_umbenennen": getattr(r, 'nur_umbenennen', False)
} for r in regeln]
# Sorter mit Ordner-spezifischen Regeln
sorter = Sorter(regeln_dicts) if regeln_dicts else None
fallback_sorter = Sorter(fallback_dicts) if fallback_dicts else None
print(f"=== Ordner '{quell_ordner.name}': {len(dateien)} Dateien, {len(regeln_dicts)} zugewiesene Regeln, {len(fallback_dicts)} Fallback-Regeln ===", flush=True)
for r in regeln_dicts:
print(f" Regel: {r.get('name')} - Muster: {r.get('muster')} - Schema: {r.get('schema')}", flush=True)
for datei in dateien:
ergebnis["gesamt"] += 1
try:
rel_pfad = str(datei.relative_to(pfad))
except:
rel_pfad = datei.name
datei_info = {"original": rel_pfad, "ordner": quell_ordner.name}
try:
ist_pdf = datei.suffix.lower() == ".pdf"
text = ""
ist_zugferd = False
ocr_gemacht = False
# Nur PDFs durch den PDF-Processor
if ist_pdf:
# OCR-Optionen aus Ordner-Einstellungen
ocr_erlaubt = getattr(quell_ordner, 'ocr_aktivieren', True)
original_backup = getattr(quell_ordner, 'original_sichern', None)
pdf_result = pdf_processor.verarbeite(
str(datei),
ocr_erlaubt=ocr_erlaubt,
original_backup_pfad=original_backup
)
if pdf_result.get("fehler"):
raise Exception(pdf_result["fehler"])
text = pdf_result.get("text", "")
ist_zugferd = pdf_result.get("ist_zugferd", False)
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
# Info über Original-Sicherung
if pdf_result.get("original_gesichert"):
datei_info["original_gesichert"] = pdf_result["original_gesichert"]
# ZUGFeRD-Info für Anzeige merken
if ist_zugferd:
datei_info["zugferd"] = True
# Signierte PDF-Behandlung
ist_signiert = pdf_result.get("ist_signiert", False)
if ist_signiert:
signiert_behandlung = getattr(quell_ordner, 'signiert_behandlung', 'normal')
if signiert_behandlung == "separieren":
# Signierte PDFs bleiben im gleichen Ordner
pfad.mkdir(parents=True, exist_ok=True)
neuer_pfad = pfad / datei.name
counter = 1
while neuer_pfad.exists():
neuer_pfad = pfad / f"{datei.stem}_{counter}{datei.suffix}"
counter += 1
datei.rename(neuer_pfad)
ergebnis["sortiert"] += 1
datei_info["signiert"] = True
datei_info["neuer_name"] = neuer_pfad.name
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=str(neuer_pfad),
neuer_name=neuer_pfad.name,
status="signiert"
))
ergebnis["verarbeitet"].append(datei_info)
continue
elif signiert_behandlung == "ignorieren":
datei_info["uebersprungen"] = "Signierte PDF ignoriert"
ergebnis["verarbeitet"].append(datei_info)
continue
# Bei "normal": weiter mit Regel-Matching
# Prüfe ob PDF nur für ZUGFeRD/Signiert-Erkennung gescannt wurde
# Wenn ja und keine besondere Dateiart erkannt: überspringen
erlaubte_typen = [t.lower() for t in (quell_ordner.dateitypen or [".pdf"])]
if ist_pdf and ".pdf" not in erlaubte_typen:
# PDF war nur für ZUGFeRD/Signiert-Erkennung, aber keine erkannt
datei_info["uebersprungen"] = "Kein ZUGFeRD/Signiert - PDF nicht in Dateitypen"
ergebnis["verarbeitet"].append(datei_info)
continue
# Dokument-Info für Regel-Matching
doc_info = {
"text": text,
"original_name": datei.name,
"absender": "",
"dateityp": datei.suffix.lower()
}
# Passende Regel finden (erst zugewiesene, dann Fallback)
regel = None
if sorter:
regel = sorter.finde_passende_regel(doc_info)
# Fallback-Regel wenn keine zugewiesene passt
if not regel and fallback_sorter:
regel = fallback_sorter.finde_passende_regel(doc_info)
if regel:
datei_info["fallback"] = True
if not regel:
print(f"!!! Keine passende Regel für {datei.name}", flush=True)
datei_info["fehler"] = "Keine passende Regel (keine Regeln zugewiesen)"
ergebnis["fehler"] += 1
ergebnis["verarbeitet"].append(datei_info)
continue
print(f">>> Regel '{regel.get('name')}' passt für {datei.name}", flush=True)
# Felder extrahieren
extrahiert = sorter.extrahiere_felder(regel, doc_info) if sorter else \
fallback_sorter.extrahiere_felder(regel, doc_info)
# Dateiendung beibehalten
schema = regel.get("schema", "{datum} - Dokument.pdf")
if schema.endswith(".pdf"):
schema = schema[:-4] + datei.suffix
neuer_name = (sorter or fallback_sorter).generiere_dateinamen(
{"schema": schema, **regel}, extrahiert
)
# Zielordner bestimmen
if regel.get("nur_umbenennen"):
# Nur umbenennen - Datei bleibt im aktuellen Ordner
ziel = datei.parent
elif regel.get("ziel_ordner"):
# Regel hat eigenen Zielordner
ziel = Path(regel["ziel_ordner"])
if regel.get("unterordner"):
ziel = ziel / regel["unterordner"]
else:
# Kein Zielordner - bleibt im gleichen Ordner
ziel = pfad
if regel.get("unterordner"):
ziel = ziel / regel["unterordner"]
ziel.mkdir(parents=True, exist_ok=True)
# Verschieben
print(f">>> Verschiebe: {datei.name} -> {ziel}/{neuer_name}", flush=True)
print(f" Extrahierte Felder: {extrahiert}", flush=True)
neuer_pfad = (sorter or fallback_sorter).verschiebe_datei(str(datei), str(ziel), neuer_name)
ergebnis["sortiert"] += 1
datei_info["neuer_name"] = neuer_name
datei_info["regel"] = regel.get("name", "Unbekannt")
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=neuer_pfad,
neuer_name=neuer_name,
ist_zugferd=False,
ocr_durchgefuehrt=ocr_gemacht,
status="sortiert",
extrahierte_daten=extrahiert
))
except Exception as e:
ergebnis["fehler"] += 1
datei_info["fehler"] = str(e)
ergebnis["verarbeitet"].append(datei_info)
# ============ Freie Ordner verarbeiten ============
# Lade alle Regeln mit freien Ordnern
alle_regeln = db.query(SortierRegel).filter(
SortierRegel.aktiv == True
).all()
for regel in alle_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
# Zielordner = der freie Ordner selbst (Dateien werden darin umbenannt)
# ODER wir brauchen einen separaten Zielordner?
# Vorerst: Dateien werden im gleichen Ordner umbenannt (in-place)
dateien = sammle_dateien_aus_pfad(freier_ordner_pfad, [".pdf"])
for datei in dateien:
ergebnis["gesamt"] += 1
datei_info = {"original": datei.name, "ordner": f"Freier Ordner: {freier_ordner_pfad}"}
try:
ist_pdf = datei.suffix.lower() == ".pdf"
text = ""
ist_zugferd = False
ocr_gemacht = 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)
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", 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:
datei_info["uebersprungen"] = "Regel passt nicht"
ergebnis["verarbeitet"].append(datei_info)
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
neuer_pfad = regel_sorter.verschiebe_datei(str(datei), str(ziel), neuer_name)
ergebnis["sortiert"] += 1
datei_info["neuer_name"] = neuer_name
datei_info["regel"] = passend.get("name", "Unbekannt")
if ist_zugferd:
datei_info["zugferd"] = True
db.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=neuer_pfad,
neuer_name=neuer_name,
ist_zugferd=ist_zugferd,
ocr_durchgefuehrt=ocr_gemacht,
status="sortiert",
extrahierte_daten=extrahiert
))
except Exception as e:
ergebnis["fehler"] += 1
datei_info["fehler"] = str(e)
ergebnis["verarbeitet"].append(datei_info)
db.commit()
return ergebnis
@router.get("/sortierung/starten/stream")
async def starte_sortierung_stream(db: Session = Depends(get_db)):
"""Streaming-Endpoint für Feinsortierung mit Live-Updates"""
from ..models.database import SessionLocal
# Daten vorab laden (Session ist im Generator nicht verfügbar)
ordner_liste = db.query(QuellOrdner).filter(QuellOrdner.aktiv == True).all()
ordner_daten = [{
"id": o.id, "name": o.name, "ziel_ordner": o.ziel_ordner,
"dateitypen": o.dateitypen, "ocr_aktivieren": getattr(o, 'ocr_aktivieren', True),
"original_sichern": getattr(o, 'original_sichern', None),
"signiert_behandlung": getattr(o, 'signiert_behandlung', 'normal')
} for o in ordner_liste]
# Fallback-Regeln laden
fallback_regeln = db.query(SortierRegel).filter(
SortierRegel.aktiv == True,
SortierRegel.ist_fallback == True
).order_by(SortierRegel.prioritaet).all()
fallback_dicts = [{
"id": r.id, "name": r.name, "prioritaet": r.prioritaet,
"muster": r.muster, "extraktion": r.extraktion,
"schema": r.schema, "unterordner": r.unterordner,
"ziel_ordner": getattr(r, 'ziel_ordner', None),
"nur_umbenennen": getattr(r, 'nur_umbenennen', False)
} for r in fallback_regeln]
# Ordner-Regeln vorladen
ordner_regeln_map = {}
for o in ordner_daten:
zuweisungen = db.query(OrdnerRegel).filter(OrdnerRegel.ordner_id == o["id"]).all()
zugewiesene_ids = [z.regel_id for z in zuweisungen]
if zugewiesene_ids:
regeln = db.query(SortierRegel).filter(
SortierRegel.id.in_(zugewiesene_ids),
SortierRegel.aktiv == True,
SortierRegel.ist_fallback == False
).order_by(SortierRegel.prioritaet).all()
ordner_regeln_map[o["id"]] = [{
"id": r.id, "name": r.name, "prioritaet": r.prioritaet,
"muster": r.muster, "extraktion": r.extraktion,
"schema": r.schema, "unterordner": r.unterordner,
"ziel_ordner": getattr(r, 'ziel_ordner', None),
"nur_umbenennen": getattr(r, 'nur_umbenennen', False)
} for r in regeln]
else:
ordner_regeln_map[o["id"]] = []
# Dateien vorab zählen für Gesamtübersicht
gesamt_dateien = 0
for o in ordner_daten:
pfad = Path(o["ziel_ordner"])
if pfad.exists():
dateien = sammle_dateien_aus_pfad(str(pfad), [".pdf"], rekursiv=True)
gesamt_dateien += len(dateien)
async def event_generator():
def send_event(data):
return f"data: {json.dumps(data)}\n\n"
if not ordner_daten:
yield send_event({"type": "fehler", "nachricht": "Keine Quell-Ordner konfiguriert"})
return
pdf_processor = PDFProcessor()
ergebnis = {"gesamt": 0, "sortiert": 0, "zugferd": 0, "fehler": 0}
yield send_event({"type": "start", "ordner_count": len(ordner_daten), "gesamt": gesamt_dateien})
await asyncio.sleep(0) # Sofort senden
session = SessionLocal()
try:
for quell_ordner in ordner_daten:
pfad = Path(quell_ordner["ziel_ordner"])
if not pfad.exists():
continue
dateien = sammle_dateien_aus_pfad(str(pfad), [".pdf"], rekursiv=True)
regeln_dicts = ordner_regeln_map.get(quell_ordner["id"], [])
yield send_event({
"type": "ordner",
"ordner": quell_ordner["name"],
"dateien": len(dateien),
"regeln": len(regeln_dicts)
})
await asyncio.sleep(0) # Sofort senden
sorter = Sorter(regeln_dicts) if regeln_dicts else None
fallback_sorter = Sorter(fallback_dicts) if fallback_dicts else None
for datei in dateien:
ergebnis["gesamt"] += 1
datei_info = {"original": datei.name, "ordner": quell_ordner["name"]}
yield send_event({"type": "datei_start", "datei": datei.name})
await asyncio.sleep(0) # Sofort senden
try:
ist_pdf = datei.suffix.lower() == ".pdf"
text = ""
ocr_gemacht = False
if ist_pdf:
pdf_result = pdf_processor.verarbeite(
str(datei),
ocr_erlaubt=quell_ordner.get("ocr_aktivieren", True),
original_backup_pfad=quell_ordner.get("original_sichern")
)
if pdf_result.get("fehler"):
raise Exception(pdf_result["fehler"])
text = pdf_result.get("text", "")
ocr_gemacht = pdf_result.get("ocr_durchgefuehrt", False)
doc_info = {
"text": text,
"original_name": datei.name,
"absender": "",
"dateityp": datei.suffix.lower()
}
# Passende Regel finden
regel = None
if sorter:
regel = sorter.finde_passende_regel(doc_info)
if not regel and fallback_sorter:
regel = fallback_sorter.finde_passende_regel(doc_info)
if regel:
datei_info["fallback"] = True
if not regel:
datei_info["fehler"] = "Keine passende Regel"
ergebnis["fehler"] += 1
yield send_event({"type": "datei_fehler", **datei_info})
await asyncio.sleep(0)
continue
# Felder extrahieren und umbenennen
extrahiert = (sorter or fallback_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 or fallback_sorter).generiere_dateinamen(
{"schema": schema, **regel}, extrahiert
)
# Zielordner
if regel.get("nur_umbenennen"):
ziel = datei.parent
elif regel.get("ziel_ordner"):
ziel = Path(regel["ziel_ordner"])
if regel.get("unterordner"):
ziel = ziel / regel["unterordner"]
else:
ziel = pfad
if regel.get("unterordner"):
ziel = ziel / regel["unterordner"]
ziel.mkdir(parents=True, exist_ok=True)
neuer_pfad = (sorter or fallback_sorter).verschiebe_datei(str(datei), str(ziel), neuer_name)
ergebnis["sortiert"] += 1
datei_info["neuer_name"] = neuer_name
datei_info["regel"] = regel.get("name", "Unbekannt")
session.add(VerarbeiteteDatei(
original_pfad=str(datei),
original_name=datei.name,
neuer_pfad=neuer_pfad,
neuer_name=neuer_name,
ocr_durchgefuehrt=ocr_gemacht,
status="sortiert",
extrahierte_daten=extrahiert
))
yield send_event({"type": "datei_fertig", **datei_info})
await asyncio.sleep(0) # Sofort senden
except Exception as e:
ergebnis["fehler"] += 1
datei_info["fehler"] = str(e)
yield send_event({"type": "datei_fehler", **datei_info})
await asyncio.sleep(0)
session.commit()
finally:
session.close()
yield send_event({"type": "fertig", **ergebnis})
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
# ============ PDF Test / Regel-Vorschau ============
@router.post("/pdf/extrahieren")
async def extrahiere_pdf_text(datei: UploadFile = File(...)):
"""Extrahiert Text aus einer hochgeladenen PDF für Regel-Tests"""
if not datei.filename.lower().endswith('.pdf'):
raise HTTPException(status_code=400, detail="Nur PDF-Dateien erlaubt")
# Temporäre Datei erstellen
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
inhalt = await datei.read()
tmp.write(inhalt)
tmp_pfad = tmp.name
try:
pdf_processor = PDFProcessor()
ergebnis = pdf_processor.verarbeite(tmp_pfad)
return {
"dateiname": datei.filename,
"text": ergebnis.get("text", ""),
"ist_zugferd": ergebnis.get("ist_zugferd", False),
"ist_signiert": ergebnis.get("ist_signiert", False),
"hat_text": ergebnis.get("hat_text", False),
"ocr_durchgefuehrt": ergebnis.get("ocr_durchgefuehrt", False),
"seiten": ergebnis.get("seiten", 0)
}
finally:
# Temporäre Datei löschen
Path(tmp_pfad).unlink(missing_ok=True)
@router.post("/pdf/testen")
async def teste_pdf_mit_regel(datei: UploadFile = File(...), regel_json: str = "{}"):
"""Testet eine Regel gegen eine hochgeladene PDF"""
if not datei.filename.lower().endswith('.pdf'):
raise HTTPException(status_code=400, detail="Nur PDF-Dateien erlaubt")
# Regel parsen
try:
regel = json.loads(regel_json)
except:
regel = {}
# Temporäre Datei erstellen
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
inhalt = await datei.read()
tmp.write(inhalt)
tmp_pfad = tmp.name
try:
# PDF verarbeiten
pdf_processor = PDFProcessor()
pdf_ergebnis = pdf_processor.verarbeite(tmp_pfad)
text = pdf_ergebnis.get("text", "")
# Regel testen
regel["aktiv"] = True
regel["prioritaet"] = 1
sorter = Sorter([regel])
doc_info = {
"text": text,
"original_name": datei.filename,
"absender": ""
}
passend = sorter.finde_passende_regel(doc_info)
ergebnis = {
"dateiname": datei.filename,
"text": text,
"text_vorschau": text[:2000] if len(text) > 2000 else text,
"ist_zugferd": pdf_ergebnis.get("ist_zugferd", False),
"regel_passt": passend is not None,
"extrahiert": {},
"vorgeschlagener_name": ""
}
if passend:
extrahiert = sorter.extrahiere_felder(passend, doc_info)
ergebnis["extrahiert"] = extrahiert
ergebnis["vorgeschlagener_name"] = sorter.generiere_dateinamen(passend, extrahiert)
# Muster-Matches hervorheben
muster = regel.get("muster", {})
matches = []
# Keywords finden
if "keywords" in muster:
keywords = muster["keywords"]
if isinstance(keywords, str):
keywords = [k.strip() for k in keywords.split(",")]
for kw in keywords:
if kw.strip():
for m in re.finditer(re.escape(kw.strip()), text, re.IGNORECASE):
matches.append({"start": m.start(), "end": m.end(), "text": m.group(), "typ": "keyword"})
# text_match finden
if "text_match" in muster:
patterns = muster["text_match"]
if isinstance(patterns, str):
patterns = [patterns]
for p in patterns:
for m in re.finditer(re.escape(p), text, re.IGNORECASE):
matches.append({"start": m.start(), "end": m.end(), "text": m.group(), "typ": "text_match"})
# text_regex finden
if "text_regex" in muster:
try:
for m in re.finditer(muster["text_regex"], text, re.IGNORECASE):
matches.append({"start": m.start(), "end": m.end(), "text": m.group(), "typ": "regex"})
except:
pass
ergebnis["matches"] = matches
return ergebnis
finally:
Path(tmp_pfad).unlink(missing_ok=True)
# ============ Debug Log ============
import logging
from collections import deque
# In-Memory Log-Speicher (maximal 500 Einträge)
log_speicher = deque(maxlen=500)
class UILogHandler(logging.Handler):
"""Log Handler der Einträge für die UI speichert"""
def emit(self, record):
log_speicher.append({
"zeit": datetime.now().strftime("%H:%M:%S"),
"level": record.levelname,
"nachricht": self.format(record)
})
# Handler registrieren
ui_handler = UILogHandler()
ui_handler.setLevel(logging.DEBUG)
ui_handler.setFormatter(logging.Formatter('%(message)s'))
# Zu allen relevanten Loggern hinzufügen
for logger_name in ['backend.app', 'root', '']:
logger = logging.getLogger(logger_name)
logger.addHandler(ui_handler)
@router.get("/logs")
def hole_logs(level: str = None):
"""Gibt Log-Einträge zurück"""
logs = list(log_speicher)
if level:
logs = [l for l in logs if l["level"] == level]
return logs
@router.delete("/logs")
def loesche_logs():
"""Leert den Log-Speicher"""
log_speicher.clear()
return {"message": "Logs gelöscht"}
@router.get("/health")
def health():
return {"status": "ok"}
# ============ Auto-Regex Generator ============
@router.post("/pdf/auto-regex")
async def auto_regex_generator(datei: UploadFile = File(...)):
"""
Analysiert eine PDF und generiert automatisch Regex-Muster für erkannte Felder.
Erkennt: Datum, Betrag, Rechnungsnummer, Firma
"""
if not datei.filename.lower().endswith('.pdf'):
raise HTTPException(status_code=400, detail="Nur PDF-Dateien erlaubt")
# Temporäre Datei erstellen
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
inhalt = await datei.read()
tmp.write(inhalt)
tmp_pfad = tmp.name
try:
# PDF verarbeiten
pdf_processor = PDFProcessor()
pdf_ergebnis = pdf_processor.verarbeite(tmp_pfad)
text = pdf_ergebnis.get("text", "")
erkannte_felder = []
# ============ DATUM erkennen ============
datum_patterns = [
{"regex": r"Rechnungsdatum[:\s]*(\d{2})[./](\d{2})[./](\d{4})", "kontext": "Rechnungsdatum", "prio": 1},
{"regex": r"Belegdatum[:\s]*(\d{2})[./](\d{2})[./](\d{4})", "kontext": "Belegdatum", "prio": 1},
{"regex": r"Datum[:\s]*(\d{2})[./](\d{2})[./](\d{4})", "kontext": "Datum", "prio": 2},
{"regex": r"vom[:\s]*(\d{2})[./](\d{2})[./](\d{4})", "kontext": "vom", "prio": 2},
{"regex": r"(\d{2})\.(\d{2})\.(\d{4})", "kontext": "Format TT.MM.JJJJ", "prio": 3},
{"regex": r"(\d{4})-(\d{2})-(\d{2})", "kontext": "ISO Format", "prio": 3},
]
for pattern in datum_patterns:
match = re.search(pattern["regex"], text, re.IGNORECASE)
if match:
erkannte_felder.append({
"feld": "datum",
"wert": match.group(0),
"position": match.start(),
"kontext": pattern["kontext"],
"regex_vorschlag": pattern["regex"],
"prioritaet": pattern["prio"]
})
break
# ============ BETRAG erkennen ============
betrag_patterns = [
{"regex": r"Gesamtbetrag[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "Gesamtbetrag", "prio": 1},
{"regex": r"Rechnungsbetrag[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "Rechnungsbetrag", "prio": 1},
{"regex": r"Endbetrag[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "Endbetrag", "prio": 1},
{"regex": r"Summe[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "Summe", "prio": 2},
{"regex": r"Total[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "Total", "prio": 2},
{"regex": r"Brutto[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "Brutto", "prio": 2},
{"regex": r"zu zahlen[:\s]*([\d.,]+)\s*(?:EUR|€)?", "kontext": "zu zahlen", "prio": 1},
{"regex": r"([\d.,]+)\s*(?:EUR|€)", "kontext": "Betrag mit EUR/€", "prio": 3},
]
for pattern in betrag_patterns:
match = re.search(pattern["regex"], text, re.IGNORECASE)
if match:
erkannte_felder.append({
"feld": "betrag",
"wert": match.group(0),
"extrahiert": match.group(1) if match.lastindex else match.group(0),
"position": match.start(),
"kontext": pattern["kontext"],
"regex_vorschlag": pattern["regex"],
"prioritaet": pattern["prio"]
})
break
# ============ RECHNUNGSNUMMER erkennen ============
nummer_patterns = [
{"regex": r"Rechnungsnummer[:\s#]*([A-Z0-9][\w\-/]+)", "kontext": "Rechnungsnummer", "prio": 1},
{"regex": r"Rechnung\s*Nr\.?[:\s#]*([A-Z0-9][\w\-/]+)", "kontext": "Rechnung Nr.", "prio": 1},
{"regex": r"Rechnungs-Nr\.?[:\s#]*([A-Z0-9][\w\-/]+)", "kontext": "Rechnungs-Nr.", "prio": 1},
{"regex": r"Invoice\s*(?:No\.?|Number)?[:\s#]*([A-Z0-9][\w\-/]+)", "kontext": "Invoice", "prio": 1},
{"regex": r"Beleg-?Nr\.?[:\s#]*([A-Z0-9][\w\-/]+)", "kontext": "Beleg-Nr.", "prio": 2},
{"regex": r"Dokumentnummer[:\s#]*([A-Z0-9][\w\-/]+)", "kontext": "Dokumentnummer", "prio": 2},
{"regex": r"RE-?(\d{4,})", "kontext": "RE-Nummer", "prio": 3},
{"regex": r"INV-?(\d{4,})", "kontext": "INV-Nummer", "prio": 3},
]
for pattern in nummer_patterns:
match = re.search(pattern["regex"], text, re.IGNORECASE)
if match:
erkannte_felder.append({
"feld": "nummer",
"wert": match.group(0),
"extrahiert": match.group(1) if match.lastindex else match.group(0),
"position": match.start(),
"kontext": pattern["kontext"],
"regex_vorschlag": pattern["regex"],
"prioritaet": pattern["prio"]
})
break
# ============ FIRMA erkennen ============
bekannte_firmen = [
"Sonepar", "Amazon", "Ebay", "MediaMarkt", "Saturn", "Conrad", "Reichelt",
"Hornbach", "Bauhaus", "OBI", "Hagebau", "Toom", "Hellweg",
"Telekom", "Vodafone", "O2", "1&1",
"Allianz", "HUK", "Provinzial", "DEVK", "Gothaer",
"IKEA", "Poco", "XXXLutz", "Roller",
"Alternate", "Mindfactory", "Caseking", "Notebooksbilliger",
"DHL", "DPD", "Hermes", "UPS", "GLS",
]
text_lower = text.lower()
for firma in bekannte_firmen:
if firma.lower() in text_lower:
# Position im Original-Text finden
pos = text_lower.find(firma.lower())
erkannte_felder.append({
"feld": "firma",
"wert": firma,
"position": pos,
"kontext": "Bekannte Firma",
"regex_vorschlag": f"(?i){re.escape(firma)}",
"prioritaet": 1
})
break
# Wenn keine bekannte Firma: nach GmbH, AG etc. suchen
if not any(f["feld"] == "firma" for f in erkannte_felder):
firma_match = re.search(r"([A-ZÄÖÜ][A-Za-zäöüÄÖÜß\s&\-\.]+(?:GmbH|AG|KG|e\.K\.|Inc|Ltd|SE|UG))", text)
if firma_match:
erkannte_felder.append({
"feld": "firma",
"wert": firma_match.group(1).strip(),
"position": firma_match.start(),
"kontext": "Firmenname mit Rechtsform",
"regex_vorschlag": r"([A-ZÄÖÜ][A-Za-zäöüÄÖÜß\s&\-\.]+(?:GmbH|AG|KG|e\.K\.|Inc|Ltd|SE|UG))",
"prioritaet": 2
})
# ============ Keywords für Dokumenttyp extrahieren ============
dokumenttyp_keywords = {
"rechnung": ["rechnung", "invoice", "faktura"],
"angebot": ["angebot", "quotation", "offerte"],
"gutschrift": ["gutschrift", "credit note"],
"lieferschein": ["lieferschein", "delivery note"],
}
gefundene_keywords = []
for typ, keywords in dokumenttyp_keywords.items():
for kw in keywords:
if kw in text_lower:
gefundene_keywords.append(kw)
# Regel-Vorschlag erstellen
regel_vorschlag = {
"muster": {},
"extraktion": {},
"schema": "{datum} - Rechnung - {firma} - {nummer} - {betrag} EUR.pdf"
}
if gefundene_keywords:
regel_vorschlag["muster"]["keywords"] = ", ".join(gefundene_keywords[:3])
for feld in erkannte_felder:
if feld["feld"] in ["datum", "betrag", "nummer"]:
regel_vorschlag["extraktion"][feld["feld"]] = {
"regex": feld["regex_vorschlag"]
}
elif feld["feld"] == "firma":
regel_vorschlag["extraktion"]["firma"] = {
"wert": feld["wert"]
}
return {
"dateiname": datei.filename,
"text_laenge": len(text),
"erkannte_felder": erkannte_felder,
"gefundene_keywords": gefundene_keywords,
"regel_vorschlag": regel_vorschlag,
"text_vorschau": text[:3000] if len(text) > 3000 else text
}
finally:
Path(tmp_pfad).unlink(missing_ok=True)
# ============ Einfache Regeln (UI-freundlich) ============
@router.get("/dokumenttypen")
def liste_dokumenttypen():
"""Gibt alle verfügbaren Dokumenttypen für das UI zurück"""
from ..modules.sorter import DOKUMENTTYPEN
return [
{"id": key, "name": config["name"], "schema": config["schema"], "unterordner": config["unterordner"]}
for key, config in DOKUMENTTYPEN.items()
]
class EinfacheRegelCreate(BaseModel):
name: str
dokumenttyp: str # z.B. "rechnung", "vertrag"
keywords: str # Komma-getrennt
firma: Optional[str] = None # Fester Firmenwert
unterordner: Optional[str] = None
prioritaet: int = 50
@router.post("/regeln/einfach")
def erstelle_einfache_regel_api(data: EinfacheRegelCreate, db: Session = Depends(get_db)):
"""Erstellt eine Regel basierend auf Dokumenttyp - für einfaches UI"""
from ..modules.sorter import DOKUMENTTYPEN
typ_config = DOKUMENTTYPEN.get(data.dokumenttyp, DOKUMENTTYPEN["sonstiges"])
# Muster als Dict (keywords werden vom Sorter geparst)
muster = {"keywords": data.keywords}
# Extraktion (nur Firma wenn angegeben)
extraktion = {}
if data.firma:
extraktion["firma"] = {"wert": data.firma}
regel = SortierRegel(
name=data.name,
prioritaet=data.prioritaet,
aktiv=True,
muster=muster,
extraktion=extraktion,
schema=typ_config["schema"],
unterordner=data.unterordner or typ_config["unterordner"]
)
db.add(regel)
db.commit()
db.refresh(regel)
return {
"id": regel.id,
"name": regel.name,
"dokumenttyp": data.dokumenttyp,
"keywords": data.keywords,
"schema": regel.schema
}
class ExtraktionTestRequest(BaseModel):
text: str
dateiname: Optional[str] = "test.pdf"
@router.post("/extraktion/test")
def teste_extraktion(data: ExtraktionTestRequest):
"""Testet die automatische Extraktion auf einem Text"""
from ..modules.extraktoren import extrahiere_alle_felder, baue_dateiname
dokument_info = {
"original_name": data.dateiname,
"absender": ""
}
# Felder extrahieren
felder = extrahiere_alle_felder(data.text, dokument_info)
# Beispiel-Dateinamen für verschiedene Typen generieren
beispiele = {}
from ..modules.sorter import DOKUMENTTYPEN
for typ_id, typ_config in DOKUMENTTYPEN.items():
beispiele[typ_id] = baue_dateiname(typ_config["schema"], felder, ".pdf")
return {
"extrahiert": felder,
"beispiel_dateinamen": beispiele
}
@router.post("/regeln/{id}/vorschau")
def regel_vorschau(id: int, data: ExtraktionTestRequest, db: Session = Depends(get_db)):
"""Zeigt Vorschau wie eine Regel auf einen Text angewendet würde"""
regel = db.query(SortierRegel).filter(SortierRegel.id == id).first()
if not regel:
raise HTTPException(status_code=404, detail="Regel nicht gefunden")
from ..modules.sorter import Sorter
sorter = Sorter([{
"id": regel.id,
"name": regel.name,
"prioritaet": regel.prioritaet,
"aktiv": True,
"muster": regel.muster,
"extraktion": regel.extraktion,
"schema": regel.schema,
"unterordner": regel.unterordner
}])
dokument_info = {
"text": data.text,
"original_name": data.dateiname or "test.pdf",
"absender": ""
}
# Prüfen ob Regel matched
passende_regel = sorter.finde_passende_regel(dokument_info)
if not passende_regel:
return {
"matched": False,
"grund": "Keywords nicht gefunden"
}
# Felder extrahieren
felder = sorter.extrahiere_felder(passende_regel, dokument_info)
# Dateiname generieren
dateiname = sorter.generiere_dateinamen(passende_regel, felder)
return {
"matched": True,
"extrahiert": felder,
"dateiname": dateiname,
"unterordner": passende_regel.get("unterordner")
}
# ============ BEREICH 3: Zeitpläne / Scheduler ============
class ZeitplanCreate(BaseModel):
name: str
typ: str # "mail_abruf", "grobsortierung", "sortierregeln", "db_backup"
postfach_id: Optional[int] = None
quell_ordner_id: Optional[int] = None
regel_id: Optional[int] = None # Für typ="sortierregeln"
datenbank_id: Optional[int] = None # Für typ="db_backup"
intervall: str # "stündlich", "täglich", "wöchentlich", "monatlich"
stunde: int = 6
minute: int = 0
wochentag: Optional[int] = None # 0=Montag
monatstag: Optional[int] = None
class ZeitplanResponse(BaseModel):
id: int
name: str
aktiv: bool
typ: str
postfach_id: Optional[int]
quell_ordner_id: Optional[int]
regel_id: Optional[int]
datenbank_id: Optional[int]
intervall: str
stunde: int
minute: int
wochentag: Optional[int]
monatstag: Optional[int]
letzte_ausfuehrung: Optional[datetime]
naechste_ausfuehrung: Optional[datetime]
letzter_status: Optional[str]
letzte_meldung: Optional[str]
class Config:
from_attributes = True
@router.get("/zeitplaene")
def liste_zeitplaene(db: Session = Depends(get_db)):
"""Listet alle Zeitpläne mit Status"""
from ..services.scheduler_service import get_scheduler_status
return get_scheduler_status()
@router.get("/zeitplaene/{id}", response_model=ZeitplanResponse)
def hole_zeitplan(id: int, db: Session = Depends(get_db)):
"""Gibt einen einzelnen Zeitplan zurück"""
zeitplan = db.query(Zeitplan).filter(Zeitplan.id == id).first()
if not zeitplan:
raise HTTPException(404, "Zeitplan nicht gefunden")
return zeitplan
@router.post("/zeitplaene", response_model=ZeitplanResponse)
def erstelle_zeitplan(data: ZeitplanCreate, db: Session = Depends(get_db)):
"""Erstellt einen neuen Zeitplan"""
zeitplan = Zeitplan(**data.dict())
db.add(zeitplan)
db.commit()
db.refresh(zeitplan)
# Scheduler aktualisieren
from ..services.scheduler_service import sync_zeitplaene, trigger_zeitplan_manuell
sync_zeitplaene()
# Zeitplan direkt beim Erstellen einmal ausführen (in separatem Thread)
import threading
def run_zeitplan():
try:
trigger_zeitplan_manuell(zeitplan.id)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Fehler bei sofortiger Ausführung: {e}")
thread = threading.Thread(target=run_zeitplan, daemon=True)
thread.start()
return zeitplan
@router.put("/zeitplaene/{id}", response_model=ZeitplanResponse)
def aktualisiere_zeitplan(id: int, data: ZeitplanCreate, db: Session = Depends(get_db)):
"""Aktualisiert einen Zeitplan"""
zeitplan = db.query(Zeitplan).filter(Zeitplan.id == id).first()
if not zeitplan:
raise HTTPException(status_code=404, detail="Nicht gefunden")
for key, value in data.dict().items():
setattr(zeitplan, key, value)
db.commit()
db.refresh(zeitplan)
# Scheduler aktualisieren
from ..services.scheduler_service import sync_zeitplaene, trigger_zeitplan_manuell
sync_zeitplaene()
# Zeitplan direkt beim Speichern einmal ausführen (in separatem Thread)
import threading
def run_zeitplan():
try:
trigger_zeitplan_manuell(zeitplan.id)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Fehler bei sofortiger Ausführung: {e}")
thread = threading.Thread(target=run_zeitplan, daemon=True)
thread.start()
return zeitplan
@router.delete("/zeitplaene/{id}")
def loesche_zeitplan(id: int, db: Session = Depends(get_db)):
"""Löscht einen Zeitplan"""
zeitplan = db.query(Zeitplan).filter(Zeitplan.id == id).first()
if not zeitplan:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(zeitplan)
db.commit()
# Scheduler aktualisieren
from ..services.scheduler_service import sync_zeitplaene
sync_zeitplaene()
return {"message": "Gelöscht"}
@router.post("/zeitplaene/{id}/aktivieren")
def aktiviere_zeitplan(id: int, db: Session = Depends(get_db)):
"""Aktiviert/Deaktiviert einen Zeitplan"""
zeitplan = db.query(Zeitplan).filter(Zeitplan.id == id).first()
if not zeitplan:
raise HTTPException(status_code=404, detail="Nicht gefunden")
zeitplan.aktiv = not zeitplan.aktiv
db.commit()
# Scheduler aktualisieren
from ..services.scheduler_service import sync_zeitplaene
sync_zeitplaene()
return {"aktiv": zeitplan.aktiv}
@router.post("/zeitplaene/{id}/ausfuehren")
def fuehre_zeitplan_aus(id: int, db: Session = Depends(get_db)):
"""Führt einen Zeitplan sofort manuell aus"""
from ..services.scheduler_service import trigger_zeitplan_manuell
return trigger_zeitplan_manuell(id)
@router.get("/status/uebersicht")
def status_uebersicht(db: Session = Depends(get_db)):
"""Gibt eine Übersicht über alle Services und deren Status"""
from ..services.scheduler_service import get_scheduler_status
# Postfächer Status
postfaecher = db.query(Postfach).all()
postfach_status = [{
"id": p.id,
"name": p.name,
"aktiv": p.aktiv,
"letzter_abruf": p.letzter_abruf.isoformat() if p.letzter_abruf else None,
"letzte_anzahl": p.letzte_anzahl
} for p in postfaecher]
# Quellordner Status
ordner = db.query(QuellOrdner).all()
ordner_status = [{
"id": o.id,
"name": o.name,
"aktiv": o.aktiv,
"pfad": o.pfad
} for o in ordner]
# Scheduler Status
scheduler_status = get_scheduler_status()
return {
"postfaecher": postfach_status,
"quell_ordner": ordner_status,
"scheduler": scheduler_status
}
# ============ DB-Server API ============
class DbServerCreate(BaseModel):
name: str
typ: str # mariadb, mysql, postgresql
host: str
port: int = 3306
user: str
password: str
class DbServerResponse(BaseModel):
id: int
name: str
typ: str
host: str
port: int
user: str
aktiv: bool
class Config:
from_attributes = True
@router.get("/dbserver")
def liste_dbserver(db: Session = Depends(get_db)):
"""Liste aller DB-Server"""
return db.query(DbServer).all()
@router.get("/dbserver/{id}")
def hole_dbserver(id: int, db: Session = Depends(get_db)):
"""Einzelnen DB-Server abrufen"""
server = db.query(DbServer).filter(DbServer.id == id).first()
if not server:
raise HTTPException(status_code=404, detail="Nicht gefunden")
return {
"id": server.id,
"name": server.name,
"typ": server.typ,
"host": server.host,
"port": server.port,
"user": server.user,
"aktiv": server.aktiv
}
@router.post("/dbserver")
def erstelle_dbserver(data: DbServerCreate, db: Session = Depends(get_db)):
"""Neuen DB-Server erstellen"""
server = DbServer(
name=data.name,
typ=data.typ,
host=data.host,
port=data.port,
user=data.user,
passwort=data.password
)
db.add(server)
db.commit()
db.refresh(server)
return {"id": server.id, "message": "Erstellt"}
@router.put("/dbserver/{id}")
def aktualisiere_dbserver(id: int, data: DbServerCreate, db: Session = Depends(get_db)):
"""DB-Server aktualisieren"""
server = db.query(DbServer).filter(DbServer.id == id).first()
if not server:
raise HTTPException(status_code=404, detail="Nicht gefunden")
server.name = data.name
server.typ = data.typ
server.host = data.host
server.port = data.port
server.user = data.user
if data.password:
server.passwort = data.password
db.commit()
return {"message": "Aktualisiert"}
@router.delete("/dbserver/{id}")
def loesche_dbserver(id: int, db: Session = Depends(get_db)):
"""DB-Server löschen"""
server = db.query(DbServer).filter(DbServer.id == id).first()
if not server:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(server)
db.commit()
return {"message": "Gelöscht"}
@router.post("/dbserver/{id}/aktivieren")
def aktiviere_dbserver(id: int, db: Session = Depends(get_db)):
"""DB-Server aktivieren/deaktivieren"""
server = db.query(DbServer).filter(DbServer.id == id).first()
if not server:
raise HTTPException(status_code=404, detail="Nicht gefunden")
server.aktiv = not server.aktiv
db.commit()
return {"aktiv": server.aktiv}
class DbServerTest(BaseModel):
typ: str
host: str
port: int = 3306
user: str
password: str
@router.post("/dbserver/test")
def teste_dbserver(data: DbServerTest):
"""Testet die Verbindung zu einem DB-Server"""
try:
if data.typ in ["mariadb", "mysql"]:
import pymysql
conn = pymysql.connect(
host=data.host,
port=data.port,
user=data.user,
password=data.password,
connect_timeout=5
)
conn.close()
elif data.typ == "postgresql":
import psycopg2
conn = psycopg2.connect(
host=data.host,
port=data.port,
user=data.user,
password=data.password,
connect_timeout=5
)
conn.close()
else:
raise HTTPException(status_code=400, detail="Unbekannter DB-Typ")
return {"message": "Verbindung erfolgreich!"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# ============ Datenbanken API ============
class DatenbankCreate(BaseModel):
name: str
server_id: int
database: str
backup_pfad: str
aufbewahrung: int = 7
format: str = "sql" # sql, sql.gz, zip
class DatenbankResponse(BaseModel):
id: int
name: str
server_id: int
database: str
backup_pfad: str
aufbewahrung: int
format: str
aktiv: bool
letztes_backup: Optional[datetime]
server_name: Optional[str] = None
class Config:
from_attributes = True
@router.get("/datenbanken")
def liste_datenbanken(db: Session = Depends(get_db)):
"""Liste aller Datenbanken mit Server-Namen"""
dbs = db.query(Datenbank).all()
result = []
for d in dbs:
server = db.query(DbServer).filter(DbServer.id == d.server_id).first()
result.append({
"id": d.id,
"name": d.name,
"server_id": d.server_id,
"server_name": server.name if server else None,
"database": d.database,
"backup_pfad": d.backup_pfad,
"aufbewahrung": d.aufbewahrung,
"format": d.format,
"aktiv": d.aktiv,
"letztes_backup": d.letztes_backup
})
return result
@router.get("/datenbanken/{id}")
def hole_datenbank(id: int, db: Session = Depends(get_db)):
"""Einzelne Datenbank abrufen"""
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
if not datenbank:
raise HTTPException(status_code=404, detail="Nicht gefunden")
return datenbank
@router.post("/datenbanken")
def erstelle_datenbank(data: DatenbankCreate, db: Session = Depends(get_db)):
"""Neue Datenbank-Konfiguration erstellen"""
datenbank = Datenbank(**data.dict())
db.add(datenbank)
db.commit()
db.refresh(datenbank)
return {"id": datenbank.id, "message": "Erstellt"}
@router.put("/datenbanken/{id}")
def aktualisiere_datenbank(id: int, data: DatenbankCreate, db: Session = Depends(get_db)):
"""Datenbank-Konfiguration aktualisieren"""
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
if not datenbank:
raise HTTPException(status_code=404, detail="Nicht gefunden")
for key, value in data.dict().items():
setattr(datenbank, key, value)
db.commit()
return {"message": "Aktualisiert"}
@router.delete("/datenbanken/{id}")
def loesche_datenbank(id: int, db: Session = Depends(get_db)):
"""Datenbank-Konfiguration löschen"""
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
if not datenbank:
raise HTTPException(status_code=404, detail="Nicht gefunden")
db.delete(datenbank)
db.commit()
return {"message": "Gelöscht"}
@router.post("/datenbanken/{id}/aktivieren")
def aktiviere_datenbank(id: int, db: Session = Depends(get_db)):
"""Datenbank aktivieren/deaktivieren"""
datenbank = db.query(Datenbank).filter(Datenbank.id == id).first()
if not datenbank:
raise HTTPException(status_code=404, detail="Nicht gefunden")
datenbank.aktiv = not datenbank.aktiv
db.commit()
return {"aktiv": datenbank.aktiv}
@router.post("/datenbanken/{id}/backup")
def erstelle_backup(id: int, db: Session = Depends(get_db)):
"""Erstellt ein Backup der angegebenen Datenbank"""
from ..services.backup_service import erstelle_db_backup
return erstelle_db_backup(id, db)
@router.get("/backups")
def liste_backups(db: Session = Depends(get_db)):
"""Liste der letzten Backups"""
logs = db.query(BackupLog).order_by(BackupLog.erstellt_am.desc()).limit(50).all()
result = []
for log in logs:
datenbank = db.query(Datenbank).filter(Datenbank.id == log.datenbank_id).first()
result.append({
"id": log.id,
"datenbank_name": datenbank.name if datenbank else "Unbekannt",
"dateiname": log.dateiname,
"groesse_mb": round(log.groesse_bytes / 1024 / 1024, 2) if log.groesse_bytes else 0,
"status": log.status,
"erstellt": log.erstellt_am.strftime("%d.%m.%Y %H:%M") if log.erstellt_am else None
})
return result