Files
MyClub/internal/middleware/asset_cache.go
T
Tomas Dvorak b9cea0cd77 dev day #79
2025-11-02 01:04:02 +01:00

71 lines
2.8 KiB
Go

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., .<hash>.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
}