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.
70 lines
1.6 KiB
Go
70 lines
1.6 KiB
Go
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
|
|
}
|