mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
feat(cache): Add fixed path cache support to admin dashboard and API
- Integrate fixed path cache into cache admin handler and API endpoints - Update cache admin dashboard to display fixed path cache statistics - Modify cache management functions to support fixed path cache operations - Add new cache type to frontend cache management UI
This commit is contained in:
parent
9c3c48f4b6
commit
ffc64bb73a
242
internal/cache/manager.go
vendored
242
internal/cache/manager.go
vendored
@ -10,7 +10,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@ -22,20 +21,18 @@ type CacheKey struct {
|
|||||||
URL string
|
URL string
|
||||||
AcceptHeaders string
|
AcceptHeaders string
|
||||||
UserAgent string
|
UserAgent string
|
||||||
VaryHeaders string // 存储 Vary 头部的值,格式:key1=value1&key2=value2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String 实现 Stringer 接口,用于生成唯一的字符串表示
|
// String 实现 Stringer 接口,用于生成唯一的字符串表示
|
||||||
func (k CacheKey) String() string {
|
func (k CacheKey) String() string {
|
||||||
return fmt.Sprintf("%s|%s|%s|%s", k.URL, k.AcceptHeaders, k.UserAgent, k.VaryHeaders)
|
return fmt.Sprintf("%s|%s|%s", k.URL, k.AcceptHeaders, k.UserAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equal 比较两个 CacheKey 是否相等
|
// Equal 比较两个 CacheKey 是否相等
|
||||||
func (k CacheKey) Equal(other CacheKey) bool {
|
func (k CacheKey) Equal(other CacheKey) bool {
|
||||||
return k.URL == other.URL &&
|
return k.URL == other.URL &&
|
||||||
k.AcceptHeaders == other.AcceptHeaders &&
|
k.AcceptHeaders == other.AcceptHeaders &&
|
||||||
k.UserAgent == other.UserAgent &&
|
k.UserAgent == other.UserAgent
|
||||||
k.VaryHeaders == other.VaryHeaders
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash 生成 CacheKey 的哈希值
|
// Hash 生成 CacheKey 的哈希值
|
||||||
@ -47,19 +44,13 @@ func (k CacheKey) Hash() uint64 {
|
|||||||
|
|
||||||
// CacheItem 表示一个缓存项
|
// CacheItem 表示一个缓存项
|
||||||
type CacheItem struct {
|
type CacheItem struct {
|
||||||
FilePath string
|
FilePath string
|
||||||
ContentType string
|
ContentType string
|
||||||
Size int64
|
Size int64
|
||||||
LastAccess time.Time
|
LastAccess time.Time
|
||||||
Hash string
|
Hash string
|
||||||
ETag string
|
CreatedAt time.Time
|
||||||
LastModified time.Time
|
AccessCount int64
|
||||||
CacheControl string
|
|
||||||
VaryHeaders []string
|
|
||||||
// 新增防穿透字段
|
|
||||||
NegativeCache bool // 标记是否为空结果缓存
|
|
||||||
AccessCount int64 // 访问计数
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheStats 缓存统计信息
|
// CacheStats 缓存统计信息
|
||||||
@ -124,208 +115,101 @@ func (cm *CacheManager) GenerateCacheKey(r *http.Request) CacheKey {
|
|||||||
URL: r.URL.String(),
|
URL: r.URL.String(),
|
||||||
AcceptHeaders: r.Header.Get("Accept"),
|
AcceptHeaders: r.Header.Get("Accept"),
|
||||||
UserAgent: r.Header.Get("User-Agent"),
|
UserAgent: r.Header.Get("User-Agent"),
|
||||||
VaryHeaders: strings.Join(varyHeaders, "&"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 获取缓存项
|
// Get 获取缓存项
|
||||||
func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
||||||
// 如果缓存被禁用,直接返回未命中
|
|
||||||
if !cm.enabled.Load() {
|
if !cm.enabled.Load() {
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存项是否存在
|
||||||
|
value, ok := cm.items.Load(key)
|
||||||
|
if !ok {
|
||||||
cm.missCount.Add(1)
|
cm.missCount.Add(1)
|
||||||
return nil, false, false
|
return nil, false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否存在缓存项
|
item := value.(*CacheItem)
|
||||||
if value, ok := cm.items.Load(key); ok {
|
|
||||||
item := value.(*CacheItem)
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
// 验证文件是否存在
|
||||||
if _, err := os.Stat(item.FilePath); err != nil {
|
if _, err := os.Stat(item.FilePath); err != nil {
|
||||||
cm.items.Delete(key)
|
cm.items.Delete(key)
|
||||||
cm.missCount.Add(1)
|
cm.missCount.Add(1)
|
||||||
return nil, false, false
|
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 {
|
|
||||||
requestValue := r.Header.Get(varyHeader)
|
|
||||||
varyPair := varyHeader + "=" + requestValue
|
|
||||||
if !strings.Contains(key.VaryHeaders, varyPair) {
|
|
||||||
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
|
if time.Since(item.CreatedAt) > cm.maxAge {
|
||||||
}
|
cm.items.Delete(key)
|
||||||
|
os.Remove(item.FilePath)
|
||||||
// isCacheExpired 检查缓存是否过期
|
cm.missCount.Add(1)
|
||||||
func (cm *CacheManager) isCacheExpired(item *CacheItem) bool {
|
return nil, false, false
|
||||||
if item.CacheControl == "" {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 max-age
|
// 更新访问信息
|
||||||
if strings.Contains(item.CacheControl, "max-age=") {
|
item.LastAccess = time.Now()
|
||||||
parts := strings.Split(item.CacheControl, "max-age=")
|
atomic.AddInt64(&item.AccessCount, 1)
|
||||||
if len(parts) > 1 {
|
cm.hitCount.Add(1)
|
||||||
maxAge := strings.Split(parts[1], ",")[0]
|
cm.bytesSaved.Add(item.Size)
|
||||||
if seconds, err := strconv.Atoi(maxAge); err == nil {
|
|
||||||
return time.Since(item.CreatedAt) > time.Duration(seconds)*time.Second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return item, true, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put 添加缓存项
|
// Put 添加缓存项
|
||||||
func (cm *CacheManager) Put(key CacheKey, resp *http.Response, body []byte) (*CacheItem, error) {
|
func (cm *CacheManager) Put(key CacheKey, resp *http.Response, body []byte) (*CacheItem, error) {
|
||||||
// 检查缓存控制头
|
// 只检查基本的响应状态
|
||||||
if !cm.shouldCache(resp) {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("response should not be cached")
|
return nil, fmt.Errorf("response status not OK")
|
||||||
}
|
|
||||||
|
|
||||||
// 生成文件名
|
|
||||||
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)
|
contentHash := sha256.Sum256(body)
|
||||||
|
hashStr := hex.EncodeToString(contentHash[:])
|
||||||
|
|
||||||
// 解析缓存控制头
|
// 检查是否存在相同哈希的缓存项
|
||||||
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
|
var existingItem *CacheItem
|
||||||
cm.items.Range(func(k, v interface{}) bool {
|
cm.items.Range(func(k, v interface{}) bool {
|
||||||
if i := v.(*CacheItem); i.Hash == item.Hash {
|
if item := v.(*CacheItem); item.Hash == hashStr {
|
||||||
existingItem = i
|
if _, err := os.Stat(item.FilePath); err == nil {
|
||||||
return false
|
existingItem = item
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cm.items.Delete(k)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if existingItem != nil {
|
if existingItem != nil {
|
||||||
// 如果找到相同内容的缓存,删除新文件,复用现有缓存
|
|
||||||
os.Remove(filePath)
|
|
||||||
cm.items.Store(key, existingItem)
|
cm.items.Store(key, existingItem)
|
||||||
log.Printf("[Cache] Found duplicate content for %s, reusing existing cache", key.URL)
|
log.Printf("[Cache] Reusing existing cache for %s", key.URL)
|
||||||
return existingItem, nil
|
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"),
|
||||||
|
Size: int64(len(body)),
|
||||||
|
LastAccess: time.Now(),
|
||||||
|
Hash: hashStr,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
AccessCount: 1,
|
||||||
|
}
|
||||||
|
|
||||||
cm.items.Store(key, item)
|
cm.items.Store(key, item)
|
||||||
log.Printf("[Cache] Cached %s (%s)", key.URL, formatBytes(item.Size))
|
log.Printf("[Cache] Cached %s (%s)", key.URL, formatBytes(item.Size))
|
||||||
return item, nil
|
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 定期清理过期的缓存项
|
// cleanup 定期清理过期的缓存项
|
||||||
func (cm *CacheManager) cleanup() {
|
func (cm *CacheManager) cleanup() {
|
||||||
ticker := time.NewTicker(cm.cleanupTick)
|
ticker := time.NewTicker(cm.cleanupTick)
|
||||||
|
@ -7,14 +7,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CacheAdminHandler struct {
|
type CacheAdminHandler struct {
|
||||||
proxyCache *cache.CacheManager
|
proxyCache *cache.CacheManager
|
||||||
mirrorCache *cache.CacheManager
|
mirrorCache *cache.CacheManager
|
||||||
|
fixedPathCache *cache.CacheManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCacheAdminHandler(proxyCache, mirrorCache *cache.CacheManager) *CacheAdminHandler {
|
func NewCacheAdminHandler(proxyCache, mirrorCache, fixedPathCache *cache.CacheManager) *CacheAdminHandler {
|
||||||
return &CacheAdminHandler{
|
return &CacheAdminHandler{
|
||||||
proxyCache: proxyCache,
|
proxyCache: proxyCache,
|
||||||
mirrorCache: mirrorCache,
|
mirrorCache: mirrorCache,
|
||||||
|
fixedPathCache: fixedPathCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,8 +28,9 @@ func (h *CacheAdminHandler) GetCacheStats(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats := map[string]cache.CacheStats{
|
stats := map[string]cache.CacheStats{
|
||||||
"proxy": h.proxyCache.GetStats(),
|
"proxy": h.proxyCache.GetStats(),
|
||||||
"mirror": h.mirrorCache.GetStats(),
|
"mirror": h.mirrorCache.GetStats(),
|
||||||
|
"fixedPath": h.fixedPathCache.GetStats(),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@ -42,7 +45,7 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Type string `json:"type"` // "proxy" 或 "mirror"
|
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
|
||||||
Enabled bool `json:"enabled"` // true 或 false
|
Enabled bool `json:"enabled"` // true 或 false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +59,8 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
|
|||||||
h.proxyCache.SetEnabled(req.Enabled)
|
h.proxyCache.SetEnabled(req.Enabled)
|
||||||
case "mirror":
|
case "mirror":
|
||||||
h.mirrorCache.SetEnabled(req.Enabled)
|
h.mirrorCache.SetEnabled(req.Enabled)
|
||||||
|
case "fixedPath":
|
||||||
|
h.fixedPathCache.SetEnabled(req.Enabled)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@ -72,7 +77,7 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Type string `json:"type"` // "proxy", "mirror" 或 "all"
|
Type string `json:"type"` // "proxy", "mirror", "fixedPath" 或 "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
@ -86,11 +91,16 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = h.proxyCache.ClearCache()
|
err = h.proxyCache.ClearCache()
|
||||||
case "mirror":
|
case "mirror":
|
||||||
err = h.mirrorCache.ClearCache()
|
err = h.mirrorCache.ClearCache()
|
||||||
|
case "fixedPath":
|
||||||
|
err = h.fixedPathCache.ClearCache()
|
||||||
case "all":
|
case "all":
|
||||||
err = h.proxyCache.ClearCache()
|
err = h.proxyCache.ClearCache()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = h.mirrorCache.ClearCache()
|
err = h.mirrorCache.ClearCache()
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = h.fixedPathCache.ClearCache()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -155,3 +155,8 @@ func isConnectionClosed(err error) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFixedPathCache 获取固定路径缓存管理器
|
||||||
|
func GetFixedPathCache() *cache.CacheManager {
|
||||||
|
return fixedPathCache
|
||||||
|
}
|
||||||
|
7
main.go
7
main.go
@ -40,6 +40,7 @@ func main() {
|
|||||||
// 创建代理处理器
|
// 创建代理处理器
|
||||||
mirrorHandler := handler.NewMirrorProxyHandler()
|
mirrorHandler := handler.NewMirrorProxyHandler()
|
||||||
proxyHandler := handler.NewProxyHandler(cfg)
|
proxyHandler := handler.NewProxyHandler(cfg)
|
||||||
|
fixedPathCache := middleware.GetFixedPathCache()
|
||||||
|
|
||||||
// 创建处理器链
|
// 创建处理器链
|
||||||
handlers := []struct {
|
handlers := []struct {
|
||||||
@ -81,11 +82,11 @@ func main() {
|
|||||||
case "/admin/api/config/save":
|
case "/admin/api/config/save":
|
||||||
proxyHandler.AuthMiddleware(handler.NewConfigHandler(cfg).ServeHTTP)(w, r)
|
proxyHandler.AuthMiddleware(handler.NewConfigHandler(cfg).ServeHTTP)(w, r)
|
||||||
case "/admin/api/cache/stats":
|
case "/admin/api/cache/stats":
|
||||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).GetCacheStats)(w, r)
|
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).GetCacheStats)(w, r)
|
||||||
case "/admin/api/cache/enable":
|
case "/admin/api/cache/enable":
|
||||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).SetCacheEnabled)(w, r)
|
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).SetCacheEnabled)(w, r)
|
||||||
case "/admin/api/cache/clear":
|
case "/admin/api/cache/clear":
|
||||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).ClearCache)(w, r)
|
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).ClearCache)(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
55
web/app/dashboard/cache/page.tsx
vendored
55
web/app/dashboard/cache/page.tsx
vendored
@ -19,6 +19,7 @@ interface CacheStats {
|
|||||||
interface CacheData {
|
interface CacheData {
|
||||||
proxy: CacheStats
|
proxy: CacheStats
|
||||||
mirror: CacheStats
|
mirror: CacheStats
|
||||||
|
fixedPath: CacheStats
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number) {
|
function formatBytes(bytes: number) {
|
||||||
@ -65,7 +66,7 @@ export default function CachePage() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [fetchStats])
|
}, [fetchStats])
|
||||||
|
|
||||||
const handleToggleCache = async (type: "proxy" | "mirror", enabled: boolean) => {
|
const handleToggleCache = async (type: "proxy" | "mirror" | "fixedPath", enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/admin/api/cache/enable", {
|
const response = await fetch("/admin/api/cache/enable", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -77,7 +78,7 @@ export default function CachePage() {
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "成功",
|
title: "成功",
|
||||||
description: `${type === "proxy" ? "代理" : "镜像"}缓存已${enabled ? "启用" : "禁用"}`,
|
description: `${type === "proxy" ? "代理" : type === "mirror" ? "镜像" : "固定路径"}缓存已${enabled ? "启用" : "禁用"}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
fetchStats()
|
fetchStats()
|
||||||
@ -90,7 +91,7 @@ export default function CachePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClearCache = async (type: "proxy" | "mirror" | "all") => {
|
const handleClearCache = async (type: "proxy" | "mirror" | "fixedPath" | "all") => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/admin/api/cache/clear", {
|
const response = await fetch("/admin/api/cache/clear", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -230,6 +231,54 @@ export default function CachePage() {
|
|||||||
</dl>
|
</dl>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 固定路径缓存 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle>固定路径缓存</CardTitle>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={stats?.fixedPath.enabled ?? false}
|
||||||
|
onCheckedChange={(checked) => handleToggleCache("fixedPath", checked)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleClearCache("fixedPath")}
|
||||||
|
>
|
||||||
|
清理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<dl className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">缓存项数量</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{stats?.fixedPath.total_items ?? 0}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">总大小</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{formatBytes(stats?.fixedPath.total_size ?? 0)}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">命中次数</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{stats?.fixedPath.hit_count ?? 0}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">未命中次数</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{stats?.fixedPath.miss_count ?? 0}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">命中率</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{(stats?.fixedPath.hit_rate ?? 0).toFixed(2)}%</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{formatBytes(stats?.fixedPath.bytes_saved ?? 0)}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user