mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
删除固定路径代理配置, 因为普通代理好像已经支持了
This commit is contained in:
parent
f614692f33
commit
4af1592021
@ -76,7 +76,6 @@ func (c *configImpl) Update(newConfig *Config) {
|
||||
// 更新配置
|
||||
c.MAP = newConfig.MAP
|
||||
c.Compression = newConfig.Compression
|
||||
c.FixedPaths = newConfig.FixedPaths
|
||||
|
||||
// 触发回调
|
||||
for _, callback := range c.onConfigUpdate {
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
type Config struct {
|
||||
MAP map[string]PathConfig `json:"MAP"` // 改为使用PathConfig
|
||||
Compression CompressionConfig `json:"Compression"`
|
||||
FixedPaths []FixedPathConfig `json:"FixedPaths"`
|
||||
}
|
||||
|
||||
type PathConfig struct {
|
||||
@ -30,19 +29,12 @@ type CompressorConfig struct {
|
||||
Level int `json:"Level"`
|
||||
}
|
||||
|
||||
type FixedPathConfig struct {
|
||||
Path string `json:"Path"`
|
||||
TargetHost string `json:"TargetHost"`
|
||||
TargetURL string `json:"TargetURL"`
|
||||
}
|
||||
|
||||
// 添加一个辅助方法来处理字符串到 PathConfig 的转换
|
||||
func (c *Config) UnmarshalJSON(data []byte) error {
|
||||
// 创建一个临时结构来解析原始JSON
|
||||
type TempConfig struct {
|
||||
MAP map[string]json.RawMessage `json:"MAP"`
|
||||
Compression CompressionConfig `json:"Compression"`
|
||||
FixedPaths []FixedPathConfig `json:"FixedPaths"`
|
||||
}
|
||||
|
||||
var temp TempConfig
|
||||
@ -77,7 +69,6 @@ func (c *Config) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// 复制其他字段
|
||||
c.Compression = temp.Compression
|
||||
c.FixedPaths = temp.FixedPaths
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -9,14 +9,12 @@ import (
|
||||
type CacheAdminHandler struct {
|
||||
proxyCache *cache.CacheManager
|
||||
mirrorCache *cache.CacheManager
|
||||
fixedPathCache *cache.CacheManager
|
||||
}
|
||||
|
||||
func NewCacheAdminHandler(proxyCache, mirrorCache, fixedPathCache *cache.CacheManager) *CacheAdminHandler {
|
||||
func NewCacheAdminHandler(proxyCache, mirrorCache *cache.CacheManager) *CacheAdminHandler {
|
||||
return &CacheAdminHandler{
|
||||
proxyCache: proxyCache,
|
||||
mirrorCache: mirrorCache,
|
||||
fixedPathCache: fixedPathCache,
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +35,6 @@ func (h *CacheAdminHandler) GetCacheStats(w http.ResponseWriter, r *http.Request
|
||||
stats := map[string]cache.CacheStats{
|
||||
"proxy": h.proxyCache.GetStats(),
|
||||
"mirror": h.mirrorCache.GetStats(),
|
||||
"fixedPath": h.fixedPathCache.GetStats(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@ -54,7 +51,6 @@ func (h *CacheAdminHandler) GetCacheConfig(w http.ResponseWriter, r *http.Reques
|
||||
configs := map[string]cache.CacheConfig{
|
||||
"proxy": h.proxyCache.GetConfig(),
|
||||
"mirror": h.mirrorCache.GetConfig(),
|
||||
"fixedPath": h.fixedPathCache.GetConfig(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@ -69,7 +65,7 @@ func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
|
||||
Type string `json:"type"` // "proxy", "mirror"
|
||||
Config CacheConfig `json:"config"` // 新的配置
|
||||
}
|
||||
|
||||
@ -84,8 +80,6 @@ func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Req
|
||||
targetCache = h.proxyCache
|
||||
case "mirror":
|
||||
targetCache = h.mirrorCache
|
||||
case "fixedPath":
|
||||
targetCache = h.fixedPathCache
|
||||
default:
|
||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||
return
|
||||
@ -107,7 +101,7 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
|
||||
Type string `json:"type"` // "proxy", "mirror"
|
||||
Enabled bool `json:"enabled"` // true 或 false
|
||||
}
|
||||
|
||||
@ -121,8 +115,6 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
|
||||
h.proxyCache.SetEnabled(req.Enabled)
|
||||
case "mirror":
|
||||
h.mirrorCache.SetEnabled(req.Enabled)
|
||||
case "fixedPath":
|
||||
h.fixedPathCache.SetEnabled(req.Enabled)
|
||||
default:
|
||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||
return
|
||||
@ -139,7 +131,7 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type"` // "proxy", "mirror", "fixedPath" 或 "all"
|
||||
Type string `json:"type"` // "proxy", "mirror" 或 "all"
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@ -153,16 +145,11 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
|
||||
err = h.proxyCache.ClearCache()
|
||||
case "mirror":
|
||||
err = h.mirrorCache.ClearCache()
|
||||
case "fixedPath":
|
||||
err = h.fixedPathCache.ClearCache()
|
||||
case "all":
|
||||
err = h.proxyCache.ClearCache()
|
||||
if err == nil {
|
||||
err = h.mirrorCache.ClearCache()
|
||||
}
|
||||
if err == nil {
|
||||
err = h.fixedPathCache.ClearCache()
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||
return
|
||||
|
@ -118,18 +118,5 @@ func (h *ConfigHandler) validateConfig(cfg *config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证FixedPaths配置
|
||||
for _, fp := range cfg.FixedPaths {
|
||||
if fp.Path == "" {
|
||||
return fmt.Errorf("固定路径不能为空")
|
||||
}
|
||||
if fp.TargetURL == "" {
|
||||
return fmt.Errorf("固定路径 %s 的目标URL不能为空", fp.Path)
|
||||
}
|
||||
if _, err := url.Parse(fp.TargetURL); err != nil {
|
||||
return fmt.Errorf("固定路径 %s 的目标URL无效: %v", fp.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -40,10 +40,6 @@ var hopHeadersBase = map[string]bool{
|
||||
"Upgrade": true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 移除旧的初始化代码,因为我们直接在 map 字面量中定义了所有值
|
||||
}
|
||||
|
||||
// ErrorHandler 定义错误处理函数类型
|
||||
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
|
||||
|
||||
|
@ -1,165 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"proxy-go/internal/cache"
|
||||
"proxy-go/internal/config"
|
||||
"proxy-go/internal/metrics"
|
||||
"proxy-go/internal/utils"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FixedPathConfig struct {
|
||||
Path string `json:"Path"`
|
||||
TargetHost string `json:"TargetHost"`
|
||||
TargetURL string `json:"TargetURL"`
|
||||
}
|
||||
|
||||
var fixedPathCache *cache.CacheManager
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
fixedPathCache, err = cache.NewCacheManager("data/fixed_path_cache")
|
||||
if err != nil {
|
||||
log.Printf("[Cache] Failed to initialize fixed path cache manager: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func FixedPathProxyMiddleware(configs []config.FixedPathConfig) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
collector := metrics.GetCollector()
|
||||
collector.BeginRequest()
|
||||
defer collector.EndRequest()
|
||||
|
||||
// 检查是否匹配任何固定路径
|
||||
for _, cfg := range configs {
|
||||
if strings.HasPrefix(r.URL.Path, cfg.Path) {
|
||||
// 创建新的请求
|
||||
targetPath := strings.TrimPrefix(r.URL.Path, cfg.Path)
|
||||
targetURL := cfg.TargetURL + targetPath
|
||||
|
||||
// 检查是否可以使用缓存
|
||||
if r.Method == http.MethodGet && fixedPathCache != nil {
|
||||
cacheKey := fixedPathCache.GenerateCacheKey(r)
|
||||
if item, hit, notModified := fixedPathCache.Get(cacheKey, r); hit {
|
||||
// 从缓存提供响应
|
||||
w.Header().Set("Content-Type", item.ContentType)
|
||||
if item.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", item.ContentEncoding)
|
||||
}
|
||||
w.Header().Set("Proxy-Go-Cache", "HIT")
|
||||
if notModified {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, item.FilePath)
|
||||
collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(startTime), item.Size, utils.GetClientIP(r), r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Error creating proxy request", http.StatusInternalServerError)
|
||||
log.Printf("[Fixed] ERR %s %s -> 500 (%s) create request error from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
|
||||
return
|
||||
}
|
||||
|
||||
// 复制原始请求的 header
|
||||
for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
proxyReq.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置必要的头部
|
||||
proxyReq.Host = cfg.TargetHost
|
||||
proxyReq.Header.Set("Host", cfg.TargetHost)
|
||||
proxyReq.Header.Set("X-Real-IP", utils.GetClientIP(r))
|
||||
proxyReq.Header.Set("X-Scheme", r.URL.Scheme)
|
||||
|
||||
// 发送代理请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(proxyReq)
|
||||
if err != nil {
|
||||
http.Error(w, "Error forwarding request", http.StatusBadGateway)
|
||||
log.Printf("[Fixed] ERR %s %s -> 502 (%s) proxy error from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 复制响应头
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
w.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Proxy-Go-Cache", "MISS")
|
||||
|
||||
// 设置响应状态码
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
var written int64
|
||||
// 如果是GET请求且响应成功,使用TeeReader同时写入缓存
|
||||
if r.Method == http.MethodGet && resp.StatusCode == http.StatusOK && fixedPathCache != nil {
|
||||
cacheKey := fixedPathCache.GenerateCacheKey(r)
|
||||
if cacheFile, err := fixedPathCache.CreateTemp(cacheKey, resp); err == nil {
|
||||
defer cacheFile.Close()
|
||||
teeReader := io.TeeReader(resp.Body, cacheFile)
|
||||
written, err = io.Copy(w, teeReader)
|
||||
if err == nil {
|
||||
fixedPathCache.Commit(cacheKey, cacheFile.Name(), resp, written)
|
||||
}
|
||||
} else {
|
||||
written, err = io.Copy(w, resp.Body)
|
||||
if err != nil && !isConnectionClosed(err) {
|
||||
log.Printf("[Fixed] ERR %s %s -> write error (%s) from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
written, err = io.Copy(w, resp.Body)
|
||||
if err != nil && !isConnectionClosed(err) {
|
||||
log.Printf("[Fixed] ERR %s %s -> write error (%s) from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r))
|
||||
}
|
||||
}
|
||||
|
||||
// 记录统计信息
|
||||
collector.RecordRequest(r.URL.Path, resp.StatusCode, time.Since(startTime), written, utils.GetClientIP(r), r)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配的固定路径,继续下一个处理器
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isConnectionClosed(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 忽略常见的连接关闭错误
|
||||
if errors.Is(err, syscall.EPIPE) || // broken pipe
|
||||
errors.Is(err, syscall.ECONNRESET) || // connection reset by peer
|
||||
strings.Contains(err.Error(), "broken pipe") ||
|
||||
strings.Contains(err.Error(), "connection reset by peer") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetFixedPathCache 获取固定路径缓存管理器
|
||||
func GetFixedPathCache() *cache.CacheManager {
|
||||
return fixedPathCache
|
||||
}
|
23
main.go
23
main.go
@ -40,7 +40,6 @@ func main() {
|
||||
// 创建代理处理器
|
||||
mirrorHandler := handler.NewMirrorProxyHandler()
|
||||
proxyHandler := handler.NewProxyHandler(cfg)
|
||||
fixedPathCache := middleware.GetFixedPathCache()
|
||||
|
||||
// 创建处理器链
|
||||
handlers := []struct {
|
||||
@ -88,16 +87,16 @@ func main() {
|
||||
case "/admin/api/config/save":
|
||||
proxyHandler.AuthMiddleware(handler.NewConfigHandler(cfg).ServeHTTP)(w, r)
|
||||
case "/admin/api/cache/stats":
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).GetCacheStats)(w, r)
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).GetCacheStats)(w, r)
|
||||
case "/admin/api/cache/enable":
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).SetCacheEnabled)(w, r)
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).SetCacheEnabled)(w, r)
|
||||
case "/admin/api/cache/clear":
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).ClearCache)(w, r)
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).ClearCache)(w, r)
|
||||
case "/admin/api/cache/config":
|
||||
if r.Method == http.MethodGet {
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).GetCacheConfig)(w, r)
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).GetCacheConfig)(w, r)
|
||||
} else if r.Method == http.MethodPost {
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).UpdateCacheConfig)(w, r)
|
||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).UpdateCacheConfig)(w, r)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
@ -129,18 +128,6 @@ func main() {
|
||||
},
|
||||
handler: mirrorHandler,
|
||||
},
|
||||
// 固定路径处理器
|
||||
{
|
||||
matcher: func(r *http.Request) bool {
|
||||
for _, fp := range cfg.FixedPaths {
|
||||
if strings.HasPrefix(r.URL.Path, fp.Path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
handler: middleware.FixedPathProxyMiddleware(cfg.FixedPaths)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})),
|
||||
},
|
||||
// 默认代理处理器
|
||||
{
|
||||
matcher: func(r *http.Request) bool {
|
||||
|
61
web/app/dashboard/cache/page.tsx
vendored
61
web/app/dashboard/cache/page.tsx
vendored
@ -28,13 +28,11 @@ interface CacheConfig {
|
||||
interface CacheData {
|
||||
proxy: CacheStats
|
||||
mirror: CacheStats
|
||||
fixedPath: CacheStats
|
||||
}
|
||||
|
||||
interface CacheConfigs {
|
||||
proxy: CacheConfig
|
||||
mirror: CacheConfig
|
||||
fixedPath: CacheConfig
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number) {
|
||||
@ -133,7 +131,7 @@ export default function CachePage() {
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchStats, fetchConfigs])
|
||||
|
||||
const handleToggleCache = async (type: "proxy" | "mirror" | "fixedPath", enabled: boolean) => {
|
||||
const handleToggleCache = async (type: "proxy" | "mirror", enabled: boolean) => {
|
||||
try {
|
||||
const token = localStorage.getItem("token")
|
||||
if (!token) {
|
||||
@ -160,7 +158,7 @@ export default function CachePage() {
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: `${type === "proxy" ? "代理" : type === "mirror" ? "镜像" : "固定路径"}缓存已${enabled ? "启用" : "禁用"}`,
|
||||
description: `${type === "proxy" ? "代理" : "镜像"}缓存已${enabled ? "启用" : "禁用"}`,
|
||||
})
|
||||
|
||||
fetchStats()
|
||||
@ -173,7 +171,7 @@ export default function CachePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateConfig = async (type: "proxy" | "mirror" | "fixedPath", config: CacheConfig) => {
|
||||
const handleUpdateConfig = async (type: "proxy" | "mirror", config: CacheConfig) => {
|
||||
try {
|
||||
const token = localStorage.getItem("token")
|
||||
if (!token) {
|
||||
@ -213,7 +211,7 @@ export default function CachePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearCache = async (type: "proxy" | "mirror" | "fixedPath" | "all") => {
|
||||
const handleClearCache = async (type: "proxy" | "mirror" | "all") => {
|
||||
try {
|
||||
const token = localStorage.getItem("token")
|
||||
if (!token) {
|
||||
@ -253,7 +251,7 @@ export default function CachePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const renderCacheConfig = (type: "proxy" | "mirror" | "fixedPath") => {
|
||||
const renderCacheConfig = (type: "proxy" | "mirror" ) => {
|
||||
if (!configs) return null
|
||||
|
||||
const config = configs[type]
|
||||
@ -425,55 +423,6 @@ export default function CachePage() {
|
||||
{renderCacheConfig("mirror")}
|
||||
</CardContent>
|
||||
</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>
|
||||
{renderCacheConfig("fixedPath")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -44,12 +44,6 @@ interface PathMapping {
|
||||
MaxSize?: number // 最大文件大小阈值
|
||||
}
|
||||
|
||||
interface FixedPath {
|
||||
Path: string
|
||||
TargetHost: string
|
||||
TargetURL: string
|
||||
}
|
||||
|
||||
interface CompressionConfig {
|
||||
Enabled: boolean
|
||||
Level: number
|
||||
@ -61,7 +55,6 @@ interface Config {
|
||||
Gzip: CompressionConfig
|
||||
Brotli: CompressionConfig
|
||||
}
|
||||
FixedPaths: FixedPath[]
|
||||
}
|
||||
|
||||
export default function ConfigPage() {
|
||||
@ -85,13 +78,7 @@ export default function ConfigPage() {
|
||||
sizeThresholdUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
||||
maxSizeUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
||||
})
|
||||
const [fixedPathDialogOpen, setFixedPathDialogOpen] = useState(false)
|
||||
const [editingFixedPath, setEditingFixedPath] = useState<FixedPath | null>(null)
|
||||
const [newFixedPath, setNewFixedPath] = useState<FixedPath>({
|
||||
Path: "",
|
||||
TargetHost: "",
|
||||
TargetURL: "",
|
||||
})
|
||||
|
||||
const [extensionMapDialogOpen, setExtensionMapDialogOpen] = useState(false)
|
||||
const [editingPath, setEditingPath] = useState<string | null>(null)
|
||||
const [editingExtension, setEditingExtension] = useState<{ext: string, target: string} | null>(null)
|
||||
@ -107,7 +94,6 @@ export default function ConfigPage() {
|
||||
} | null>(null);
|
||||
|
||||
const [deletingPath, setDeletingPath] = useState<string | null>(null)
|
||||
const [deletingFixedPath, setDeletingFixedPath] = useState<FixedPath | null>(null)
|
||||
const [deletingExtension, setDeletingExtension] = useState<{path: string, ext: string} | null>(null)
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
@ -231,20 +217,6 @@ export default function ConfigPage() {
|
||||
})
|
||||
}, [handleDialogOpenChange])
|
||||
|
||||
const handleFixedPathDialogOpenChange = useCallback((open: boolean) => {
|
||||
handleDialogOpenChange(open, (isOpen) => {
|
||||
setFixedPathDialogOpen(isOpen)
|
||||
if (!isOpen) {
|
||||
setEditingFixedPath(null)
|
||||
setNewFixedPath({
|
||||
Path: "",
|
||||
TargetHost: "",
|
||||
TargetURL: "",
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [handleDialogOpenChange])
|
||||
|
||||
const handleExtensionMapDialogOpenChange = useCallback((open: boolean) => {
|
||||
handleDialogOpenChange(open, (isOpen) => {
|
||||
setExtensionMapDialogOpen(isOpen)
|
||||
@ -453,95 +425,8 @@ export default function ConfigPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const addFixedPath = () => {
|
||||
if (!config) return
|
||||
const { Path, TargetHost, TargetURL } = newFixedPath
|
||||
|
||||
// 验证输入
|
||||
if (!Path.trim() || !TargetHost.trim() || !TargetURL.trim()) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "所有字段都不能为空",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证路径格式
|
||||
if (!Path.startsWith('/')) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "路径必须以/开头",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
try {
|
||||
new URL(TargetURL)
|
||||
} catch {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "目标URL格式不正确",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主机名格式
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_.]+[a-zA-Z0-9]$/.test(TargetHost)) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "目标主机格式不正确",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const newConfig = { ...config }
|
||||
if (editingFixedPath) {
|
||||
const index = newConfig.FixedPaths.findIndex(p => p.Path === editingFixedPath.Path)
|
||||
if (index !== -1) {
|
||||
newConfig.FixedPaths[index] = newFixedPath
|
||||
}
|
||||
} else {
|
||||
// 检查路径是否已存在
|
||||
if (newConfig.FixedPaths.some(p => p.Path === Path)) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "该路径已存在",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
newConfig.FixedPaths.push(newFixedPath)
|
||||
}
|
||||
|
||||
setConfig(newConfig)
|
||||
setFixedPathDialogOpen(false)
|
||||
setEditingFixedPath(null)
|
||||
setNewFixedPath({
|
||||
Path: "",
|
||||
TargetHost: "",
|
||||
TargetURL: "",
|
||||
})
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "固定路径已更新",
|
||||
})
|
||||
}
|
||||
|
||||
const editFixedPath = (path: FixedPath) => {
|
||||
setEditingFixedPath(path)
|
||||
setNewFixedPath({
|
||||
Path: path.Path,
|
||||
TargetHost: path.TargetHost,
|
||||
TargetURL: path.TargetURL,
|
||||
})
|
||||
setFixedPathDialogOpen(true)
|
||||
}
|
||||
|
||||
const openAddPathDialog = () => {
|
||||
setEditingPathData(null)
|
||||
@ -557,32 +442,6 @@ export default function ConfigPage() {
|
||||
setPathDialogOpen(true)
|
||||
}
|
||||
|
||||
const openAddFixedPathDialog = () => {
|
||||
setEditingFixedPath(null)
|
||||
setNewFixedPath({
|
||||
Path: "",
|
||||
TargetHost: "",
|
||||
TargetURL: "",
|
||||
})
|
||||
setFixedPathDialogOpen(true)
|
||||
}
|
||||
|
||||
const deleteFixedPath = (path: FixedPath) => {
|
||||
setDeletingFixedPath(path)
|
||||
}
|
||||
|
||||
const confirmDeleteFixedPath = () => {
|
||||
if (!config || !deletingFixedPath) return
|
||||
const newConfig = { ...config }
|
||||
newConfig.FixedPaths = newConfig.FixedPaths.filter(p => p.Path !== deletingFixedPath.Path)
|
||||
setConfig(newConfig)
|
||||
setDeletingFixedPath(null)
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "固定路径已删除",
|
||||
})
|
||||
}
|
||||
|
||||
const exportConfig = () => {
|
||||
if (!config) return
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
|
||||
@ -618,10 +477,6 @@ export default function ConfigPage() {
|
||||
throw new Error('配置文件压缩设置格式不正确')
|
||||
}
|
||||
|
||||
if (!Array.isArray(newConfig.FixedPaths)) {
|
||||
throw new Error('配置文件固定路径格式不正确')
|
||||
}
|
||||
|
||||
// 验证路径映射
|
||||
for (const [path, target] of Object.entries(newConfig.MAP)) {
|
||||
if (!path.startsWith('/')) {
|
||||
@ -1059,92 +914,6 @@ export default function ConfigPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="fixed-paths">
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={openAddFixedPathDialog}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加固定路径
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>路径</TableHead>
|
||||
<TableHead>目标主机</TableHead>
|
||||
<TableHead>目标 URL</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{config?.FixedPaths.map((path, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{path.Path}</TableCell>
|
||||
<TableCell>{path.TargetHost}</TableCell>
|
||||
<TableCell>{path.TargetURL}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => editFixedPath(path)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteFixedPath(path)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Dialog open={fixedPathDialogOpen} onOpenChange={handleFixedPathDialogOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingFixedPath ? "编辑固定路径" : "添加固定路径"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>路径</Label>
|
||||
<Input
|
||||
value={editingFixedPath ? editingFixedPath.Path : newFixedPath.Path}
|
||||
onChange={(e) => setNewFixedPath({ ...newFixedPath, Path: e.target.value })}
|
||||
placeholder="/example"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>目标主机</Label>
|
||||
<Input
|
||||
value={editingFixedPath ? editingFixedPath.TargetHost : newFixedPath.TargetHost}
|
||||
onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetHost: e.target.value })}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>目标 URL</Label>
|
||||
<Input
|
||||
value={editingFixedPath ? editingFixedPath.TargetURL : newFixedPath.TargetURL}
|
||||
onChange={(e) => setNewFixedPath({ ...newFixedPath, TargetURL: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={addFixedPath}>
|
||||
{editingFixedPath ? "保存" : "添加"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -1204,24 +973,6 @@ export default function ConfigPage() {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog
|
||||
open={!!deletingFixedPath}
|
||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingFixedPath)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除固定路径 “{deletingFixedPath?.Path}” 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDeleteFixedPath}>删除</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog
|
||||
open={!!deletingExtension}
|
||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user