feat: hochladen aus dem Dashboard -- Bilder, Videos, Ton, PDF

POST /api/dash/upload, eigener Weg neben /api/upload. Der dortige haengt an
einem Bearer-Token; wer im Dashboard sitzt, hat eine SITZUNG und keinen Token,
und einen Token nur zum Hochladen anzulegen waere ein Geheimnis mehr, das
jemand verlieren kann.

Mehrere Dateien auf einmal, und JEDE bekommt ihr eigenes Ergebnis mit Pfad,
Groesse und Grund im Fehlerfall. Ein "3 von 5 hochgeladen" ohne die Namen der
zwei anderen ist genau die Sorte Rueckmeldung, wegen der wir hier neu bauen.
Eine kaputte Datei bricht die anderen nicht ab.

parseBody({ all: true }) ist noetig: ohne das liefert Hono bei mehreren Feldern
gleichen Namens nur das LETZTE, und aus fuenf Dateien wuerde eine.

DATEINAMEN WERDEN ZURECHTGEBOGEN, und zwar sichtbar. checkPath laesst
"Mein Video (final) 2.MP4" mit Recht nicht durch, also macht safeFilename
daraus "mein-video-final-2.mp4" -- Umlaute werden umschrieben und nicht
weggeworfen ("Grüße" -> "gruesse", nicht "gr-e"). Die Ergebnisliste zeigt den
neuen Pfad neben dem alten Namen.

BEIM MESSEN GEFUNDEN: bleibt vom Namen nichts uebrig -- "ЖУРНАЛ.txt",
"   .png", "★★★.txt" --, hiessen bisher ALLE "datei.txt", und die zweite
Datei ueberschrieb die erste stillschweigend. Jetzt haengen die ersten acht
Stellen des SHA-256 daran: gleicher Inhalt ergibt denselben Namen,
verschiedener Inhalt verschiedene. Nachgemessen mit drei solchen Namen.

Der Fortschrittsbalken laeuft ueber XMLHttpRequest und nicht fetch: nur der
kennt upload.onprogress, und bei einem 40-MB-Video ist ein Balken der
Unterschied zwischen "es tut sich was" und "ist es abgestuerzt?".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:29:33 +02:00
co-authored by Claude Opus 5
parent f7086c231c
commit 3ddab937e7
5 changed files with 387 additions and 0 deletions
+47
View File
@@ -88,6 +88,53 @@ export function mimeFor(path: string): string {
return MIME[ext] ?? 'application/octet-stream'
}
/** Aus einem Dateinamen von der Platte einen zulaessigen Pfadteil machen.
*
* Der Weg vom Dashboard ist ein anderer als der vom Skript: dort schickt
* jemand `X-Path` und weiss genau, was er will. Hier zieht jemand
* "Mein Video (final) 2.MP4" ins Fenster, und `checkPath` wuerde das mit
* Recht ablehnen — Klammern, Leerzeichen, alles nicht erlaubt.
*
* Deshalb wird der Name hier zurechtgebogen, und zwar VORHERSAGBAR:
* Kleinbuchstaben, alles Fremde wird zum Strich, Striche werden
* zusammengefasst. Aus obigem wird "mein-video-final-2.mp4". Wer den
* Dateinamen exakt behalten will, benennt ihn vorher um — das ist ehrlicher,
* als so zu tun, als koennte jeder Name eine URL werden.
*
* `eindeutig` ist der Rettungsanker fuer den Fall, dass vom Namen NICHTS
* uebrig bleibt — bei "ЖУРНАЛ.txt" oder " .png". Ohne ihn hiessen beide
* "datei.txt" bzw. "datei.png", und die zweite Datei ueberschriebe die erste
* stillschweigend. Genau das ist beim Messen passiert. Ueblicherweise gibt
* der Aufrufer die ersten Stellen des SHA-256 mit: gleicher Inhalt ergibt
* denselben Namen, verschiedener Inhalt verschiedene. */
export function safeFilename(name: string, eindeutig?: string): string {
const punkt = name.lastIndexOf('.')
const roh = punkt > 0 ? name.slice(0, punkt) : name
const endung = punkt > 0 ? name.slice(punkt + 1) : ''
const zaehmen = (s: string) =>
s
.toLowerCase()
// Umlaute NICHT einfach wegwerfen: "grün" wuerde sonst zu "grn".
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^[^a-z0-9]+/, '')
.replace(/[-.]+$/, '')
const stamm = zaehmen(roh)
const ende = zaehmen(endung)
// Bleibt nichts uebrig — etwa bei einem rein kyrillischen Namen —, ist das
// kein Fehlerfall fuer den Aufrufer: er bekommt einen erzeugten Namen und
// sieht im Ergebnis, was daraus geworden ist.
const basis = stamm || (eindeutig ? `datei-${eindeutig}` : 'datei')
return ende ? `${basis}.${ende}` : basis
}
export function extensionFor(mime: string): string {
const base = mime.split(';')[0]?.trim().toLowerCase()
for (const [ext, value] of Object.entries(MIME)) {