Kameras speichern die Bildpunkte fast immer quer und legen die Drehung als EXIF-Notiz daneben. Browser befolgen sie von selbst - Go nicht. Deshalb stand dasselbe Foto in der grossen Ansicht aufrecht und lag im Vorschaubild auf der Seite. Neues Paket internal/imageutil liest genau ein Feld (0x0112) aus dem Anfang der Datei und dreht danach. Von Hand statt mit einer Bibliothek: Der Weg dorthin ist kurz und liegt fest, und gebraucht wird ein Feld. Gelesen werden nur die ersten 64 KB, damit ein 40-MB-Foto nicht zweimal im Speicher liegt. Gedreht wird vor dem Verkleinern - andersherum bekaeme ein hochkantes Foto den Zuschnitt eines Querformats. Bestehende Vorschaubilder aendern sich davon nicht. Dafuer neu: POST /admin/thumbnails/rebuild samt Knopf im Admin-Bereich. Auch Profilbilder aus OIDC laufen jetzt durch dieselbe Auswertung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
1.9 KiB
Go
72 lines
1.9 KiB
Go
package admin
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"aether/internal/audit"
|
||
"aether/internal/thumbnails"
|
||
"aether/pkg/response"
|
||
)
|
||
|
||
/*
|
||
POST /api/v1/admin/thumbnails/rebuild – Vorschaubilder neu erzeugen.
|
||
|
||
Anlass: Bis heute wurde die EXIF-Drehung beim Erzeugen nicht ausgewertet.
|
||
Jedes hochkant aufgenommene Foto liegt im bestehenden Bestand deshalb quer,
|
||
während dasselbe Bild in der großen Ansicht aufrecht steht – der Browser
|
||
befolgt die Notiz von selbst, der Server tat es nicht.
|
||
|
||
Beim Hochladen entstehen Vorschaubilder von allein; für das, was schon da ist,
|
||
braucht es diesen einen Anstoß. Er läuft im Vordergrund und meldet Zahlen:
|
||
Ein Vorgang, der den halben Bestand neu schreibt, soll nicht stumm im
|
||
Hintergrund verschwinden.
|
||
*/
|
||
func (h *Handler) RebuildThumbnails(w http.ResponseWriter, r *http.Request) {
|
||
if _, ok := requireAdmin(r); !ok {
|
||
response.Error(w, http.StatusForbidden, "admin required")
|
||
return
|
||
}
|
||
|
||
list, err := h.files.ImageFiles(r.Context())
|
||
if err != nil {
|
||
response.Error(w, http.StatusInternalServerError, "could not list images")
|
||
return
|
||
}
|
||
|
||
var rebuilt, failed int
|
||
for _, f := range list {
|
||
if !thumbnails.SupportedMIME(f.MimeType) {
|
||
continue
|
||
}
|
||
rc, err := h.storage.Get(r.Context(), f.StorageKey)
|
||
if err != nil {
|
||
failed++
|
||
continue
|
||
}
|
||
err = thumbnails.Generate(r.Context(), h.storage, f.ID.String(), f.MimeType, rc)
|
||
rc.Close()
|
||
if err != nil {
|
||
failed++
|
||
continue
|
||
}
|
||
// Auch setzen, wenn es schon stand: Dateien, bei denen die Erzeugung
|
||
// beim Hochladen fehlschlug, bekommen so nachträglich eines.
|
||
if err := h.files.UpdateThumbnail(r.Context(), f.ID, true); err != nil {
|
||
failed++
|
||
continue
|
||
}
|
||
rebuilt++
|
||
}
|
||
|
||
h.logAdmin(r, audit.ActionGarbageCollect, "storage", "", "",
|
||
fmt.Sprintf("thumbnails rebuilt: %d of %d images, %d failed",
|
||
rebuilt, len(list), failed))
|
||
|
||
response.JSON(w, http.StatusOK, map[string]any{
|
||
"images": len(list),
|
||
"rebuilt": rebuilt,
|
||
"failed": failed,
|
||
})
|
||
}
|