feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
31a027c93a
225 files changed
+38042
No files matched your search
@@ -0,0 +1,95 @@
|
||||
package thumbnails
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
_ "golang.org/x/image/webp" // SupportedMIME nennt WebP – ohne den Dekodierer entstand dafür nie ein Vorschaubild
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
|
||||
"aether/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
maxWidth = 256
|
||||
maxHeight = 256
|
||||
)
|
||||
|
||||
// SupportedMIME returns true if we can generate a thumbnail for this MIME type.
|
||||
func SupportedMIME(mime string) bool {
|
||||
m := strings.ToLower(mime)
|
||||
return strings.HasPrefix(m, "image/jpeg") ||
|
||||
strings.HasPrefix(m, "image/png") ||
|
||||
strings.HasPrefix(m, "image/gif") ||
|
||||
strings.HasPrefix(m, "image/webp")
|
||||
}
|
||||
|
||||
// ThumbnailKey returns the storage key for a file's thumbnail.
|
||||
func ThumbnailKey(fileID string) string {
|
||||
return "thumbs/" + fileID + ".jpg"
|
||||
}
|
||||
|
||||
// Generate reads an image from src, creates a thumbnail, and stores it.
|
||||
// Returns nil if generation succeeds, error otherwise.
|
||||
func Generate(ctx context.Context, st storage.Storage, fileID, mimeType string, src io.Reader) error {
|
||||
if !SupportedMIME(mimeType) {
|
||||
return fmt.Errorf("unsupported MIME type: %s", mimeType)
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode image: %w", err)
|
||||
}
|
||||
|
||||
thumb := resize(img, maxWidth, maxHeight)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, thumb, &jpeg.Options{Quality: 80}); err != nil {
|
||||
return fmt.Errorf("encode thumbnail: %w", err)
|
||||
}
|
||||
|
||||
key := ThumbnailKey(fileID)
|
||||
if _, _, err := st.Put(ctx, key, &buf); err != nil {
|
||||
return fmt.Errorf("store thumbnail: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resize(src image.Image, maxW, maxH int) image.Image {
|
||||
bounds := src.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
if w == 0 || h == 0 {
|
||||
return src
|
||||
}
|
||||
|
||||
scaleW := float64(maxW) / float64(w)
|
||||
scaleH := float64(maxH) / float64(h)
|
||||
scale := scaleW
|
||||
if scaleH < scale {
|
||||
scale = scaleH
|
||||
}
|
||||
if scale >= 1 {
|
||||
return src // already smaller than max
|
||||
}
|
||||
|
||||
newW := int(float64(w) * scale)
|
||||
newH := int(float64(h) * scale)
|
||||
if newW < 1 {
|
||||
newW = 1
|
||||
}
|
||||
if newH < 1 {
|
||||
newH = 1
|
||||
}
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, newW, newH))
|
||||
draw.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
|
||||
return dst
|
||||
}
|
||||
Reference in new issue
Block a user