Vorschaubilder entstehen sonst serverseitig aus den hochgeladenen Bytes – bei
einer Zeichnung steht dort JSON, daraus wird ohne Browser kein Bild. Wer sie
gerade bearbeitet, hat aber genau den Browser, der sie ohnehin zeichnet.
Neu: POST /api/v1/files/{id}/thumbnail. Der Editor schickt nach dem Speichern
ein PNG hinterher.
Den Bytes wird dabei nicht geglaubt: Sie werden nicht abgelegt, sondern durch
denselben Weg geschickt wie jedes andere Vorschaubild – dekodieren,
verkleinern, neu kodieren. Danach ist es mit Sicherheit ein Bild und hat die
übliche Grösse; nichts anderes kann im thumbs-Bereich landen. Dazu Schreibrecht
auf die Datei: Wer sie nicht ändern darf, bestimmt auch nicht, wie sie in der
Liste aussieht.
Das Hochladen läuft nach dem Speichern und ohne darauf zu warten. Ein
fehlgeschlagenes Vorschaubild ist ein Schönheitsfehler, kein Grund, das
Speichern scheitern zu lassen – und bleibt deshalb stumm.
Politur am Zeichenbrett: dunkles Thema (ein weisses Brett in dunkler Oberfläche
war der auffälligste Bruch), „Unsaved" als Marke statt eines Punktes hinter dem
Namen, Strg+S, Dateisymbol und Name ohne Endung.
„Open" und „Save to…" sind aus. Beide gehen am Browser vorbei: „Open" ersetzte
den Inhalt dieser Zeichnung durch eine fremde Datei, „Save to…" schriebe eine
zweite Fassung in den Download-Ordner, die niemand wiederfindet.
Selbsttest 12 → 18 Prüfungen, darunter die beiden Ablehnungen (kein Bild → 400,
fremder Typ → 415).
Geprüft: go build/vet sauber, alle Go-Pakete grün, 261 Prüfungen der acht
Selbsttest-Suiten grün, 53 Vitest-Tests, tsc sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
2.5 KiB
Go
73 lines
2.5 KiB
Go
package files
|
||
|
||
import (
|
||
"net/http"
|
||
|
||
"github.com/rs/zerolog/log"
|
||
|
||
"aether/internal/thumbnails"
|
||
"aether/pkg/response"
|
||
)
|
||
|
||
// maxThumbUpload begrenzt, was als Vorschaubild hereinkommen darf. Ein
|
||
// Vorschaubild ist ein paar Kilobyte gross; zwei Megabyte sind grosszuegig und
|
||
// halten trotzdem jeden Unfug vom Dekodierer fern.
|
||
const maxThumbUpload = 2 << 20
|
||
|
||
// SetThumbnail nimmt ein Vorschaubild entgegen, das nur der Browser erzeugen
|
||
// kann.
|
||
//
|
||
// POST /api/v1/files/{id}/thumbnail
|
||
//
|
||
// **Warum es das ueberhaupt gibt.** Vorschaubilder entstehen sonst
|
||
// serverseitig aus den hochgeladenen Bytes. Bei einer Zeichnung steht dort
|
||
// JSON – daraus laesst sich ohne Browser kein Bild rechnen. Wer die Zeichnung
|
||
// gerade bearbeitet, hat aber genau den Browser, der sie zeichnen kann.
|
||
//
|
||
// **Und warum man den Bytes trotzdem nicht glauben darf.** Was hier ankommt,
|
||
// kommt von einem Klienten. Es wird deshalb nicht abgelegt, sondern durch
|
||
// denselben Weg geschickt wie jedes andere Vorschaubild: dekodieren,
|
||
// verkleinern, neu kodieren. Danach ist es mit Sicherheit ein Bild und hat die
|
||
// uebliche Groesse – und nichts anderes kann im thumbs-Bereich landen.
|
||
func (h *Handler) SetThumbnail(w http.ResponseWriter, r *http.Request) {
|
||
id, err := parseID(r, "id")
|
||
if err != nil {
|
||
response.Error(w, http.StatusBadRequest, "invalid file id")
|
||
return
|
||
}
|
||
// Schreibrecht: Wer die Datei nicht aendern darf, bestimmt auch nicht, wie
|
||
// sie in der Liste aussieht.
|
||
owner, ok := h.resolveFile(w, r, id, true)
|
||
if !ok {
|
||
return
|
||
}
|
||
f, err := h.store.GetFile(r.Context(), id, owner.OwnerID)
|
||
if err != nil {
|
||
response.Error(w, http.StatusNotFound, "file not found")
|
||
return
|
||
}
|
||
|
||
typ := r.Header.Get("Content-Type")
|
||
if !thumbnails.SupportedMIME(typ) {
|
||
response.Error(w, http.StatusUnsupportedMediaType, "thumbnail must be a PNG, JPEG, GIF or WebP image")
|
||
return
|
||
}
|
||
|
||
// MaxBytesReader statt einer eigenen Zaehlung: Er bricht die Verbindung
|
||
// ab, statt erst alles zu lesen und dann abzulehnen.
|
||
rc := http.MaxBytesReader(w, r.Body, maxThumbUpload)
|
||
defer rc.Close()
|
||
|
||
if err := thumbnails.Generate(r.Context(), h.storage, f.ID.String(), typ, rc); err != nil {
|
||
log.Ctx(r.Context()).Warn().Err(err).Str("file_id", f.ID.String()).
|
||
Msg("Vorschaubild vom Klienten liess sich nicht verarbeiten")
|
||
response.Error(w, http.StatusBadRequest, "could not read that image")
|
||
return
|
||
}
|
||
if err := h.store.UpdateThumbnail(r.Context(), f.ID, true); err != nil {
|
||
response.Error(w, http.StatusInternalServerError, "database error")
|
||
return
|
||
}
|
||
response.NoContent(w)
|
||
}
|