11package gateway
22
33import (
4+ "io"
5+ "io/fs"
46 "log"
57 "net/http"
68 "os"
79 "path"
8- "path/filepath"
910 "strings"
11+ "time"
1012)
1113
1214// knownAPIPrefixes 定义属于 Gateway API 的路径前缀,静态文件中间件不会拦截这些路径。
@@ -23,6 +25,18 @@ var knownAPIPrefixes = map[string]bool{
2325// WithStaticFileHandler 返回一个 http.Handler,将 API 请求转发给 apiHandler,
2426// 其余请求从 staticDir 提供静态文件。对于 SPA 路由,不存在的路径会回退到 index.html。
2527func WithStaticFileHandler (apiHandler http.Handler , staticDir string , logger * log.Logger ) http.Handler {
28+ if staticDir == "" {
29+ return apiHandler
30+ }
31+ return WithFSStaticFileHandler (apiHandler , os .DirFS (staticDir ), logger )
32+ }
33+
34+ // WithFSStaticFileHandler 返回一个 http.Handler,将 API 请求转发给 apiHandler,
35+ // 其余请求从 fsys 提供静态文件。对于 SPA 路由,不存在的路径会回退到 index.html。
36+ func WithFSStaticFileHandler (apiHandler http.Handler , fsys fs.FS , logger * log.Logger ) http.Handler {
37+ if fsys == nil {
38+ return apiHandler
39+ }
2640 return http .HandlerFunc (func (writer http.ResponseWriter , request * http.Request ) {
2741 cleanPath := path .Clean ("/" + request .URL .Path )
2842
@@ -38,30 +52,49 @@ func WithStaticFileHandler(apiHandler http.Handler, staticDir string, logger *lo
3852 relPath = "index.html"
3953 }
4054
41- // 检查文件是否存在
42- fullPath := filepath .Join (staticDir , filepath .FromSlash (relPath ))
43- info , err := os .Stat (fullPath )
44- if err == nil && ! info .IsDir () {
45- setCacheHeaders (writer , relPath )
46- http .ServeFile (writer , request , fullPath )
47- return
55+ // 尝试从 fs.FS 中打开文件
56+ file , err := fsys .Open (relPath )
57+ if err == nil {
58+ stat , statErr := file .Stat ()
59+ if statErr == nil && ! stat .IsDir () {
60+ setCacheHeaders (writer , relPath )
61+ serveFileContent (writer , request , relPath , stat .ModTime (), file )
62+ _ = file .Close ()
63+ return
64+ }
65+ _ = file .Close ()
4866 }
4967
5068 // SPA fallback:文件不存在时返回 index.html
51- indexPath := filepath .Join (staticDir , "index.html" )
52- if _ , statErr := os .Stat (indexPath ); statErr == nil {
53- writer .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
54- http .ServeFile (writer , request , indexPath )
55- return
69+ indexFile , err := fsys .Open ("index.html" )
70+ if err == nil {
71+ stat , statErr := indexFile .Stat ()
72+ if statErr == nil {
73+ writer .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
74+ serveFileContent (writer , request , "index.html" , stat .ModTime (), indexFile )
75+ _ = indexFile .Close ()
76+ return
77+ }
78+ _ = indexFile .Close ()
5679 }
5780
5881 if logger != nil {
59- logger .Printf ("static files: index.html not found in %s" , staticDir )
82+ logger .Printf ("static files: index.html not found" )
6083 }
6184 http .NotFound (writer , request )
6285 })
6386}
6487
88+ // serveFileContent 将 fs.File 内容写入 HTTP 响应。如果文件支持 io.ReadSeeker,
89+ // 使用 http.ServeContent 以支持 Range 请求和 If-Modified-Since;否则回退到 io.Copy。
90+ func serveFileContent (writer http.ResponseWriter , request * http.Request , name string , modTime time.Time , file fs.File ) {
91+ if rs , ok := file .(io.ReadSeeker ); ok {
92+ http .ServeContent (writer , request , name , modTime , rs )
93+ return
94+ }
95+ _ , _ = io .Copy (writer , file )
96+ }
97+
6598// isAPIPath 判断请求路径是否属于 Gateway API。
6699func isAPIPath (cleanPath string ) bool {
67100 if knownAPIPrefixes [cleanPath ] {
0 commit comments