Files
calypso/backend/internal/common/router/cache.go
2025-12-26 17:47:20 +00:00

182 lines
4.8 KiB
Go

package router
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
"github.com/atlasos/calypso/internal/common/cache"
"github.com/gin-gonic/gin"
)
// GenerateKey generates a cache key from parts (local helper)
func GenerateKey(prefix string, parts ...string) string {
key := prefix
for _, part := range parts {
key += ":" + part
}
// Hash long keys to keep them manageable
if len(key) > 200 {
hash := sha256.Sum256([]byte(key))
return prefix + ":" + hex.EncodeToString(hash[:])
}
return key
}
// CacheConfig holds cache configuration
type CacheConfig struct {
Enabled bool
DefaultTTL time.Duration
MaxAge int // seconds for Cache-Control header
}
// cacheMiddleware creates a caching middleware
func cacheMiddleware(cfg CacheConfig, cache *cache.Cache) gin.HandlerFunc {
if !cfg.Enabled || cache == nil {
return func(c *gin.Context) {
c.Next()
}
}
return func(c *gin.Context) {
// Only cache GET requests
if c.Request.Method != http.MethodGet {
c.Next()
return
}
// Don't cache VTL endpoints - they change frequently
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api/v1/tape/vtl/") {
c.Next()
return
}
// Generate cache key from request path and query string
keyParts := []string{c.Request.URL.Path}
if c.Request.URL.RawQuery != "" {
keyParts = append(keyParts, c.Request.URL.RawQuery)
}
cacheKey := GenerateKey("http", keyParts...)
// Try to get from cache
if cached, found := cache.Get(cacheKey); found {
if cachedResponse, ok := cached.([]byte); ok {
// Set cache headers
if cfg.MaxAge > 0 {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", cfg.MaxAge))
c.Header("X-Cache", "HIT")
}
c.Data(http.StatusOK, "application/json", cachedResponse)
c.Abort()
return
}
}
// Cache miss - capture response
writer := &responseWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = writer
c.Next()
// Only cache successful responses
if writer.Status() == http.StatusOK {
// Cache the response body
responseBody := writer.body.Bytes()
cache.Set(cacheKey, responseBody)
// Set cache headers
if cfg.MaxAge > 0 {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", cfg.MaxAge))
c.Header("X-Cache", "MISS")
}
}
}
}
// responseWriter wraps gin.ResponseWriter to capture response body
type responseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w *responseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func (w *responseWriter) WriteString(s string) (int, error) {
w.body.WriteString(s)
return w.ResponseWriter.WriteString(s)
}
// cacheControlMiddleware adds Cache-Control headers based on endpoint
func cacheControlMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
// Set appropriate cache control for different endpoints
switch {
case path == "/api/v1/health":
// Health check can be cached for a short time
c.Header("Cache-Control", "public, max-age=30")
case path == "/api/v1/monitoring/metrics":
// Metrics can be cached for a short time
c.Header("Cache-Control", "public, max-age=60")
case path == "/api/v1/monitoring/alerts":
// Alerts should have minimal caching
c.Header("Cache-Control", "public, max-age=10")
case path == "/api/v1/storage/disks":
// Disk list can be cached for a moderate time
c.Header("Cache-Control", "public, max-age=300")
case path == "/api/v1/storage/repositories":
// Repositories can be cached
c.Header("Cache-Control", "public, max-age=180")
case path == "/api/v1/system/services":
// Service list can be cached briefly
c.Header("Cache-Control", "public, max-age=60")
case strings.HasPrefix(path, "/api/v1/storage/zfs/pools"):
// ZFS pools and datasets should not be cached - they change frequently
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
default:
// Default: no cache for other endpoints
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
}
c.Next()
}
}
// InvalidateCacheKey invalidates a specific cache key
func InvalidateCacheKey(cache *cache.Cache, key string) {
if cache != nil {
cache.Delete(key)
}
}
// InvalidateCachePattern invalidates all cache keys matching a pattern
func InvalidateCachePattern(cache *cache.Cache, pattern string) {
if cache == nil {
return
}
// Get all keys and delete matching ones
// Note: This is a simple implementation. For production, consider using
// a cache library that supports pattern matching (like Redis)
stats := cache.Stats()
if total, ok := stats["total_entries"].(int); ok && total > 0 {
// For now, we'll clear the entire cache if pattern matching is needed
// In production, use Redis with pattern matching
cache.Clear()
}
}