mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 16:41:54 +08:00
1050 lines
27 KiB
Go
1050 lines
27 KiB
Go
package cache
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"hash/fnv"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"proxy-go/internal/config"
|
||
"proxy-go/internal/utils"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
// 内存池用于复用缓冲区
|
||
var (
|
||
bufferPool = sync.Pool{
|
||
New: func() interface{} {
|
||
return make([]byte, 32*1024) // 32KB 缓冲区
|
||
},
|
||
}
|
||
|
||
// 大缓冲区池(用于大文件)
|
||
largeBufPool = sync.Pool{
|
||
New: func() interface{} {
|
||
return make([]byte, 1024*1024) // 1MB 缓冲区
|
||
},
|
||
}
|
||
)
|
||
|
||
// GetBuffer 从池中获取缓冲区
|
||
func GetBuffer(size int) []byte {
|
||
if size <= 32*1024 {
|
||
buf := bufferPool.Get().([]byte)
|
||
if cap(buf) >= size {
|
||
return buf[:size]
|
||
}
|
||
bufferPool.Put(buf)
|
||
} else if size <= 1024*1024 {
|
||
buf := largeBufPool.Get().([]byte)
|
||
if cap(buf) >= size {
|
||
return buf[:size]
|
||
}
|
||
largeBufPool.Put(buf)
|
||
}
|
||
// 如果池中的缓冲区不够大,创建新的
|
||
return make([]byte, size)
|
||
}
|
||
|
||
// PutBuffer 将缓冲区放回池中
|
||
func PutBuffer(buf []byte) {
|
||
if cap(buf) == 32*1024 {
|
||
bufferPool.Put(buf)
|
||
} else if cap(buf) == 1024*1024 {
|
||
largeBufPool.Put(buf)
|
||
}
|
||
// 其他大小的缓冲区让GC处理
|
||
}
|
||
|
||
// LRU 缓存节点
|
||
type LRUNode struct {
|
||
key CacheKey
|
||
value *CacheItem
|
||
prev *LRUNode
|
||
next *LRUNode
|
||
}
|
||
|
||
// LRU 缓存实现
|
||
type LRUCache struct {
|
||
capacity int
|
||
size int
|
||
head *LRUNode
|
||
tail *LRUNode
|
||
cache map[CacheKey]*LRUNode
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// NewLRUCache 创建LRU缓存
|
||
func NewLRUCache(capacity int) *LRUCache {
|
||
lru := &LRUCache{
|
||
capacity: capacity,
|
||
cache: make(map[CacheKey]*LRUNode),
|
||
head: &LRUNode{},
|
||
tail: &LRUNode{},
|
||
}
|
||
lru.head.next = lru.tail
|
||
lru.tail.prev = lru.head
|
||
return lru
|
||
}
|
||
|
||
// Get 从LRU缓存中获取
|
||
func (lru *LRUCache) Get(key CacheKey) (*CacheItem, bool) {
|
||
lru.mu.Lock()
|
||
defer lru.mu.Unlock()
|
||
|
||
if node, exists := lru.cache[key]; exists {
|
||
lru.moveToHead(node)
|
||
return node.value, true
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
// Put 向LRU缓存中添加
|
||
func (lru *LRUCache) Put(key CacheKey, value *CacheItem) {
|
||
lru.mu.Lock()
|
||
defer lru.mu.Unlock()
|
||
|
||
if node, exists := lru.cache[key]; exists {
|
||
node.value = value
|
||
lru.moveToHead(node)
|
||
} else {
|
||
newNode := &LRUNode{key: key, value: value}
|
||
lru.cache[key] = newNode
|
||
lru.addToHead(newNode)
|
||
lru.size++
|
||
|
||
if lru.size > lru.capacity {
|
||
tail := lru.removeTail()
|
||
delete(lru.cache, tail.key)
|
||
lru.size--
|
||
}
|
||
}
|
||
}
|
||
|
||
// Delete 从LRU缓存中删除
|
||
func (lru *LRUCache) Delete(key CacheKey) {
|
||
lru.mu.Lock()
|
||
defer lru.mu.Unlock()
|
||
|
||
if node, exists := lru.cache[key]; exists {
|
||
lru.removeNode(node)
|
||
delete(lru.cache, key)
|
||
lru.size--
|
||
}
|
||
}
|
||
|
||
// moveToHead 将节点移到头部
|
||
func (lru *LRUCache) moveToHead(node *LRUNode) {
|
||
lru.removeNode(node)
|
||
lru.addToHead(node)
|
||
}
|
||
|
||
// addToHead 添加到头部
|
||
func (lru *LRUCache) addToHead(node *LRUNode) {
|
||
node.prev = lru.head
|
||
node.next = lru.head.next
|
||
lru.head.next.prev = node
|
||
lru.head.next = node
|
||
}
|
||
|
||
// removeNode 移除节点
|
||
func (lru *LRUCache) removeNode(node *LRUNode) {
|
||
node.prev.next = node.next
|
||
node.next.prev = node.prev
|
||
}
|
||
|
||
// removeTail 移除尾部节点
|
||
func (lru *LRUCache) removeTail() *LRUNode {
|
||
lastNode := lru.tail.prev
|
||
lru.removeNode(lastNode)
|
||
return lastNode
|
||
}
|
||
|
||
// Range 遍历所有缓存项
|
||
func (lru *LRUCache) Range(fn func(key CacheKey, value *CacheItem) bool) {
|
||
lru.mu.RLock()
|
||
defer lru.mu.RUnlock()
|
||
|
||
for key, node := range lru.cache {
|
||
if !fn(key, node.value) {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// Size 返回缓存大小
|
||
func (lru *LRUCache) Size() int {
|
||
lru.mu.RLock()
|
||
defer lru.mu.RUnlock()
|
||
return lru.size
|
||
}
|
||
|
||
// CacheKey 用于标识缓存项的唯一键
|
||
type CacheKey struct {
|
||
URL string
|
||
AcceptHeaders string
|
||
UserAgent string
|
||
}
|
||
|
||
// String 实现 Stringer 接口,用于生成唯一的字符串表示
|
||
func (k CacheKey) String() string {
|
||
return fmt.Sprintf("%s|%s|%s", k.URL, k.AcceptHeaders, k.UserAgent)
|
||
}
|
||
|
||
// Equal 比较两个 CacheKey 是否相等
|
||
func (k CacheKey) Equal(other CacheKey) bool {
|
||
return k.URL == other.URL &&
|
||
k.AcceptHeaders == other.AcceptHeaders &&
|
||
k.UserAgent == other.UserAgent
|
||
}
|
||
|
||
// Hash 生成 CacheKey 的哈希值
|
||
func (k CacheKey) Hash() uint64 {
|
||
h := fnv.New64a()
|
||
h.Write([]byte(k.String()))
|
||
return h.Sum64()
|
||
}
|
||
|
||
// CacheItem 表示一个缓存项
|
||
type CacheItem struct {
|
||
FilePath string
|
||
ContentType string
|
||
ContentEncoding string
|
||
Size int64
|
||
LastAccess time.Time
|
||
Hash string
|
||
CreatedAt time.Time
|
||
AccessCount int64
|
||
Priority int // 缓存优先级
|
||
}
|
||
|
||
// CacheStats 缓存统计信息
|
||
type CacheStats struct {
|
||
TotalItems int `json:"total_items"` // 缓存项数量
|
||
TotalSize int64 `json:"total_size"` // 总大小
|
||
HitCount int64 `json:"hit_count"` // 命中次数
|
||
MissCount int64 `json:"miss_count"` // 未命中次数
|
||
HitRate float64 `json:"hit_rate"` // 命中率
|
||
BytesSaved int64 `json:"bytes_saved"` // 节省的带宽
|
||
Enabled bool `json:"enabled"` // 缓存开关状态
|
||
FormatFallbackHit int64 `json:"format_fallback_hit"` // 格式回退命中次数
|
||
ImageCacheHit int64 `json:"image_cache_hit"` // 图片缓存命中次数
|
||
RegularCacheHit int64 `json:"regular_cache_hit"` // 常规缓存命中次数
|
||
}
|
||
|
||
// CacheManager 缓存管理器
|
||
type CacheManager struct {
|
||
cacheDir string
|
||
items sync.Map // 保持原有的 sync.Map 用于文件缓存
|
||
lruCache *LRUCache // 新增LRU缓存用于热点数据
|
||
maxAge time.Duration
|
||
cleanupTick time.Duration
|
||
maxCacheSize int64
|
||
enabled atomic.Bool // 缓存开关
|
||
hitCount atomic.Int64 // 命中计数
|
||
missCount atomic.Int64 // 未命中计数
|
||
bytesSaved atomic.Int64 // 节省的带宽
|
||
cleanupTimer *time.Ticker // 添加清理定时器
|
||
stopCleanup chan struct{} // 添加停止信号通道
|
||
|
||
// 新增:格式回退统计
|
||
formatFallbackHit atomic.Int64 // 格式回退命中次数
|
||
imageCacheHit atomic.Int64 // 图片缓存命中次数
|
||
regularCacheHit atomic.Int64 // 常规缓存命中次数
|
||
|
||
// ExtensionMatcher缓存
|
||
extensionMatcherCache *ExtensionMatcherCache
|
||
}
|
||
|
||
// NewCacheManager 创建新的缓存管理器
|
||
func NewCacheManager(cacheDir string) (*CacheManager, error) {
|
||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||
return nil, fmt.Errorf("failed to create cache directory: %v", err)
|
||
}
|
||
|
||
cm := &CacheManager{
|
||
cacheDir: cacheDir,
|
||
lruCache: NewLRUCache(10000), // 10000个热点缓存项
|
||
maxAge: 30 * time.Minute,
|
||
cleanupTick: 5 * time.Minute,
|
||
maxCacheSize: 10 * 1024 * 1024 * 1024, // 10GB
|
||
stopCleanup: make(chan struct{}),
|
||
|
||
// 初始化ExtensionMatcher缓存
|
||
extensionMatcherCache: NewExtensionMatcherCache(),
|
||
}
|
||
|
||
cm.enabled.Store(true) // 默认启用缓存
|
||
|
||
// 尝试加载配置
|
||
if err := cm.loadConfig(); err != nil {
|
||
log.Printf("[Cache] Failed to load config: %v, using default values", err)
|
||
}
|
||
|
||
// 启动时清理过期和临时文件
|
||
if err := cm.cleanStaleFiles(); err != nil {
|
||
log.Printf("[Cache] Failed to clean stale files: %v", err)
|
||
}
|
||
|
||
// 启动清理协程
|
||
cm.startCleanup()
|
||
|
||
return cm, nil
|
||
}
|
||
|
||
// GenerateCacheKey 生成缓存键
|
||
func (cm *CacheManager) GenerateCacheKey(r *http.Request) CacheKey {
|
||
// 处理 Vary 头部
|
||
varyHeaders := make([]string, 0)
|
||
for _, vary := range strings.Split(r.Header.Get("Vary"), ",") {
|
||
vary = strings.TrimSpace(vary)
|
||
if vary != "" {
|
||
value := r.Header.Get(vary)
|
||
varyHeaders = append(varyHeaders, vary+"="+value)
|
||
}
|
||
}
|
||
sort.Strings(varyHeaders)
|
||
|
||
url := r.URL.String()
|
||
acceptHeaders := r.Header.Get("Accept")
|
||
userAgent := r.Header.Get("User-Agent")
|
||
|
||
// 🎯 针对图片请求进行智能缓存键优化
|
||
if utils.IsImageRequest(r.URL.Path) {
|
||
// 解析Accept头中的图片格式偏好
|
||
imageFormat := cm.parseImageFormatPreference(acceptHeaders)
|
||
|
||
// 为图片请求生成格式感知的缓存键
|
||
return CacheKey{
|
||
URL: url,
|
||
AcceptHeaders: imageFormat, // 使用标准化的图片格式
|
||
UserAgent: cm.normalizeUserAgent(userAgent), // 标准化UserAgent
|
||
}
|
||
}
|
||
|
||
return CacheKey{
|
||
URL: url,
|
||
AcceptHeaders: acceptHeaders,
|
||
UserAgent: userAgent,
|
||
}
|
||
}
|
||
|
||
// parseImageFormatPreference 解析图片格式偏好,返回标准化的格式标识
|
||
func (cm *CacheManager) parseImageFormatPreference(accept string) string {
|
||
if accept == "" {
|
||
return "image/jpeg" // 默认格式
|
||
}
|
||
|
||
accept = strings.ToLower(accept)
|
||
|
||
// 按优先级检查现代图片格式
|
||
switch {
|
||
case strings.Contains(accept, "image/avif"):
|
||
return "image/avif"
|
||
case strings.Contains(accept, "image/webp"):
|
||
return "image/webp"
|
||
case strings.Contains(accept, "image/jpeg") || strings.Contains(accept, "image/jpg"):
|
||
return "image/jpeg"
|
||
case strings.Contains(accept, "image/png"):
|
||
return "image/png"
|
||
case strings.Contains(accept, "image/gif"):
|
||
return "image/gif"
|
||
case strings.Contains(accept, "image/*"):
|
||
return "image/auto" // 自动格式
|
||
default:
|
||
return "image/jpeg" // 默认格式
|
||
}
|
||
}
|
||
|
||
// normalizeUserAgent 标准化UserAgent,减少缓存键的变化
|
||
func (cm *CacheManager) normalizeUserAgent(ua string) string {
|
||
if ua == "" {
|
||
return "default"
|
||
}
|
||
|
||
ua = strings.ToLower(ua)
|
||
|
||
// 根据主要浏览器类型进行分类
|
||
switch {
|
||
case strings.Contains(ua, "chrome") && !strings.Contains(ua, "edge"):
|
||
return "chrome"
|
||
case strings.Contains(ua, "firefox"):
|
||
return "firefox"
|
||
case strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome"):
|
||
return "safari"
|
||
case strings.Contains(ua, "edge"):
|
||
return "edge"
|
||
case strings.Contains(ua, "bot") || strings.Contains(ua, "crawler"):
|
||
return "bot"
|
||
default:
|
||
return "other"
|
||
}
|
||
}
|
||
|
||
// Get 获取缓存项
|
||
func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
||
if !cm.enabled.Load() {
|
||
return nil, false, false
|
||
}
|
||
|
||
// 🎯 针对图片请求实现智能格式回退
|
||
if utils.IsImageRequest(r.URL.Path) {
|
||
return cm.getImageWithFallback(key, r)
|
||
}
|
||
|
||
return cm.getRegularItem(key)
|
||
}
|
||
|
||
// getImageWithFallback 获取图片缓存项,支持格式回退
|
||
func (cm *CacheManager) getImageWithFallback(key CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
||
// 首先尝试精确匹配
|
||
if item, found, notModified := cm.getRegularItem(key); found {
|
||
cm.imageCacheHit.Add(1)
|
||
return item, found, notModified
|
||
}
|
||
|
||
// 如果精确匹配失败,尝试格式回退
|
||
if item, found, notModified := cm.tryFormatFallback(key, r); found {
|
||
cm.formatFallbackHit.Add(1)
|
||
return item, found, notModified
|
||
}
|
||
|
||
return nil, false, false
|
||
}
|
||
|
||
// tryFormatFallback 尝试格式回退
|
||
func (cm *CacheManager) tryFormatFallback(originalKey CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
||
requestedFormat := originalKey.AcceptHeaders
|
||
|
||
// 定义格式回退顺序
|
||
fallbackFormats := cm.getFormatFallbackOrder(requestedFormat)
|
||
|
||
for _, format := range fallbackFormats {
|
||
fallbackKey := CacheKey{
|
||
URL: originalKey.URL,
|
||
AcceptHeaders: format,
|
||
UserAgent: originalKey.UserAgent,
|
||
}
|
||
|
||
if item, found, notModified := cm.getRegularItem(fallbackKey); found {
|
||
// 找到了兼容格式,检查是否真的兼容
|
||
if cm.isFormatCompatible(requestedFormat, format, item.ContentType) {
|
||
log.Printf("[Cache] 格式回退: %s -> %s (%s)", requestedFormat, format, originalKey.URL)
|
||
return item, found, notModified
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil, false, false
|
||
}
|
||
|
||
// getFormatFallbackOrder 获取格式回退顺序
|
||
func (cm *CacheManager) getFormatFallbackOrder(requestedFormat string) []string {
|
||
switch requestedFormat {
|
||
case "image/avif":
|
||
return []string{"image/webp", "image/jpeg", "image/png"}
|
||
case "image/webp":
|
||
return []string{"image/jpeg", "image/png", "image/avif"}
|
||
case "image/jpeg":
|
||
return []string{"image/webp", "image/png", "image/avif"}
|
||
case "image/png":
|
||
return []string{"image/webp", "image/jpeg", "image/avif"}
|
||
case "image/auto":
|
||
return []string{"image/webp", "image/avif", "image/jpeg", "image/png"}
|
||
default:
|
||
return []string{"image/jpeg", "image/webp", "image/png"}
|
||
}
|
||
}
|
||
|
||
// isFormatCompatible 检查格式是否兼容
|
||
func (cm *CacheManager) isFormatCompatible(requestedFormat, cachedFormat, actualContentType string) bool {
|
||
// 如果是自动格式,接受任何现代格式
|
||
if requestedFormat == "image/auto" {
|
||
return true
|
||
}
|
||
|
||
// 现代浏览器通常可以处理多种格式
|
||
modernFormats := map[string]bool{
|
||
"image/webp": true,
|
||
"image/avif": true,
|
||
"image/jpeg": true,
|
||
"image/png": true,
|
||
}
|
||
|
||
// 检查实际内容类型是否为现代格式
|
||
if actualContentType != "" {
|
||
return modernFormats[strings.ToLower(actualContentType)]
|
||
}
|
||
|
||
return modernFormats[cachedFormat]
|
||
}
|
||
|
||
// getRegularItem 获取常规缓存项(原有逻辑)
|
||
func (cm *CacheManager) getRegularItem(key CacheKey) (*CacheItem, bool, bool) {
|
||
// 检查LRU缓存
|
||
if item, found := cm.lruCache.Get(key); found {
|
||
// 检查LRU缓存项是否过期
|
||
if time.Since(item.LastAccess) > cm.maxAge {
|
||
cm.lruCache.Delete(key)
|
||
cm.missCount.Add(1)
|
||
return nil, false, false
|
||
}
|
||
// 更新访问时间
|
||
item.LastAccess = time.Now()
|
||
atomic.AddInt64(&item.AccessCount, 1)
|
||
cm.hitCount.Add(1)
|
||
cm.regularCacheHit.Add(1)
|
||
return item, true, false
|
||
}
|
||
|
||
// 检查文件缓存
|
||
value, ok := cm.items.Load(key)
|
||
if !ok {
|
||
cm.missCount.Add(1)
|
||
return nil, false, false
|
||
}
|
||
|
||
item := value.(*CacheItem)
|
||
|
||
// 验证文件是否存在
|
||
if _, err := os.Stat(item.FilePath); err != nil {
|
||
cm.items.Delete(key)
|
||
cm.missCount.Add(1)
|
||
return nil, false, false
|
||
}
|
||
|
||
// 检查是否过期(使用LastAccess而不是CreatedAt)
|
||
if time.Since(item.LastAccess) > cm.maxAge {
|
||
cm.items.Delete(key)
|
||
os.Remove(item.FilePath)
|
||
cm.missCount.Add(1)
|
||
return nil, false, false
|
||
}
|
||
|
||
// 更新访问信息(重置过期时间)
|
||
item.LastAccess = time.Now()
|
||
atomic.AddInt64(&item.AccessCount, 1)
|
||
cm.hitCount.Add(1)
|
||
cm.regularCacheHit.Add(1)
|
||
cm.bytesSaved.Add(item.Size)
|
||
|
||
// 将缓存项添加到LRU缓存
|
||
cm.lruCache.Put(key, item)
|
||
|
||
return item, true, false
|
||
}
|
||
|
||
// Put 添加缓存项
|
||
func (cm *CacheManager) Put(key CacheKey, resp *http.Response, body []byte) (*CacheItem, error) {
|
||
// 只检查基本的响应状态
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("response status not OK")
|
||
}
|
||
|
||
// 计算内容哈希
|
||
contentHash := sha256.Sum256(body)
|
||
hashStr := hex.EncodeToString(contentHash[:])
|
||
|
||
// 检查是否存在相同哈希的缓存项
|
||
var existingItem *CacheItem
|
||
cm.items.Range(func(k, v interface{}) bool {
|
||
if item := v.(*CacheItem); item.Hash == hashStr {
|
||
if _, err := os.Stat(item.FilePath); err == nil {
|
||
existingItem = item
|
||
return false
|
||
}
|
||
cm.items.Delete(k)
|
||
}
|
||
return true
|
||
})
|
||
|
||
if existingItem != nil {
|
||
cm.items.Store(key, existingItem)
|
||
log.Printf("[Cache] HIT %s %s (%s) from %s", resp.Request.Method, key.URL, formatBytes(existingItem.Size), utils.GetRequestSource(resp.Request))
|
||
return existingItem, nil
|
||
}
|
||
|
||
// 生成文件名并存储
|
||
fileName := hashStr
|
||
filePath := filepath.Join(cm.cacheDir, fileName)
|
||
|
||
if err := os.WriteFile(filePath, body, 0600); err != nil {
|
||
return nil, fmt.Errorf("failed to write cache file: %v", err)
|
||
}
|
||
|
||
item := &CacheItem{
|
||
FilePath: filePath,
|
||
ContentType: resp.Header.Get("Content-Type"),
|
||
ContentEncoding: resp.Header.Get("Content-Encoding"),
|
||
Size: int64(len(body)),
|
||
LastAccess: time.Now(),
|
||
Hash: hashStr,
|
||
CreatedAt: time.Now(),
|
||
AccessCount: 1,
|
||
}
|
||
|
||
cm.items.Store(key, item)
|
||
method := "GET"
|
||
if resp.Request != nil {
|
||
method = resp.Request.Method
|
||
}
|
||
log.Printf("[Cache] NEW %s %s (%s) from %s", method, key.URL, formatBytes(item.Size), utils.GetRequestSource(resp.Request))
|
||
return item, nil
|
||
}
|
||
|
||
// cleanup 定期清理过期的缓存项
|
||
func (cm *CacheManager) cleanup() {
|
||
var totalSize int64
|
||
var keysToDelete []CacheKey
|
||
|
||
// 收集需要删除的键和计算总大小
|
||
cm.items.Range(func(k, v interface{}) bool {
|
||
key := k.(CacheKey)
|
||
item := v.(*CacheItem)
|
||
totalSize += item.Size
|
||
|
||
if time.Since(item.LastAccess) > cm.maxAge {
|
||
keysToDelete = append(keysToDelete, key)
|
||
}
|
||
return true
|
||
})
|
||
|
||
// 如果总大小超过限制,按最后访问时间排序删除
|
||
if totalSize > cm.maxCacheSize {
|
||
var items []*CacheItem
|
||
cm.items.Range(func(k, v interface{}) bool {
|
||
items = append(items, v.(*CacheItem))
|
||
return true
|
||
})
|
||
|
||
// 按最后访问时间排序
|
||
sort.Slice(items, func(i, j int) bool {
|
||
return items[i].LastAccess.Before(items[j].LastAccess)
|
||
})
|
||
|
||
// 删除最旧的项直到总大小小于限制
|
||
for _, item := range items {
|
||
if totalSize <= cm.maxCacheSize {
|
||
break
|
||
}
|
||
cm.items.Range(func(k, v interface{}) bool {
|
||
if v.(*CacheItem) == item {
|
||
keysToDelete = append(keysToDelete, k.(CacheKey))
|
||
totalSize -= item.Size
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
}
|
||
|
||
// 删除过期和超出大小限制的缓存项
|
||
for _, key := range keysToDelete {
|
||
if item, ok := cm.items.Load(key); ok {
|
||
cacheItem := item.(*CacheItem)
|
||
os.Remove(cacheItem.FilePath)
|
||
cm.items.Delete(key)
|
||
log.Printf("[Cache] DEL %s (expired)", key.URL)
|
||
}
|
||
}
|
||
}
|
||
|
||
// formatBytes 格式化字节大小
|
||
func formatBytes(bytes int64) string {
|
||
const (
|
||
KB = 1024
|
||
MB = 1024 * KB
|
||
GB = 1024 * MB
|
||
)
|
||
|
||
switch {
|
||
case bytes >= GB:
|
||
return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
|
||
case bytes >= MB:
|
||
return fmt.Sprintf("%.2f MB", float64(bytes)/float64(MB))
|
||
case bytes >= KB:
|
||
return fmt.Sprintf("%.2f KB", float64(bytes)/float64(KB))
|
||
default:
|
||
return fmt.Sprintf("%d B", bytes)
|
||
}
|
||
}
|
||
|
||
// GetStats 获取缓存统计信息
|
||
func (cm *CacheManager) GetStats() CacheStats {
|
||
var totalItems int
|
||
var totalSize int64
|
||
|
||
cm.items.Range(func(_, value interface{}) bool {
|
||
item := value.(*CacheItem)
|
||
totalItems++
|
||
totalSize += item.Size
|
||
return true
|
||
})
|
||
|
||
hitCount := cm.hitCount.Load()
|
||
missCount := cm.missCount.Load()
|
||
totalRequests := hitCount + missCount
|
||
hitRate := float64(0)
|
||
if totalRequests > 0 {
|
||
hitRate = float64(hitCount) / float64(totalRequests) * 100
|
||
}
|
||
|
||
return CacheStats{
|
||
TotalItems: totalItems,
|
||
TotalSize: totalSize,
|
||
HitCount: hitCount,
|
||
MissCount: missCount,
|
||
HitRate: hitRate,
|
||
BytesSaved: cm.bytesSaved.Load(),
|
||
Enabled: cm.enabled.Load(),
|
||
FormatFallbackHit: cm.formatFallbackHit.Load(),
|
||
ImageCacheHit: cm.imageCacheHit.Load(),
|
||
RegularCacheHit: cm.regularCacheHit.Load(),
|
||
}
|
||
}
|
||
|
||
// SetEnabled 设置缓存开关状态
|
||
func (cm *CacheManager) SetEnabled(enabled bool) {
|
||
cm.enabled.Store(enabled)
|
||
}
|
||
|
||
// ClearCache 清空缓存
|
||
func (cm *CacheManager) ClearCache() error {
|
||
// 清除内存中的缓存项
|
||
var keysToDelete []CacheKey
|
||
cm.items.Range(func(key, value interface{}) bool {
|
||
cacheKey := key.(CacheKey)
|
||
keysToDelete = append(keysToDelete, cacheKey)
|
||
return true
|
||
})
|
||
|
||
for _, key := range keysToDelete {
|
||
cm.items.Delete(key)
|
||
}
|
||
|
||
// 清理缓存目录中的所有文件
|
||
entries, err := os.ReadDir(cm.cacheDir)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read cache directory: %v", err)
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.Name() == "config.json" {
|
||
continue // 保留配置文件
|
||
}
|
||
filePath := filepath.Join(cm.cacheDir, entry.Name())
|
||
if err := os.Remove(filePath); err != nil {
|
||
log.Printf("[Cache] ERR Failed to remove file: %s", entry.Name())
|
||
}
|
||
}
|
||
|
||
// 重置统计信息
|
||
cm.hitCount.Store(0)
|
||
cm.missCount.Store(0)
|
||
cm.bytesSaved.Store(0)
|
||
cm.formatFallbackHit.Store(0)
|
||
cm.imageCacheHit.Store(0)
|
||
cm.regularCacheHit.Store(0)
|
||
|
||
return nil
|
||
}
|
||
|
||
// cleanStaleFiles 清理过期和临时文件
|
||
func (cm *CacheManager) cleanStaleFiles() error {
|
||
entries, err := os.ReadDir(cm.cacheDir)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read cache directory: %v", err)
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.Name() == "config.json" {
|
||
continue // 保留配置文件
|
||
}
|
||
|
||
filePath := filepath.Join(cm.cacheDir, entry.Name())
|
||
|
||
// 清理临时文件
|
||
if strings.HasPrefix(entry.Name(), "temp-") {
|
||
if err := os.Remove(filePath); err != nil {
|
||
log.Printf("[Cache] ERR Failed to remove temp file: %s", entry.Name())
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 检查文件是否仍在缓存记录中
|
||
fileFound := false
|
||
cm.items.Range(func(_, value interface{}) bool {
|
||
item := value.(*CacheItem)
|
||
if item.FilePath == filePath {
|
||
fileFound = true
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
// 如果文件不在缓存记录中,删除它
|
||
if !fileFound {
|
||
if err := os.Remove(filePath); err != nil {
|
||
log.Printf("[Cache] ERR Failed to remove stale file: %s", entry.Name())
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CreateTemp 创建临时缓存文件
|
||
func (cm *CacheManager) CreateTemp(key CacheKey, resp *http.Response) (*os.File, error) {
|
||
if !cm.enabled.Load() {
|
||
return nil, fmt.Errorf("cache is disabled")
|
||
}
|
||
|
||
// 创建临时文件
|
||
tempFile, err := os.CreateTemp(cm.cacheDir, "temp-*")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create temp file: %v", err)
|
||
}
|
||
|
||
return tempFile, nil
|
||
}
|
||
|
||
// Commit 提交缓存文件
|
||
func (cm *CacheManager) Commit(key CacheKey, tempPath string, resp *http.Response, size int64) error {
|
||
if !cm.enabled.Load() {
|
||
os.Remove(tempPath)
|
||
return fmt.Errorf("cache is disabled")
|
||
}
|
||
|
||
// 读取临时文件内容以计算哈希
|
||
tempData, err := os.ReadFile(tempPath)
|
||
if err != nil {
|
||
os.Remove(tempPath)
|
||
return fmt.Errorf("failed to read temp file: %v", err)
|
||
}
|
||
|
||
// 计算内容哈希,与Put方法保持一致
|
||
contentHash := sha256.Sum256(tempData)
|
||
hashStr := hex.EncodeToString(contentHash[:])
|
||
|
||
// 检查是否存在相同哈希的缓存项
|
||
var existingItem *CacheItem
|
||
cm.items.Range(func(k, v interface{}) bool {
|
||
if item := v.(*CacheItem); item.Hash == hashStr {
|
||
if _, err := os.Stat(item.FilePath); err == nil {
|
||
existingItem = item
|
||
return false
|
||
}
|
||
cm.items.Delete(k)
|
||
}
|
||
return true
|
||
})
|
||
|
||
if existingItem != nil {
|
||
// 删除临时文件,使用现有缓存
|
||
os.Remove(tempPath)
|
||
cm.items.Store(key, existingItem)
|
||
log.Printf("[Cache] HIT %s %s (%s) from %s", resp.Request.Method, key.URL, formatBytes(existingItem.Size), utils.GetRequestSource(resp.Request))
|
||
return nil
|
||
}
|
||
|
||
// 生成最终的缓存文件名(使用内容哈希)
|
||
fileName := hashStr
|
||
filePath := filepath.Join(cm.cacheDir, fileName)
|
||
|
||
// 重命名临时文件
|
||
if err := os.Rename(tempPath, filePath); err != nil {
|
||
os.Remove(tempPath)
|
||
return fmt.Errorf("failed to rename temp file: %v", err)
|
||
}
|
||
|
||
// 创建缓存项
|
||
item := &CacheItem{
|
||
FilePath: filePath,
|
||
ContentType: resp.Header.Get("Content-Type"),
|
||
ContentEncoding: resp.Header.Get("Content-Encoding"),
|
||
Size: size,
|
||
LastAccess: time.Now(),
|
||
Hash: hashStr,
|
||
CreatedAt: time.Now(),
|
||
AccessCount: 1,
|
||
}
|
||
|
||
cm.items.Store(key, item)
|
||
cm.bytesSaved.Add(size)
|
||
log.Printf("[Cache] NEW %s %s (%s)", resp.Request.Method, key.URL, formatBytes(size))
|
||
return nil
|
||
}
|
||
|
||
// GetConfig 获取缓存配置
|
||
func (cm *CacheManager) GetConfig() CacheConfig {
|
||
return CacheConfig{
|
||
MaxAge: int64(cm.maxAge.Minutes()),
|
||
CleanupTick: int64(cm.cleanupTick.Minutes()),
|
||
MaxCacheSize: cm.maxCacheSize / (1024 * 1024 * 1024), // 转换为GB
|
||
}
|
||
}
|
||
|
||
// UpdateConfig 更新缓存配置
|
||
func (cm *CacheManager) UpdateConfig(maxAge, cleanupTick, maxCacheSize int64) error {
|
||
if maxAge <= 0 || cleanupTick <= 0 || maxCacheSize <= 0 {
|
||
return fmt.Errorf("invalid config values: all values must be positive")
|
||
}
|
||
|
||
cm.maxAge = time.Duration(maxAge) * time.Minute
|
||
cm.maxCacheSize = maxCacheSize * 1024 * 1024 * 1024 // 转换为字节
|
||
|
||
// 如果清理间隔发生变化,重启清理协程
|
||
newCleanupTick := time.Duration(cleanupTick) * time.Minute
|
||
if cm.cleanupTick != newCleanupTick {
|
||
cm.cleanupTick = newCleanupTick
|
||
// 停止当前的清理协程
|
||
cm.stopCleanup <- struct{}{}
|
||
// 启动新的清理协程
|
||
cm.startCleanup()
|
||
}
|
||
|
||
// 保存配置到文件
|
||
if err := cm.saveConfig(); err != nil {
|
||
log.Printf("[Cache] Failed to save config: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CacheConfig 缓存配置结构
|
||
type CacheConfig struct {
|
||
MaxAge int64 `json:"max_age"` // 最大缓存时间(分钟)
|
||
CleanupTick int64 `json:"cleanup_tick"` // 清理间隔(分钟)
|
||
MaxCacheSize int64 `json:"max_cache_size"` // 最大缓存大小(GB)
|
||
}
|
||
|
||
// startCleanup 启动清理协程
|
||
func (cm *CacheManager) startCleanup() {
|
||
cm.cleanupTimer = time.NewTicker(cm.cleanupTick)
|
||
go func() {
|
||
for {
|
||
select {
|
||
case <-cm.cleanupTimer.C:
|
||
cm.cleanup()
|
||
case <-cm.stopCleanup:
|
||
cm.cleanupTimer.Stop()
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// saveConfig 保存配置到文件
|
||
func (cm *CacheManager) saveConfig() error {
|
||
config := CacheConfig{
|
||
MaxAge: int64(cm.maxAge.Minutes()),
|
||
CleanupTick: int64(cm.cleanupTick.Minutes()),
|
||
MaxCacheSize: cm.maxCacheSize / (1024 * 1024 * 1024), // 转换为GB
|
||
}
|
||
|
||
configPath := filepath.Join(cm.cacheDir, "config.json")
|
||
data, err := json.Marshal(config)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to marshal config: %v", err)
|
||
}
|
||
|
||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||
return fmt.Errorf("failed to write config file: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// loadConfig 从文件加载配置
|
||
func (cm *CacheManager) loadConfig() error {
|
||
configPath := filepath.Join(cm.cacheDir, "config.json")
|
||
data, err := os.ReadFile(configPath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
// 如果配置文件不存在,使用默认配置并保存
|
||
return cm.saveConfig()
|
||
}
|
||
return fmt.Errorf("failed to read config file: %v", err)
|
||
}
|
||
|
||
var config CacheConfig
|
||
if err := json.Unmarshal(data, &config); err != nil {
|
||
return fmt.Errorf("failed to unmarshal config: %v", err)
|
||
}
|
||
|
||
// 更新配置
|
||
cm.maxAge = time.Duration(config.MaxAge) * time.Minute
|
||
cm.maxCacheSize = config.MaxCacheSize * 1024 * 1024 * 1024 // 转换为字节
|
||
|
||
// 如果清理间隔发生变化,重启清理协程
|
||
newCleanupTick := time.Duration(config.CleanupTick) * time.Minute
|
||
if cm.cleanupTick != newCleanupTick {
|
||
cm.cleanupTick = newCleanupTick
|
||
if cm.cleanupTimer != nil {
|
||
cm.stopCleanup <- struct{}{}
|
||
}
|
||
cm.startCleanup()
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetExtensionMatcher 获取缓存的ExtensionMatcher
|
||
func (cm *CacheManager) GetExtensionMatcher(pathKey string, rules []config.ExtensionRule) *utils.ExtensionMatcher {
|
||
if cm.extensionMatcherCache == nil {
|
||
return utils.NewExtensionMatcher(rules)
|
||
}
|
||
return cm.extensionMatcherCache.GetOrCreate(pathKey, rules)
|
||
}
|
||
|
||
// InvalidateExtensionMatcherPath 使指定路径的ExtensionMatcher缓存失效
|
||
func (cm *CacheManager) InvalidateExtensionMatcherPath(pathKey string) {
|
||
if cm.extensionMatcherCache != nil {
|
||
cm.extensionMatcherCache.InvalidatePath(pathKey)
|
||
}
|
||
}
|
||
|
||
// InvalidateAllExtensionMatchers 清空所有ExtensionMatcher缓存
|
||
func (cm *CacheManager) InvalidateAllExtensionMatchers() {
|
||
if cm.extensionMatcherCache != nil {
|
||
cm.extensionMatcherCache.InvalidateAll()
|
||
}
|
||
}
|
||
|
||
// GetExtensionMatcherStats 获取ExtensionMatcher缓存统计信息
|
||
func (cm *CacheManager) GetExtensionMatcherStats() ExtensionMatcherCacheStats {
|
||
if cm.extensionMatcherCache != nil {
|
||
return cm.extensionMatcherCache.GetStats()
|
||
}
|
||
return ExtensionMatcherCacheStats{}
|
||
}
|
||
|
||
// UpdateExtensionMatcherConfig 更新ExtensionMatcher缓存配置
|
||
func (cm *CacheManager) UpdateExtensionMatcherConfig(maxAge, cleanupTick time.Duration) {
|
||
if cm.extensionMatcherCache != nil {
|
||
cm.extensionMatcherCache.UpdateConfig(maxAge, cleanupTick)
|
||
}
|
||
}
|
||
|
||
// Stop 停止缓存管理器(包括ExtensionMatcher缓存)
|
||
func (cm *CacheManager) Stop() {
|
||
// 停止主缓存清理
|
||
if cm.cleanupTimer != nil {
|
||
cm.cleanupTimer.Stop()
|
||
}
|
||
close(cm.stopCleanup)
|
||
|
||
// 停止ExtensionMatcher缓存
|
||
if cm.extensionMatcherCache != nil {
|
||
cm.extensionMatcherCache.Stop()
|
||
}
|
||
}
|