mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-22 23:43:39 +00:00
* Feature: [Cache] add ExpirationGenerator for generate custom Expiration * fix: add document and code snippet for README Co-authored-by: dj <github@djunny.com>
154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
// Special thanks to @codemicro for moving this to fiber core
|
|
// Original middleware: github.com/codemicro/fiber-cache
|
|
package cache
|
|
|
|
import (
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/utils"
|
|
)
|
|
|
|
// timestampUpdatePeriod is the period which is used to check the cache expiration.
|
|
// It should not be too long to provide more or less acceptable expiration error, and in the same
|
|
// time it should not be too short to avoid overwhelming of the system
|
|
const timestampUpdatePeriod = 300 * time.Millisecond
|
|
|
|
// cache status
|
|
// unreachable: when cache is bypass, or invalid
|
|
// hit: cache is served
|
|
// miss: do not have cache record
|
|
const (
|
|
cacheUnreachable = "unreachable"
|
|
cacheHit = "hit"
|
|
cacheMiss = "miss"
|
|
)
|
|
|
|
// New creates a new middleware handler
|
|
func New(config ...Config) fiber.Handler {
|
|
// Set default config
|
|
cfg := configDefault(config...)
|
|
|
|
// Nothing to cache
|
|
if int(cfg.Expiration.Seconds()) < 0 {
|
|
return func(c *fiber.Ctx) error {
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
var (
|
|
// Cache settings
|
|
mux = &sync.RWMutex{}
|
|
timestamp = uint64(time.Now().Unix())
|
|
)
|
|
// Create manager to simplify storage operations ( see manager.go )
|
|
manager := newManager(cfg.Storage)
|
|
|
|
// Update timestamp in the configured interval
|
|
go func() {
|
|
for {
|
|
atomic.StoreUint64(×tamp, uint64(time.Now().Unix()))
|
|
time.Sleep(timestampUpdatePeriod)
|
|
}
|
|
}()
|
|
|
|
// Return new handler
|
|
return func(c *fiber.Ctx) error {
|
|
// Only cache GET methods
|
|
if c.Method() != fiber.MethodGet {
|
|
c.Set(cfg.CacheHeader, cacheUnreachable)
|
|
return c.Next()
|
|
}
|
|
|
|
// Get key from request
|
|
key := cfg.KeyGenerator(c)
|
|
|
|
// Get entry from pool
|
|
e := manager.get(key)
|
|
|
|
// Lock entry and unlock when finished
|
|
mux.Lock()
|
|
defer mux.Unlock()
|
|
|
|
// Get timestamp
|
|
ts := atomic.LoadUint64(×tamp)
|
|
|
|
if e.exp != 0 && ts >= e.exp {
|
|
// Check if entry is expired
|
|
manager.delete(key)
|
|
// External storage saves body data with different key
|
|
if cfg.Storage != nil {
|
|
manager.delete(key + "_body")
|
|
}
|
|
} else if e.exp != 0 {
|
|
// Separate body value to avoid msgp serialization
|
|
// We can store raw bytes with Storage 👍
|
|
if cfg.Storage != nil {
|
|
e.body = manager.getRaw(key + "_body")
|
|
}
|
|
// Set response headers from cache
|
|
c.Response().SetBodyRaw(e.body)
|
|
c.Response().SetStatusCode(e.status)
|
|
c.Response().Header.SetContentTypeBytes(e.ctype)
|
|
if len(e.cencoding) > 0 {
|
|
c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding)
|
|
}
|
|
// Set Cache-Control header if enabled
|
|
if cfg.CacheControl {
|
|
maxAge := strconv.FormatUint(e.exp-ts, 10)
|
|
c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge)
|
|
}
|
|
|
|
c.Set(cfg.CacheHeader, cacheHit)
|
|
|
|
// Return response
|
|
return nil
|
|
}
|
|
|
|
// Continue stack, return err to Fiber if exist
|
|
if err := c.Next(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Don't cache response if Next returns true
|
|
if cfg.Next != nil && cfg.Next(c) {
|
|
c.Set(cfg.CacheHeader, cacheUnreachable)
|
|
return nil
|
|
}
|
|
|
|
// Cache response
|
|
e.body = utils.CopyBytes(c.Response().Body())
|
|
e.status = c.Response().StatusCode()
|
|
e.ctype = utils.CopyBytes(c.Response().Header.ContentType())
|
|
e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding))
|
|
|
|
// default cache expiration
|
|
expiration := uint64(cfg.Expiration.Seconds())
|
|
// Calculate expiration by response header or other setting
|
|
if cfg.ExpirationGenerator != nil {
|
|
expiration = uint64(cfg.ExpirationGenerator(c, &cfg).Seconds())
|
|
}
|
|
e.exp = ts + expiration
|
|
|
|
// For external Storage we store raw body separated
|
|
if cfg.Storage != nil {
|
|
manager.setRaw(key+"_body", e.body, cfg.Expiration)
|
|
// avoid body msgp encoding
|
|
e.body = nil
|
|
manager.set(key, e, cfg.Expiration)
|
|
manager.release(e)
|
|
} else {
|
|
// Store entry in memory
|
|
manager.set(key, e, cfg.Expiration)
|
|
}
|
|
|
|
c.Set(cfg.CacheHeader, cacheMiss)
|
|
|
|
// Finish response
|
|
return nil
|
|
}
|
|
}
|