chore: custom error
This commit is contained in:
1 parent
abfddca518
commit
19b9532f21
21 files changed
+359
-56
No files matched your search
@@ -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"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
fs fileSystem
|
||||
cache valkey.Client
|
||||
}
|
||||
|
||||
func New(fs fileSystem) *Service {
|
||||
func New(fs fileSystem, cache valkey.Client) *Service {
|
||||
return &Service{
|
||||
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
|
||||
@@ -26,7 +26,7 @@ type CachedClient struct {
|
||||
func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration) *CachedClient {
|
||||
c := &CachedClient{
|
||||
impl: impl,
|
||||
client: client,
|
||||
cache: client,
|
||||
ttl: refreshtime * 5,
|
||||
}
|
||||
|
||||
@@ -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 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit stLine truncated
|
||||
@@ -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