GoBlog/cache.go

168 lines
4.4 KiB
Go
Raw Normal View History

2020-07-29 20:45:26 +00:00
package main
import (
2020-11-25 13:59:48 +00:00
"bytes"
"crypto/sha256"
2020-11-20 14:33:20 +00:00
"fmt"
2020-11-25 13:59:48 +00:00
"io"
"io/ioutil"
2020-07-29 20:45:26 +00:00
"net/http"
"net/http/httptest"
2020-11-20 14:33:20 +00:00
"strconv"
2020-07-29 20:45:26 +00:00
"time"
2020-11-26 08:44:44 +00:00
"github.com/araddon/dateparse"
2020-11-20 14:33:20 +00:00
lru "github.com/hashicorp/golang-lru"
"golang.org/x/sync/singleflight"
2020-07-29 20:45:26 +00:00
)
2020-11-20 14:33:20 +00:00
const (
cacheInternalExpirationHeader = "GoBlog-Expire"
)
var (
cacheGroup singleflight.Group
2020-11-20 14:33:20 +00:00
cacheLru *lru.Cache
)
2020-09-22 14:42:36 +00:00
2020-11-20 14:33:20 +00:00
func initCache() (err error) {
2020-11-20 15:24:18 +00:00
cacheLru, err = lru.New(500)
2020-11-20 14:33:20 +00:00
return
}
2020-09-22 14:42:36 +00:00
2020-07-30 19:18:13 +00:00
func cacheMiddleware(next http.Handler) http.Handler {
2020-07-29 20:45:26 +00:00
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2020-08-04 17:42:09 +00:00
if appConfig.Cache.Enable &&
// check method
2020-10-20 16:15:15 +00:00
(r.Method == http.MethodGet || r.Method == http.MethodHead) &&
// check bypass query
2020-11-20 14:33:20 +00:00
!(r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false") {
key := cacheKey(r)
// Get cache or render it
cacheInterface, _, _ := cacheGroup.Do(key, func() (interface{}, error) {
return getCache(key, next, r), nil
})
cache := cacheInterface.(*cacheItem)
2020-07-30 19:18:13 +00:00
// copy cached headers
for k, v := range cache.header {
2020-07-30 19:18:13 +00:00
w.Header()[k] = v
}
2020-11-26 08:44:44 +00:00
setCacheHeaders(w, cache)
2020-11-25 13:59:48 +00:00
// check conditional request
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == cache.eTag {
2020-11-25 13:59:48 +00:00
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
2020-11-26 08:44:44 +00:00
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
t, err := dateparse.ParseAny(ifModifiedSinceHeader)
2021-01-10 13:22:02 +00:00
if err == nil && t.After(cache.creationTime) {
2020-11-26 08:44:44 +00:00
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
}
// set status code
w.WriteHeader(cache.code)
2020-07-30 19:18:13 +00:00
// write cached body
_, _ = w.Write(cache.body)
2020-07-30 19:08:41 +00:00
return
2020-07-29 20:45:26 +00:00
}
2020-07-30 19:18:13 +00:00
next.ServeHTTP(w, r)
return
2020-07-29 20:45:26 +00:00
})
}
func cacheKey(r *http.Request) string {
2020-10-20 16:15:15 +00:00
return r.URL.String()
}
2020-11-26 08:44:44 +00:00
func setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
w.Header().Set("ETag", cache.eTag)
2021-01-10 13:22:02 +00:00
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
2020-11-26 16:40:40 +00:00
if w.Header().Get("Cache-Control") == "" {
if cache.expiration != 0 {
2021-01-10 13:22:02 +00:00
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration))
2020-11-26 16:40:40 +00:00
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", appConfig.Cache.Expiration, appConfig.Cache.Expiration/3, appConfig.Cache.Expiration))
}
2020-11-20 14:33:20 +00:00
}
2020-07-30 19:08:41 +00:00
}
type cacheItem struct {
2020-11-20 14:33:20 +00:00
expiration int
2021-01-10 13:22:02 +00:00
creationTime time.Time
eTag string
code int
header http.Header
body []byte
2020-10-07 15:35:52 +00:00
}
func (c *cacheItem) expired() bool {
2020-11-20 14:33:20 +00:00
if c.expiration != 0 {
2021-01-10 13:22:02 +00:00
return time.Now().After(c.creationTime.Add(time.Duration(c.expiration) * time.Second))
2020-11-20 14:33:20 +00:00
}
return false
}
2020-11-20 14:33:20 +00:00
func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
if lruItem, ok := cacheLru.Get(key); ok {
item = lruItem.(*cacheItem)
}
if item == nil || item.expired() {
// No cache available
2021-01-10 14:45:34 +00:00
// Remove problematic headers
r.Header.Del("If-Modified-Since")
r.Header.Del("If-Unmodified-Since")
r.Header.Del("If-None-Match")
r.Header.Del("If-Match")
r.Header.Del("If-Range")
r.Header.Del("Range")
// Record request
recorder := httptest.NewRecorder()
next.ServeHTTP(recorder, r)
// Cache values from recorder
result := recorder.Result()
body, _ := ioutil.ReadAll(result.Body)
2020-11-25 13:59:48 +00:00
_ = result.Body.Close()
eTag := result.Header.Get("ETag")
if eTag == "" {
h := sha256.New()
_, _ = io.Copy(h, bytes.NewReader(body))
eTag = fmt.Sprintf("%x", h.Sum(nil))
}
2021-01-10 14:45:34 +00:00
lastMod := time.Now()
if lm := result.Header.Get("Last-Modified"); lm != "" {
2021-01-10 15:11:53 +00:00
if parsedTime, te := dateparse.ParseLocal(lm); te == nil {
lastMod = parsedTime
}
2021-01-10 14:45:34 +00:00
}
exp, _ := strconv.Atoi(result.Header.Get(cacheInternalExpirationHeader))
// Remove problematic headers
result.Header.Del(cacheInternalExpirationHeader)
result.Header.Del("Accept-Ranges")
result.Header.Del("ETag")
result.Header.Del("Last-Modified")
// Create cache item
2020-10-20 16:15:15 +00:00
item = &cacheItem{
2020-11-20 14:33:20 +00:00
expiration: exp,
2021-01-10 14:45:34 +00:00
creationTime: lastMod,
eTag: eTag,
code: result.StatusCode,
header: result.Header,
body: body,
2020-10-20 16:15:15 +00:00
}
// Save cache
2020-11-20 14:33:20 +00:00
cacheLru.Add(key, item)
}
return item
2020-07-29 20:45:26 +00:00
}
2020-10-06 17:07:48 +00:00
func purgeCache() {
2020-11-20 14:33:20 +00:00
cacheLru.Purge()
}
func setInternalCacheExpirationHeader(w http.ResponseWriter, expiration int) {
w.Header().Set(cacheInternalExpirationHeader, strconv.Itoa(expiration))
}