From f38dcfba3b5a6298da23327ef3fc903a1648d590 Mon Sep 17 00:00:00 2001 From: Nazar Kanaev Date: Thu, 27 May 2021 13:16:03 +0100 Subject: [PATCH] cache feed icons --- src/server/routes.go | 50 ++++++++++++++++++++++++++++++++------- src/server/routes_test.go | 41 ++++++++++++++++++++++++++++++++ src/server/server.go | 2 ++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/server/routes.go b/src/server/routes.go index a55a8b3..4529dc6 100644 --- a/src/server/routes.go +++ b/src/server/routes.go @@ -1,12 +1,15 @@ package server import ( + "crypto/md5" "encoding/json" + "fmt" "log" "math" "net/http" "path/filepath" "reflect" + "strconv" "strings" "github.com/nkanaev/yarr/src/assets" @@ -143,20 +146,51 @@ func (s *Server) handleFeedErrors(c *router.Context) { c.JSON(http.StatusOK, errors) } +type feedicon struct { + ctype string + bytes []byte + etag string +} + func (s *Server) handleFeedIcon(c *router.Context) { - // TODO: caching id, err := c.VarInt64("id") if err != nil { c.Out.WriteHeader(http.StatusBadRequest) return } - feed := s.db.GetFeed(id) - if feed != nil && feed.Icon != nil { - c.Out.Header().Set("Content-Type", http.DetectContentType(*feed.Icon)) - c.Out.Write(*feed.Icon) - } else { - c.Out.WriteHeader(http.StatusNotFound) + + cachekey := "icon:" + strconv.FormatInt(id, 10) + cachedat := s.cache[cachekey] + if cachedat == nil { + feed := s.db.GetFeed(id) + if feed == nil || feed.Icon == nil { + c.Out.WriteHeader(http.StatusNotFound) + return + } + + hash := md5.New() + hash.Write(*feed.Icon) + + etag := fmt.Sprintf("%x", hash.Sum(nil))[:16] + + cachedat = feedicon{ + ctype: http.DetectContentType(*feed.Icon), + bytes: *(*feed).Icon, + etag: etag, + } + s.cache[cachekey] = cachedat } + + icon := cachedat.(feedicon) + + if c.Req.Header.Get("If-None-Match") == icon.etag { + c.Out.WriteHeader(http.StatusNotModified) + return + } + + c.Out.Header().Set("Content-Type", icon.ctype) + c.Out.Header().Set("Etag", icon.etag) + c.Out.Write(icon.bytes) } func (s *Server) handleFeedList(c *router.Context) { @@ -191,7 +225,7 @@ func (s *Server) handleFeedList(c *router.Context) { c.JSON(http.StatusOK, map[string]interface{}{ "status": "success", - "feed": feed, + "feed": feed, }) default: c.JSON(http.StatusOK, map[string]string{"status": "notfound"}) diff --git a/src/server/routes_test.go b/src/server/routes_test.go index 053d2d7..cb53bb7 100644 --- a/src/server/routes_test.go +++ b/src/server/routes_test.go @@ -1,10 +1,13 @@ package server import ( + "fmt" "io" "log" + "net/http" "net/http/httptest" "os" + "reflect" "testing" "github.com/nkanaev/yarr/src/storage" @@ -71,3 +74,41 @@ func TestIndexGzipped(t *testing.T) { t.Errorf("invalid content-type header: %#v", response.Header.Get("content-type")) } } + +func TestFeedIcons(t *testing.T) { + log.SetOutput(io.Discard) + db, _ := storage.New(":memory:") + icon := []byte("test") + feed := db.CreateFeed("", "", "", "", nil) + db.UpdateFeedIcon(feed.Id, &icon) + log.SetOutput(os.Stderr) + + recorder := httptest.NewRecorder() + url := fmt.Sprintf("/api/feeds/%d/icon", feed.Id) + request := httptest.NewRequest("GET", url, nil) + + handler := NewServer(db, "127.0.0.1:8000").handler() + handler.ServeHTTP(recorder, request) + response := recorder.Result() + + if response.StatusCode != http.StatusOK { + t.Fatal() + } + body, _ := io.ReadAll(response.Body) + if !reflect.DeepEqual(body, icon) { + t.Fatal() + } + if response.Header.Get("Etag") == "" { + t.Fatal() + } + + recorder2 := httptest.NewRecorder() + request2 := httptest.NewRequest("GET", url, nil) + request2.Header.Set("If-None-Match", response.Header.Get("Etag")) + handler.ServeHTTP(recorder2, request2) + response2 := recorder2.Result() + + if response2.StatusCode != http.StatusNotModified { + t.Fatal("got", response2.StatusCode) + } +} diff --git a/src/server/server.go b/src/server/server.go index 0946b01..7aeebac 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -12,6 +12,7 @@ type Server struct { Addr string db *storage.Storage worker *worker.Worker + cache map[string]interface{} BasePath string @@ -28,6 +29,7 @@ func NewServer(db *storage.Storage, addr string) *Server { db: db, Addr: addr, worker: worker.NewWorker(db), + cache: make(map[string]interface{}), } }