package middleware import ( "net/http" "path/filepath" "strings" "github.com/gin-gonic/gin" ) // AssetCacheControl sets optimal Cache-Control headers for static assets served by this server. // It only affects GET requests for well-known static prefixes and does not override // cache headers explicitly set by handlers downstream (they can still modify after c.Next if needed). func AssetCacheControl() gin.HandlerFunc { return func(c *gin.Context) { // Only apply to GET requests if c.Request.Method == http.MethodGet { p := c.Request.URL.Path lower := strings.ToLower(p) switch { // Specific: YouTube channel static cache can be cached longer (content changes infrequently) case strings.HasPrefix(lower, "/cache/prefetch/") && strings.HasSuffix(lower, "youtube_channel.json"): c.Header("Cache-Control", "public, max-age=3600") // 1 hour case strings.HasPrefix(lower, "/dist/"): // Fingerprinted build assets should be cached for a year and immutable c.Header("Cache-Control", "public, max-age=31536000, immutable") case strings.HasPrefix(lower, "/uploads/"): // User uploads: cache for a week; allow clients to revalidate if replaced // Heuristic: if file name appears fingerprinted (e.g., ..ext), use longer cache base := filepath.Base(lower) if looksFingerprinted(base) { c.Header("Cache-Control", "public, max-age=31536000, immutable") } else { c.Header("Cache-Control", "public, max-age=604800") // 7 days } case strings.HasPrefix(lower, "/cache/"): // Prefetched JSON and other generated cache files: short to medium cache c.Header("Cache-Control", "public, max-age=300") // 5 minutes } } c.Next() } } // looksFingerprinted checks if a filename contains a long hex-like segment before the extension // e.g. logo.7eacd9f0bfa04928a9b6936140168f58.png func looksFingerprinted(name string) bool { dot := strings.LastIndexByte(name, '.') if dot <= 0 || dot >= len(name)-1 { return false } core := name[:dot] // Find final segment after last dot/underscore/hyphen in core lastSep := strings.LastIndexAny(core, "._-") if lastSep < 0 || lastSep+1 >= len(core) { return false } seg := core[lastSep+1:] if len(seg) < 16 { // require at least 16 chars to treat as a hash return false } // hex-like check for i := 0; i < len(seg); i++ { ch := seg[i] if !((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f')) { return false } } return true }