cache feed icons

This commit is contained in:
Nazar Kanaev 2021-05-27 13:16:03 +01:00
parent 214c7aacfc
commit f38dcfba3b
3 changed files with 85 additions and 8 deletions

View File

@ -1,12 +1,15 @@
package server package server
import ( import (
"crypto/md5"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"math" "math"
"net/http" "net/http"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strconv"
"strings" "strings"
"github.com/nkanaev/yarr/src/assets" "github.com/nkanaev/yarr/src/assets"
@ -143,20 +146,51 @@ func (s *Server) handleFeedErrors(c *router.Context) {
c.JSON(http.StatusOK, errors) c.JSON(http.StatusOK, errors)
} }
type feedicon struct {
ctype string
bytes []byte
etag string
}
func (s *Server) handleFeedIcon(c *router.Context) { func (s *Server) handleFeedIcon(c *router.Context) {
// TODO: caching
id, err := c.VarInt64("id") id, err := c.VarInt64("id")
if err != nil { if err != nil {
c.Out.WriteHeader(http.StatusBadRequest) c.Out.WriteHeader(http.StatusBadRequest)
return return
} }
cachekey := "icon:" + strconv.FormatInt(id, 10)
cachedat := s.cache[cachekey]
if cachedat == nil {
feed := s.db.GetFeed(id) feed := s.db.GetFeed(id)
if feed != nil && feed.Icon != nil { 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) 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) { func (s *Server) handleFeedList(c *router.Context) {

View File

@ -1,10 +1,13 @@
package server package server
import ( import (
"fmt"
"io" "io"
"log" "log"
"net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"reflect"
"testing" "testing"
"github.com/nkanaev/yarr/src/storage" "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")) 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)
}
}

View File

@ -12,6 +12,7 @@ type Server struct {
Addr string Addr string
db *storage.Storage db *storage.Storage
worker *worker.Worker worker *worker.Worker
cache map[string]interface{}
BasePath string BasePath string
@ -28,6 +29,7 @@ func NewServer(db *storage.Storage, addr string) *Server {
db: db, db: db,
Addr: addr, Addr: addr,
worker: worker.NewWorker(db), worker: worker.NewWorker(db),
cache: make(map[string]interface{}),
} }
} }