mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
- Add CacheManager to handle caching for proxy and mirror handlers - Introduce cache-related API endpoints for stats, enabling, and clearing - Modify proxy and mirror handlers to support caching GET requests - Remove rate limiting and simplify request handling - Update go.mod dependencies and remove unused imports - Add cache hit/miss headers to responses - Enhance metrics collection for cached requests
434 lines
11 KiB
Go
434 lines
11 KiB
Go
package cache
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// CacheKey 用于标识缓存项的唯一键
|
|
type CacheKey struct {
|
|
URL string
|
|
AcceptHeaders string
|
|
UserAgent string
|
|
VaryHeadersMap map[string]string // 存储 Vary 头部的值
|
|
}
|
|
|
|
// CacheItem 表示一个缓存项
|
|
type CacheItem struct {
|
|
FilePath string
|
|
ContentType string
|
|
Size int64
|
|
LastAccess time.Time
|
|
Hash string
|
|
ETag string
|
|
LastModified time.Time
|
|
CacheControl string
|
|
VaryHeaders []string
|
|
// 新增防穿透字段
|
|
NegativeCache bool // 标记是否为空结果缓存
|
|
AccessCount int64 // 访问计数
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// 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"` // 缓存开关状态
|
|
}
|
|
|
|
// CacheManager 缓存管理器
|
|
type CacheManager struct {
|
|
cacheDir string
|
|
items sync.Map
|
|
maxAge time.Duration
|
|
cleanupTick time.Duration
|
|
maxCacheSize int64
|
|
enabled atomic.Bool // 缓存开关
|
|
hitCount atomic.Int64 // 命中计数
|
|
missCount atomic.Int64 // 未命中计数
|
|
bytesSaved atomic.Int64 // 节省的带宽
|
|
}
|
|
|
|
// 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,
|
|
maxAge: 30 * time.Minute,
|
|
cleanupTick: 5 * time.Minute,
|
|
maxCacheSize: 10 * 1024 * 1024 * 1024, // 10GB
|
|
}
|
|
|
|
cm.enabled.Store(true) // 默认启用缓存
|
|
|
|
// 启动清理协程
|
|
go cm.cleanup()
|
|
|
|
return cm, nil
|
|
}
|
|
|
|
// GenerateCacheKey 生成缓存键
|
|
func (cm *CacheManager) GenerateCacheKey(r *http.Request) CacheKey {
|
|
return CacheKey{
|
|
URL: r.URL.String(),
|
|
AcceptHeaders: r.Header.Get("Accept"),
|
|
UserAgent: r.Header.Get("User-Agent"),
|
|
}
|
|
}
|
|
|
|
// Get 获取缓存项
|
|
func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
|
// 如果缓存被禁用,直接返回未命中
|
|
if !cm.enabled.Load() {
|
|
cm.missCount.Add(1)
|
|
return nil, false, false
|
|
}
|
|
|
|
// 检查是否存在缓存项
|
|
if value, ok := cm.items.Load(key); ok {
|
|
item := value.(*CacheItem)
|
|
|
|
// 检查文件是否存在
|
|
if _, err := os.Stat(item.FilePath); err != nil {
|
|
cm.items.Delete(key)
|
|
cm.missCount.Add(1)
|
|
return nil, false, false
|
|
}
|
|
|
|
// 检查是否为负缓存(防止缓存穿透)
|
|
if item.NegativeCache {
|
|
// 如果访问次数较少且是负缓存,允许重新验证
|
|
if item.AccessCount < 10 {
|
|
item.AccessCount++
|
|
return nil, false, false
|
|
}
|
|
// 返回空结果,但标记为命中
|
|
cm.hitCount.Add(1)
|
|
return nil, true, true
|
|
}
|
|
|
|
// 检查 Vary 头部
|
|
for _, varyHeader := range item.VaryHeaders {
|
|
if r.Header.Get(varyHeader) != key.VaryHeadersMap[varyHeader] {
|
|
cm.missCount.Add(1)
|
|
return nil, false, false
|
|
}
|
|
}
|
|
|
|
// 处理条件请求
|
|
ifNoneMatch := r.Header.Get("If-None-Match")
|
|
ifModifiedSince := r.Header.Get("If-Modified-Since")
|
|
|
|
// ETag 匹配
|
|
if ifNoneMatch != "" && item.ETag != "" {
|
|
if ifNoneMatch == item.ETag {
|
|
cm.hitCount.Add(1)
|
|
return item, true, true
|
|
}
|
|
}
|
|
|
|
// Last-Modified 匹配
|
|
if ifModifiedSince != "" && !item.LastModified.IsZero() {
|
|
if modifiedSince, err := time.Parse(time.RFC1123, ifModifiedSince); err == nil {
|
|
if !item.LastModified.After(modifiedSince) {
|
|
cm.hitCount.Add(1)
|
|
return item, true, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// 检查 Cache-Control
|
|
if item.CacheControl != "" {
|
|
if cm.isCacheExpired(item) {
|
|
cm.items.Delete(key)
|
|
cm.missCount.Add(1)
|
|
return nil, false, false
|
|
}
|
|
}
|
|
|
|
// 更新访问统计
|
|
item.LastAccess = time.Now()
|
|
item.AccessCount++
|
|
cm.hitCount.Add(1)
|
|
cm.bytesSaved.Add(item.Size)
|
|
return item, true, false
|
|
}
|
|
|
|
cm.missCount.Add(1)
|
|
return nil, false, false
|
|
}
|
|
|
|
// isCacheExpired 检查缓存是否过期
|
|
func (cm *CacheManager) isCacheExpired(item *CacheItem) bool {
|
|
if item.CacheControl == "" {
|
|
return false
|
|
}
|
|
|
|
// 解析 max-age
|
|
if strings.Contains(item.CacheControl, "max-age=") {
|
|
parts := strings.Split(item.CacheControl, "max-age=")
|
|
if len(parts) > 1 {
|
|
maxAge := strings.Split(parts[1], ",")[0]
|
|
if seconds, err := strconv.Atoi(maxAge); err == nil {
|
|
return time.Since(item.CreatedAt) > time.Duration(seconds)*time.Second
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Put 添加缓存项
|
|
func (cm *CacheManager) Put(key CacheKey, resp *http.Response, body []byte) (*CacheItem, error) {
|
|
// 检查缓存控制头
|
|
if !cm.shouldCache(resp) {
|
|
return nil, fmt.Errorf("response should not be cached")
|
|
}
|
|
|
|
// 生成文件名
|
|
hash := sha256.Sum256([]byte(fmt.Sprintf("%v-%v-%v-%v", key.URL, key.AcceptHeaders, key.UserAgent, time.Now().UnixNano())))
|
|
fileName := hex.EncodeToString(hash[:])
|
|
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)
|
|
}
|
|
|
|
// 计算内容哈希
|
|
contentHash := sha256.Sum256(body)
|
|
|
|
// 解析缓存控制头
|
|
cacheControl := resp.Header.Get("Cache-Control")
|
|
lastModified := resp.Header.Get("Last-Modified")
|
|
etag := resp.Header.Get("ETag")
|
|
|
|
var lastModifiedTime time.Time
|
|
if lastModified != "" {
|
|
if t, err := time.Parse(time.RFC1123, lastModified); err == nil {
|
|
lastModifiedTime = t
|
|
}
|
|
}
|
|
|
|
// 处理 Vary 头部
|
|
varyHeaders := strings.Split(resp.Header.Get("Vary"), ",")
|
|
for i, h := range varyHeaders {
|
|
varyHeaders[i] = strings.TrimSpace(h)
|
|
}
|
|
|
|
item := &CacheItem{
|
|
FilePath: filePath,
|
|
ContentType: resp.Header.Get("Content-Type"),
|
|
Size: int64(len(body)),
|
|
LastAccess: time.Now(),
|
|
Hash: hex.EncodeToString(contentHash[:]),
|
|
ETag: etag,
|
|
LastModified: lastModifiedTime,
|
|
CacheControl: cacheControl,
|
|
VaryHeaders: varyHeaders,
|
|
CreatedAt: time.Now(),
|
|
AccessCount: 1,
|
|
}
|
|
|
|
// 检查是否有相同内容的缓存
|
|
var existingItem *CacheItem
|
|
cm.items.Range(func(k, v interface{}) bool {
|
|
if i := v.(*CacheItem); i.Hash == item.Hash {
|
|
existingItem = i
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
if existingItem != nil {
|
|
// 如果找到相同内容的缓存,删除新文件,复用现有缓存
|
|
os.Remove(filePath)
|
|
cm.items.Store(key, existingItem)
|
|
log.Printf("[Cache] Found duplicate content for %s, reusing existing cache", key.URL)
|
|
return existingItem, nil
|
|
}
|
|
|
|
// 存储新的缓存项
|
|
cm.items.Store(key, item)
|
|
log.Printf("[Cache] Cached %s (%s)", key.URL, formatBytes(item.Size))
|
|
return item, nil
|
|
}
|
|
|
|
// shouldCache 检查响应是否应该被缓存
|
|
func (cm *CacheManager) shouldCache(resp *http.Response) bool {
|
|
// 检查状态码
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotModified {
|
|
return false
|
|
}
|
|
|
|
// 解析 Cache-Control 头
|
|
cacheControl := resp.Header.Get("Cache-Control")
|
|
if strings.Contains(cacheControl, "no-store") ||
|
|
strings.Contains(cacheControl, "no-cache") ||
|
|
strings.Contains(cacheControl, "private") {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// cleanup 定期清理过期的缓存项
|
|
func (cm *CacheManager) cleanup() {
|
|
ticker := time.NewTicker(cm.cleanupTick)
|
|
for range ticker.C {
|
|
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] Removed expired item: %s", 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(),
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
item := value.(*CacheItem)
|
|
os.Remove(item.FilePath)
|
|
keysToDelete = append(keysToDelete, cacheKey)
|
|
return true
|
|
})
|
|
|
|
// 清除缓存项
|
|
for _, key := range keysToDelete {
|
|
cm.items.Delete(key)
|
|
}
|
|
|
|
// 重置统计信息
|
|
cm.hitCount.Store(0)
|
|
cm.missCount.Store(0)
|
|
cm.bytesSaved.Store(0)
|
|
|
|
return nil
|
|
}
|