package utils import ( "crypto/md5" "encoding/json" "fmt" "strings" "sync" "time" "github.com/gin-gonic/gin" "hubproxy/config" ) // CachedItem 通用缓存项 type CachedItem struct { Data []byte ContentType string Headers map[string]string ExpiresAt time.Time } // UniversalCache 通用缓存 type UniversalCache struct { cache sync.Map } var GlobalCache = &UniversalCache{} // Get 获取缓存项 func (c *UniversalCache) Get(key string) *CachedItem { if v, ok := c.cache.Load(key); ok { if cached := v.(*CachedItem); time.Now().Before(cached.ExpiresAt) { return cached } c.cache.Delete(key) } return nil } func (c *UniversalCache) Set(key string, data []byte, contentType string, headers map[string]string, ttl time.Duration) { c.cache.Store(key, &CachedItem{ Data: data, ContentType: contentType, Headers: headers, ExpiresAt: time.Now().Add(ttl), }) } func (c *UniversalCache) GetToken(key string) string { if item := c.Get(key); item != nil { return string(item.Data) } return "" } func (c *UniversalCache) SetToken(key, token string, ttl time.Duration) { c.Set(key, []byte(token), "application/json", nil, ttl) } // BuildCacheKey 构建稳定的缓存key func BuildCacheKey(prefix, query string) string { return fmt.Sprintf("%s:%x", prefix, md5.Sum([]byte(query))) } func BuildTokenCacheKey(query string) string { return BuildCacheKey("token", query) } func BuildManifestCacheKey(imageRef, reference string) string { key := fmt.Sprintf("%s:%s", imageRef, reference) return BuildCacheKey("manifest", key) } func GetManifestTTL(reference string) time.Duration { cfg := config.GetConfig() defaultTTL := 30 * time.Minute if cfg.TokenCache.DefaultTTL != "" { if parsed, err := time.ParseDuration(cfg.TokenCache.DefaultTTL); err == nil { defaultTTL = parsed } } if strings.HasPrefix(reference, "sha256:") { return 24 * time.Hour } if reference == "latest" || reference == "main" || reference == "master" || reference == "dev" || reference == "develop" { return 10 * time.Minute } return defaultTTL } // ExtractTTLFromResponse 从响应中智能提取TTL func ExtractTTLFromResponse(responseBody []byte) time.Duration { var tokenResp struct { ExpiresIn int `json:"expires_in"` } defaultTTL := 30 * time.Minute if json.Unmarshal(responseBody, &tokenResp) == nil && tokenResp.ExpiresIn > 0 { safeTTL := time.Duration(tokenResp.ExpiresIn-300) * time.Second if safeTTL > 5*time.Minute { return safeTTL } } return defaultTTL } func WriteTokenResponse(c *gin.Context, cachedBody string) { c.Header("Content-Type", "application/json") c.String(200, cachedBody) } func WriteCachedResponse(c *gin.Context, item *CachedItem) { if item.ContentType != "" { c.Header("Content-Type", item.ContentType) } for key, value := range item.Headers { c.Header(key, value) } c.Data(200, item.ContentType, item.Data) } // IsCacheEnabled 检查缓存是否启用 func IsCacheEnabled() bool { cfg := config.GetConfig() return cfg.TokenCache.Enabled } // IsTokenCacheEnabled 检查token缓存是否启用 func IsTokenCacheEnabled() bool { return IsCacheEnabled() } // 定期清理过期缓存 func init() { go func() { ticker := time.NewTicker(20 * time.Minute) defer ticker.Stop() for range ticker.C { now := time.Now() expiredKeys := make([]string, 0) GlobalCache.cache.Range(func(key, value interface{}) bool { if cached := value.(*CachedItem); now.After(cached.ExpiresAt) { expiredKeys = append(expiredKeys, key.(string)) } return true }) for _, key := range expiredKeys { GlobalCache.cache.Delete(key) } } }() }