Files
Aether/backend/internal/files/handler_thumbnail.go
T
marcandClaude Opus 5 dd68bc594d feat(drawing): Vorschaubilder und ein Zeichenbrett, das zur Oberfläche passt
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>
2026-09-29 20:13:04 +02:00

73 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}