182 lines
4.8 KiB
Go
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()
|
|
}
|
|
}
|