From a1a1998d1e8d7d032b6196053469c888520a28eb Mon Sep 17 00:00:00 2001 From: guyg1474 <340572510+guyg1474@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:51:42 +0200 Subject: [PATCH] Start robuster: Problembilder verhindern die Umstellung nicht MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migration Schritt 2: Lese-/Schreibfehler (z. B. Dateirechte) und kaputte Bilder werden protokolliert und übersprungen, statt den Serverstart abzubrechen - bilder.py: alle Pillow-Fehler werden zu einer Eingabe-Fehlermeldung bzw. "keine Metadaten" statt zu Fehler 500 - Test mit schreibgeschütztem und kaputtem Bild Co-Authored-By: Claude Opus 5.5 --- backend/app/bilder.py | 4 ++-- backend/app/migrations.py | 13 +++++++------ backend/tests/test_datenschutz.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/app/bilder.py b/backend/app/bilder.py index ae5b8ab..220717f 100644 --- a/backend/app/bilder.py +++ b/backend/app/bilder.py @@ -23,7 +23,7 @@ def hat_metadaten(data: bytes) -> bool: with Image.open(io.BytesIO(data)) as img: return bool(img.getexif()) or any(k in img.info for k in ("xmp", "XML:com.adobe.xmp", "comment", "exif")) \ or bool(getattr(img, "text", None)) - except (OSError, ValueError, Image.DecompressionBombError): + except Exception: # kaputte oder exotische Datei: als "ohne Metadaten" behandeln return False @@ -50,5 +50,5 @@ def ohne_metadaten(data: bytes, ext: str) -> bytes: else: img.save(out, "WEBP", quality=90, icc_profile=icc) return out.getvalue() - except (OSError, ValueError, Image.DecompressionBombError): + except Exception: # Pillow wirft je nach Format verschiedene Fehler raise ValidationError("Das Bild konnte nicht gelesen werden oder ist zu groß.") diff --git a/backend/app/migrations.py b/backend/app/migrations.py index fca704c..e0b4b1b 100644 --- a/backend/app/migrations.py +++ b/backend/app/migrations.py @@ -205,18 +205,19 @@ def _v2_datenschutz(conn) -> None: cleaned = sanitize(row["content"] or "") if cleaned != (row["content"] or ""): conn.execute("UPDATE projects SET content = ? WHERE id = ?", (cleaned, row["id"])) - # Hochgeladene Bilder: nur solche mit Metadaten neu schreiben (die anderen bleiben unverändert) + # Hochgeladene Bilder: nur solche mit Metadaten neu schreiben (die anderen bleiben unverändert). + # Ein einzelnes Problembild (Rechte, kaputte Datei) darf den Serverstart nie verhindern. uploads = DATA_DIR / "uploads" if uploads.is_dir(): for f in uploads.iterdir(): if not re.fullmatch(r"[a-f0-9]{24}\.(jpg|png|webp)", f.name): continue - data = f.read_bytes() - if hat_metadaten(data): - try: + try: + data = f.read_bytes() + if hat_metadaten(data): f.write_bytes(ohne_metadaten(data, f.suffix[1:])) - except ValidationError: - log.warning("Bild %s konnte nicht bereinigt werden", f.name) + except (OSError, ValidationError) as e: + log.warning("Bild %s konnte nicht bereinigt werden: %s", f.name, e) # Reihenfolge = Versionsnummer (Index + 1). Nur anhängen, nie umsortieren oder entfernen. diff --git a/backend/tests/test_datenschutz.py b/backend/tests/test_datenschutz.py index f2347cb..1391a45 100644 --- a/backend/tests/test_datenschutz.py +++ b/backend/tests/test_datenschutz.py @@ -143,3 +143,19 @@ def test_umstellung_bestehender_daten(conn, login): assert rows == {(99, "geheim"), (home, "alt")} assert "tracker" not in conn.execute("SELECT content FROM projects WHERE id = 'a1'").fetchone()[0] assert not Image.open(foto).getexif() + + +def test_problembilder_verhindern_start_nicht(conn): + """Schreibgeschütztes Foto mit GPS und kaputte Datei: Umstellung läuft trotzdem durch.""" + uploads = DATA_DIR / "uploads" + gesperrt = uploads / ("cd" * 12 + ".jpg") + gesperrt.write_bytes(jpeg_mit_gps()) + gesperrt.chmod(0o444) + (uploads / ("ef" * 12 + ".png")).write_bytes(b"\x89PNG kaputt") + try: + with conn: + conn.execute("PRAGMA user_version = 1") + db.init() + assert conn.execute("PRAGMA user_version").fetchone()[0] == 2 + finally: + gesperrt.chmod(0o644)