feat(gallery): caching
This commit is contained in:
@@ -2,11 +2,8 @@ package gallery
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
|
||||||
"math"
|
"math"
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
|
||||||
"github.com/InfinityTools/go-binpack2d"
|
"github.com/InfinityTools/go-binpack2d"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,32 +13,6 @@ type Gallery struct {
|
|||||||
Images []Image
|
Images []Image
|
||||||
}
|
}
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
UID string
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetImages(ctx context.Context) (imgs []images.Image, err error) {
|
|
||||||
files, err := s.fs.ReadDir(s.cfg.Path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
imagePath := path.Join(s.cfg.Path, file.Name())
|
|
||||||
img, err := s.img.GetImage(imagePath)
|
|
||||||
if err != nil || img.Image == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
imgs = append(imgs, img)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetGallery(ctx context.Context, width int) (gallery Gallery, err error) {
|
func (s *Service) GetGallery(ctx context.Context, width int) (gallery Gallery, err error) {
|
||||||
imgs, err := s.GetImages(ctx)
|
imgs, err := s.GetImages(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,25 +22,19 @@ func (s *Service) GetGallery(ctx context.Context, width int) (gallery Gallery, e
|
|||||||
packer := binpack2d.Create(50, 999999)
|
packer := binpack2d.Create(50, 999999)
|
||||||
|
|
||||||
for len(imgs) > 0 {
|
for len(imgs) > 0 {
|
||||||
unused := make([]images.Image, 0, len(imgs))
|
unused := make([]Image, 0, len(imgs))
|
||||||
for _, img := range imgs {
|
for _, img := range imgs {
|
||||||
width, height := adjustImageSize(img, 200)
|
adjustImageSize(&img, 200)
|
||||||
rect, ok := packer.Insert(width, height, binpack2d.RULE_BEST_AREA_FIT)
|
rect, ok := packer.Insert(img.Width, img.Height, binpack2d.RULE_BEST_SHORT_SIDE_FIT)
|
||||||
if !ok {
|
if !ok {
|
||||||
unused = append(unused, img)
|
unused = append(unused, img)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
uid, err := img.UID()
|
|
||||||
if err != nil {
|
img.X = rect.X
|
||||||
return gallery, err
|
img.Y = rect.Y
|
||||||
}
|
|
||||||
gallery.Images = append(gallery.Images, Image{
|
gallery.Images = append(gallery.Images, img)
|
||||||
UID: uid,
|
|
||||||
X: rect.X,
|
|
||||||
Y: rect.Y,
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
imgs = unused
|
imgs = unused
|
||||||
}
|
}
|
||||||
@@ -79,21 +44,20 @@ func (s *Service) GetGallery(ctx context.Context, width int) (gallery Gallery, e
|
|||||||
gallery.GridHeight = packer.GetHeight()
|
gallery.GridHeight = packer.GetHeight()
|
||||||
gallery.GridWith = packer.GetWidth()
|
gallery.GridWith = packer.GetWidth()
|
||||||
|
|
||||||
slog.Info("serving gallery", "g", gallery)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjustImageSize adjusts the image size so that the aspect ratio stays the same
|
// adjustImageSize adjusts the image size so that the aspect ratio stays the same
|
||||||
// but the image area is the given parameter.
|
// but the image area is the given parameter.
|
||||||
func adjustImageSize(img images.Image, area int) (width, height int) {
|
func adjustImageSize(img *Image, area int) {
|
||||||
// Calculate the original aspect ratio (W / H)
|
// Calculate the original aspect ratio (W / H)
|
||||||
aspectRatio := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy())
|
aspectRatio := float64(img.Width) / float64(img.Height)
|
||||||
|
|
||||||
// Calculate new height and width using the derived formulas
|
// Calculate new height and width using the derived formulas
|
||||||
newHeight := math.Sqrt(float64(area) / aspectRatio)
|
newHeight := math.Sqrt(float64(area) / aspectRatio)
|
||||||
newWidth := newHeight * aspectRatio
|
newWidth := newHeight * aspectRatio
|
||||||
|
|
||||||
// Round to the nearest integer to minimize precision loss
|
// Round to the nearest integer to minimize precision loss
|
||||||
return int(math.Round(newWidth)), int(math.Round(newHeight))
|
img.Width = int(math.Round(newWidth))
|
||||||
|
img.Height = int(math.Round(newHeight))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package gallery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/valkey-io/valkey-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
UID string
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
imagesKey = "gallery:images:list"
|
||||||
|
hashKey = "gallery:images:hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) autoRefresh() {
|
||||||
|
t := time.NewTicker(time.Minute)
|
||||||
|
for {
|
||||||
|
ctx, cancle := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
|
||||||
|
if err := s.assembleImageList(ctx); err != nil {
|
||||||
|
slog.Error("error while refreshing gallery", "err", err)
|
||||||
|
}
|
||||||
|
cancle()
|
||||||
|
<-t.C
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) assembleImageList(ctx context.Context) error {
|
||||||
|
files, err := s.fs.ReadDir(s.cfg.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not read contents of %s: %w", s.cfg.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
savedHash, _ := s.cache.Do(ctx, s.cache.B().Get().Key(hashKey).Build()).AsBytes()
|
||||||
|
|
||||||
|
newHash := hashImageList(files)
|
||||||
|
|
||||||
|
if bytes.Equal(savedHash, newHash) {
|
||||||
|
slog.Debug("images up to date no refreshing of gallery", "savedHash", savedHash, "newHash", newHash)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("starting gallery refresh")
|
||||||
|
defer slog.Info("gallery refresh completed")
|
||||||
|
|
||||||
|
imgs := make([]Image, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasSuffix(file.Name(), ".md") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePath := path.Join(s.cfg.Path, file.Name())
|
||||||
|
img, err := s.img.GetImage(imagePath)
|
||||||
|
if err != nil || img.Image == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := img.UID()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
imgs = append(imgs, Image{
|
||||||
|
UID: uid,
|
||||||
|
Width: img.Bounds().Dx(),
|
||||||
|
Height: img.Bounds().Dy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.cache.Do(ctx, s.cache.B().Set().Key(hashKey).Value(valkey.BinaryString(newHash)).Build()).Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.cache.Do(ctx, s.cache.B().Set().Key(imagesKey).Value(valkey.JSON(imgs)).Build()).Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashImageList(files []os.FileInfo) []byte {
|
||||||
|
h := fnv.New128()
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
fmt.Fprint(h, file.ModTime())
|
||||||
|
fmt.Fprint(h, file.Size())
|
||||||
|
fmt.Fprint(h, file.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum([]byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetImages(ctx context.Context) (images []Image, err error) {
|
||||||
|
err = s.cache.Do(ctx, s.cache.B().Get().Key(imagesKey).Build()).DecodeJSON(&images)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package gallery
|
package gallery
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
|
||||||
|
"github.com/valkey-io/valkey-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getImager interface {
|
type getImager interface {
|
||||||
@@ -11,15 +14,22 @@ type getImager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
fs filesystem.FS
|
fs filesystem.FS
|
||||||
cfg *config.Gallery
|
cfg *config.Gallery
|
||||||
img getImager
|
img getImager
|
||||||
|
cache valkey.Client
|
||||||
|
|
||||||
|
refreshIntervall time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(fs filesystem.FS, cfg *config.Gallery, img getImager) *Service {
|
func New(fs filesystem.FS, cfg *config.Gallery, img getImager, cache valkey.Client) *Service {
|
||||||
return &Service{
|
s := &Service{
|
||||||
fs: fs,
|
fs: fs,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
img: img,
|
img: img,
|
||||||
|
cache: cache,
|
||||||
}
|
}
|
||||||
|
go s.autoRefresh()
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) gallery(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) gallery(w http.ResponseWriter, r *http.Request) {
|
||||||
imgs, err := h.srv.GetGallery(r.Context(), 5)
|
imgs, err := h.srv.GetGallery(r.Context(), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("could not get images", "err", err)
|
slog.Error("could not get images", "err", err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/gallery"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/gallery"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
)
|
)
|
||||||
@@ -34,7 +33,6 @@ func New(renderer renderer, srv galleryService, cfg *config.Gallery) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type galleryService interface {
|
type galleryService interface {
|
||||||
GetImages(ctx context.Context) (imgs []images.Image, err error)
|
|
||||||
GetGallery(ctx context.Context, width int) (gallery gallery.Gallery, err error)
|
GetGallery(ctx context.Context, width int) (gallery gallery.Gallery, err error)
|
||||||
}
|
}
|
||||||
type renderer interface {
|
type renderer interface {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type Valkey struct {
|
|||||||
Password string `env:"PASSWORD"`
|
Password string `env:"PASSWORD"`
|
||||||
|
|
||||||
RefreshTime time.Duration `env:"REFRESH_TIME"`
|
RefreshTime time.Duration `env:"REFRESH_TIME"`
|
||||||
LifeTime time.Duration `env:"REFRESH_TIME"`
|
LifeTime time.Duration `env:"LIFE_TIME"`
|
||||||
|
|
||||||
// InitAddress point to valkey nodes.
|
// InitAddress point to valkey nodes.
|
||||||
// Valkey will connect to them one by one and issue a CLUSTER SLOT command to initialize the cluster client until success.
|
// Valkey will connect to them one by one and issue a CLUSTER SLOT command to initialize the cluster client until success.
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ func (c *CachedClient) Read(path string) ([]byte, error) {
|
|||||||
if err == nil && len(cached) > 0 {
|
if err == nil && len(cached) > 0 {
|
||||||
c.readHits.Add(1)
|
c.readHits.Add(1)
|
||||||
c.hits.Add(1)
|
c.hits.Add(1)
|
||||||
slog.Debug("cache hit", "key", key)
|
|
||||||
|
|
||||||
var file readFile
|
var file readFile
|
||||||
err := json.Unmarshal(cached, &file)
|
err := json.Unmarshal(cached, &file)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ func (c *CachedClient) ReadDir(path string) ([]os.FileInfo, error) {
|
|||||||
if err := json.Unmarshal([]byte(val), &cached); err == nil {
|
if err := json.Unmarshal([]byte(val), &cached); err == nil {
|
||||||
c.readDirHits.Add(1)
|
c.readDirHits.Add(1)
|
||||||
c.hits.Add(1)
|
c.hits.Add(1)
|
||||||
slog.Debug("cache hit", "key", key)
|
|
||||||
infos := make([]os.FileInfo, len(cached))
|
infos := make([]os.FileInfo, len(cached))
|
||||||
for i, f := range cached {
|
for i, f := range cached {
|
||||||
infos[i] = f
|
infos[i] = f
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ func (c *CachedClient) Stat(path string) (info os.FileInfo, err error) {
|
|||||||
var f File
|
var f File
|
||||||
if err := json.Unmarshal([]byte(val), &f); err == nil {
|
if err := json.Unmarshal([]byte(val), &f); err == nil {
|
||||||
c.hits.Add(1)
|
c.hits.Add(1)
|
||||||
slog.Debug("cache hit", "key", key)
|
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (s *Server) dynamicRoutes() (h http.Handler, err error) {
|
|||||||
func() http.Handler {
|
func() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/gallery", restgallery.New(renderer, gallery.New(fs, s.cfg.Gallery, imageSrv), s.cfg.Gallery))
|
mux.Handle("/gallery", restgallery.New(renderer, gallery.New(fs, s.cfg.Gallery, imageSrv, valkeyClient), s.cfg.Gallery))
|
||||||
mux.Handle("/", resthome.New(renderer, page.New(fs)))
|
mux.Handle("/", resthome.New(renderer, page.New(fs)))
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ templ Gallery(g gallery.Gallery) {
|
|||||||
>
|
>
|
||||||
for _, img := range g.Images {
|
for _, img := range g.Images {
|
||||||
<div
|
<div
|
||||||
style={ fmt.Sprintf("grid-column: %d / span %d; grid-row: %d / span %d;", img.X+1, img.Width, img.Y+1, img.Height) }
|
style={ fmt.Sprintf("grid-column: %d / span %d; grid-row: %d / span %d;aspect-ratio: %d / %d;", img.X+1, img.Width, img.Y+1, img.Height, img.Width, img.Height) }
|
||||||
class="overflow-hidden shadow-sm"
|
class="overflow-hidden shadow-sm"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user