mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 16:41:54 +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.MAP = newConfig.MAP
|
||||||
c.Compression = newConfig.Compression
|
c.Compression = newConfig.Compression
|
||||||
c.FixedPaths = newConfig.FixedPaths
|
|
||||||
|
|
||||||
// 触发回调
|
// 触发回调
|
||||||
for _, callback := range c.onConfigUpdate {
|
for _, callback := range c.onConfigUpdate {
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
MAP map[string]PathConfig `json:"MAP"` // 改为使用PathConfig
|
MAP map[string]PathConfig `json:"MAP"` // 改为使用PathConfig
|
||||||
Compression CompressionConfig `json:"Compression"`
|
Compression CompressionConfig `json:"Compression"`
|
||||||
FixedPaths []FixedPathConfig `json:"FixedPaths"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PathConfig struct {
|
type PathConfig struct {
|
||||||
@ -30,19 +29,12 @@ type CompressorConfig struct {
|
|||||||
Level int `json:"Level"`
|
Level int `json:"Level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FixedPathConfig struct {
|
|
||||||
Path string `json:"Path"`
|
|
||||||
TargetHost string `json:"TargetHost"`
|
|
||||||
TargetURL string `json:"TargetURL"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加一个辅助方法来处理字符串到 PathConfig 的转换
|
// 添加一个辅助方法来处理字符串到 PathConfig 的转换
|
||||||
func (c *Config) UnmarshalJSON(data []byte) error {
|
func (c *Config) UnmarshalJSON(data []byte) error {
|
||||||
// 创建一个临时结构来解析原始JSON
|
// 创建一个临时结构来解析原始JSON
|
||||||
type TempConfig struct {
|
type TempConfig struct {
|
||||||
MAP map[string]json.RawMessage `json:"MAP"`
|
MAP map[string]json.RawMessage `json:"MAP"`
|
||||||
Compression CompressionConfig `json:"Compression"`
|
Compression CompressionConfig `json:"Compression"`
|
||||||
FixedPaths []FixedPathConfig `json:"FixedPaths"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var temp TempConfig
|
var temp TempConfig
|
||||||
@ -77,7 +69,6 @@ func (c *Config) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
// 复制其他字段
|
// 复制其他字段
|
||||||
c.Compression = temp.Compression
|
c.Compression = temp.Compression
|
||||||
c.FixedPaths = temp.FixedPaths
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -7,16 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CacheAdminHandler struct {
|
type CacheAdminHandler struct {
|
||||||
proxyCache *cache.CacheManager
|
proxyCache *cache.CacheManager
|
||||||
mirrorCache *cache.CacheManager
|
mirrorCache *cache.CacheManager
|
||||||
fixedPathCache *cache.CacheManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCacheAdminHandler(proxyCache, mirrorCache, fixedPathCache *cache.CacheManager) *CacheAdminHandler {
|
func NewCacheAdminHandler(proxyCache, mirrorCache *cache.CacheManager) *CacheAdminHandler {
|
||||||
return &CacheAdminHandler{
|
return &CacheAdminHandler{
|
||||||
proxyCache: proxyCache,
|
proxyCache: proxyCache,
|
||||||
mirrorCache: mirrorCache,
|
mirrorCache: mirrorCache,
|
||||||
fixedPathCache: fixedPathCache,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,9 +33,8 @@ func (h *CacheAdminHandler) GetCacheStats(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats := map[string]cache.CacheStats{
|
stats := map[string]cache.CacheStats{
|
||||||
"proxy": h.proxyCache.GetStats(),
|
"proxy": h.proxyCache.GetStats(),
|
||||||
"mirror": h.mirrorCache.GetStats(),
|
"mirror": h.mirrorCache.GetStats(),
|
||||||
"fixedPath": h.fixedPathCache.GetStats(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@ -52,9 +49,8 @@ func (h *CacheAdminHandler) GetCacheConfig(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
configs := map[string]cache.CacheConfig{
|
configs := map[string]cache.CacheConfig{
|
||||||
"proxy": h.proxyCache.GetConfig(),
|
"proxy": h.proxyCache.GetConfig(),
|
||||||
"mirror": h.mirrorCache.GetConfig(),
|
"mirror": h.mirrorCache.GetConfig(),
|
||||||
"fixedPath": h.fixedPathCache.GetConfig(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@ -69,7 +65,7 @@ func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
|
Type string `json:"type"` // "proxy", "mirror"
|
||||||
Config CacheConfig `json:"config"` // 新的配置
|
Config CacheConfig `json:"config"` // 新的配置
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +80,6 @@ func (h *CacheAdminHandler) UpdateCacheConfig(w http.ResponseWriter, r *http.Req
|
|||||||
targetCache = h.proxyCache
|
targetCache = h.proxyCache
|
||||||
case "mirror":
|
case "mirror":
|
||||||
targetCache = h.mirrorCache
|
targetCache = h.mirrorCache
|
||||||
case "fixedPath":
|
|
||||||
targetCache = h.fixedPathCache
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@ -107,7 +101,7 @@ func (h *CacheAdminHandler) SetCacheEnabled(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Type string `json:"type"` // "proxy", "mirror" 或 "fixedPath"
|
Type string `json:"type"` // "proxy", "mirror"
|
||||||
Enabled bool `json:"enabled"` // true 或 false
|
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)
|
h.proxyCache.SetEnabled(req.Enabled)
|
||||||
case "mirror":
|
case "mirror":
|
||||||
h.mirrorCache.SetEnabled(req.Enabled)
|
h.mirrorCache.SetEnabled(req.Enabled)
|
||||||
case "fixedPath":
|
|
||||||
h.fixedPathCache.SetEnabled(req.Enabled)
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@ -139,7 +131,7 @@ func (h *CacheAdminHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
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 {
|
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()
|
err = h.proxyCache.ClearCache()
|
||||||
case "mirror":
|
case "mirror":
|
||||||
err = h.mirrorCache.ClearCache()
|
err = h.mirrorCache.ClearCache()
|
||||||
case "fixedPath":
|
|
||||||
err = h.fixedPathCache.ClearCache()
|
|
||||||
case "all":
|
case "all":
|
||||||
err = h.proxyCache.ClearCache()
|
err = h.proxyCache.ClearCache()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = h.mirrorCache.ClearCache()
|
err = h.mirrorCache.ClearCache()
|
||||||
}
|
}
|
||||||
if err == nil {
|
|
||||||
err = h.fixedPathCache.ClearCache()
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
http.Error(w, "Invalid cache type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,6 @@ var hopHeadersBase = map[string]bool{
|
|||||||
"Upgrade": true,
|
"Upgrade": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
// 移除旧的初始化代码,因为我们直接在 map 字面量中定义了所有值
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorHandler 定义错误处理函数类型
|
// ErrorHandler 定义错误处理函数类型
|
||||||
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
|
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()
|
mirrorHandler := handler.NewMirrorProxyHandler()
|
||||||
proxyHandler := handler.NewProxyHandler(cfg)
|
proxyHandler := handler.NewProxyHandler(cfg)
|
||||||
fixedPathCache := middleware.GetFixedPathCache()
|
|
||||||
|
|
||||||
// 创建处理器链
|
// 创建处理器链
|
||||||
handlers := []struct {
|
handlers := []struct {
|
||||||
@ -88,16 +87,16 @@ func main() {
|
|||||||
case "/admin/api/config/save":
|
case "/admin/api/config/save":
|
||||||
proxyHandler.AuthMiddleware(handler.NewConfigHandler(cfg).ServeHTTP)(w, r)
|
proxyHandler.AuthMiddleware(handler.NewConfigHandler(cfg).ServeHTTP)(w, r)
|
||||||
case "/admin/api/cache/stats":
|
case "/admin/api/cache/stats":
|
||||||
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache, fixedPathCache).GetCacheStats)(w, r)
|
proxyHandler.AuthMiddleware(handler.NewCacheAdminHandler(proxyHandler.Cache, mirrorHandler.Cache).GetCacheStats)(w, r)
|
||||||
case "/admin/api/cache/enable":
|
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":
|
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":
|
case "/admin/api/cache/config":
|
||||||
if r.Method == http.MethodGet {
|
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 {
|
} 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 {
|
} else {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
@ -129,18 +128,6 @@ func main() {
|
|||||||
},
|
},
|
||||||
handler: mirrorHandler,
|
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 {
|
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 {
|
interface CacheData {
|
||||||
proxy: CacheStats
|
proxy: CacheStats
|
||||||
mirror: CacheStats
|
mirror: CacheStats
|
||||||
fixedPath: CacheStats
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CacheConfigs {
|
interface CacheConfigs {
|
||||||
proxy: CacheConfig
|
proxy: CacheConfig
|
||||||
mirror: CacheConfig
|
mirror: CacheConfig
|
||||||
fixedPath: CacheConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number) {
|
function formatBytes(bytes: number) {
|
||||||
@ -133,7 +131,7 @@ export default function CachePage() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [fetchStats, fetchConfigs])
|
}, [fetchStats, fetchConfigs])
|
||||||
|
|
||||||
const handleToggleCache = async (type: "proxy" | "mirror" | "fixedPath", enabled: boolean) => {
|
const handleToggleCache = async (type: "proxy" | "mirror", enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("token")
|
const token = localStorage.getItem("token")
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@ -160,7 +158,7 @@ export default function CachePage() {
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "成功",
|
title: "成功",
|
||||||
description: `${type === "proxy" ? "代理" : type === "mirror" ? "镜像" : "固定路径"}缓存已${enabled ? "启用" : "禁用"}`,
|
description: `${type === "proxy" ? "代理" : "镜像"}缓存已${enabled ? "启用" : "禁用"}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
fetchStats()
|
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 {
|
try {
|
||||||
const token = localStorage.getItem("token")
|
const token = localStorage.getItem("token")
|
||||||
if (!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 {
|
try {
|
||||||
const token = localStorage.getItem("token")
|
const token = localStorage.getItem("token")
|
||||||
if (!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
|
if (!configs) return null
|
||||||
|
|
||||||
const config = configs[type]
|
const config = configs[type]
|
||||||
@ -425,55 +423,6 @@ export default function CachePage() {
|
|||||||
{renderCacheConfig("mirror")}
|
{renderCacheConfig("mirror")}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 固定路径缓存 */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle>固定路径缓存</CardTitle>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
checked={stats?.fixedPath.enabled ?? false}
|
|
||||||
onCheckedChange={(checked) => handleToggleCache("fixedPath", checked)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleClearCache("fixedPath")}
|
|
||||||
>
|
|
||||||
清理
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<dl className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<dt className="text-sm font-medium text-gray-500">缓存项数量</dt>
|
|
||||||
<dd className="text-sm text-gray-900">{stats?.fixedPath.total_items ?? 0}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<dt className="text-sm font-medium text-gray-500">总大小</dt>
|
|
||||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.fixedPath.total_size ?? 0)}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<dt className="text-sm font-medium text-gray-500">命中次数</dt>
|
|
||||||
<dd className="text-sm text-gray-900">{stats?.fixedPath.hit_count ?? 0}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<dt className="text-sm font-medium text-gray-500">未命中次数</dt>
|
|
||||||
<dd className="text-sm text-gray-900">{stats?.fixedPath.miss_count ?? 0}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<dt className="text-sm font-medium text-gray-500">命中率</dt>
|
|
||||||
<dd className="text-sm text-gray-900">{(stats?.fixedPath.hit_rate ?? 0).toFixed(2)}%</dd>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
|
||||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.fixedPath.bytes_saved ?? 0)}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
{renderCacheConfig("fixedPath")}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -44,12 +44,6 @@ interface PathMapping {
|
|||||||
MaxSize?: number // 最大文件大小阈值
|
MaxSize?: number // 最大文件大小阈值
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FixedPath {
|
|
||||||
Path: string
|
|
||||||
TargetHost: string
|
|
||||||
TargetURL: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CompressionConfig {
|
interface CompressionConfig {
|
||||||
Enabled: boolean
|
Enabled: boolean
|
||||||
Level: number
|
Level: number
|
||||||
@ -61,7 +55,6 @@ interface Config {
|
|||||||
Gzip: CompressionConfig
|
Gzip: CompressionConfig
|
||||||
Brotli: CompressionConfig
|
Brotli: CompressionConfig
|
||||||
}
|
}
|
||||||
FixedPaths: FixedPath[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfigPage() {
|
export default function ConfigPage() {
|
||||||
@ -85,13 +78,7 @@ export default function ConfigPage() {
|
|||||||
sizeThresholdUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
sizeThresholdUnit: 'MB' as 'B' | 'KB' | 'MB' | 'GB',
|
||||||
maxSizeUnit: '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 [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 [editingExtension, setEditingExtension] = useState<{ext: string, target: string} | null>(null)
|
||||||
@ -107,7 +94,6 @@ export default function ConfigPage() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [deletingPath, setDeletingPath] = useState<string | 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 [deletingExtension, setDeletingExtension] = useState<{path: string, ext: string} | null>(null)
|
||||||
|
|
||||||
const fetchConfig = useCallback(async () => {
|
const fetchConfig = useCallback(async () => {
|
||||||
@ -231,20 +217,6 @@ export default function ConfigPage() {
|
|||||||
})
|
})
|
||||||
}, [handleDialogOpenChange])
|
}, [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) => {
|
const handleExtensionMapDialogOpenChange = useCallback((open: boolean) => {
|
||||||
handleDialogOpenChange(open, (isOpen) => {
|
handleDialogOpenChange(open, (isOpen) => {
|
||||||
setExtensionMapDialogOpen(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 = () => {
|
const openAddPathDialog = () => {
|
||||||
setEditingPathData(null)
|
setEditingPathData(null)
|
||||||
@ -557,32 +442,6 @@ export default function ConfigPage() {
|
|||||||
setPathDialogOpen(true)
|
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 = () => {
|
const exportConfig = () => {
|
||||||
if (!config) return
|
if (!config) return
|
||||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
|
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
|
||||||
@ -618,10 +477,6 @@ export default function ConfigPage() {
|
|||||||
throw new Error('配置文件压缩设置格式不正确')
|
throw new Error('配置文件压缩设置格式不正确')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(newConfig.FixedPaths)) {
|
|
||||||
throw new Error('配置文件固定路径格式不正确')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证路径映射
|
// 验证路径映射
|
||||||
for (const [path, target] of Object.entries(newConfig.MAP)) {
|
for (const [path, target] of Object.entries(newConfig.MAP)) {
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
@ -1059,92 +914,6 @@ export default function ConfigPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -1204,24 +973,6 @@ export default function ConfigPage() {
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</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
|
<AlertDialog
|
||||||
open={!!deletingExtension}
|
open={!!deletingExtension}
|
||||||
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}
|
onOpenChange={(open) => handleDeleteDialogOpenChange(open, setDeletingExtension)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user