1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-23 19:23:48 +00:00

251 lines
5.3 KiB
Go
Raw Normal View History

// 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 (
"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
// 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
// CacheControl enables client side caching if set to true
//
// Optional. Default: false
CacheControl bool
2020-10-28 02:29:47 +01: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{
Next: nil,
2020-10-08 18:11:26 +02:00
Expiration: 1 * time.Minute,
CacheControl: false,
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
}
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(&timestamp, uint64(time.Now().Unix()))
time.Sleep(1 * time.Second)
}
}()
// Nothing to cache
if int(cfg.Expiration.Seconds()) < 0 {
return func(c *fiber.Ctx) error {
return c.Next()
}
}
// 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(&timestamp) >= 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 {
// 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
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(&timestamp)
// Set expiration if entry does not exist
if entry.exp == 0 {
entry.exp = ts + expiration
} else if ts >= entry.exp {
// 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-29 06:32:09 +01:00
if err := cfg.Store.Delete(key + "_body"); err != nil {
return err
}
}
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()
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
}
}