feat(dokumente): DOC-001 Duplikat-Erkennung + Fix Online-Ansicht (Popup-Blocker)
CI / backend-tests (push) Failing after 2m11s
CI / frontend-build (push) Successful in 20s

Duplikat-Erkennung: SHA-256-Hash je Dokument (Migration 0024), Upload wird
mit 409 abgelehnt, wenn dieselbe Datei bereits an derselben Entität liegt
(Vergleich bewusst pro Entität, nicht global - gleiche Datei an zwei
Objekten ist kein Duplikat). Frontend zeigt die Backend-Fehlermeldung
(Dateiname/Datum des bestehenden Dokuments) statt generischem Text.

Nebenbei gefunden+gefixt: "Ansehen" (PDF/JPG/PNG online statt Download)
funktionierte nicht zuverlässig, weil window.open() erst nach einem await
fetch() aufgerufen wurde - Popup-Blocker werten das nicht mehr als direkte
Nutzeraktion. Jetzt öffnet der Tab sofort synchron, die Blob-URL wird
nachgeladen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-08 00:59:15 +02:00
co-authored by Claude Sonnet 5
parent 307914ccf7
commit 707701d897
7 changed files with 155 additions and 11 deletions
+22
View File
@@ -1,3 +1,4 @@
import hashlib
import os
import uuid
from datetime import datetime, timezone
@@ -29,6 +30,14 @@ class DateizugrossError(Exception):
pass
class DokumentDuplikatError(Exception):
"""DOC-001: identische Datei (SHA-256) bereits an dieser Entität vorhanden."""
def __init__(self, bestehendes_dokument: Dokument):
self.bestehendes_dokument = bestehendes_dokument
super().__init__(f"Duplikat von Dokument {bestehendes_dokument.id}")
def _upload_pfad() -> Path:
pfad = Path(settings.upload_dir)
pfad.mkdir(parents=True, exist_ok=True)
@@ -51,6 +60,18 @@ async def speichere_dokument(
if len(inhalt) > settings.max_upload_size_mb * 1024 * 1024:
raise DateizugrossError(len(inhalt))
dateihash = hashlib.sha256(inhalt).hexdigest()
bestehendes = await db.execute(
select(Dokument).where(
Dokument.entitaet_typ == entitaet_typ,
Dokument.entitaet_id == entitaet_id,
Dokument.dateihash == dateihash,
)
)
duplikat = bestehendes.scalars().first()
if duplikat is not None:
raise DokumentDuplikatError(duplikat)
# Speichername ist server-generiert (UUID), NIEMALS der Original-Dateiname -
# verhindert Path-Traversal (../../etc/passwd) und Namenskollisionen.
endung = Path(dateiname).suffix[:10]
@@ -65,6 +86,7 @@ async def speichere_dokument(
speicherpfad=speichername,
mime_type=mime_type,
groesse_bytes=len(inhalt),
dateihash=dateihash,
beschreibung=beschreibung,
hochgeladen_von=hochgeladen_von,
hochgeladen_am=datetime.now(timezone.utc),