New Etagger interface

- new Etagger that must implement a GetSumReader method
- fixed "tager" spelling
- use fnv instead of crc64 to hash etag values
This commit is contained in:
Olivier Meunier
2025-06-21 10:05:44 +02:00
parent 1939dcf01e
commit ca8693c2d1
10 changed files with 72 additions and 65 deletions

View File

@@ -67,6 +67,7 @@ linters:
- fmt.Fprintf(net/http.ResponseWriter)
- fmt.Fprintln(net/http.ResponseWriter)
- io.Copy(net/http.ResponseWriter)
- io.WriteString(hash.Hash)
- io.WriteString(net/http.ResponseWriter)
- (*strings.Replacer).WriteString(net/http.ResponseWriter)
- (*codeberg.org/readeck/readeck/internal/server.Server).AddFlash

View File

@@ -8,6 +8,7 @@ package docs
import (
"embed"
"encoding/json"
"hash"
"io/fs"
"net/http"
)
@@ -43,9 +44,9 @@ type Manifest struct {
var manifest *Manifest
// GetSumStrings implements the Etager interface.
func (f *File) GetSumStrings() []string {
return []string{f.Etag}
// UpdateEtag implements the [server.Etagger] interface.
func (f *File) UpdateEtag(h hash.Hash) {
h.Write([]byte(f.Etag))
}
func init() {

View File

@@ -7,6 +7,8 @@ package assets
import (
"context"
"fmt"
"hash"
"io"
"math/rand/v2"
"net/http"
"strconv"
@@ -42,8 +44,8 @@ func newRandom(data uint64) *random {
return &random{rand.New(rand.NewPCG(data, data))} //nolint:gosec
}
func (r *random) GetSumStrings() []string {
return []string{configs.BuildTime().String(), strconv.Itoa(r.Int())}
func (r *random) UpdateEtag(h hash.Hash) {
io.WriteString(h, configs.BuildTime().String()+strconv.Itoa(r.Int()))
}
func (r *random) GetLastModified() []time.Time {
@@ -53,8 +55,6 @@ func (r *random) GetLastModified() []time.Time {
// randomSvg sends an SVG image with a gradient. The gradient's color
// is based on the name.
func randomSvg(s *server.Server) http.Handler {
r := chi.NewRouter()
withHashCode := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
@@ -76,7 +76,7 @@ func randomSvg(s *server.Server) http.Handler {
})
}
r.With(withHashCode).Get("/", func(w http.ResponseWriter, r *http.Request) {
return withHashCode(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rd := r.Context().Value(ctxNameKey{}).(*random)
w.Header().Set("Content-Type", "image/svg+xml")
@@ -87,8 +87,7 @@ func randomSvg(s *server.Server) http.Handler {
rd.Perm(70)[1]+20, // bottom saturation
randomCircles(rd),
)
})
return r
}))
}
func randomCircles(r *random) string {

View File

@@ -9,6 +9,8 @@ import (
"database/sql/driver"
"encoding/json"
"errors"
"hash"
"io"
"log/slog"
"math/rand/v2"
"strconv"
@@ -152,12 +154,9 @@ func (u *User) Delete() error {
return err
}
// GetSumStrings implements Etager interface.
func (u *User) GetSumStrings() []string {
return []string{
strconv.Itoa(u.ID),
strconv.FormatInt(u.Updated.Unix(), 10),
}
// UpdateEtag implements [server.Etagger] interface.
func (u *User) UpdateEtag(h hash.Hash) {
io.WriteString(h, u.UID+strconv.FormatInt(u.Updated.UTC().UnixNano(), 10))
}
// GetLastModified implements LastModer interface.

View File

@@ -10,11 +10,14 @@ import (
"database/sql/driver"
"encoding/json"
"errors"
"hash"
"io"
"log/slog"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
@@ -537,10 +540,10 @@ func (b *Bookmark) replaceLabel(oldLabel, newLabel string) {
b.Labels = slices.Compact(b.Labels)
}
// GetSumStrings returns the string used to generate the etag
// UpdateEtag returns the string used to generate the etag
// of the bookmark(s).
func (b *Bookmark) GetSumStrings() []string {
return []string{b.UID, b.Updated.String()}
func (b *Bookmark) UpdateEtag(h hash.Hash) {
io.WriteString(h, b.UID+strconv.FormatInt(b.Updated.UTC().UnixNano(), 10))
}
// GetLastModified returns the last modified times.

View File

@@ -6,6 +6,9 @@ package bookmarks
import (
"errors"
"hash"
"io"
"strconv"
"time"
"github.com/doug-martin/goqu/v9"
@@ -123,8 +126,8 @@ func (c *Collection) Delete() error {
return err
}
// GetSumStrings returns the string used to generate the etag
// UpdateEtag returns the string used to generate the etag
// of the collection(s).
func (c *Collection) GetSumStrings() []string {
return []string{c.UID, c.Updated.String()}
func (c *Collection) UpdateEtag(h hash.Hash) {
io.WriteString(h, c.UID+strconv.FormatInt(c.Updated.UTC().UnixNano(), 10))
}

View File

@@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
"hash"
"io"
"log/slog"
"net/http"
@@ -35,17 +36,17 @@ import (
)
type (
ctxAnnotationListKey struct{}
ctxBookmarkKey struct{}
ctxBookmarkListKey struct{}
ctxBookmarkListTagerKey struct{}
ctxBookmarkOrderKey struct{}
ctxBookmarkSyncListKey struct{}
ctxLabelKey struct{}
ctxLabelListKey struct{}
ctxSharedInfoKey struct{}
ctxFiltersKey struct{}
ctxDefaultLimitKey struct{}
ctxAnnotationListKey struct{}
ctxBookmarkKey struct{}
ctxBookmarkListKey struct{}
ctxBookmarkListTaggerKey struct{}
ctxBookmarkOrderKey struct{}
ctxBookmarkSyncListKey struct{}
ctxLabelKey struct{}
ctxLabelListKey struct{}
ctxSharedInfoKey struct{}
ctxFiltersKey struct{}
ctxDefaultLimitKey struct{}
)
// bookmarkList renders a paginated list of the connected
@@ -757,14 +758,14 @@ func (api *apiRouter) withBookmarkList(next http.Handler) http.Handler {
ctx := filterForm.saveContext(r.Context())
ctx = context.WithValue(ctx, ctxBookmarkListKey{}, res)
tagers := []server.Etager{res}
t, ok := r.Context().Value(ctxBookmarkListTagerKey{}).([]server.Etager)
taggers := []server.Etagger{res}
t, ok := r.Context().Value(ctxBookmarkListTaggerKey{}).([]server.Etagger)
if ok {
tagers = append(tagers, t...)
taggers = append(taggers, t...)
}
if r.Method == http.MethodGet {
api.srv.WriteEtag(w, r, tagers...)
api.srv.WriteEtag(w, r, taggers...)
}
api.srv.WithCaching(next).ServeHTTP(w, r.WithContext(ctx))
})
@@ -788,8 +789,8 @@ func (api *apiRouter) withBookmarkSyncList(next http.Handler) http.Handler {
}
ctx := context.WithValue(r.Context(), ctxBookmarkSyncListKey{}, res)
tagers := []server.Etager{res}
api.srv.WriteEtag(w, r, tagers...)
taggers := []server.Etagger{res}
api.srv.WriteEtag(w, r, taggers...)
api.srv.WithCaching(next).ServeHTTP(w, r.WithContext(ctx))
})
@@ -949,13 +950,10 @@ type bookmarkList struct {
Items []bookmarkItem
}
func (bl bookmarkList) GetSumStrings() []string {
r := []string{}
func (bl bookmarkList) UpdateEtag(h hash.Hash) {
for i := range bl.items {
r = append(r, bl.items[i].Updated.String(), bl.items[i].UID)
io.WriteString(h, bl.items[i].UID+strconv.FormatInt(bl.items[i].Updated.UTC().Unix(), 10))
}
return r
}
// bookmarkItem is a serialized bookmark instance that can
@@ -1221,13 +1219,10 @@ func (bi *bookmarkItem) setEmbed() error {
type bookmarkSyncList []*bookmarkSyncItem
func (bl bookmarkSyncList) GetSumStrings() []string {
r := []string{}
for i := range bl {
r = append(r, bl[i].Updated.String(), bl[i].ID)
func (bl bookmarkSyncList) UpdateEtag(h hash.Hash) {
for _, b := range bl {
io.WriteString(h, b.ID+strconv.FormatInt(b.Updated.UTC().Unix(), 10))
}
return r
}
type bookmarkSyncItem struct {

View File

@@ -157,7 +157,7 @@ func (api *apiRouter) withCollection(next http.Handler) http.Handler {
}
ctx := context.WithValue(r.Context(), ctxCollectionKey{}, c)
ctx = context.WithValue(ctx, ctxBookmarkListTagerKey{}, []server.Etager{c})
ctx = context.WithValue(ctx, ctxBookmarkListTaggerKey{}, []server.Etagger{c})
if ctx.Value(ctxBookmarkOrderKey{}) == nil {
ctx = context.WithValue(ctx, ctxBookmarkOrderKey{}, orderExpressionList{goqu.T("b").Col("created").Desc()})

View File

@@ -5,7 +5,8 @@
package server
import (
"hash/crc64"
"hash"
"hash/fnv"
"net/http"
"sort"
"strconv"
@@ -16,10 +17,9 @@ import (
"codeberg.org/readeck/readeck/internal/auth"
)
// Etager must provides a function that returns a list of
// strings used to build an etag header.
type Etager interface {
GetSumStrings() []string
// Etagger must provides a function that can update a [hash.Hash].
type Etagger interface {
UpdateEtag(hash.Hash)
}
// LastModer must provides a function that returns a list
@@ -37,15 +37,15 @@ const (
)
// WriteEtag adds an Etag header to the response, based on
// the values sent by GetSumStrings. The build date is always
// the values from UpdateEtag. The build date is always
// included.
func (s *Server) WriteEtag(w http.ResponseWriter, r *http.Request, taggers ...Etager) {
func (s *Server) WriteEtag(w http.ResponseWriter, r *http.Request, taggers ...Etagger) {
if len(taggers) == 0 {
w.Header().Del("Etag")
return
}
h := crc64.New(crc64.MakeTable(crc64.ISO))
h := fnv.New64()
h.Write([]byte(strconv.FormatInt(configs.BuildTime().Unix(), 10)))
if user := auth.GetRequestUser(r); user.ID != 0 {
@@ -55,10 +55,11 @@ func (s *Server) WriteEtag(w http.ResponseWriter, r *http.Request, taggers ...Et
taggers = append(taggers, sess)
}
for _, tager := range taggers {
for _, x := range tager.GetSumStrings() {
h.Write([]byte(x))
for _, tagger := range taggers {
if tagger == nil {
continue
}
tagger.UpdateEtag(h)
}
w.Header().Set("Etag", strconv.FormatUint(h.Sum64(), 16))
@@ -74,6 +75,9 @@ func (s *Server) WriteLastModified(w http.ResponseWriter, r *http.Request, moder
mtimes := []time.Time{configs.BuildTime()}
for _, m := range moders {
if m == nil {
continue
}
mtimes = append(mtimes, m.GetLastModified()...)
}

View File

@@ -8,6 +8,8 @@
package sessions
import (
"hash"
"io"
"net/http"
"strconv"
"time"
@@ -75,9 +77,9 @@ func (s *Session) AddFlash(typ, msg string) {
s.Payload.Flashes = append(s.Payload.Flashes, FlashMessage{typ, msg})
}
// GetSumStrings implements Etager interface.
func (s *Session) GetSumStrings() []string {
return []string{strconv.FormatInt(s.Payload.LastUpdate.Unix(), 10)}
// UpdateEtag implements [server.Etagger] interface.
func (s *Session) UpdateEtag(h hash.Hash) {
io.WriteString(h, strconv.FormatInt(s.Payload.LastUpdate.UTC().UnixNano(), 10))
}
// GetLastModified implement LastModer interface.