diff --git a/.gitignore b/.gitignore index a5842a0..c964fd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ .log.meta.json - .ai/ - .geminiignore .gemini env.json env.yml -env.yaml \ No newline at end of file +env.yaml +/CODE-FULL.md diff --git a/go.mod b/go.mod index b0fa66e..84a30be 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( apigo.cc/go/log v1.3.0 apigo.cc/go/redis v1.3.0 apigo.cc/go/safe v1.3.0 + apigo.cc/go/starter v1.0.1 apigo.cc/go/timer v1.3.0 github.com/gorilla/websocket v1.5.3 golang.org/x/net v0.54.0 @@ -22,7 +23,7 @@ require ( apigo.cc/go/encoding v1.3.0 // indirect apigo.cc/go/rand v1.3.0 // indirect apigo.cc/go/shell v1.3.0 // indirect - github.com/gomodule/redigo v1.9.3 // indirect + github.com/gomodule/redigo v2.0.0+incompatible // indirect golang.org/x/crypto v0.51.0 // indirect golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.37.0 // indirect diff --git a/go.sum b/go.sum index 0eb98e4..2eeaaf5 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,43 @@ -apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8= -apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= -apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60= -apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw= -apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig= -apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw= -apigo.cc/go/discover v1.0.11 h1:aeAC+xAwGlOeXsRptXJkEn8MvRZ7lom5N5jfBAg9/CE= -apigo.cc/go/discover v1.0.11/go.mod h1:TcIpl1Ocu51koRxugV81Jnz4NH0+Q5f5PF105VczS/0= -apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA= -apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20= -apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs= -apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s= -apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4= -apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68= -apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g= -apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE= -apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ= -apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk= -apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE= -apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= -apigo.cc/go/redis v1.0.9 h1:5MnCchcDgoVnGHQn+KJF9PJXXRayK9nlZg+Q0lTnMoU= -apigo.cc/go/redis v1.0.9/go.mod h1:SVOD7iuUL/jxYEa28qObDQf4GY9UVSKjhM9vVj9TXLI= -apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI= -apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE= -apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA= -apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc= -apigo.cc/go/timer v1.0.7 h1:QUH0t7l9kBiGU/QdDNSthnXLfJXOEp+mpdY2+QPlrEI= -apigo.cc/go/timer v1.0.7/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= -github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= +apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ= +apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= +apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg= +apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI= +apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8= +apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc= +apigo.cc/go/discover v1.3.0 h1:CXuKtAZygU+4TMHtebVkjWyyWmPgoLbsJFdKFGiCOd8= +apigo.cc/go/discover v1.3.0/go.mod h1:VMu1qC6AngVFQMdaCwGoq3/PPX0xDnjkG+1AcSA+Zvs= +apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs= +apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo= +apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg= +apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY= +apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg= +apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U= +apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE= +apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA= +apigo.cc/go/log v1.3.0 h1:61Z80WGN6SnhgxgoR8xuVYIieMdjlJKmf8JX1HXzp0Y= +apigo.cc/go/log v1.3.0/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE= +apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU= +apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= +apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU= +apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ= +apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok= +apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs= +apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw= +apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4= +apigo.cc/go/starter v1.0.1 h1:7Qv/rRlEVlTX7wjr1LpV1XX1wUD4UAssDi6J+YCh73s= +apigo.cc/go/starter v1.0.1/go.mod h1:xHfo+36hXGdVhhnRqd1l+Vk6Fp1ecN2LDAcsDOVodXk= +apigo.cc/go/timer v1.3.0 h1:dorVGKw0xR6Gj8Pwfl86K46szMBfD31XyO+uUqxU+EI= +apigo.cc/go/timer v1.3.0/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= diff --git a/proxy.go b/proxy.go index 82c4018..4b9b847 100644 --- a/proxy.go +++ b/proxy.go @@ -162,3 +162,31 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) { } } } + +// ProxyRule 定义了外部传递或 Redis 中获取的代理规则配置 +type ProxyRule struct { + Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$ + AuthLevel int // 所需鉴权级别 + ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换) + ToPath string // 目标路径 (可含 $1 变量替换) +} + +// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的所有代理规则。 +// 该方法非常轻量,仅在赋值瞬间短暂持有写锁,不会阻塞任何并发请求,并且自动淘汰旧规则。 +func ReplaceProxies(host string, rules []ProxyRule) { + newProxies := make([]*proxyType, 0, len(rules)) + for _, r := range rules { + p := &proxyType{authLevel: r.AuthLevel, fromPath: r.Path, toApp: r.ToApp, toPath: r.ToPath} + if strings.ContainsRune(r.Path, '(') { + matcher, err := regexp.Compile("^" + r.Path + "$") + if err == nil { + p.matcher = matcher + } + } + newProxies = append(newProxies, p) + } + + hostPoliciesLock.Lock() + defer hostPoliciesLock.Unlock() + hostProxies[host] = newProxies +} diff --git a/reload.go b/reload.go new file mode 100644 index 0000000..1dde371 --- /dev/null +++ b/reload.go @@ -0,0 +1,33 @@ +package service + +import ( + "apigo.cc/go/log" + "sync" +) + +var ( + reloadHooks []func() error + reloadLock sync.RWMutex +) + +// OnReload 注册一个在接收到 SIGHUP 信号时触发的重新加载钩子 +func OnReload(handler func() error) { + reloadLock.Lock() + defer reloadLock.Unlock() + reloadHooks = append(reloadHooks, handler) +} + +// triggerReload 触发所有注册的重新加载钩子 +func triggerReload() error { + reloadLock.RLock() + hooks := make([]func() error, len(reloadHooks)) + copy(hooks, reloadHooks) + reloadLock.RUnlock() + + for _, hook := range hooks { + if err := hook(); err != nil { + log.DefaultLogger.Error("reload hook failed", "error", err.Error()) + } + } + return nil +} diff --git a/rewrite.go b/rewrite.go index 35c9cb1..c17ce77 100644 --- a/rewrite.go +++ b/rewrite.go @@ -101,3 +101,28 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo return false } + +// RewriteRule 定义了外部传递的 URL 重写规则 +type RewriteRule struct { + Path string // 原始路径或匹配正则,例如 ^/old/(.*)$ + ToPath string // 重写后的路径,例如 /new/$1 +} + +// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的所有重写规则。 +func ReplaceRewrites(host string, rules []RewriteRule) { + newRewrites := make([]*rewriteType, 0, len(rules)) + for _, r := range rules { + s := &rewriteType{fromPath: r.Path, toPath: r.ToPath} + if strings.ContainsRune(r.Path, '(') { + matcher, err := regexp.Compile("^" + r.Path + "$") + if err == nil { + s.matcher = matcher + } + } + newRewrites = append(newRewrites, s) + } + + hostPoliciesLock.Lock() + defer hostPoliciesLock.Unlock() + hostRewrites[host] = newRewrites +} diff --git a/server.go b/server.go index b865d9d..60a325e 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ import ( "apigo.cc/go/log" "apigo.cc/go/redis" "apigo.cc/go/safe" + "apigo.cc/go/starter" "context" "fmt" "golang.org/x/net/http2" @@ -12,46 +13,35 @@ import ( "net" "net/http" "os" - "os/signal" "strings" - "syscall" "time" ) // GlobalDiscoverer 供服务框架内部使用的发现实例 var GlobalDiscoverer *discover.Discoverer -// AsyncServer 异步服务实例 -type AsyncServer struct { +// WebServer 实现了 starter.Service 和 starter.Reloader 接口 +type WebServer struct { server *http.Server listener net.Listener Addr string - stopChan chan os.Signal - startChan chan bool useDiscover bool discoverer *discover.Discoverer } -// AsyncStart 异步启动服务 -func AsyncStart() *AsyncServer { - as := &AsyncServer{ - startChan: make(chan bool, 1), - stopChan: make(chan os.Signal, 1), - } - - go as.start() - - <-as.startChan - return as +// NewWebServer 创建并返回一个新的 WebServer 实例 +func NewWebServer() *WebServer { + return &WebServer{} } -func (as *AsyncServer) start() { +// Start 启动服务,实现 starter.Service 接口 +func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error { listenStr := Config.Listen - as.useDiscover = false + ws.useDiscover = false if listenStr == "" { listenStr = ":0,h2c" - as.useDiscover = true + ws.useDiscover = true } // 解析第一个监听配置 @@ -76,7 +66,7 @@ func (as *AsyncServer) start() { } appName := Config.App if appName != "" || Config.Register != "" { - as.useDiscover = true + ws.useDiscover = true } // 初始化服务器唯一标识 (8位,物理上限 3,844/s) @@ -92,18 +82,16 @@ func (as *AsyncServer) start() { listener, err := net.Listen("tcp", addr) if err != nil { - log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error()) - as.startChan <- false - return + return fmt.Errorf("failed to listen on %s: %w", addr, err) } - as.listener = listener - as.Addr = listener.Addr().String() - serverAddr = as.Addr + ws.listener = listener + ws.Addr = listener.Addr().String() + serverAddr = ws.Addr // 如果使用了随机端口且没有明确指定不需要服务发现,则开启 if addr == ":0" || strings.HasSuffix(addr, ":0") { - as.useDiscover = true + ws.useDiscover = true } h2s := &http2.Server{} @@ -112,7 +100,7 @@ func (as *AsyncServer) start() { handler = h2c.NewHandler(handler, h2s) } - as.server = &http.Server{ + ws.server = &http.Server{ Handler: handler, ReadTimeout: time.Duration(Config.ReadTimeout) * time.Millisecond, ReadHeaderTimeout: time.Duration(Config.ReadHeaderTimeout) * time.Millisecond, @@ -122,8 +110,8 @@ func (as *AsyncServer) start() { } // 启动服务发现 - if as.useDiscover { - _, port, _ := net.SplitHostPort(as.Addr) + if ws.useDiscover { + _, port, _ := net.SplitHostPort(ws.Addr) ip := GetServerIp() discoverAddr := fmt.Sprintf("%s:%s", ip, port) @@ -163,52 +151,98 @@ func (as *AsyncServer) start() { registry = "127.0.0.1:6379::15" // Default fallback } - as.discoverer = discover.Start(registry, appName, discoverAddr, log.DefaultLogger, discConf) - GlobalDiscoverer = as.discoverer - if as.discoverer != nil { - log.DefaultLogger.Info("discover registered", "app", appName, "addr", discoverAddr) + ws.discoverer = discover.Start(registry, appName, discoverAddr, logger, discConf) + GlobalDiscoverer = ws.discoverer + if ws.discoverer != nil { + logger.Info("discover registered", "app", appName, "addr", discoverAddr) } } - signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM) + errChan := make(chan error, 1) go func() { - log.DefaultLogger.Info("service starting", "addr", as.Addr, "proto", protocol) - as.startChan <- true - if err := as.server.Serve(listener); err != nil && err != http.ErrServerClosed { - log.DefaultLogger.Error("server error", "error", err.Error()) + logger.Info("service starting", "addr", ws.Addr, "proto", protocol) + if err := ws.server.Serve(listener); err != nil && err != http.ErrServerClosed { + errChan <- err } + close(errChan) }() -} -// Stop 停止服务 -func (as *AsyncServer) Stop() { - log.DefaultLogger.Info("service stopping") - if as.discoverer != nil { - as.discoverer.Stop() + // 短暂等待验证是否闪退 + select { + case err := <-errChan: + if err != nil { + return err + } + case <-time.After(100 * time.Millisecond): } + return nil +} + +// Stop 停止服务,实现 starter.Service 接口 +func (ws *WebServer) Stop(ctx context.Context) error { + log.DefaultLogger.Info("service stopping") + if ws.discoverer != nil { + ws.discoverer.Stop() + } + if ws.server != nil { + if err := ws.server.Shutdown(ctx); err != nil { + return err + } + } + log.DefaultLogger.Info("service stopped") + return nil +} + +// Health 检查服务健康状态,实现 starter.Service 接口 +func (ws *WebServer) Health() error { + if ws.server == nil { + return fmt.Errorf("server is not running") + } + return nil +} + +// Reload 实现配置重新加载,实现 starter.Reloader 接口 +func (ws *WebServer) Reload() error { + log.DefaultLogger.Info("reloading configurations...") + return triggerReload() +} + +// AsyncServer 兼容旧版异步服务实例 +type AsyncServer struct { + *WebServer +} + +// Stop 兼容旧版的无参数停止方法 +func (as *AsyncServer) Stop() { stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond if stopTimeout <= 0 { stopTimeout = 5 * time.Second } - ctx, cancel := context.WithTimeout(context.Background(), stopTimeout) defer cancel() - - if err := as.server.Shutdown(ctx); err != nil { - log.DefaultLogger.Error("server shutdown error", "error", err.Error()) - } - log.DefaultLogger.Info("service stopped") + _ = as.WebServer.Stop(ctx) } -// Wait 等待服务结束 (信号监听) +// AsyncStart 兼容旧版的异步启动方法 +func AsyncStart() *AsyncServer { + ws := NewWebServer() + _ = ws.Start(context.Background(), log.DefaultLogger) + return &AsyncServer{WebServer: ws} +} + +// Wait 等待服务结束 (兼容旧版,直接阻塞) func (as *AsyncServer) Wait() { - <-as.stopChan - as.Stop() + select {} } -// Start 同步启动服务 +// Start 兼容旧版的同步启动方法 (通过内部注册 starter 实现) func Start() { - AsyncStart().Wait() + stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond + if stopTimeout <= 0 { + stopTimeout = 5 * time.Second + } + starter.Register("web-server", NewWebServer(), 100, 5*time.Second, stopTimeout) + starter.Run() } diff --git a/static.go b/static.go index ab3db34..d86175d 100644 --- a/static.go +++ b/static.go @@ -43,6 +43,29 @@ func StaticByHost(path, rootPath, host string) { } } +// ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的所有静态目录规则 +func ReplaceStatics(host string, config map[string]string) { + newStatics := make(map[string]*string, len(config)) + for path, rootPath := range config { + rp := rootPath + if !filepath.IsAbs(rp) { + if absPath, err := filepath.Abs(rp); err == nil { + rp = absPath + } + } + newStatics[path] = &rp + } + + staticsByHostLock.Lock() + defer staticsByHostLock.Unlock() + + if host == "" { + statics = newStatics + } else { + staticsByHost[host] = newStatics + } +} + func getStaticFilePath(requestPath, host string) string { staticsByHostLock.RLock() defer staticsByHostLock.RUnlock()