forked from gitsight/go-echo-cache
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cache.go
150 lines (121 loc) · 3.16 KB
/
cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package cache
import (
"fmt"
"net/http"
"time"
"github.com/coocood/freecache"
"github.com/labstack/echo/v4"
"github.com/mcuadros/go-defaults"
)
// Config defiens the configuration for a cache middleware.
type Config struct {
// TTL time to life of the cache.
TTL time.Duration `default:"1m"`
// Methods methods to be cached.
Methods []string `default:"[GET]"`
// StatusCode method to be cached.
StatusCode []int `default:"[200,404]"`
// IgnoreQuery if true the Query values from the requests are ignored on
// the key generation.
IgnoreQuery bool
// Refresh fuction called before use the cache, if true, the cache is deleted.
Refresh func(r *http.Request) bool
// Cache fuction called before cache a request, if false, the request is not
// cached. If set Method is ignored.
Cache func(r *http.Request) bool
}
func New(cfg *Config, cache *freecache.Cache) echo.MiddlewareFunc {
if cfg == nil {
cfg = &Config{}
}
defaults.SetDefaults(cfg)
m := &CacheMiddleware{cfg: cfg, cache: cache}
return m.Handler
}
type CacheMiddleware struct {
cfg *Config
cache *freecache.Cache
}
func (m *CacheMiddleware) Handler(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !m.isCacheable(c.Request()) {
return next(c)
}
if mayHasBody(c.Request().Method) {
c.Logger().Warnf("request with body are cached ignoring the content")
}
key := m.getKey(c.Request())
err := m.readCache(key, c)
if err == nil {
return nil
}
if err != freecache.ErrNotFound {
c.Logger().Errorf("error reading cache: %s", err)
}
recorder := NewResponseRecorder(c.Response().Writer)
c.Response().Writer = recorder
err = next(c)
if err := m.cacheResult(key, recorder); err != nil {
c.Logger().Error(err)
}
return err
}
}
func (m *CacheMiddleware) readCache(key []byte, c echo.Context) error {
if m.cfg.Refresh != nil && m.cfg.Refresh(c.Request()) {
return freecache.ErrNotFound
}
value, err := m.cache.Get(key)
if err != nil {
return err
}
entry := &CacheEntry{}
if err := entry.Decode(value); err != nil {
return err
}
return entry.Replay(c.Response())
}
func (m *CacheMiddleware) cacheResult(key []byte, r *ResponseRecorder) error {
e := r.Result()
b, err := e.Encode()
if err != nil {
return fmt.Errorf("unable to read recorded response: %s", err)
}
if !m.isStatusCacheable(e) {
return nil
}
return m.cache.Set(key, b, int(m.cfg.TTL.Seconds()))
}
func (m *CacheMiddleware) isStatusCacheable(e *CacheEntry) bool {
for _, status := range m.cfg.StatusCode {
if e.StatusCode == status {
return true
}
}
return false
}
func (m *CacheMiddleware) isCacheable(r *http.Request) bool {
if m.cfg.Cache != nil {
return m.cfg.Cache(r)
}
for _, method := range m.cfg.Methods {
if r.Method == method {
return true
}
}
return false
}
func (m *CacheMiddleware) getKey(r *http.Request) []byte {
base := r.Method + "|" + r.URL.Path
if !m.cfg.IgnoreQuery {
base += "|" + r.URL.Query().Encode()
}
return []byte(base)
}
func mayHasBody(method string) bool {
m := method
if m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete || m == http.MethodPatch {
return true
}
return false
}