chore: custom error
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
module git.schreifuchs.ch/schreifuchs/schreifuchs.ch
|
||||
|
||||
go 1.25.7
|
||||
go 1.26
|
||||
|
||||
tool github.com/a-h/templ/cmd/templ
|
||||
|
||||
@@ -22,6 +22,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/natefinch/atomic v1.0.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/net v0.48.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=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
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/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
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/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
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/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -3,10 +3,20 @@ package images
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
var ErrNotAnImage = errors.New("not an image")
|
||||
@@ -24,7 +34,7 @@ func (s *Service) getMime(path string) (mimeType string, err error) {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
file, err := s.fs.Read(path)
|
||||
file, err = s.fs.Read(path)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
|
||||
type Service struct {
|
||||
fs fileSystem
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
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{
|
||||
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.
|
||||
type CachedClient struct {
|
||||
impl FS
|
||||
client valkey.Client
|
||||
cache valkey.Client
|
||||
ttl time.Duration
|
||||
hits atomic.Int64
|
||||
misses atomic.Int64
|
||||
@@ -25,9 +25,9 @@ type CachedClient struct {
|
||||
// NewCachedClient creates a new CachedClient.
|
||||
func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration) *CachedClient {
|
||||
c := &CachedClient{
|
||||
impl: impl,
|
||||
client: client,
|
||||
ttl: refreshtime * 5,
|
||||
impl: impl,
|
||||
cache: client,
|
||||
ttl: refreshtime * 5,
|
||||
}
|
||||
|
||||
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) {
|
||||
cursor := uint64(0)
|
||||
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 {
|
||||
slog.Error("scan failed", "match", match, "err", err)
|
||||
return
|
||||
|
||||
@@ -22,7 +22,7 @@ func (c *CachedClient) Read(path string) ([]byte, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 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 {
|
||||
c.readHits.Add(1)
|
||||
c.hits.Add(1)
|
||||
@@ -58,7 +58,7 @@ func (c *CachedClient) Read(path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (c *CachedClient) revalidateRead(ctx context.Context) {
|
||||
startTime := time.Now()
|
||||
c.scanAndProcess(ctx, keyRead+"*", func(key string) error {
|
||||
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 {
|
||||
// Key might be gone or error fetching
|
||||
return nil
|
||||
@@ -76,13 +76,13 @@ func (c *CachedClient) revalidateRead(ctx context.Context) {
|
||||
var cachedFile readFile
|
||||
err = json.Unmarshal(cached, &cachedFile)
|
||||
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
|
||||
}
|
||||
|
||||
info, err := c.impl.Stat(path)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ func (c *CachedClient) revalidateRead(ctx context.Context) {
|
||||
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 {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ func (c *CachedClient) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 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 != "" {
|
||||
var cached []File
|
||||
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)
|
||||
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
|
||||
@@ -59,7 +59,7 @@ func (c *CachedClient) revalidateReadDir(ctx context.Context) {
|
||||
path := strings.TrimPrefix(key, keyReadDir)
|
||||
infos, err := c.impl.ReadDir(path)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (c *CachedClient) revalidateReadDir(ctx context.Context) {
|
||||
|
||||
bytes, err := json.Marshal(cached)
|
||||
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
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ func (c *CachedClient) Stat(path string) (info os.FileInfo, err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 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 != "" {
|
||||
var f File
|
||||
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)
|
||||
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
|
||||
@@ -51,7 +51,7 @@ func (c *CachedClient) revalidateStat(ctx context.Context) {
|
||||
path := strings.TrimPrefix(key, keyStat)
|
||||
info, err := c.impl.Stat(path)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (c *CachedClient) revalidateStat(ctx context.Context) {
|
||||
f := ParseToFile(info)
|
||||
bytes, err := json.Marshal(f)
|
||||
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
|
||||
})
|
||||
|
||||
@@ -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 (
|
||||
"net/http"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlehome
|
||||
package resthome
|
||||
|
||||
import (
|
||||
"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 (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
@@ -20,5 +22,5 @@ func New(mux *http.ServeMux, srv imageService) *Handler {
|
||||
}
|
||||
|
||||
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/translate"
|
||||
"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/handleimage"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/resthome"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restimage"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
|
||||
"github.com/studio-b12/gowebdav"
|
||||
@@ -54,8 +54,8 @@ func registerOther() (h http.Handler) {
|
||||
fs = filesystem.NewCachedClient(webdavClient, valkeyClient, 5*time.Hour)
|
||||
}
|
||||
|
||||
_ = handlehome.New(mux, page.New(fs))
|
||||
_ = handleimage.New(mux, images.New(fs))
|
||||
_ = resthome.New(mux, page.New(fs))
|
||||
_ = restimage.New(mux, images.New(fs, valkeyClient))
|
||||
|
||||
mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS())))
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package components
|
||||
|
||||
import "strings"
|
||||
import "fmt"
|
||||
|
||||
templ ImageTile(src string) {
|
||||
<div class="grid grid-cols-1 grid-rows-1">
|
||||
<img
|
||||
src={ src }
|
||||
srcset={ makeSrcset(src) }
|
||||
fetchpriority="high"
|
||||
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>
|
||||
}
|
||||
|
||||
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 templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "strings"
|
||||
import "fmt"
|
||||
|
||||
func ImageTile(src string) templ.Component {
|
||||
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
|
||||
@@ -36,13 +39,26 @@ func ImageTile(src string) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(src)
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -50,7 +66,7 @@ func ImageTile(src string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user