- Grobsortierung zeigt jetzt live den Fortschritt (wie Feinsortierung) - Neuer Streaming-Endpoint /api/grobsortierung/stream - Sticky Header mit laufender Statistik - Auto-Scroll zum aktuellen Eintrag Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3016 lines
110 KiB
Python
Executable file
3016 lines
110 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)}
|
|
|
|
|
|
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"
|
|
postfach_id: Optional[int] = None
|
|
quell_ordner_id: Optional[int] = None
|
|
regel_id: Optional[int] = None # Für typ="sortierregeln"
|
|
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]
|
|
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
|
|
trigger_zeitplan_manuell(zeitplan.id)
|
|
|
|
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
|
|
trigger_zeitplan_manuell(zeitplan.id)
|
|
|
|
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
|