mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-19 17:01:56 +08:00
feat(config): 更新配置管理和扩展名规则处理
- 在配置中添加新的扩展名规则支持,允许用户定义文件扩展名与目标URL的映射 - 优化配置加载逻辑,确保路径配置的扩展名规则在初始化时得到处理 - 更新前端配置页面,支持添加、编辑和删除扩展名规则 - 增强错误处理和用户提示,确保用户体验流畅
This commit is contained in:
parent
c85d08d7a4
commit
cc45cac622
11
.gitignore
vendored
11
.gitignore
vendored
@ -24,6 +24,15 @@ vendor/
|
|||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/dist/
|
web/dist/
|
||||||
data/config.json
|
data/config.json
|
||||||
data/config.json
|
|
||||||
kaifa.md
|
kaifa.md
|
||||||
.cursor
|
.cursor
|
||||||
|
data/cache/config.json
|
||||||
|
data/mirror_cache/config.json
|
||||||
|
data/metrics/latency_distribution.json
|
||||||
|
data/metrics/metrics.json
|
||||||
|
data/metrics/path_stats.json
|
||||||
|
data/metrics/referer_stats.json
|
||||||
|
data/metrics/status_codes.json
|
||||||
|
data/config.example.json
|
||||||
|
data/config.json
|
||||||
|
.env
|
||||||
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.23.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1
|
github.com/andybalholm/brotli v1.1.1
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
golang.org/x/net v0.37.0
|
golang.org/x/net v0.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
2
go.sum
2
go.sum
@ -1,5 +1,7 @@
|
|||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config 配置结构体
|
// Config 配置结构体
|
||||||
@ -30,18 +29,26 @@ type ConfigManager struct {
|
|||||||
configPath string
|
configPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigManager(path string) *ConfigManager {
|
func NewConfigManager(configPath string) (*ConfigManager, error) {
|
||||||
cm := &ConfigManager{configPath: path}
|
cm := &ConfigManager{
|
||||||
cm.loadConfig()
|
configPath: configPath,
|
||||||
go cm.watchConfig()
|
|
||||||
return cm
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *ConfigManager) watchConfig() {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
for range ticker.C {
|
|
||||||
cm.loadConfig()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
config, err := Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有路径配置的扩展名规则都已更新
|
||||||
|
for _, pathConfig := range config.MAP {
|
||||||
|
pathConfig.ProcessExtensionMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.config.Store(config)
|
||||||
|
log.Printf("[ConfigManager] 配置已加载: %d 个路径映射", len(config.MAP))
|
||||||
|
|
||||||
|
return cm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 加载配置
|
// Load 加载配置
|
||||||
@ -75,6 +82,21 @@ func createDefaultConfig(path string) error {
|
|||||||
MAP: map[string]PathConfig{
|
MAP: map[string]PathConfig{
|
||||||
"/": {
|
"/": {
|
||||||
DefaultTarget: "http://localhost:8080",
|
DefaultTarget: "http://localhost:8080",
|
||||||
|
// 添加新式扩展名规则映射示例
|
||||||
|
ExtensionMap: []ExtRuleConfig{
|
||||||
|
{
|
||||||
|
Extensions: "jpg,png,webp",
|
||||||
|
Target: "https://img1.example.com",
|
||||||
|
SizeThreshold: 500 * 1024, // 500KB
|
||||||
|
MaxSize: 2 * 1024 * 1024, // 2MB
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Extensions: "jpg,png,webp",
|
||||||
|
Target: "https://img2.example.com",
|
||||||
|
SizeThreshold: 2 * 1024 * 1024, // 2MB
|
||||||
|
MaxSize: 5 * 1024 * 1024, // 5MB
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Compression: CompressionConfig{
|
Compression: CompressionConfig{
|
||||||
@ -108,7 +130,7 @@ func RegisterUpdateCallback(callback func(*Config)) {
|
|||||||
|
|
||||||
// TriggerCallbacks 触发所有回调
|
// TriggerCallbacks 触发所有回调
|
||||||
func TriggerCallbacks(cfg *Config) {
|
func TriggerCallbacks(cfg *Config) {
|
||||||
// 确保所有路径配置的processedExtMap都已更新
|
// 确保所有路径配置的扩展名规则都已更新
|
||||||
for _, pathConfig := range cfg.MAP {
|
for _, pathConfig := range cfg.MAP {
|
||||||
pathConfig.ProcessExtensionMap()
|
pathConfig.ProcessExtensionMap()
|
||||||
}
|
}
|
||||||
@ -128,7 +150,7 @@ func (c *configImpl) Update(newConfig *Config) {
|
|||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
// 确保所有路径配置的processedExtMap都已更新
|
// 确保所有路径配置的扩展名规则都已更新
|
||||||
for _, pathConfig := range newConfig.MAP {
|
for _, pathConfig := range newConfig.MAP {
|
||||||
pathConfig.ProcessExtensionMap()
|
pathConfig.ProcessExtensionMap()
|
||||||
}
|
}
|
||||||
@ -168,7 +190,7 @@ func (cm *ConfigManager) loadConfig() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保所有路径配置的processedExtMap都已更新
|
// 确保所有路径配置的扩展名规则都已更新
|
||||||
for _, pathConfig := range config.MAP {
|
for _, pathConfig := range config.MAP {
|
||||||
pathConfig.ProcessExtensionMap()
|
pathConfig.ProcessExtensionMap()
|
||||||
}
|
}
|
||||||
|
16
internal/config/init.go
Normal file
16
internal/config/init.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
func Init(configPath string) (*ConfigManager, error) {
|
||||||
|
log.Printf("[Config] 初始化配置管理器...")
|
||||||
|
|
||||||
|
configManager, err := NewConfigManager(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Config] 初始化配置管理器失败: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Config] 配置管理器初始化成功")
|
||||||
|
return configManager, nil
|
||||||
|
}
|
@ -1,22 +1,26 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
MAP map[string]PathConfig `json:"MAP"` // 改为使用PathConfig
|
MAP map[string]PathConfig `json:"MAP"` // 路径映射配置
|
||||||
Compression CompressionConfig `json:"Compression"`
|
Compression CompressionConfig `json:"Compression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PathConfig struct {
|
type PathConfig struct {
|
||||||
Path string `json:"Path"`
|
DefaultTarget string `json:"DefaultTarget"` // 默认目标URL
|
||||||
DefaultTarget string `json:"DefaultTarget"`
|
ExtensionMap []ExtRuleConfig `json:"ExtensionMap"` // 扩展名映射规则
|
||||||
ExtensionMap map[string]string `json:"ExtensionMap"`
|
ExtRules []ExtensionRule `json:"-"` // 内部使用,存储处理后的扩展名规则
|
||||||
SizeThreshold int64 `json:"SizeThreshold"` // 最小文件大小阈值
|
}
|
||||||
MaxSize int64 `json:"MaxSize"` // 最大文件大小阈值
|
|
||||||
processedExtMap map[string]string // 内部使用,存储拆分后的映射
|
// ExtensionRule 表示一个扩展名映射规则(内部使用)
|
||||||
|
type ExtensionRule struct {
|
||||||
|
Extensions []string // 支持的扩展名列表
|
||||||
|
Target string // 目标服务器
|
||||||
|
SizeThreshold int64 // 最小文件大小阈值
|
||||||
|
MaxSize int64 // 最大文件大小阈值
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompressionConfig struct {
|
type CompressionConfig struct {
|
||||||
@ -29,87 +33,40 @@ type CompressorConfig struct {
|
|||||||
Level int `json:"Level"`
|
Level int `json:"Level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加一个辅助方法来处理字符串到 PathConfig 的转换
|
// 扩展名映射配置结构
|
||||||
func (c *Config) UnmarshalJSON(data []byte) error {
|
type ExtRuleConfig struct {
|
||||||
// 创建一个临时结构来解析原始JSON
|
Extensions string `json:"Extensions"` // 逗号分隔的扩展名
|
||||||
type TempConfig struct {
|
Target string `json:"Target"` // 目标服务器
|
||||||
MAP map[string]json.RawMessage `json:"MAP"`
|
SizeThreshold int64 `json:"SizeThreshold"` // 最小文件大小阈值
|
||||||
Compression CompressionConfig `json:"Compression"`
|
MaxSize int64 `json:"MaxSize"` // 最大文件大小阈值
|
||||||
}
|
|
||||||
|
|
||||||
var temp TempConfig
|
|
||||||
if err := json.Unmarshal(data, &temp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 MAP
|
|
||||||
c.MAP = make(map[string]PathConfig)
|
|
||||||
|
|
||||||
// 处理每个路径配置
|
|
||||||
for key, raw := range temp.MAP {
|
|
||||||
// 尝试作为字符串解析
|
|
||||||
var strValue string
|
|
||||||
if err := json.Unmarshal(raw, &strValue); err == nil {
|
|
||||||
pathConfig := PathConfig{
|
|
||||||
DefaultTarget: strValue,
|
|
||||||
}
|
|
||||||
pathConfig.ProcessExtensionMap() // 处理扩展名映射
|
|
||||||
c.MAP[key] = pathConfig
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不是字符串,尝试作为PathConfig解析
|
|
||||||
var pathConfig PathConfig
|
|
||||||
if err := json.Unmarshal(raw, &pathConfig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pathConfig.ProcessExtensionMap() // 处理扩展名映射
|
|
||||||
c.MAP[key] = pathConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制其他字段
|
|
||||||
c.Compression = temp.Compression
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加处理扩展名映射的方法
|
// 处理扩展名映射的方法
|
||||||
func (p *PathConfig) ProcessExtensionMap() {
|
func (p *PathConfig) ProcessExtensionMap() {
|
||||||
|
p.ExtRules = nil
|
||||||
|
|
||||||
if p.ExtensionMap == nil {
|
if p.ExtensionMap == nil {
|
||||||
p.processedExtMap = nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新创建processedExtMap,确保它是最新的
|
// 处理扩展名规则
|
||||||
p.processedExtMap = make(map[string]string)
|
for _, rule := range p.ExtensionMap {
|
||||||
|
extRule := ExtensionRule{
|
||||||
|
Target: rule.Target,
|
||||||
|
SizeThreshold: rule.SizeThreshold,
|
||||||
|
MaxSize: rule.MaxSize,
|
||||||
|
}
|
||||||
|
|
||||||
for exts, target := range p.ExtensionMap {
|
// 处理扩展名列表
|
||||||
// 分割扩展名
|
for _, ext := range strings.Split(rule.Extensions, ",") {
|
||||||
for _, ext := range strings.Split(exts, ",") {
|
ext = strings.TrimSpace(ext)
|
||||||
ext = strings.TrimSpace(ext) // 移除可能的空格
|
|
||||||
if ext != "" {
|
if ext != "" {
|
||||||
p.processedExtMap[ext] = target
|
extRule.Extensions = append(extRule.Extensions, ext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加获取目标URL的方法
|
if len(extRule.Extensions) > 0 {
|
||||||
func (p *PathConfig) GetTargetForExt(ext string) string {
|
p.ExtRules = append(p.ExtRules, extRule)
|
||||||
if p.processedExtMap == nil {
|
|
||||||
p.ProcessExtensionMap()
|
|
||||||
}
|
}
|
||||||
if target, exists := p.processedExtMap[ext]; exists {
|
|
||||||
return target
|
|
||||||
}
|
}
|
||||||
return p.DefaultTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加检查扩展名是否存在的方法
|
|
||||||
func (p *PathConfig) GetExtensionTarget(ext string) (string, bool) {
|
|
||||||
if p.processedExtMap == nil {
|
|
||||||
p.ProcessExtensionMap()
|
|
||||||
}
|
|
||||||
target, exists := p.processedExtMap[ext]
|
|
||||||
return target, exists
|
|
||||||
}
|
}
|
||||||
|
137
internal/initapp/config_migration_20250322.go
Normal file
137
internal/initapp/config_migration_20250322.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package initapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 旧配置结构
|
||||||
|
type OldPathConfig struct {
|
||||||
|
DefaultTarget string `json:"DefaultTarget"`
|
||||||
|
ExtensionMap interface{} `json:"ExtensionMap"`
|
||||||
|
SizeThreshold int64 `json:"SizeThreshold,omitempty"`
|
||||||
|
MaxSize int64 `json:"MaxSize,omitempty"`
|
||||||
|
Path string `json:"Path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新配置结构
|
||||||
|
type NewPathConfig struct {
|
||||||
|
DefaultTarget string `json:"DefaultTarget"`
|
||||||
|
ExtensionMap []ExtRuleConfig `json:"ExtensionMap"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtRuleConfig struct {
|
||||||
|
Extensions string `json:"Extensions"`
|
||||||
|
Target string `json:"Target"`
|
||||||
|
SizeThreshold int64 `json:"SizeThreshold"`
|
||||||
|
MaxSize int64 `json:"MaxSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompressionConfig struct {
|
||||||
|
Gzip CompressorConfig `json:"Gzip"`
|
||||||
|
Brotli CompressorConfig `json:"Brotli"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompressorConfig struct {
|
||||||
|
Enabled bool `json:"Enabled"`
|
||||||
|
Level int `json:"Level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
MAP map[string]interface{} `json:"MAP"`
|
||||||
|
Compression CompressionConfig `json:"Compression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateConfig 检查并迁移配置文件,确保使用新格式
|
||||||
|
func MigrateConfig(configPath string) error {
|
||||||
|
// 读取配置文件
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析为通用配置结构
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configChanged := false
|
||||||
|
|
||||||
|
// 遍历所有路径配置
|
||||||
|
for path, rawPathConfig := range config.MAP {
|
||||||
|
// 将接口转换为JSON
|
||||||
|
rawData, err := json.Marshal(rawPathConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Init] 无法序列化路径 %s 的配置: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析为旧格式
|
||||||
|
var oldPathConfig OldPathConfig
|
||||||
|
if err := json.Unmarshal(rawData, &oldPathConfig); err != nil {
|
||||||
|
log.Printf("[Init] 无法解析路径 %s 的配置: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新格式配置
|
||||||
|
newPathConfig := NewPathConfig{
|
||||||
|
DefaultTarget: oldPathConfig.DefaultTarget,
|
||||||
|
ExtensionMap: []ExtRuleConfig{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查ExtensionMap类型
|
||||||
|
if oldPathConfig.ExtensionMap != nil {
|
||||||
|
// 尝试将ExtensionMap解析为旧格式的map
|
||||||
|
oldFormatMap := make(map[string]string)
|
||||||
|
if rawExtMap, err := json.Marshal(oldPathConfig.ExtensionMap); err == nil {
|
||||||
|
if json.Unmarshal(rawExtMap, &oldFormatMap) == nil && len(oldFormatMap) > 0 {
|
||||||
|
// 是旧格式的map,转换为数组
|
||||||
|
for exts, target := range oldFormatMap {
|
||||||
|
rule := ExtRuleConfig{
|
||||||
|
Extensions: exts,
|
||||||
|
Target: target,
|
||||||
|
SizeThreshold: oldPathConfig.SizeThreshold,
|
||||||
|
MaxSize: oldPathConfig.MaxSize,
|
||||||
|
}
|
||||||
|
newPathConfig.ExtensionMap = append(newPathConfig.ExtensionMap, rule)
|
||||||
|
}
|
||||||
|
configChanged = true
|
||||||
|
log.Printf("[Init] 路径 %s 的配置已从旧版格式迁移到新版格式", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试将ExtensionMap解析为新格式的数组
|
||||||
|
if len(newPathConfig.ExtensionMap) == 0 {
|
||||||
|
var newFormatArray []ExtRuleConfig
|
||||||
|
if rawExtMap, err := json.Marshal(oldPathConfig.ExtensionMap); err == nil {
|
||||||
|
if json.Unmarshal(rawExtMap, &newFormatArray) == nil {
|
||||||
|
newPathConfig.ExtensionMap = newFormatArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
config.MAP[path] = newPathConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有配置变更,保存回文件
|
||||||
|
newData, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, newData, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if configChanged {
|
||||||
|
log.Printf("[Init] 配置文件已成功迁移到新格式并保存: %s", configPath)
|
||||||
|
} else {
|
||||||
|
log.Printf("[Init] 配置文件格式已规范化: %s", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
19
internal/initapp/init.go
Normal file
19
internal/initapp/init.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package initapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(configPath string) error {
|
||||||
|
|
||||||
|
log.Printf("[Init] 开始初始化应用程序...")
|
||||||
|
|
||||||
|
// 迁移配置文件
|
||||||
|
if err := MigrateConfig(configPath); err != nil {
|
||||||
|
log.Printf("[Init] 配置迁移失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Init] 应用程序初始化完成")
|
||||||
|
return nil
|
||||||
|
}
|
@ -2,47 +2,25 @@ package metrics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
|
||||||
"proxy-go/internal/config"
|
"proxy-go/internal/config"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func Init(cfg *config.Config) error {
|
||||||
metricsStorage *MetricsStorage
|
// 初始化收集器
|
||||||
)
|
|
||||||
|
|
||||||
// InitMetricsStorage 初始化指标存储服务
|
|
||||||
func InitMetricsStorage(cfg *config.Config) error {
|
|
||||||
// 确保收集器已初始化
|
|
||||||
if err := InitCollector(cfg); err != nil {
|
if err := InitCollector(cfg); err != nil {
|
||||||
|
log.Printf("[Metrics] 初始化收集器失败: %v", err)
|
||||||
|
//继续运行
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建指标存储服务
|
// 初始化指标存储服务
|
||||||
dataDir := filepath.Join("data", "metrics")
|
if err := InitMetricsStorage(cfg); err != nil {
|
||||||
saveInterval := 30 * time.Minute // 默认30分钟保存一次,减少IO操作
|
log.Printf("[Metrics] 初始化指标存储服务失败: %v", err)
|
||||||
|
//继续运行
|
||||||
metricsStorage = NewMetricsStorage(GetCollector(), dataDir, saveInterval)
|
|
||||||
|
|
||||||
// 启动指标存储服务
|
|
||||||
if err := metricsStorage.Start(); err != nil {
|
|
||||||
log.Printf("[Metrics] 启动指标存储服务失败: %v", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[Metrics] 指标存储服务已初始化,保存间隔: %v", saveInterval)
|
log.Printf("[Metrics] 初始化完成")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopMetricsStorage 停止指标存储服务
|
|
||||||
func StopMetricsStorage() {
|
|
||||||
if metricsStorage != nil {
|
|
||||||
metricsStorage.Stop()
|
|
||||||
log.Printf("[Metrics] 指标存储服务已停止")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetricsStorage 获取指标存储服务实例
|
|
||||||
func GetMetricsStorage() *MetricsStorage {
|
|
||||||
return metricsStorage
|
|
||||||
}
|
|
||||||
|
44
internal/metrics/metricsstorage.go
Normal file
44
internal/metrics/metricsstorage.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"proxy-go/internal/config"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
metricsStorage *MetricsStorage
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitMetricsStorage 初始化指标存储服务
|
||||||
|
func InitMetricsStorage(cfg *config.Config) error {
|
||||||
|
|
||||||
|
// 创建指标存储服务
|
||||||
|
dataDir := filepath.Join("data", "metrics")
|
||||||
|
saveInterval := 30 * time.Minute // 默认30分钟保存一次,减少IO操作
|
||||||
|
|
||||||
|
metricsStorage = NewMetricsStorage(GetCollector(), dataDir, saveInterval)
|
||||||
|
|
||||||
|
// 启动指标存储服务
|
||||||
|
if err := metricsStorage.Start(); err != nil {
|
||||||
|
log.Printf("[Metrics] 启动指标存储服务失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Metrics] 指标存储服务已初始化,保存间隔: %v", saveInterval)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopMetricsStorage 停止指标存储服务
|
||||||
|
func StopMetricsStorage() {
|
||||||
|
if metricsStorage != nil {
|
||||||
|
metricsStorage.Stop()
|
||||||
|
log.Printf("[Metrics] 指标存储服务已停止")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricsStorage 获取指标存储服务实例
|
||||||
|
func GetMetricsStorage() *MetricsStorage {
|
||||||
|
return metricsStorage
|
||||||
|
}
|
@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"proxy-go/internal/config"
|
"proxy-go/internal/config"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -188,33 +189,90 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo
|
|||||||
targetBase := pathConfig.DefaultTarget
|
targetBase := pathConfig.DefaultTarget
|
||||||
usedAltTarget := false
|
usedAltTarget := false
|
||||||
|
|
||||||
// 如果配置了扩展名映射
|
// 获取文件扩展名
|
||||||
if pathConfig.ExtensionMap != nil {
|
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
if ext != "" {
|
if ext != "" {
|
||||||
ext = ext[1:] // 移除开头的点
|
ext = ext[1:] // 移除开头的点
|
||||||
// 检查是否在扩展名映射中
|
} else {
|
||||||
if altTarget, exists := pathConfig.GetExtensionTarget(ext); exists {
|
log.Printf("[Route] %s -> %s (无扩展名)", path, targetBase)
|
||||||
// 检查文件大小
|
// 即使没有扩展名,也要尝试匹配 * 通配符规则
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件大小
|
||||||
contentLength, err := GetFileSize(client, targetBase+path)
|
contentLength, err := GetFileSize(client, targetBase+path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Route] %s -> %s (error getting size: %v)", path, targetBase, err)
|
log.Printf("[Route] %s -> %s (获取文件大小出错: %v)", path, targetBase, err)
|
||||||
return targetBase, false
|
return targetBase, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有设置最小阈值,使用默认值 500KB
|
// 获取匹配的扩展名规则
|
||||||
minThreshold := pathConfig.SizeThreshold
|
matchingRules := []config.ExtensionRule{}
|
||||||
if minThreshold <= 0 {
|
wildcardRules := []config.ExtensionRule{} // 存储通配符规则
|
||||||
minThreshold = 500 * 1024
|
|
||||||
|
// 处理扩展名,找出所有匹配的规则
|
||||||
|
if pathConfig.ExtRules == nil {
|
||||||
|
pathConfig.ProcessExtensionMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有设置最大阈值,使用默认值 10MB
|
// 找出所有匹配当前扩展名的规则
|
||||||
maxThreshold := pathConfig.MaxSize
|
ext = strings.ToLower(ext)
|
||||||
if maxThreshold <= 0 {
|
for _, rule := range pathConfig.ExtRules {
|
||||||
maxThreshold = 10 * 1024 * 1024
|
// 处理阈值默认值
|
||||||
|
if rule.SizeThreshold <= 0 {
|
||||||
|
rule.SizeThreshold = 500 * 1024 // 默认最小阈值 500KB
|
||||||
|
}
|
||||||
|
if rule.MaxSize <= 0 {
|
||||||
|
rule.MaxSize = 10 * 1024 * 1024 // 默认最大阈值 10MB
|
||||||
}
|
}
|
||||||
|
|
||||||
if contentLength > minThreshold && contentLength <= maxThreshold {
|
// 检查是否包含通配符
|
||||||
|
if slices.Contains(rule.Extensions, "*") {
|
||||||
|
wildcardRules = append(wildcardRules, rule)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查具体扩展名匹配
|
||||||
|
if slices.Contains(rule.Extensions, ext) {
|
||||||
|
matchingRules = append(matchingRules, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到匹配的具体扩展名规则,使用通配符规则
|
||||||
|
if len(matchingRules) == 0 {
|
||||||
|
if len(wildcardRules) > 0 {
|
||||||
|
log.Printf("[Route] %s -> 使用通配符规则 (扩展名: %s)", path, ext)
|
||||||
|
matchingRules = wildcardRules
|
||||||
|
} else {
|
||||||
|
log.Printf("[Route] %s -> %s (没有找到扩展名 %s 的规则)", path, targetBase, ext)
|
||||||
|
return targetBase, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按阈值排序规则,优先使用阈值范围更精确的规则
|
||||||
|
// 先按最小阈值升序排序,再按最大阈值降序排序(在最小阈值相同的情况下)
|
||||||
|
sort.Slice(matchingRules, func(i, j int) bool {
|
||||||
|
if matchingRules[i].SizeThreshold == matchingRules[j].SizeThreshold {
|
||||||
|
return matchingRules[i].MaxSize > matchingRules[j].MaxSize
|
||||||
|
}
|
||||||
|
return matchingRules[i].SizeThreshold < matchingRules[j].SizeThreshold
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据文件大小找出最匹配的规则
|
||||||
|
var bestRule *config.ExtensionRule
|
||||||
|
|
||||||
|
for i := range matchingRules {
|
||||||
|
rule := &matchingRules[i]
|
||||||
|
|
||||||
|
// 检查文件大小是否在阈值范围内
|
||||||
|
if contentLength > rule.SizeThreshold && contentLength <= rule.MaxSize {
|
||||||
|
// 找到匹配的规则
|
||||||
|
bestRule = rule
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到匹配的规则
|
||||||
|
if bestRule != nil {
|
||||||
// 创建一个带超时的 context
|
// 创建一个带超时的 context
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -227,7 +285,7 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo
|
|||||||
|
|
||||||
// 在 goroutine 中检查备用源可访问性
|
// 在 goroutine 中检查备用源可访问性
|
||||||
go func() {
|
go func() {
|
||||||
accessible := isTargetAccessible(client, altTarget+path)
|
accessible := isTargetAccessible(client, bestRule.Target+path)
|
||||||
select {
|
select {
|
||||||
case altChan <- struct {
|
case altChan <- struct {
|
||||||
accessible bool
|
accessible bool
|
||||||
@ -242,32 +300,37 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo
|
|||||||
select {
|
select {
|
||||||
case result := <-altChan:
|
case result := <-altChan:
|
||||||
if result.accessible {
|
if result.accessible {
|
||||||
log.Printf("[Route] %s -> %s (size: %s > %s and <= %s)",
|
log.Printf("[Route] %s -> %s (文件大小: %s, 在区间 %s 到 %s 之间)",
|
||||||
path, altTarget, FormatBytes(contentLength),
|
path, bestRule.Target, FormatBytes(contentLength),
|
||||||
FormatBytes(minThreshold), FormatBytes(maxThreshold))
|
FormatBytes(bestRule.SizeThreshold), FormatBytes(bestRule.MaxSize))
|
||||||
return altTarget, true
|
return bestRule.Target, true
|
||||||
}
|
}
|
||||||
log.Printf("[Route] %s -> %s (fallback: alternative target not accessible)",
|
// 如果是通配符规则但不可访问,记录日志
|
||||||
|
if slices.Contains(bestRule.Extensions, "*") {
|
||||||
|
log.Printf("[Route] %s -> %s (回退: 通配符规则目标不可访问)",
|
||||||
path, targetBase)
|
path, targetBase)
|
||||||
|
} else {
|
||||||
|
log.Printf("[Route] %s -> %s (回退: 备用目标不可访问)",
|
||||||
|
path, targetBase)
|
||||||
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Printf("[Route] %s -> %s (fallback: alternative target check timeout)",
|
log.Printf("[Route] %s -> %s (回退: 备用目标检查超时)",
|
||||||
path, targetBase)
|
path, targetBase)
|
||||||
}
|
}
|
||||||
} else if contentLength <= minThreshold {
|
|
||||||
log.Printf("[Route] %s -> %s (size: %s <= %s)",
|
|
||||||
path, targetBase, FormatBytes(contentLength), FormatBytes(minThreshold))
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[Route] %s -> %s (size: %s > %s)",
|
// 记录日志,为什么没有匹配的规则
|
||||||
path, targetBase, FormatBytes(contentLength), FormatBytes(maxThreshold))
|
allThresholds := ""
|
||||||
|
for i, rule := range matchingRules {
|
||||||
|
if i > 0 {
|
||||||
|
allThresholds += ", "
|
||||||
}
|
}
|
||||||
} else {
|
allThresholds += fmt.Sprintf("[%s-%s]",
|
||||||
log.Printf("[Route] %s -> %s (no extension mapping)", path, targetBase)
|
FormatBytes(rule.SizeThreshold),
|
||||||
|
FormatBytes(rule.MaxSize))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.Printf("[Route] %s -> %s (no extension)", path, targetBase)
|
log.Printf("[Route] %s -> %s (文件大小: %s 不在任何阈值范围内: %s)",
|
||||||
}
|
path, targetBase, FormatBytes(contentLength), allThresholds)
|
||||||
} else {
|
|
||||||
log.Printf("[Route] %s -> %s (no extension map)", path, targetBase)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetBase, usedAltTarget
|
return targetBase, usedAltTarget
|
||||||
|
35
main.go
35
main.go
@ -10,10 +10,13 @@ import (
|
|||||||
"proxy-go/internal/config"
|
"proxy-go/internal/config"
|
||||||
"proxy-go/internal/constants"
|
"proxy-go/internal/constants"
|
||||||
"proxy-go/internal/handler"
|
"proxy-go/internal/handler"
|
||||||
|
"proxy-go/internal/initapp"
|
||||||
"proxy-go/internal/metrics"
|
"proxy-go/internal/metrics"
|
||||||
"proxy-go/internal/middleware"
|
"proxy-go/internal/middleware"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Route 定义路由结构
|
// Route 定义路由结构
|
||||||
@ -25,25 +28,29 @@ type Route struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 加载配置
|
// 加载.env文件
|
||||||
cfg, err := config.Load("data/config.json")
|
if err := godotenv.Load(); err != nil {
|
||||||
if err != nil {
|
log.Printf("警告: 无法加载.env文件: %v", err)
|
||||||
log.Fatal("Error loading config:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化应用程序(包括配置迁移)
|
||||||
|
configPath := "data/config.json"
|
||||||
|
initapp.Init(configPath)
|
||||||
|
|
||||||
|
// 初始化配置管理器
|
||||||
|
configManager, err := config.Init(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error initializing config manager:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
cfg := configManager.GetConfig()
|
||||||
|
|
||||||
// 更新常量配置
|
// 更新常量配置
|
||||||
constants.UpdateFromConfig(cfg)
|
constants.UpdateFromConfig(cfg)
|
||||||
|
|
||||||
// 初始化指标收集器
|
// 初始化统计服务
|
||||||
if err := metrics.InitCollector(cfg); err != nil {
|
metrics.Init(cfg)
|
||||||
log.Fatal("Error initializing metrics collector:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化指标存储服务
|
|
||||||
if err := metrics.InitMetricsStorage(cfg); err != nil {
|
|
||||||
log.Printf("Warning: Failed to initialize metrics storage: %v", err)
|
|
||||||
// 不致命,继续运行
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建压缩管理器
|
// 创建压缩管理器
|
||||||
compManager := compression.NewManager(compression.Config{
|
compManager := compression.NewManager(compression.Config{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from "react"
|
import React, { useEffect, useState, useCallback, useRef } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
@ -8,14 +8,6 @@ import { useRouter } from "next/navigation"
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -37,11 +29,18 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
|
|
||||||
|
interface ExtRuleConfig {
|
||||||
|
Extensions: string; // 逗号分隔的扩展名
|
||||||
|
Target: string; // 目标服务器
|
||||||
|
SizeThreshold: number; // 最小文件大小阈值(字节)
|
||||||
|
MaxSize: number; // 最大文件大小阈值(字节)
|
||||||
|
}
|
||||||
|
|
||||||
interface PathMapping {
|
interface PathMapping {
|
||||||
DefaultTarget: string
|
DefaultTarget: string
|
||||||
ExtensionMap?: Record<string, string>
|
ExtensionMap?: ExtRuleConfig[] // 只支持新格式
|
||||||
SizeThreshold?: number // 最小文件大小阈值
|
SizeThreshold?: number // 保留全局阈值字段(向后兼容)
|
||||||
MaxSize?: number // 最大文件大小阈值
|
MaxSize?: number // 保留全局阈值字段(向后兼容)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompressionConfig {
|
interface CompressionConfig {
|
||||||
@ -85,10 +84,7 @@ export default function ConfigPage() {
|
|||||||
maxSizeUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
maxSizeUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
||||||
})
|
})
|
||||||
|
|
||||||
const [extensionMapDialogOpen, setExtensionMapDialogOpen] = useState(false)
|
|
||||||
const [editingPath, setEditingPath] = useState<string | null>(null)
|
const [editingPath, setEditingPath] = useState<string | null>(null)
|
||||||
const [editingExtension, setEditingExtension] = useState<{ext: string, target: string} | null>(null)
|
|
||||||
const [newExtension, setNewExtension] = useState({ ext: "", target: "" })
|
|
||||||
|
|
||||||
const [editingPathData, setEditingPathData] = useState<{
|
const [editingPathData, setEditingPathData] = useState<{
|
||||||
path: string;
|
path: string;
|
||||||
@ -100,7 +96,36 @@ export default function ConfigPage() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [deletingPath, setDeletingPath] = useState<string | null>(null)
|
const [deletingPath, setDeletingPath] = useState<string | null>(null)
|
||||||
const [deletingExtension, setDeletingExtension] = useState<{path: string, ext: string} | null>(null)
|
|
||||||
|
// 添加扩展名规则状态
|
||||||
|
const [newExtensionRule, setNewExtensionRule] = useState<{
|
||||||
|
extensions: string;
|
||||||
|
target: string;
|
||||||
|
sizeThreshold: number;
|
||||||
|
maxSize: number;
|
||||||
|
sizeThresholdUnit: 'B' | 'KB' | 'MB' | 'GB';
|
||||||
|
maxSizeUnit: 'B' | 'KB' | 'MB' | 'GB';
|
||||||
|
}>({
|
||||||
|
extensions: "",
|
||||||
|
target: "",
|
||||||
|
sizeThreshold: 0,
|
||||||
|
maxSize: 0,
|
||||||
|
sizeThresholdUnit: 'MB',
|
||||||
|
maxSizeUnit: 'MB',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [editingExtensionRule, setEditingExtensionRule] = useState<{
|
||||||
|
index: number,
|
||||||
|
extensions: string;
|
||||||
|
target: string;
|
||||||
|
sizeThreshold: number;
|
||||||
|
maxSize: number;
|
||||||
|
sizeThresholdUnit: 'B' | 'KB' | 'MB' | 'GB';
|
||||||
|
maxSizeUnit: 'B' | 'KB' | 'MB' | 'GB';
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// 添加扩展名规则对话框状态
|
||||||
|
const [extensionRuleDialogOpen, setExtensionRuleDialogOpen] = useState(false);
|
||||||
|
|
||||||
const fetchConfig = useCallback(async () => {
|
const fetchConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -266,22 +291,11 @@ export default function ConfigPage() {
|
|||||||
})
|
})
|
||||||
}, [handleDialogOpenChange])
|
}, [handleDialogOpenChange])
|
||||||
|
|
||||||
const handleExtensionMapDialogOpenChange = useCallback((open: boolean) => {
|
|
||||||
handleDialogOpenChange(open, (isOpen) => {
|
|
||||||
setExtensionMapDialogOpen(isOpen)
|
|
||||||
if (!isOpen) {
|
|
||||||
setEditingPath(null)
|
|
||||||
setEditingExtension(null)
|
|
||||||
setNewExtension({ ext: "", target: "" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [handleDialogOpenChange])
|
|
||||||
|
|
||||||
const addOrUpdatePath = () => {
|
const addOrUpdatePath = () => {
|
||||||
if (!config) return
|
if (!config) return
|
||||||
|
|
||||||
const data = editingPathData || newPathData
|
const data = editingPathData || newPathData
|
||||||
const { path, defaultTarget, sizeThreshold, maxSize, sizeThresholdUnit, maxSizeUnit } = data
|
const { path, defaultTarget } = data
|
||||||
|
|
||||||
if (!path || !defaultTarget) {
|
if (!path || !defaultTarget) {
|
||||||
toast({
|
toast({
|
||||||
@ -292,26 +306,10 @@ export default function ConfigPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换大小为字节
|
|
||||||
const sizeThresholdBytes = convertToBytes(sizeThreshold, sizeThresholdUnit)
|
|
||||||
const maxSizeBytes = convertToBytes(maxSize, maxSizeUnit)
|
|
||||||
|
|
||||||
// 验证阈值
|
|
||||||
if (maxSizeBytes > 0 && sizeThresholdBytes >= maxSizeBytes) {
|
|
||||||
toast({
|
|
||||||
title: "错误",
|
|
||||||
description: "最大文件大小阈值必须大于最小文件大小阈值",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newConfig = { ...config }
|
const newConfig = { ...config }
|
||||||
const pathConfig: PathMapping = {
|
const pathConfig: PathMapping = {
|
||||||
DefaultTarget: defaultTarget,
|
DefaultTarget: defaultTarget,
|
||||||
ExtensionMap: {},
|
ExtensionMap: []
|
||||||
SizeThreshold: sizeThresholdBytes,
|
|
||||||
MaxSize: maxSizeBytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是编辑现有路径,保留原有的扩展名映射
|
// 如果是编辑现有路径,保留原有的扩展名映射
|
||||||
@ -363,98 +361,14 @@ export default function ConfigPage() {
|
|||||||
updateConfig(newConfig)
|
updateConfig(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExtensionMapEdit = (path: string, ext?: string, target?: string) => {
|
const handleExtensionMapEdit = (path: string) => {
|
||||||
setEditingPath(path)
|
// 将添加规则的操作重定向到handleExtensionRuleEdit
|
||||||
if (ext && target) {
|
handleExtensionRuleEdit(path);
|
||||||
setEditingExtension({ ext, target })
|
};
|
||||||
setNewExtension({ ext, target })
|
|
||||||
} else {
|
|
||||||
setEditingExtension(null)
|
|
||||||
setNewExtension({ ext: "", target: "" })
|
|
||||||
}
|
|
||||||
setExtensionMapDialogOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addOrUpdateExtensionMap = () => {
|
const deleteExtensionRule = (path: string, index: number) => {
|
||||||
if (!config || !editingPath) return
|
setDeletingExtensionRule({ path, index });
|
||||||
const { ext, target } = newExtension
|
};
|
||||||
|
|
||||||
// 验证输入
|
|
||||||
if (!ext.trim() || !target.trim()) {
|
|
||||||
toast({
|
|
||||||
title: "错误",
|
|
||||||
description: "扩展名和目标不能为空",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证扩展名格式
|
|
||||||
const extensions = ext.split(',').map(e => e.trim())
|
|
||||||
if (extensions.some(e => !e || e.includes('.'))) {
|
|
||||||
toast({
|
|
||||||
title: "错误",
|
|
||||||
description: "扩展名格式不正确,不需要包含点号",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证URL格式
|
|
||||||
try {
|
|
||||||
new URL(target)
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: "错误",
|
|
||||||
description: "目标URL格式不正确",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newConfig = { ...config }
|
|
||||||
const mapping = newConfig.MAP[editingPath]
|
|
||||||
if (typeof mapping === "string") {
|
|
||||||
newConfig.MAP[editingPath] = {
|
|
||||||
DefaultTarget: mapping,
|
|
||||||
ExtensionMap: { [ext]: target }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果是编辑现有的扩展名映射,先删除旧的
|
|
||||||
if (editingExtension) {
|
|
||||||
const newExtMap = { ...mapping.ExtensionMap }
|
|
||||||
delete newExtMap[editingExtension.ext]
|
|
||||||
mapping.ExtensionMap = newExtMap
|
|
||||||
}
|
|
||||||
// 添加新的映射
|
|
||||||
mapping.ExtensionMap = {
|
|
||||||
...mapping.ExtensionMap,
|
|
||||||
[ext]: target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig(newConfig)
|
|
||||||
setExtensionMapDialogOpen(false)
|
|
||||||
setEditingExtension(null)
|
|
||||||
setNewExtension({ ext: "", target: "" })
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteExtensionMap = (path: string, ext: string) => {
|
|
||||||
setDeletingExtension({ path, ext })
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDeleteExtensionMap = () => {
|
|
||||||
if (!config || !deletingExtension) return
|
|
||||||
const newConfig = { ...config }
|
|
||||||
const mapping = newConfig.MAP[deletingExtension.path]
|
|
||||||
if (typeof mapping !== "string" && mapping.ExtensionMap) {
|
|
||||||
const newExtensionMap = { ...mapping.ExtensionMap }
|
|
||||||
delete newExtensionMap[deletingExtension.ext]
|
|
||||||
mapping.ExtensionMap = newExtensionMap
|
|
||||||
}
|
|
||||||
updateConfig(newConfig)
|
|
||||||
setDeletingExtension(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openAddPathDialog = () => {
|
const openAddPathDialog = () => {
|
||||||
setEditingPathData(null)
|
setEditingPathData(null)
|
||||||
@ -587,6 +501,196 @@ export default function ConfigPage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 为扩展名规则对话框添加处理函数
|
||||||
|
const handleExtensionRuleDialogOpenChange = useCallback((open: boolean) => {
|
||||||
|
handleDialogOpenChange(open, (isOpen) => {
|
||||||
|
setExtensionRuleDialogOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setEditingExtensionRule(null);
|
||||||
|
setNewExtensionRule({
|
||||||
|
extensions: "",
|
||||||
|
target: "",
|
||||||
|
sizeThreshold: 0,
|
||||||
|
maxSize: 0,
|
||||||
|
sizeThresholdUnit: 'MB',
|
||||||
|
maxSizeUnit: 'MB',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [handleDialogOpenChange]);
|
||||||
|
|
||||||
|
// 处理扩展名规则的编辑
|
||||||
|
const handleExtensionRuleEdit = (path: string, index?: number, rule?: { Extensions: string; Target: string; SizeThreshold?: number; MaxSize?: number }) => {
|
||||||
|
setEditingPath(path);
|
||||||
|
|
||||||
|
if (index !== undefined && rule) {
|
||||||
|
// 转换规则的阈值到合适的单位显示
|
||||||
|
const { value: thresholdValue, unit: thresholdUnit } = convertBytesToUnit(rule.SizeThreshold || 0);
|
||||||
|
const { value: maxValue, unit: maxUnit } = convertBytesToUnit(rule.MaxSize || 0);
|
||||||
|
|
||||||
|
setEditingExtensionRule({
|
||||||
|
index,
|
||||||
|
extensions: rule.Extensions,
|
||||||
|
target: rule.Target,
|
||||||
|
sizeThreshold: thresholdValue,
|
||||||
|
maxSize: maxValue,
|
||||||
|
sizeThresholdUnit: thresholdUnit,
|
||||||
|
maxSizeUnit: maxUnit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同时更新表单显示数据
|
||||||
|
setNewExtensionRule({
|
||||||
|
extensions: rule.Extensions,
|
||||||
|
target: rule.Target,
|
||||||
|
sizeThreshold: thresholdValue,
|
||||||
|
maxSize: maxValue,
|
||||||
|
sizeThresholdUnit: thresholdUnit,
|
||||||
|
maxSizeUnit: maxUnit,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setEditingExtensionRule(null);
|
||||||
|
// 重置表单
|
||||||
|
setNewExtensionRule({
|
||||||
|
extensions: "",
|
||||||
|
target: "",
|
||||||
|
sizeThreshold: 0,
|
||||||
|
maxSize: 0,
|
||||||
|
sizeThresholdUnit: 'MB',
|
||||||
|
maxSizeUnit: 'MB',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setExtensionRuleDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加或更新扩展名规则
|
||||||
|
const addOrUpdateExtensionRule = () => {
|
||||||
|
if (!config || !editingPath) return;
|
||||||
|
|
||||||
|
const { extensions, target, sizeThreshold, maxSize, sizeThresholdUnit, maxSizeUnit } = newExtensionRule;
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
if (!extensions.trim() || !target.trim()) {
|
||||||
|
toast({
|
||||||
|
title: "错误",
|
||||||
|
description: "扩展名和目标不能为空",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证扩展名格式
|
||||||
|
const extensionList = extensions.split(',').map(e => e.trim());
|
||||||
|
if (extensionList.some(e => !e || (e !== "*" && e.includes('.')))) {
|
||||||
|
toast({
|
||||||
|
title: "错误",
|
||||||
|
description: "扩展名格式不正确,不需要包含点号",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URL格式
|
||||||
|
try {
|
||||||
|
new URL(target);
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: "错误",
|
||||||
|
description: "目标URL格式不正确",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换大小为字节
|
||||||
|
const sizeThresholdBytes = convertToBytes(sizeThreshold, sizeThresholdUnit);
|
||||||
|
const maxSizeBytes = convertToBytes(maxSize, maxSizeUnit);
|
||||||
|
|
||||||
|
// 验证阈值
|
||||||
|
if (maxSizeBytes > 0 && sizeThresholdBytes >= maxSizeBytes) {
|
||||||
|
toast({
|
||||||
|
title: "错误",
|
||||||
|
description: "最大文件大小阈值必须大于最小文件大小阈值",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = { ...config };
|
||||||
|
const mapping = newConfig.MAP[editingPath];
|
||||||
|
|
||||||
|
if (typeof mapping === "string") {
|
||||||
|
// 如果映射是字符串,创建新的PathConfig对象
|
||||||
|
newConfig.MAP[editingPath] = {
|
||||||
|
DefaultTarget: mapping,
|
||||||
|
ExtensionMap: [{
|
||||||
|
Extensions: extensions,
|
||||||
|
Target: target,
|
||||||
|
SizeThreshold: sizeThresholdBytes,
|
||||||
|
MaxSize: maxSizeBytes
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 确保ExtensionMap是数组
|
||||||
|
if (!Array.isArray(mapping.ExtensionMap)) {
|
||||||
|
mapping.ExtensionMap = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingExtensionRule) {
|
||||||
|
// 更新现有规则
|
||||||
|
const rules = mapping.ExtensionMap as ExtRuleConfig[];
|
||||||
|
rules[editingExtensionRule.index] = {
|
||||||
|
Extensions: extensions,
|
||||||
|
Target: target,
|
||||||
|
SizeThreshold: sizeThresholdBytes,
|
||||||
|
MaxSize: maxSizeBytes
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 添加新规则
|
||||||
|
mapping.ExtensionMap.push({
|
||||||
|
Extensions: extensions,
|
||||||
|
Target: target,
|
||||||
|
SizeThreshold: sizeThresholdBytes,
|
||||||
|
MaxSize: maxSizeBytes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(newConfig);
|
||||||
|
setExtensionRuleDialogOpen(false);
|
||||||
|
setEditingExtensionRule(null);
|
||||||
|
setNewExtensionRule({
|
||||||
|
extensions: "",
|
||||||
|
target: "",
|
||||||
|
sizeThreshold: 0,
|
||||||
|
maxSize: 0,
|
||||||
|
sizeThresholdUnit: 'MB',
|
||||||
|
maxSizeUnit: 'MB',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除扩展名规则
|
||||||
|
const [deletingExtensionRule, setDeletingExtensionRule] = useState<{path: string, index: number} | null>(null);
|
||||||
|
|
||||||
|
const confirmDeleteExtensionRule = () => {
|
||||||
|
if (!config || !deletingExtensionRule) return;
|
||||||
|
|
||||||
|
const newConfig = { ...config };
|
||||||
|
const mapping = newConfig.MAP[deletingExtensionRule.path];
|
||||||
|
|
||||||
|
if (typeof mapping !== "string" && Array.isArray(mapping.ExtensionMap)) {
|
||||||
|
// 移除指定索引的规则
|
||||||
|
const rules = mapping.ExtensionMap as ExtRuleConfig[];
|
||||||
|
mapping.ExtensionMap = [
|
||||||
|
...rules.slice(0, deletingExtensionRule.index),
|
||||||
|
...rules.slice(deletingExtensionRule.index + 1)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(newConfig);
|
||||||
|
setDeletingExtensionRule(null);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
<div className="flex h-[calc(100vh-4rem)] items-center justify-center">
|
||||||
@ -677,100 +781,6 @@ export default function ConfigPage() {
|
|||||||
默认的回源地址,所有请求都会转发到这个地址
|
默认的回源地址,所有请求都会转发到这个地址
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="sizeThreshold">最小文件大小阈值</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="sizeThreshold"
|
|
||||||
type="number"
|
|
||||||
value={editingPathData?.sizeThreshold ?? newPathData.sizeThreshold}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (editingPathData) {
|
|
||||||
setEditingPathData({
|
|
||||||
...editingPathData,
|
|
||||||
sizeThreshold: Number(e.target.value),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setNewPathData({
|
|
||||||
...newPathData,
|
|
||||||
sizeThreshold: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className="w-24 rounded-md border border-input bg-background px-3"
|
|
||||||
value={editingPathData?.sizeThresholdUnit ?? newPathData.sizeThresholdUnit}
|
|
||||||
onChange={(e) => {
|
|
||||||
const unit = e.target.value as 'B' | 'KB' | 'MB' | 'GB'
|
|
||||||
if (editingPathData) {
|
|
||||||
setEditingPathData({
|
|
||||||
...editingPathData,
|
|
||||||
sizeThresholdUnit: unit,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setNewPathData({
|
|
||||||
...newPathData,
|
|
||||||
sizeThresholdUnit: unit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="B">B</option>
|
|
||||||
<option value="KB">KB</option>
|
|
||||||
<option value="MB">MB</option>
|
|
||||||
<option value="GB">GB</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="maxSize">最大文件大小阈值</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="maxSize"
|
|
||||||
type="number"
|
|
||||||
value={editingPathData?.maxSize ?? newPathData.maxSize}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (editingPathData) {
|
|
||||||
setEditingPathData({
|
|
||||||
...editingPathData,
|
|
||||||
maxSize: Number(e.target.value),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setNewPathData({
|
|
||||||
...newPathData,
|
|
||||||
maxSize: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className="w-24 rounded-md border border-input bg-background px-3"
|
|
||||||
value={editingPathData?.maxSizeUnit ?? newPathData.maxSizeUnit}
|
|
||||||
onChange={(e) => {
|
|
||||||
const unit = e.target.value as 'B' | 'KB' | 'MB' | 'GB'
|
|
||||||
if (editingPathData) {
|
|
||||||
setEditingPathData({
|
|
||||||
...editingPathData,
|
|
||||||
maxSizeUnit: unit,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setNewPathData({
|
|
||||||
...newPathData,
|
|
||||||
maxSizeUnit: unit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="B">B</option>
|
|
||||||
<option value="KB">KB</option>
|
|
||||||
<option value="MB">MB</option>
|
|
||||||
<option value="GB">GB</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button onClick={addOrUpdatePath}>
|
<Button onClick={addOrUpdatePath}>
|
||||||
{editingPathData ? "保存" : "添加"}
|
{editingPathData ? "保存" : "添加"}
|
||||||
</Button>
|
</Button>
|
||||||
@ -779,119 +789,94 @@ export default function ConfigPage() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[10%]">路径</TableHead>
|
|
||||||
<TableHead className="w-[40%]">默认目标</TableHead>
|
|
||||||
<TableHead className="w-[10%]">最小阈值</TableHead>
|
|
||||||
<TableHead className="w-[10%]">最大阈值</TableHead>
|
|
||||||
<TableHead className="w-[15%]">扩展名映射</TableHead>
|
|
||||||
<TableHead className="w-[15%]">操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{config && Object.entries(config.MAP).map(([path, target]) => (
|
{config && Object.entries(config.MAP).map(([path, target]) => (
|
||||||
<>
|
<Card key={`${path}-card`} className="overflow-hidden">
|
||||||
<TableRow key={`${path}-main`}>
|
<CardHeader className="pb-2">
|
||||||
<TableCell>{path}</TableCell>
|
<CardTitle className="text-lg flex justify-between items-center">
|
||||||
<TableCell>
|
<span className="font-medium truncate" title={path}>{path}</span>
|
||||||
{typeof target === 'string' ? target : target.DefaultTarget}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{typeof target === 'object' && target.SizeThreshold ? (
|
|
||||||
<span title={`${target.SizeThreshold} 字节`}>
|
|
||||||
{formatBytes(target.SizeThreshold)}
|
|
||||||
</span>
|
|
||||||
) : '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{typeof target === 'object' && target.MaxSize ? (
|
|
||||||
<span title={`${target.MaxSize} 字节`}>
|
|
||||||
{formatBytes(target.MaxSize)}
|
|
||||||
</span>
|
|
||||||
) : '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleExtensionMapEdit(path)}
|
|
||||||
>
|
|
||||||
<Plus className="w-3 h-3 mr-2" />
|
|
||||||
添加扩展名映射
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleEditPath(path, target)}
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => deletePath(path)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{typeof target === 'object' && target.ExtensionMap && Object.keys(target.ExtensionMap).length > 0 && (
|
|
||||||
<TableRow key={`${path}-extensions`}>
|
|
||||||
<TableCell colSpan={6} className="p-0 border-t-0">
|
|
||||||
<div className="bg-muted/30 px-2 py-1 mx-4">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="border-0">
|
|
||||||
<TableHead className="w-[30%] h-8 text-xs">扩展名</TableHead>
|
|
||||||
<TableHead className="w-[50%] h-8 text-xs">目标地址</TableHead>
|
|
||||||
<TableHead className="w-[20%] h-8 text-xs">操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{Object.entries(target.ExtensionMap).map(([ext, url]) => (
|
|
||||||
<TableRow key={ext} className="border-0">
|
|
||||||
<TableCell className="py-1 text-sm">{ext}</TableCell>
|
|
||||||
<TableCell className="py-1 text-sm">
|
|
||||||
<span title={url}>{truncateUrl(url)}</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="py-1">
|
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="h-8 w-8"
|
||||||
onClick={() => handleExtensionMapEdit(path, ext, url)}
|
onClick={() => handleEditPath(path, target)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => deletePath(path)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-3">
|
||||||
|
<div className="text-sm text-muted-foreground mb-3">
|
||||||
|
<span className="font-medium text-primary">默认目标: </span>
|
||||||
|
<span className="break-all">{typeof target === 'string' ? target : target.DefaultTarget}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleExtensionMapEdit(path)}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加规则
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{typeof target === 'object' && target.ExtensionMap && Array.isArray(target.ExtensionMap) && target.ExtensionMap.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="text-sm font-semibold mb-2">扩展名映射规则</div>
|
||||||
|
<div className="space-y-2 max-h-[250px] overflow-y-auto pr-1">
|
||||||
|
{target.ExtensionMap.map((rule, index) => (
|
||||||
|
<div
|
||||||
|
key={`${path}-rule-${index}`}
|
||||||
|
className="bg-muted/30 rounded-md p-2 text-xs"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span className="font-semibold">{rule.Extensions}</span>
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5"
|
||||||
|
onClick={() => handleExtensionRuleEdit(path, index, rule)}
|
||||||
>
|
>
|
||||||
<Edit className="h-3 w-3" />
|
<Edit className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="h-5 w-5"
|
||||||
onClick={() => deleteExtensionMap(path, ext)}
|
onClick={() => deleteExtensionRule(path, index)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
<div className="text-muted-foreground truncate" title={rule.Target}>
|
||||||
</TableRow>
|
目标: {truncateUrl(rule.Target)}
|
||||||
)}
|
</div>
|
||||||
</>
|
<div className="flex justify-between mt-1 text-muted-foreground">
|
||||||
|
<div>阈值: {formatBytes(rule.SizeThreshold || 0)}</div>
|
||||||
|
<div>最大: {formatBytes(rule.MaxSize || 0)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="compression" className="space-y-6">
|
<TabsContent value="compression" className="space-y-6">
|
||||||
@ -949,43 +934,6 @@ export default function ConfigPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Dialog open={extensionMapDialogOpen} onOpenChange={handleExtensionMapDialogOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{editingExtension ? "编辑扩展名映射" : "添加扩展名映射"}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>扩展名</Label>
|
|
||||||
<Input
|
|
||||||
value={newExtension.ext}
|
|
||||||
onChange={(e) => setNewExtension({ ...newExtension, ext: e.target.value })}
|
|
||||||
placeholder="jpg,png,webp"
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
多个扩展名用逗号分隔,不需要包含点号
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>目标 URL</Label>
|
|
||||||
<Input
|
|
||||||
value={newExtension.target}
|
|
||||||
onChange={(e) => setNewExtension({ ...newExtension, target: e.target.value })}
|
|
||||||
placeholder="https://example.com"
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
当文件大小超过阈值且扩展名匹配时,将使用此地址
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={addOrUpdateExtensionMap}>
|
|
||||||
{editingExtension ? "保存" : "添加"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={!!deletingPath}
|
open={!!deletingPath}
|
||||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingPath)}
|
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingPath)}
|
||||||
@ -1004,20 +952,118 @@ export default function ConfigPage() {
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
<Dialog open={extensionRuleDialogOpen} onOpenChange={handleExtensionRuleDialogOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingExtensionRule ? "编辑扩展名规则" : "添加扩展名规则"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>扩展名</Label>
|
||||||
|
<Input
|
||||||
|
value={newExtensionRule.extensions}
|
||||||
|
onChange={(e) => setNewExtensionRule({ ...newExtensionRule, extensions: e.target.value })}
|
||||||
|
placeholder="jpg,png,webp"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
多个扩展名用逗号分隔,不需要包含点号。使用星号 * 表示匹配所有未指定的扩展名。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>目标 URL</Label>
|
||||||
|
<Input
|
||||||
|
value={newExtensionRule.target}
|
||||||
|
onChange={(e) => setNewExtensionRule({ ...newExtensionRule, target: e.target.value })}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="ruleSizeThreshold">最小文件大小阈值</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="ruleSizeThreshold"
|
||||||
|
type="number"
|
||||||
|
value={newExtensionRule.sizeThreshold}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewExtensionRule({
|
||||||
|
...newExtensionRule,
|
||||||
|
sizeThreshold: Number(e.target.value),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="w-24 rounded-md border border-input bg-background px-3"
|
||||||
|
value={newExtensionRule.sizeThresholdUnit}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewExtensionRule({
|
||||||
|
...newExtensionRule,
|
||||||
|
sizeThresholdUnit: e.target.value as 'B' | 'KB' | 'MB' | 'GB',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="KB">KB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
<option value="GB">GB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="ruleMaxSize">最大文件大小阈值</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="ruleMaxSize"
|
||||||
|
type="number"
|
||||||
|
value={newExtensionRule.maxSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewExtensionRule({
|
||||||
|
...newExtensionRule,
|
||||||
|
maxSize: Number(e.target.value),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="w-24 rounded-md border border-input bg-background px-3"
|
||||||
|
value={newExtensionRule.maxSizeUnit}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewExtensionRule({
|
||||||
|
...newExtensionRule,
|
||||||
|
maxSizeUnit: e.target.value as 'B' | 'KB' | 'MB' | 'GB',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="KB">KB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
<option value="GB">GB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={addOrUpdateExtensionRule}>
|
||||||
|
{editingExtensionRule ? "保存" : "添加"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={!!deletingExtension}
|
open={!!deletingExtensionRule}
|
||||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}
|
onOpenChange={(open) => handleDeleteDialogOpenChange(open, () => setDeletingExtensionRule(null))}
|
||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
确定要删除扩展名 “{deletingExtension?.ext}” 的映射吗?此操作无法撤销。
|
确定要删除这个扩展名规则吗?此操作无法撤销。
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={confirmDeleteExtensionMap}>删除</AlertDialogAction>
|
<AlertDialogAction onClick={confirmDeleteExtensionRule}>删除</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user