rewrite/go-templ #1
@@ -1,6 +1,6 @@
|
|||||||
module git.schreifuchs.ch/schreifuchs/schreifuchs.ch
|
module git.schreifuchs.ch/schreifuchs/schreifuchs.ch
|
||||||
|
|
||||||
go 1.25.7
|
go 1.26
|
||||||
|
|
||||||
tool github.com/a-h/templ/cmd/templ
|
tool github.com/a-h/templ/cmd/templ
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/natefinch/atomic v1.0.1 // indirect
|
github.com/natefinch/atomic v1.0.1 // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
|
golang.org/x/image v0.36.0 // indirect
|
||||||
golang.org/x/mod v0.26.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M
|
|||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||||
|
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
@@ -51,6 +53,7 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
|||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -3,10 +3,20 @@ package images
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/valkey-io/valkey-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNotAnImage = errors.New("not an image")
|
var ErrNotAnImage = errors.New("not an image")
|
||||||
@@ -24,7 +34,7 @@ func (s *Service) getMime(path string) (mimeType string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetImage(ctx context.Context, uid string) (img io.Reader, mimeType string, err error) {
|
func (s *Service) getImage(uid string) (file []byte, mimeType string, err error) {
|
||||||
path, err := PathFromUID(uid)
|
path, err := PathFromUID(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("could not get path: %w", err)
|
err = fmt.Errorf("could not get path: %w", err)
|
||||||
@@ -40,10 +50,69 @@ func (s *Service) GetImage(ctx context.Context, uid string) (img io.Reader, mime
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := s.fs.Read(path)
|
file, err = s.fs.Read(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("image file could not be fetched")
|
err = fmt.Errorf("image file could not be fetched")
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.NewBuffer(file), mimeType, err
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
|
||||||
|
rawImage, mimeType, err := s.getImage(uid)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := hash(rawImage, options)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if imgBytes, err := s.cache.Do(ctx, s.cache.B().Get().Key("img:"+hash).Build()).AsBytes(); err == nil {
|
||||||
|
return bytes.NewBuffer(imgBytes), mimeType, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decImage, _, err := image.Decode(bytes.NewBuffer(rawImage))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decImage = scale(decImage, options)
|
||||||
|
outBuff := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
|
|
||||||
|
if options.Quality == 0 {
|
||||||
|
options.Quality = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jpeg.Encode(outBuff, decImage, &jpeg.Options{Quality: options.Quality})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = s.cache.Do(ctx, s.cache.B().Set().Key("img:"+hash).Value(valkey.BinaryString(outBuff.Bytes())).Ex(time.Hour*48).Build()).Error(); err == nil {
|
||||||
|
return outBuff, "image/jpeg", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outBuff, "image/jpeg", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(bytes []byte, options Options) (hash string, err error) {
|
||||||
|
h := fnv.New128()
|
||||||
|
|
||||||
|
if _, err = h.Write(bytes); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(h, binary.LittleEndian, int64(options.Height)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(h, binary.LittleEndian, int64(options.Width)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(h, binary.LittleEndian, int64(options.Quality)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.Sum(make([]byte, 0, 128/8))
|
||||||
|
|
||||||
|
return base64.RawStdEncoding.EncodeToString(res), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
package images
|
package images
|
||||||
|
|
||||||
import "os"
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
type Service struct {
|
"github.com/valkey-io/valkey-go"
|
||||||
fs fileSystem
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Width int // max width, 0 is unlimited
|
||||||
|
Height int // max Height, 0 is unlimited
|
||||||
|
Quality int // quality of the output jpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(fs fileSystem) *Service {
|
type Service struct {
|
||||||
|
fs fileSystem
|
||||||
|
cache valkey.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fs fileSystem, cache valkey.Client) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
fs: fs,
|
fs: fs,
|
||||||
|
cache: cache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func scale(src image.Image, options Options) image.Image {
|
||||||
|
// Set the expected size that you want:
|
||||||
|
dst := image.NewRGBA(
|
||||||
|
calculateFit(src.Bounds(), options),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resize:
|
||||||
|
draw.BiLinear.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
|
||||||
|
|
||||||
|
// Encode to `output`:
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateFit returns the new dimensions (width, height) that fit within the
|
||||||
|
// provided Options without upscaling and while maintaining the aspect ratio.
|
||||||
|
func calculateFit(bounds image.Rectangle, opt Options) image.Rectangle {
|
||||||
|
// If no limits are set, or the image is already smaller than the limits,
|
||||||
|
// return original dimensions (No Upscaling).
|
||||||
|
if (opt.Width == 0 || bounds.Max.X <= opt.Width) && (opt.Height == 0 || bounds.Max.Y <= opt.Height) {
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate ratios for width and height
|
||||||
|
ratioW := float64(opt.Width) / float64(bounds.Max.X)
|
||||||
|
ratioH := float64(opt.Height) / float64(bounds.Max.Y)
|
||||||
|
|
||||||
|
// Determine the scale factor
|
||||||
|
var scale float64
|
||||||
|
if opt.Width > 0 && opt.Height > 0 {
|
||||||
|
// Use the smaller ratio to ensure it fits in both bounds
|
||||||
|
scale = math.Min(ratioW, ratioH)
|
||||||
|
} else if opt.Width > 0 {
|
||||||
|
scale = ratioW
|
||||||
|
} else if opt.Height > 0 {
|
||||||
|
scale = ratioH
|
||||||
|
} else {
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
return image.Rect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
int(math.Round(float64(bounds.Max.X)*scale)),
|
||||||
|
int(math.Round(float64(bounds.Max.Y)*scale)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ const keyStat = "webdav:stat:"
|
|||||||
// CachedClient wraps a FileSystem implementation with Valkey caching.
|
// CachedClient wraps a FileSystem implementation with Valkey caching.
|
||||||
type CachedClient struct {
|
type CachedClient struct {
|
||||||
impl FS
|
impl FS
|
||||||
client valkey.Client
|
cache valkey.Client
|
||||||
ttl time.Duration
|
ttl time.Duration
|
||||||
hits atomic.Int64
|
hits atomic.Int64
|
||||||
misses atomic.Int64
|
misses atomic.Int64
|
||||||
@@ -25,9 +25,9 @@ type CachedClient struct {
|
|||||||
// NewCachedClient creates a new CachedClient.
|
// NewCachedClient creates a new CachedClient.
|
||||||
func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration) *CachedClient {
|
func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration) *CachedClient {
|
||||||
c := &CachedClient{
|
c := &CachedClient{
|
||||||
impl: impl,
|
impl: impl,
|
||||||
client: client,
|
cache: client,
|
||||||
ttl: refreshtime * 5,
|
ttl: refreshtime * 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -50,7 +50,7 @@ func (c *CachedClient) revalidate(ctx context.Context) {
|
|||||||
func (c *CachedClient) scanAndProcess(ctx context.Context, match string, process func(key string) error) {
|
func (c *CachedClient) scanAndProcess(ctx context.Context, match string, process func(key string) error) {
|
||||||
cursor := uint64(0)
|
cursor := uint64(0)
|
||||||
for {
|
for {
|
||||||
res, err := c.client.Do(ctx, c.client.B().Scan().Cursor(cursor).Match(match).Build()).AsScanEntry()
|
res, err := c.cache.Do(ctx, c.cache.B().Scan().Cursor(cursor).Match(match).Build()).AsScanEntry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("scan failed", "match", match, "err", err)
|
slog.Error("scan failed", "match", match, "err", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (c *CachedClient) Read(path string) ([]byte, error) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Try cache
|
// Try cache
|
||||||
cached, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).AsBytes()
|
cached, err := c.cache.Do(ctx, c.cache.B().Get().Key(key).Build()).AsBytes()
|
||||||
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)
|
||||||
@@ -58,7 +58,7 @@ func (c *CachedClient) Read(path string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
c.client.Do(ctx, c.client.B().Set().Key(key).Value(valkey.BinaryString(cached)).Ex(c.ttl).Build())
|
c.cache.Do(ctx, c.cache.B().Set().Key(key).Value(valkey.BinaryString(cached)).Ex(c.ttl).Build())
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ func (c *CachedClient) revalidateRead(ctx context.Context) {
|
|||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
c.scanAndProcess(ctx, keyRead+"*", func(key string) error {
|
c.scanAndProcess(ctx, keyRead+"*", func(key string) error {
|
||||||
path := strings.TrimPrefix(key, keyRead)
|
path := strings.TrimPrefix(key, keyRead)
|
||||||
cached, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).AsBytes()
|
cached, err := c.cache.Do(ctx, c.cache.B().Get().Key(key).Build()).AsBytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Key might be gone or error fetching
|
// Key might be gone or error fetching
|
||||||
return nil
|
return nil
|
||||||
@@ -76,13 +76,13 @@ func (c *CachedClient) revalidateRead(ctx context.Context) {
|
|||||||
var cachedFile readFile
|
var cachedFile readFile
|
||||||
err = json.Unmarshal(cached, &cachedFile)
|
err = json.Unmarshal(cached, &cachedFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.client.Do(ctx, c.client.B().Del().Key(key).Build())
|
c.cache.Do(ctx, c.cache.B().Del().Key(key).Build())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := c.impl.Stat(path)
|
info, err := c.impl.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.client.Do(ctx, c.client.B().Del().Key(key).Build())
|
c.cache.Do(ctx, c.cache.B().Del().Key(key).Build())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +99,9 @@ func (c *CachedClient) revalidateRead(ctx context.Context) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.client.Do(ctx, c.client.B().Set().Key(key).Value(valkey.BinaryString(newCached)).Ex(c.ttl).Build())
|
c.cache.Do(ctx, c.cache.B().Set().Key(key).Value(valkey.BinaryString(newCached)).Ex(c.ttl).Build())
|
||||||
} else {
|
} else {
|
||||||
c.client.Do(ctx, c.client.B().Expire().Key(key).Seconds(int64(c.ttl/time.Second)).Build())
|
c.cache.Do(ctx, c.cache.B().Expire().Key(key).Seconds(int64(c.ttl/time.Second)).Build())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func (c *CachedClient) ReadDir(path string) ([]os.FileInfo, error) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Try cache
|
// Try cache
|
||||||
val, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).ToString()
|
val, err := c.cache.Do(ctx, c.cache.B().Get().Key(key).Build()).ToString()
|
||||||
if err == nil && val != "" {
|
if err == nil && val != "" {
|
||||||
var cached []File
|
var cached []File
|
||||||
if err := json.Unmarshal([]byte(val), &cached); err == nil {
|
if err := json.Unmarshal([]byte(val), &cached); err == nil {
|
||||||
@@ -47,7 +47,7 @@ func (c *CachedClient) ReadDir(path string) ([]os.FileInfo, error) {
|
|||||||
|
|
||||||
bytes, err := json.Marshal(cached)
|
bytes, err := json.Marshal(cached)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.client.Do(ctx, c.client.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
c.cache.Do(ctx, c.cache.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
||||||
}
|
}
|
||||||
|
|
||||||
return infos, nil
|
return infos, nil
|
||||||
@@ -59,7 +59,7 @@ func (c *CachedClient) revalidateReadDir(ctx context.Context) {
|
|||||||
path := strings.TrimPrefix(key, keyReadDir)
|
path := strings.TrimPrefix(key, keyReadDir)
|
||||||
infos, err := c.impl.ReadDir(path)
|
infos, err := c.impl.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.client.Do(ctx, c.client.B().Del().Key(key).Build())
|
c.cache.Do(ctx, c.cache.B().Del().Key(key).Build())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ func (c *CachedClient) revalidateReadDir(ctx context.Context) {
|
|||||||
|
|
||||||
bytes, err := json.Marshal(cached)
|
bytes, err := json.Marshal(cached)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.client.Do(ctx, c.client.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
c.cache.Do(ctx, c.cache.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (c *CachedClient) Stat(path string) (info os.FileInfo, err error) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Try cache
|
// Try cache
|
||||||
val, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).ToString()
|
val, err := c.cache.Do(ctx, c.cache.B().Get().Key(key).Build()).ToString()
|
||||||
if err == nil && val != "" {
|
if err == nil && val != "" {
|
||||||
var f File
|
var f File
|
||||||
if err := json.Unmarshal([]byte(val), &f); err == nil {
|
if err := json.Unmarshal([]byte(val), &f); err == nil {
|
||||||
@@ -39,7 +39,7 @@ func (c *CachedClient) Stat(path string) (info os.FileInfo, err error) {
|
|||||||
|
|
||||||
bytes, err := json.Marshal(info)
|
bytes, err := json.Marshal(info)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.client.Do(ctx, c.client.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
c.cache.Do(ctx, c.cache.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -51,7 +51,7 @@ func (c *CachedClient) revalidateStat(ctx context.Context) {
|
|||||||
path := strings.TrimPrefix(key, keyStat)
|
path := strings.TrimPrefix(key, keyStat)
|
||||||
info, err := c.impl.Stat(path)
|
info, err := c.impl.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.client.Do(ctx, c.client.B().Del().Key(key).Build())
|
c.cache.Do(ctx, c.cache.B().Del().Key(key).Build())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ func (c *CachedClient) revalidateStat(ctx context.Context) {
|
|||||||
f := ParseToFile(info)
|
f := ParseToFile(info)
|
||||||
bytes, err := json.Marshal(f)
|
bytes, err := json.Marshal(f)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.client.Do(ctx, c.client.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
c.cache.Do(ctx, c.cache.B().Set().Key(key).Value(string(bytes)).Ex(c.ttl).Build())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package handleimage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Handler) getImage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
uid := r.PathValue("uid")
|
|
||||||
|
|
||||||
img, mime, err := h.img.GetImage(r.Context(), uid)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("error wile serving image", "err", err, "uid", uid)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Add("Content-Type", mime)
|
|
||||||
io.Copy(w, img)
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package handlehome
|
package resthome
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handlehome
|
package resthome
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package restimage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
||||||
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) getImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid := r.PathValue("uid")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
options := images.Options{}
|
||||||
|
options.Height, err = restutil.IntParam(r, "h")
|
||||||
|
if err != nil {
|
||||||
|
restutil.SendErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
options.Width, err = restutil.IntParam(r, "w")
|
||||||
|
if err != nil {
|
||||||
|
restutil.SendErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Quality, err = restutil.IntParam(r, "q")
|
||||||
|
if err != nil {
|
||||||
|
restutil.SendErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img, mime, err := h.img.GetImage(r.Context(), uid, options)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error wile serving image", "err", err, "uid", uid)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", mime)
|
||||||
|
io.Copy(w, img)
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package handleimage
|
package restimage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@@ -20,5 +22,5 @@ func New(mux *http.ServeMux, srv imageService) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type imageService interface {
|
type imageService interface {
|
||||||
GetImage(ctx context.Context, uid string) (img io.Reader, mimeType string, err error)
|
GetImage(ctx context.Context, uid string, options images.Options) (img io.Reader, mimeType string, err error)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package restutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorTypeUndefinded = ""
|
||||||
|
ErrorTypeValidation = "VALIDATION"
|
||||||
|
ErrorTypeNotFound = "NOT_FOUND"
|
||||||
|
ErrorTypeServerError = "SERVER_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Type ErrorType `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrappErr(err error, errorType ErrorType, message string) *Error {
|
||||||
|
return &Error{
|
||||||
|
Type: errorType,
|
||||||
|
Message: message,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Message, e.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) StatusCode() int {
|
||||||
|
switch e.Type {
|
||||||
|
case ErrorTypeValidation:
|
||||||
|
return http.StatusBadRequest
|
||||||
|
case ErrorTypeNotFound:
|
||||||
|
return http.StatusNotFound
|
||||||
|
default:
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendErr(w http.ResponseWriter, err error) {
|
||||||
|
restErr := &Error{}
|
||||||
|
|
||||||
|
if !errors.As(err, &restErr) {
|
||||||
|
slog.Error("internal server error", "err", err)
|
||||||
|
restErr = &Error{
|
||||||
|
Type: ErrorTypeServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(restErr.StatusCode())
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(restErr)
|
||||||
|
|
||||||
|
slog.Error("could not send error", "err", err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package restutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IntParam(r *http.Request, key string) (val int, err error) {
|
||||||
|
str := r.URL.Query().Get(key)
|
||||||
|
if str == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err = strconv.Atoi(str)
|
||||||
|
if err != nil {
|
||||||
|
err = WrappErr(
|
||||||
|
err,
|
||||||
|
ErrorTypeValidation,
|
||||||
|
fmt.Sprintf("failed to parse query parameter '%s': not an int", key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/handlehome"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/resthome"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/handleimage"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restimage"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer"
|
||||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
|
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
|
||||||
"github.com/studio-b12/gowebdav"
|
"github.com/studio-b12/gowebdav"
|
||||||
@@ -54,8 +54,8 @@ func registerOther() (h http.Handler) {
|
|||||||
fs = filesystem.NewCachedClient(webdavClient, valkeyClient, 5*time.Hour)
|
fs = filesystem.NewCachedClient(webdavClient, valkeyClient, 5*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = handlehome.New(mux, page.New(fs))
|
_ = resthome.New(mux, page.New(fs))
|
||||||
_ = handleimage.New(mux, images.New(fs))
|
_ = restimage.New(mux, images.New(fs, valkeyClient))
|
||||||
|
|
||||||
mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS())))
|
mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS())))
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
templ ImageTile(src string) {
|
templ ImageTile(src string) {
|
||||||
<div class="grid grid-cols-1 grid-rows-1">
|
<div class="grid grid-cols-1 grid-rows-1">
|
||||||
<img
|
<img
|
||||||
src={ src }
|
src={ src }
|
||||||
|
srcset={ makeSrcset(src) }
|
||||||
fetchpriority="high"
|
fetchpriority="high"
|
||||||
class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover"
|
class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -12,3 +16,16 @@ templ ImageTile(src string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSrcset(src string) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
for i := range 19 {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s?w=%d %dw, ", src, 100*(i+1), 50*(i+1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 20
|
||||||
|
sb.WriteString(fmt.Sprintf("%s?w=%d %dw", src, 100*(i+1), 50*(i+1)))
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ package components
|
|||||||
import "github.com/a-h/templ"
|
import "github.com/a-h/templ"
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
func ImageTile(src string) templ.Component {
|
func ImageTile(src string) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
@@ -36,13 +39,26 @@ func ImageTile(src string) templ.Component {
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(src)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(src)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/imagetile.templ`, Line: 6, Col: 12}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/imagetile.templ`, Line: 9, Col: 12}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" fetchpriority=\"high\" class=\"row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover\"><div class=\"row-start-1 col-start-1\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" srcset=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(makeSrcset(src))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/imagetile.templ`, Line: 10, Col: 27}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" fetchpriority=\"high\" class=\"row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover\"><div class=\"row-start-1 col-start-1\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -50,7 +66,7 @@ func ImageTile(src string) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -58,4 +74,17 @@ func ImageTile(src string) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSrcset(src string) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
for i := range 19 {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s?w=%d %dw, ", src, 100*(i+1), 50*(i+1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 20
|
||||||
|
sb.WriteString(fmt.Sprintf("%s?w=%d %dw", src, 100*(i+1), 50*(i+1)))
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|||||||
Reference in New Issue
Block a user