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() } }