feat: implement shared-album multi-user photo album
Backend (Go/Gin/pgx): env config, pgx pool with retries, Garage S3 client (path-style, region garage), embedded golang-migrate migrations, JWT auth with refresh rotation, albums CRUD, members/roles/invites with no-escalation matrix, photo upload + async imaging/goexif WebP variants, cursor pagination, presigned URLs, likes/comments, SSE hub (album + personal), notifications, share links. Frontend (SvelteKit 2 + Svelte 5 + Tailwind v4): API wrapper with 401 refresh, auth store, all screens, Uploader/PhotoGrid/Lightbox/Comments, SSE client with reconnect + REST catch-up, three themes + system via data-theme. Deploy without Docker: Makefile builds the frontend into backend/static and embeds it (go:embed) into a single statically linked binary; systemd unit and nginx SSE example included. Use gen2brain/webp (-tags nodynamic) instead of cgo kolesa-team/go-webp to keep CGO_ENABLED=0 static builds; design.md/proposal.md updated accordingly.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package photos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gen2brain/webp"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
)
|
||||
|
||||
type ImageMeta struct {
|
||||
Width int
|
||||
Height int
|
||||
MimeType string
|
||||
TakenAt *time.Time
|
||||
Latitude *float64
|
||||
Longitude *float64
|
||||
Thumb []byte
|
||||
Preview []byte
|
||||
}
|
||||
|
||||
func inspect(data []byte) (width, height int, ok bool) {
|
||||
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return cfg.Width, cfg.Height, true
|
||||
}
|
||||
|
||||
func extractEXIF(data []byte) (taken *time.Time, lat, lon *float64) {
|
||||
x, err := exif.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if dt, err := x.Get(exif.DateTimeOriginal); err == nil {
|
||||
if s, err := dt.StringVal(); err == nil {
|
||||
if t, err := time.Parse("2006:01:02 15:04:05", s); err == nil {
|
||||
utc := t.UTC()
|
||||
taken = &utc
|
||||
}
|
||||
}
|
||||
}
|
||||
if la, lo, err := x.LatLong(); err == nil {
|
||||
lat, lon = &la, &lo
|
||||
}
|
||||
return taken, lat, lon
|
||||
}
|
||||
|
||||
func buildVariants(data []byte) (thumb, preview []byte, err error) {
|
||||
img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
thumbImg := imaging.Fit(img, 200, 200, imaging.Lanczos)
|
||||
previewImg := imaging.Fit(img, 1200, 1200, imaging.Lanczos)
|
||||
|
||||
opts := webp.Options{Quality: 82}
|
||||
var thumbBuf bytes.Buffer
|
||||
if err := webp.Encode(&thumbBuf, thumbImg, opts); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var previewBuf bytes.Buffer
|
||||
if err := webp.Encode(&previewBuf, previewImg, opts); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return thumbBuf.Bytes(), previewBuf.Bytes(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user