2020-09-26 11:13:11 +02:00
|
|
|
// Special thanks to @codemicro for moving this to fiber core
|
|
|
|
// Original middleware: github.com/codemicro/fiber-cache
|
2020-09-24 23:15:16 +02:00
|
|
|
package cache
|
|
|
|
|
|
|
|
import (
|
2020-10-01 19:37:34 +02:00
|
|
|
"strconv"
|
2020-09-24 23:15:16 +02:00
|
|
|
"sync"
|
2020-10-28 02:29:47 +01:00
|
|
|
"sync/atomic"
|
2020-09-24 23:15:16 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
2020-10-29 06:41:05 +01:00
|
|
|
"github.com/gofiber/fiber/v2/utils"
|
2020-09-24 23:15:16 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Config defines the config for middleware.
|
|
|
|
type Config struct {
|
|
|
|
// Next defines a function to skip this middleware when returned true.
|
|
|
|
//
|
|
|
|
// Optional. Default: nil
|
|
|
|
Next func(c *fiber.Ctx) bool
|
|
|
|
|
2020-10-01 16:53:27 +02:00
|
|
|
// Expiration is the time that an cached response will live
|
2020-09-24 23:15:16 +02:00
|
|
|
//
|
2020-10-08 18:11:26 +02:00
|
|
|
// Optional. Default: 1 * time.Minute
|
2020-09-24 23:15:16 +02:00
|
|
|
Expiration time.Duration
|
2020-10-01 19:37:34 +02:00
|
|
|
|
|
|
|
// CacheControl enables client side caching if set to true
|
|
|
|
//
|
|
|
|
// Optional. Default: false
|
|
|
|
CacheControl bool
|
2020-10-28 02:29:47 +01:00
|
|
|
|
2020-10-31 01:19:33 +00:00
|
|
|
// Key allows you to generate custom keys, by default c.Path() is used
|
|
|
|
//
|
|
|
|
// Default: func(c *fiber.Ctx) string {
|
|
|
|
// return c.Path()
|
|
|
|
// }
|
|
|
|
Key func(*fiber.Ctx) string
|
|
|
|
|
2020-10-28 02:29:47 +01:00
|
|
|
// Store is used to store the state of the middleware
|
|
|
|
//
|
|
|
|
// Default: an in memory store for this process only
|
|
|
|
Store fiber.Storage
|
|
|
|
|
|
|
|
// Internally used - if true, the simpler method of two maps is used in order to keep
|
|
|
|
// execution time down.
|
|
|
|
defaultStore bool
|
2020-09-24 23:15:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ConfigDefault is the default config
|
|
|
|
var ConfigDefault = Config{
|
2020-10-01 19:49:49 +02:00
|
|
|
Next: nil,
|
2020-10-08 18:11:26 +02:00
|
|
|
Expiration: 1 * time.Minute,
|
2020-10-01 19:49:49 +02:00
|
|
|
CacheControl: false,
|
2020-10-31 01:19:33 +00:00
|
|
|
Key: func(c *fiber.Ctx) string {
|
|
|
|
return c.Path()
|
|
|
|
},
|
2020-10-28 02:29:47 +01:00
|
|
|
defaultStore: true,
|
2020-09-24 23:15:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new middleware handler
|
|
|
|
func New(config ...Config) fiber.Handler {
|
|
|
|
// Set default config
|
|
|
|
cfg := ConfigDefault
|
|
|
|
|
|
|
|
// Override config if provided
|
|
|
|
if len(config) > 0 {
|
|
|
|
cfg = config[0]
|
|
|
|
|
|
|
|
// Set default values
|
|
|
|
if cfg.Next == nil {
|
|
|
|
cfg.Next = ConfigDefault.Next
|
|
|
|
}
|
|
|
|
if int(cfg.Expiration.Seconds()) == 0 {
|
|
|
|
cfg.Expiration = ConfigDefault.Expiration
|
|
|
|
}
|
2020-10-31 01:19:33 +00:00
|
|
|
if cfg.Key == nil {
|
|
|
|
cfg.Key = ConfigDefault.Key
|
|
|
|
}
|
2020-10-28 02:29:47 +01:00
|
|
|
if cfg.Store == nil {
|
|
|
|
cfg.defaultStore = true
|
|
|
|
}
|
2020-09-24 23:15:16 +02:00
|
|
|
}
|
|
|
|
|
2020-10-28 02:29:47 +01:00
|
|
|
var (
|
|
|
|
// Cache settings
|
|
|
|
timestamp = uint64(time.Now().Unix())
|
|
|
|
expiration = uint64(cfg.Expiration.Seconds())
|
|
|
|
mux = &sync.RWMutex{}
|
|
|
|
|
|
|
|
// Default store logic (if no Store is provided)
|
|
|
|
entries = make(map[string]entry)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Update timestamp every second
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
atomic.StoreUint64(×tamp, uint64(time.Now().Unix()))
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2020-10-01 16:53:27 +02:00
|
|
|
// Nothing to cache
|
|
|
|
if int(cfg.Expiration.Seconds()) < 0 {
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
|
|
return c.Next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-01 19:49:49 +02:00
|
|
|
// Remove expired entries
|
2020-10-29 06:14:02 +01:00
|
|
|
if cfg.defaultStore {
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
// GC the entries every 10 seconds
|
|
|
|
time.Sleep(10 * time.Second)
|
|
|
|
mux.Lock()
|
|
|
|
for k := range entries {
|
|
|
|
if atomic.LoadUint64(×tamp) >= entries[k].exp {
|
|
|
|
delete(entries, k)
|
|
|
|
}
|
2020-09-25 00:42:51 +02:00
|
|
|
}
|
2020-10-29 06:14:02 +01:00
|
|
|
mux.Unlock()
|
2020-09-25 00:42:51 +02:00
|
|
|
}
|
2020-10-29 06:14:02 +01:00
|
|
|
}()
|
|
|
|
}
|
2020-09-24 23:15:16 +02:00
|
|
|
|
|
|
|
// Return new handler
|
|
|
|
return func(c *fiber.Ctx) error {
|
2020-10-01 19:37:34 +02:00
|
|
|
// Don't execute middleware if Next returns true
|
2020-09-24 23:15:16 +02:00
|
|
|
if cfg.Next != nil && cfg.Next(c) {
|
|
|
|
return c.Next()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only cache GET methods
|
|
|
|
if c.Method() != fiber.MethodGet {
|
|
|
|
return c.Next()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get key from request
|
2020-10-31 01:19:33 +00:00
|
|
|
key := cfg.Key(c)
|
2020-09-24 23:15:16 +02:00
|
|
|
|
2020-10-28 02:29:47 +01:00
|
|
|
// Create new entry
|
2020-10-29 06:32:09 +01:00
|
|
|
var entry entry
|
|
|
|
var entryBody []byte
|
2020-10-28 02:29:47 +01:00
|
|
|
|
|
|
|
// Lock entry
|
|
|
|
mux.Lock()
|
|
|
|
defer mux.Unlock()
|
|
|
|
|
|
|
|
// Check if we need to use the default in-memory storage
|
|
|
|
if cfg.defaultStore {
|
|
|
|
entry = entries[key]
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// Load data from store
|
|
|
|
storeEntry, err := cfg.Store.Get(key)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only decode if we found an entry
|
2020-10-31 07:51:44 +01:00
|
|
|
if storeEntry != nil {
|
2020-10-28 02:29:47 +01:00
|
|
|
// Decode bytes using msgp
|
|
|
|
if _, err := entry.UnmarshalMsg(storeEntry); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2020-10-29 06:32:09 +01:00
|
|
|
|
|
|
|
if entryBody, err = cfg.Store.Get(key + "_body"); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-10-28 02:29:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get timestamp
|
|
|
|
ts := atomic.LoadUint64(×tamp)
|
|
|
|
|
|
|
|
// Set expiration if entry does not exist
|
|
|
|
if entry.exp == 0 {
|
|
|
|
entry.exp = ts + expiration
|
|
|
|
|
|
|
|
} else if ts >= entry.exp {
|
2020-10-01 16:53:27 +02:00
|
|
|
// Check if entry is expired
|
2020-10-28 02:29:47 +01:00
|
|
|
// Use default memory storage
|
|
|
|
if cfg.defaultStore {
|
|
|
|
delete(entries, key)
|
|
|
|
} else { // Use custom storage
|
|
|
|
if err := cfg.Store.Delete(key); err != nil {
|
|
|
|
return err
|
2020-10-01 19:37:34 +02:00
|
|
|
}
|
2020-10-29 06:32:09 +01:00
|
|
|
if err := cfg.Store.Delete(key + "_body"); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-10-01 16:53:27 +02:00
|
|
|
}
|
2020-10-28 02:29:47 +01:00
|
|
|
|
|
|
|
} else {
|
2020-10-29 06:38:41 +01:00
|
|
|
if cfg.defaultStore {
|
|
|
|
c.Response().SetBodyRaw(entry.body)
|
|
|
|
} else {
|
|
|
|
c.Response().SetBodyRaw(entryBody)
|
|
|
|
}
|
2020-10-28 02:29:47 +01:00
|
|
|
// Set response headers from cache
|
|
|
|
c.Response().SetStatusCode(entry.status)
|
|
|
|
c.Response().Header.SetContentTypeBytes(entry.cType)
|
|
|
|
|
|
|
|
// Set Cache-Control header if enabled
|
|
|
|
if cfg.CacheControl {
|
|
|
|
maxAge := strconv.FormatUint(entry.exp-ts, 10)
|
|
|
|
c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return response
|
|
|
|
return nil
|
2020-09-24 23:15:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Continue stack, return err to Fiber if exist
|
|
|
|
if err := c.Next(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache response
|
2020-10-29 06:41:05 +01:00
|
|
|
entryBody = utils.SafeBytes(c.Response().Body())
|
2020-10-28 02:29:47 +01:00
|
|
|
entry.status = c.Response().StatusCode()
|
2020-10-29 06:43:59 +01:00
|
|
|
entry.cType = utils.SafeBytes(c.Response().Header.ContentType())
|
2020-10-28 02:29:47 +01:00
|
|
|
|
|
|
|
// Use default memory storage
|
|
|
|
if cfg.defaultStore {
|
2020-10-29 06:32:09 +01:00
|
|
|
entry.body = entryBody
|
2020-10-28 02:29:47 +01:00
|
|
|
entries[key] = entry
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// Use custom storage
|
|
|
|
data, err := entry.MarshalMsg(nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pass bytes to Storage
|
|
|
|
if err = cfg.Store.Set(key, data, cfg.Expiration); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-10-29 06:32:09 +01:00
|
|
|
|
|
|
|
// Pass bytes to Storage
|
|
|
|
if err = cfg.Store.Set(key+"_body", entryBody, cfg.Expiration); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-09-24 23:15:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Finish response
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|