package echo import ( "crypto/sha256" "encoding/base64" "io" "io/fs" "net/http" "net/url" "path/filepath" "github.com/labstack/echo/v4" ) type routeFunc func(string, echo.HandlerFunc, ...echo.MiddlewareFunc) *echo.Route func StaticFSSha256Etags(get routeFunc, f fs.FS, prefix, root string, m ...echo.MiddlewareFunc) *echo.Route { return staticFS(get, f, prefix, root, true, m...) } func StaticFS(get routeFunc, f fs.FS, prefix, root string, m ...echo.MiddlewareFunc) *echo.Route { return staticFS(get, f, prefix, root, false, m...) } // TODO: This should support HEAD requests func staticFS(get routeFunc, f fs.FS, prefix, root string, addEtags bool, m ...echo.MiddlewareFunc) *echo.Route { if root == "" { root = "." // For security we want to restrict to CWD. } h := func(c echo.Context) error { p, err := url.PathUnescape(c.Param("*")) if err != nil { return err } name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security fp, err := f.Open(name) if err != nil { // The access path does not exist return echo.NotFoundHandler(c) } defer fp.Close() fi, err := fp.Stat() if err != nil { // The access path does not exist return echo.NotFoundHandler(c) } // If the request is for a directory and does not end with "/" p = c.Request().URL.Path // path must not be empty. if fi.IsDir() && p[len(p)-1] != '/' { // Redirect to ends with "/" // return c.Redirect(http.StatusMovedPermanently, p+"/") // TODO: Serve an index.html if there is one for this dir return echo.NotFoundHandler(c) } fs, ok := fp.(io.ReadSeeker) if !ok { c.Logger().Errorf("File %s is not a io.ReadSeeker", p) return echo.ErrInternalServerError } // Only do this if the consumer requests it since it could be expensive // for high traffic sites as it requires a full read and SHA256 // computation // TODO: Cache this? if addEtags { h := sha256.New() if _, err := io.Copy(h, fs); err != nil { c.Logger().Errorf("Error checksumming file %s: %s", p, err) return echo.ErrInternalServerError } etag := base64.RawStdEncoding.EncodeToString(h.Sum(nil)) c.Response().Header().Add("ETag", etag) if _, err := fs.Seek(0, io.SeekStart); err != nil { c.Logger().Errorf("Error seeking 0 in file %s: %s", p, err) return echo.ErrInternalServerError } } http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), fs) return nil } // Handle added routes based on trailing slash: // /prefix => exact route "/prefix" + any route "/prefix/*" // /prefix/ => only any route "/prefix/*" if prefix != "" { if prefix[len(prefix)-1] == '/' { // Only add any route for intentional trailing slash return get(prefix+"*", h, m...) } get(prefix, h, m...) } return get(prefix+"/*", h, m...) }