Etikettenvorlage: der Umlaut aus macOS sprengte den Download
macOS legt Dateinamen zerlegt ab - das "ae" in "Lagerplaetze.lbx" ist dort nicht ein Zeichen, sondern "a" plus ein kombinierender Trema-Akzent. Genau diese Form landet beim Hochladen in der Datenbank. Starlette schreibt Header aber als latin-1, und fuer den Akzent gibt es dort nichts - die Route starb mit UnicodeEncodeError und antwortete 500, obwohl der Name am Bildschirm voellig harmlos aussah. Der Header wird jetzt nach RFC 6266 zweigleisig gebaut: filename= traegt eine ASCII-Fassung als Rueckfall, filename*= den echten Namen prozentkodiert. Browser bevorzugen filename* und zeigen den Umlaut richtig. Derselbe Riegel liegt auf dem CSV-Download, wo der frei getippte Vorlagenname in den Header geht - gleicher Defekt, nur noch nicht aufgefallen. Fuer die Download-Route gab es bisher keinen Test; zwei kommen dazu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b3256457d2
commit
94f8ed4db9
@@ -14,7 +14,9 @@ from __future__ import annotations
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
import zipfile
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -55,6 +57,34 @@ def _safe_filename(name: str | None) -> str:
|
||||
return cleaned[:255] or "vorlage.lbx"
|
||||
|
||||
|
||||
def _disposition(name: str | None) -> str:
|
||||
"""``Content-Disposition`` fuer einen Dateinamen aus einem Upload.
|
||||
|
||||
Starlette kodiert Header-Werte als latin-1. Ein Dateiname kommt aber vom
|
||||
Dateisystem des Hochladenden und darf alles enthalten, was dort erlaubt
|
||||
ist: macOS legt ein "ä" ZERLEGT ab, als "a" + U+0308, und fuer diesen
|
||||
kombinierenden Akzent gibt es keine latin-1-Entsprechung. Roh eingesetzt
|
||||
sprengt so ein Name die Kodierung – die Route antwortet dann mit 500
|
||||
statt mit der Datei, obwohl der Name am Bildschirm voellig harmlos
|
||||
aussieht (".../Lagerplätze.lbx" von einem Mac ist genau dieser Fall).
|
||||
|
||||
Deshalb nach RFC 6266 zweigleisig: ``filename`` traegt eine ASCII-Fassung
|
||||
als Rueckfall, ``filename*`` den echten Namen prozentkodiert. Browser
|
||||
bevorzugen ``filename*`` und zeigen damit die Umlaute richtig an.
|
||||
"""
|
||||
sicher = _safe_filename(name)
|
||||
# NFKD zerlegt "ä" in "a" + Akzent, "ignore" wirft dann nur den Akzent
|
||||
# weg: "Lagerplätze.lbx" wird zu "Lagerplatze.lbx" und bleibt lesbar.
|
||||
ascii_fassung = (
|
||||
unicodedata.normalize("NFKD", sicher).encode("ascii", "ignore").decode("ascii")
|
||||
)
|
||||
# Ein Name ganz ohne ASCII-Anteil (z. B. rein kyrillisch) bliebe sonst leer.
|
||||
return (
|
||||
f'attachment; filename="{ascii_fassung.strip() or "vorlage.lbx"}"; '
|
||||
f"filename*=UTF-8''{quote(sicher, safe='')}"
|
||||
)
|
||||
|
||||
|
||||
def _pruefe_lbx(dateiname: str, daten: bytes) -> None:
|
||||
"""Ist das plausibel eine P-touch-Vorlage?
|
||||
|
||||
@@ -242,11 +272,13 @@ def vorlage_csv(
|
||||
"""
|
||||
vorlage = _vorlage_oder_404(db, vorlage_id)
|
||||
inhalt = csv_erzeugen(db, vorlage, basis_url=basis_url)
|
||||
dateiname = _safe_filename(f"{vorlage.name}.csv".replace("/", "-"))
|
||||
dateiname = f"{vorlage.name}.csv".replace("/", "-")
|
||||
return Response(
|
||||
content=inhalt.encode("utf-8"),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": f'attachment; filename="{dateiname}"'},
|
||||
# Derselbe Riegel wie beim .lbx-Download: Der Vorlagenname ist frei
|
||||
# getippt und kann genauso Zeichen tragen, die latin-1 nicht kennt.
|
||||
headers={"Content-Disposition": _disposition(dateiname)},
|
||||
)
|
||||
|
||||
|
||||
@@ -283,7 +315,7 @@ def datei_herunterladen(
|
||||
# attachment und nicht inline: Die Datei soll gespeichert werden, der
|
||||
# Browser kann mit ihr ohnehin nichts anfangen.
|
||||
media_type="application/octet-stream",
|
||||
headers={"Content-Disposition": f'attachment; filename="{vorlage.datei_name}"'},
|
||||
headers={"Content-Disposition": _disposition(vorlage.datei_name)},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user