go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/web/static_fileserver.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package web 9 10 import ( 11 "mime" 12 "net/http" 13 "os" 14 "path/filepath" 15 "regexp" 16 ) 17 18 // StaticFileServer serves static results for `*filepath` suffix routes. 19 // 20 // If you want to use a "cached" mode you should use embedding and http.FS(...) as 21 // a search path. 22 type StaticFileServer struct { 23 SearchPaths []http.FileSystem 24 RewriteRules []RewriteRule 25 Headers http.Header 26 UseRouteFilepathAsRequestURL bool 27 UseEmptyResponseIfNotFound bool 28 } 29 30 // AddRegexPathRewrite adds a re-write rule that modifies the url path. 31 // 32 // Typically these kinds of re-write rules are used for vanity forms or 33 // removing a cache busting string from a given path. 34 func (sc *StaticFileServer) AddRegexPathRewrite(match string, rewriteAction func(string, ...string) string) error { 35 expr, err := regexp.Compile(match) 36 if err != nil { 37 return err 38 } 39 sc.RewriteRules = append(sc.RewriteRules, RewriteRuleFunc(func(path string) (string, bool) { 40 if expr.MatchString(path) { 41 pieces := flatten(expr.FindAllStringSubmatch(path, -1)) 42 return rewriteAction(path, pieces...), true 43 } 44 return path, false 45 })) 46 return nil 47 } 48 49 // Action implements an action handler. 50 func (sc StaticFileServer) Action(ctx Context) Result { 51 // we _must_ do this to ensure that 52 // file paths will match when we look for them 53 // in the static asset path(s). 54 path, ok := ctx.RouteParams().Get("filepath") 55 if ok { 56 ctx.Request().URL.Path = path 57 } 58 sc.ServeHTTP(ctx.Response(), ctx.Request()) 59 return nil 60 } 61 62 // ServeHTTP is the entrypoint for the static server. 63 // 64 // It adds default headers if specified, and then serves the file from disk. 65 func (sc StaticFileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 66 for key, values := range sc.Headers { 67 for _, value := range values { 68 rw.Header().Set(key, value) 69 } 70 } 71 filePath := req.URL.Path 72 f, finalPath, err := sc.ResolveFile(filePath) 73 if err != nil { 74 sc.fileError(finalPath, rw, req, err) 75 return 76 } 77 defer f.Close() 78 finfo, err := f.Stat() 79 if err != nil { 80 sc.fileError(finalPath, rw, req, err) 81 return 82 } 83 if finfo.IsDir() { 84 sc.notFound(finalPath, rw, req) 85 return 86 } 87 http.ServeContent(rw, req, finalPath, finfo.ModTime(), f) 88 } 89 90 // ResolveFile resolves a file from rewrite rules and search paths. 91 // 92 // First the file path is modified according to the rewrite rules. 93 // Then each search path is checked for the resolved file path. 94 func (sc StaticFileServer) ResolveFile(filePath string) (f http.File, finalPath string, err error) { 95 for _, rule := range sc.RewriteRules { 96 if newFilePath, matched := rule.Apply(filePath); matched { 97 filePath = newFilePath 98 } 99 } 100 for _, searchPath := range sc.SearchPaths { 101 f, err = searchPath.Open(filePath) 102 if typed, ok := f.(fileNamer); ok && typed != nil { 103 finalPath = typed.Name() 104 } else { 105 finalPath = filePath 106 } 107 if err != nil { 108 if os.IsNotExist(err) { 109 continue 110 } 111 return 112 } 113 if f != nil { 114 return 115 } 116 } 117 return 118 } 119 120 func (sc StaticFileServer) fileError(name string, rw http.ResponseWriter, req *http.Request, err error) { 121 if os.IsNotExist(err) { 122 sc.notFound(name, rw, req) 123 return 124 } 125 http.Error(rw, err.Error(), http.StatusInternalServerError) 126 } 127 128 func (sc StaticFileServer) notFound(name string, rw http.ResponseWriter, req *http.Request) { 129 if sc.UseEmptyResponseIfNotFound { 130 ctype := mime.TypeByExtension(filepath.Ext(name)) 131 rw.Header().Set(HeaderContentType, ctype) 132 rw.Header().Set(HeaderContentLength, "0") 133 rw.WriteHeader(http.StatusOK) 134 return 135 } 136 http.NotFound(rw, req) 137 } 138 139 // RewriteRule is a type that modifies a request url path. 140 // 141 // It should take the `url.Path` value, and return an updated 142 // value and true, or the value unchanged and false. 143 type RewriteRule interface { 144 Apply(string) (string, bool) 145 } 146 147 // RewriteRuleFunc is a function that rewrites a url. 148 type RewriteRuleFunc func(string) (string, bool) 149 150 // Apply applies the rewrite rule. 151 func (rrf RewriteRuleFunc) Apply(path string) (string, bool) { 152 return rrf(path) 153 } 154 155 func flatten(pieces [][]string) (output []string) { 156 for _, p := range pieces { 157 output = append(output, p...) 158 } 159 return 160 } 161 162 type fileNamer interface { 163 Name() string 164 }