From f7a52a1be58ae461e96210fe834fe9fa9ff2ddcd Mon Sep 17 00:00:00 2001 From: wood chen Date: Mon, 17 Feb 2025 18:12:16 +0800 Subject: [PATCH] feat(config): Add max file size threshold and improve path configuration - Extend PathConfig with MaxSize parameter to define upper file size limit - Update routing logic to handle both minimum and maximum file size thresholds - Enhance frontend configuration UI to support max size input - Improve file routing decision-making with comprehensive size range checks --- internal/config/types.go | 8 +- internal/utils/utils.go | 136 ++++++++++++------ web/app/dashboard/config/page.tsx | 224 ++++++++++++++++++------------ 3 files changed, 239 insertions(+), 129 deletions(-) diff --git a/internal/config/types.go b/internal/config/types.go index c711ac2..5413106 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -12,9 +12,11 @@ type Config struct { } type PathConfig struct { - DefaultTarget string `json:"DefaultTarget"` // 默认回源地址 - ExtensionMap map[string]string `json:"ExtensionMap"` // 特定后缀的回源地址 - SizeThreshold int64 `json:"SizeThreshold"` // 文件大小阈值(字节),超过此大小才使用ExtensionMap + Path string `json:"Path"` + DefaultTarget string `json:"DefaultTarget"` + ExtensionMap map[string]string `json:"ExtensionMap"` + SizeThreshold int64 `json:"SizeThreshold"` // 最小文件大小阈值 + MaxSize int64 `json:"MaxSize"` // 最大文件大小阈值 processedExtMap map[string]string // 内部使用,存储拆分后的映射 } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index cbaca56..221da71 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -22,10 +22,19 @@ type fileSizeCache struct { timestamp time.Time } +// 可访问性缓存项 +type accessibilityCache struct { + accessible bool + timestamp time.Time +} + var ( // 文件大小缓存,过期时间5分钟 - sizeCache sync.Map + sizeCache sync.Map + // 可访问性缓存,过期时间30秒 + accessCache sync.Map cacheTTL = 5 * time.Minute + accessTTL = 30 * time.Second maxCacheSize = 10000 // 最大缓存条目数 ) @@ -35,6 +44,7 @@ func init() { ticker := time.NewTicker(time.Minute) for range ticker.C { now := time.Now() + // 清理文件大小缓存 var items []struct { key interface{} timestamp time.Time @@ -59,6 +69,15 @@ func init() { sizeCache.Delete(items[i].key) } } + + // 清理可访问性缓存 + accessCache.Range(func(key, value interface{}) bool { + cache := value.(accessibilityCache) + if now.Sub(cache.timestamp) > accessTTL { + accessCache.Delete(key) + } + return true + }) } }() } @@ -168,10 +187,16 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo // 默认使用默认目标 targetBase := pathConfig.DefaultTarget - // 如果没有设置阈值,使用默认值 500KB - threshold := pathConfig.SizeThreshold - if threshold <= 0 { - threshold = 500 * 1024 + // 如果没有设置最小阈值,使用默认值 500KB + minThreshold := pathConfig.SizeThreshold + if minThreshold <= 0 { + minThreshold = 500 * 1024 + } + + // 如果没有设置最大阈值,使用默认值 10MB + maxThreshold := pathConfig.MaxSize + if maxThreshold <= 0 { + maxThreshold = 10 * 1024 * 1024 } // 检查文件扩展名 @@ -181,44 +206,58 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo ext = ext[1:] // 移除开头的点 // 先检查是否在扩展名映射中 if altTarget, exists := pathConfig.GetExtensionTarget(ext); exists { - // 先检查扩展名映射的目标是否可访问 - if isTargetAccessible(client, altTarget+path) { - // 检查文件大小 - contentLength := r.ContentLength - if contentLength <= 0 { - // 如果无法获取 Content-Length,尝试发送 HEAD 请求到备用源 - if size, err := GetFileSize(client, altTarget+path); err == nil { - contentLength = size - log.Printf("[FileSize] Path: %s, Size: %s (from alternative source)", - path, FormatBytes(contentLength)) - } else { - log.Printf("[FileSize] Failed to get size from alternative source for %s: %v", path, err) - // 如果从备用源获取失败,尝试从默认源获取 - if size, err := GetFileSize(client, targetBase+path); err == nil { - contentLength = size - log.Printf("[FileSize] Path: %s, Size: %s (from default source)", - path, FormatBytes(contentLength)) - } else { - log.Printf("[FileSize] Failed to get size from default source for %s: %v", path, err) - } - } - } else { - log.Printf("[FileSize] Path: %s, Size: %s (from Content-Length)", - path, FormatBytes(contentLength)) - } + // 使用 channel 来并发获取文件大小和检查可访问性 + type result struct { + size int64 + accessible bool + err error + } + defaultChan := make(chan result, 1) + altChan := make(chan result, 1) - // 只有当文件大于阈值时才使用扩展名映射的目标 - if contentLength > threshold { - log.Printf("[Route] %s -> %s (size: %s > %s)", - path, altTarget, FormatBytes(contentLength), FormatBytes(threshold)) - targetBase = altTarget + // 并发检查默认源和备用源 + go func() { + size, err := GetFileSize(client, targetBase+path) + defaultChan <- result{size: size, err: err} + }() + go func() { + accessible := isTargetAccessible(client, altTarget+path) + altChan <- result{accessible: accessible} + }() + + // 获取默认源结果 + defaultResult := <-defaultChan + if defaultResult.err != nil { + log.Printf("[FileSize] Failed to get size from default source for %s: %v", path, defaultResult.err) + return targetBase + } + contentLength := defaultResult.size + log.Printf("[FileSize] Path: %s, Size: %s (from default source)", + path, FormatBytes(contentLength)) + + // 检查文件大小是否在阈值范围内 + if contentLength > minThreshold && contentLength <= maxThreshold { + // 获取备用源检查结果 + altResult := <-altChan + if altResult.accessible { + log.Printf("[Route] %s -> %s (size: %s > %s and <= %s)", + path, altTarget, FormatBytes(contentLength), + FormatBytes(minThreshold), FormatBytes(maxThreshold)) + return altTarget } else { - log.Printf("[Route] %s -> %s (size: %s <= %s)", - path, targetBase, FormatBytes(contentLength), FormatBytes(threshold)) + log.Printf("[Route] %s -> %s (fallback: alternative target not accessible)", + path, targetBase) } + } else if contentLength <= minThreshold { + // 如果文件大小不合适,直接丢弃备用源检查结果 + go func() { <-altChan }() + log.Printf("[Route] %s -> %s (size: %s <= %s)", + path, targetBase, FormatBytes(contentLength), FormatBytes(minThreshold)) } else { - log.Printf("[Route] %s -> %s (fallback: alternative target not accessible)", - path, targetBase) + // 如果文件大小不合适,直接丢弃备用源检查结果 + go func() { <-altChan }() + log.Printf("[Route] %s -> %s (size: %s > %s)", + path, targetBase, FormatBytes(contentLength), FormatBytes(maxThreshold)) } } else { // 记录没有匹配扩展名映射的情况 @@ -238,6 +277,15 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo // isTargetAccessible 检查目标URL是否可访问 func isTargetAccessible(client *http.Client, url string) bool { + // 先查缓存 + if cache, ok := accessCache.Load(url); ok { + cacheItem := cache.(accessibilityCache) + if time.Since(cacheItem.timestamp) < accessTTL { + return cacheItem.accessible + } + accessCache.Delete(url) + } + req, err := http.NewRequest("HEAD", url, nil) if err != nil { log.Printf("[Check] Failed to create request for %s: %v", url, err) @@ -255,8 +303,14 @@ func isTargetAccessible(client *http.Client, url string) bool { } defer resp.Body.Close() - // 检查状态码是否表示成功 - return resp.StatusCode >= 200 && resp.StatusCode < 400 + accessible := resp.StatusCode >= 200 && resp.StatusCode < 400 + // 缓存结果 + accessCache.Store(url, accessibilityCache{ + accessible: accessible, + timestamp: time.Now(), + }) + + return accessible } // SafeInt64 安全地将 interface{} 转换为 int64 diff --git a/web/app/dashboard/config/page.tsx b/web/app/dashboard/config/page.tsx index 909d12b..31ab26e 100644 --- a/web/app/dashboard/config/page.tsx +++ b/web/app/dashboard/config/page.tsx @@ -40,7 +40,8 @@ import { interface PathMapping { DefaultTarget: string ExtensionMap?: Record - SizeThreshold?: number + SizeThreshold?: number // 最小文件大小阈值 + MaxSize?: number // 最大文件大小阈值 } interface FixedPath { @@ -80,6 +81,7 @@ export default function ConfigPage() { defaultTarget: "", extensionMap: {} as Record, sizeThreshold: 0, + maxSize: 0, sizeUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB', }) const [fixedPathDialogOpen, setFixedPathDialogOpen] = useState(false) @@ -98,6 +100,7 @@ export default function ConfigPage() { path: string; defaultTarget: string; sizeThreshold: number; + maxSize: number; sizeUnit: 'B' | 'KB' | 'MB' | 'GB'; } | null>(null); @@ -218,6 +221,7 @@ export default function ConfigPage() { defaultTarget: "", extensionMap: {}, sizeThreshold: 0, + maxSize: 0, sizeUnit: 'MB', }) } @@ -251,10 +255,11 @@ export default function ConfigPage() { const addOrUpdatePath = () => { if (!config) return - const data = editingPathData ? editingPathData : newPathData - // 验证输入 - if (!data.path.trim() || !data.defaultTarget.trim()) { + const data = editingPathData || newPathData + const { path, defaultTarget, sizeThreshold, maxSize, sizeUnit } = data + + if (!path || !defaultTarget) { toast({ title: "错误", description: "路径和默认目标不能为空", @@ -263,66 +268,55 @@ export default function ConfigPage() { return } - // 验证路径格式 - if (!data.path.startsWith('/')) { + // 转换大小为字节 + const sizeThresholdBytes = convertToBytes(sizeThreshold, sizeUnit) + const maxSizeBytes = convertToBytes(maxSize, sizeUnit) + + // 验证阈值 + if (maxSizeBytes > 0 && sizeThresholdBytes >= maxSizeBytes) { toast({ title: "错误", - description: "路径必须以/开头", + description: "最大文件大小阈值必须大于最小文件大小阈值", variant: "destructive", }) return } - // 验证URL格式 - try { - new URL(data.defaultTarget) - } catch { - toast({ - title: "错误", - description: "默认目标URL格式不正确", - variant: "destructive", - }) - return - } - - const sizeInBytes = convertToBytes(data.sizeThreshold, data.sizeUnit) const newConfig = { ...config } - const existingMapping = newConfig.MAP[data.path] + const pathConfig: PathMapping = { + DefaultTarget: defaultTarget, + ExtensionMap: {}, + SizeThreshold: sizeThresholdBytes, + MaxSize: maxSizeBytes + } + + // 如果是编辑现有路径,保留原有的扩展名映射 + if (editingPathData && typeof config.MAP[path] === 'object') { + const existingConfig = config.MAP[path] as PathMapping + pathConfig.ExtensionMap = existingConfig.ExtensionMap + } + + newConfig.MAP[path] = pathConfig + setConfig(newConfig) if (editingPathData) { - if (typeof existingMapping === 'object') { - newConfig.MAP[data.path] = { - DefaultTarget: data.defaultTarget, - SizeThreshold: sizeInBytes, - ExtensionMap: existingMapping.ExtensionMap || {} - } - } else { - newConfig.MAP[data.path] = { - DefaultTarget: data.defaultTarget, - SizeThreshold: sizeInBytes - } - } + setEditingPathData(null) } else { - newConfig.MAP[data.path] = { - DefaultTarget: data.defaultTarget, - SizeThreshold: sizeInBytes - } + setNewPathData({ + path: "", + defaultTarget: "", + extensionMap: {}, + sizeThreshold: 0, + maxSize: 0, + sizeUnit: 'MB', + }) } - - setConfig(newConfig) + setPathDialogOpen(false) - setEditingPathData(null) - setNewPathData({ - path: "", - defaultTarget: "", - extensionMap: {}, - sizeThreshold: 0, - sizeUnit: 'MB', - }) - + toast({ title: "成功", - description: "路径映射已更新", + description: `${editingPathData ? '更新' : '添加'}路径配置成功`, }) } @@ -552,6 +546,7 @@ export default function ConfigPage() { defaultTarget: "", extensionMap: {}, sizeThreshold: 0, + maxSize: 0, sizeUnit: 'MB', }) setPathDialogOpen(true) @@ -671,15 +666,18 @@ export default function ConfigPage() { path, defaultTarget: target, sizeThreshold: 0, + maxSize: 0, sizeUnit: 'MB' }) } else { const sizeThreshold = target.SizeThreshold || 0 + const maxSize = target.MaxSize || 0 const { value, unit } = convertBytesToUnit(sizeThreshold) setEditingPathData({ path, defaultTarget: target.DefaultTarget, sizeThreshold: value, + maxSize: maxSize, sizeUnit: unit }) } @@ -787,43 +785,99 @@ export default function ConfigPage() { 默认的回源地址,所有请求都会转发到这个地址

-
- -
- { - const value = parseInt(e.target.value) || 0 - if (editingPathData) { - setEditingPathData({ ...editingPathData, sizeThreshold: value }) - } else { - setNewPathData({ ...newPathData, sizeThreshold: value }) - } - }} - min={0} - /> - +
+
+ +
+ { + if (editingPathData) { + setEditingPathData({ + ...editingPathData, + sizeThreshold: Number(e.target.value), + }) + } else { + setNewPathData({ + ...newPathData, + sizeThreshold: Number(e.target.value), + }) + } + }} + /> + +
+
+
+ +
+ { + if (editingPathData) { + setEditingPathData({ + ...editingPathData, + maxSize: Number(e.target.value), + }) + } else { + setNewPathData({ + ...newPathData, + maxSize: Number(e.target.value), + }) + } + }} + /> + +
-

- 文件大小超过此阈值时,将使用扩展名映射中的目标地址(如果存在) -