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  }