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),
+ })
+ }
+ }}
+ />
+
+
-
- 文件大小超过此阈值时,将使用扩展名映射中的目标地址(如果存在)
-