diff --git a/group.go b/group.go index 0b6fc7d..2d55687 100644 --- a/group.go +++ b/group.go @@ -1,6 +1,10 @@ package router -import "github.com/valyala/fasthttp" +import ( + "io/fs" + + "github.com/valyala/fasthttp" +) // Group returns a new group. // Path auto-correction, including trailing slashes, is enabled by default. @@ -86,7 +90,7 @@ func (g *Group) ANY(path string, handler fasthttp.RequestHandler) { g.router.ANY(g.prefix+path, handler) } -// ServeFiles serves files from the given file system root. +// ServeFiles serves files from the given file system root path. // The path must end with "/{filepath:*}", files are then served from the local // path /defined/root/dir/{filepath:*}. // For example if root is "/etc" and {filepath:*} is "passwd", the local file @@ -101,6 +105,21 @@ func (g *Group) ServeFiles(path string, rootPath string) { g.router.ServeFiles(g.prefix+path, rootPath) } +// ServeFS serves files from the given file system. +// The path must end with "/{filepath:*}", files are then served from the local +// path /defined/root/dir/{filepath:*}. +// For example if root is "/etc" and {filepath:*} is "passwd", the local file +// "/etc/passwd" would be served. +// Internally a fasthttp.FSHandler is used, therefore http.NotFound is used instead +// Use: +// +// router.ServeFS("/src/{filepath:*}", myFilesystem) +func (g *Group) ServeFS(path string, filesystem fs.FS) { + validatePath(path) + + g.router.ServeFS(g.prefix+path, filesystem) +} + // ServeFilesCustom serves files from the given file system settings. // The path must end with "/{filepath:*}", files are then served from the local // path /defined/root/dir/{filepath:*}. diff --git a/group_test.go b/group_test.go index 15bf0a7..4a24e6f 100644 --- a/group_test.go +++ b/group_test.go @@ -94,6 +94,7 @@ func TestGroup(t *testing.T) { ctx.SetStatusCode(fasthttp.StatusOK) }) r6.ServeFiles("/static/{filepath:*}", "./") + r6.ServeFS("/static/fs/{filepath:*}", fsTestFilesystem) r6.ServeFilesCustom("/custom/static/{filepath:*}", &fasthttp.FS{Root: "./"}) uris := []string{ @@ -110,6 +111,8 @@ func TestGroup(t *testing.T) { "POST /moo/foo/foo/bar HTTP/1.1\r\n\r\n", // testing multiple sub-router group - r6 (grouped from r5) to serve files "GET /moo/foo/foo/static/router.go HTTP/1.1\r\n\r\n", + // testing multiple sub-router group - r6 (grouped from r5) to serve fs + "GET /moo/foo/foo/static/fs/LICENSE HTTP/1.1\r\n\r\n", // testing multiple sub-router group - r6 (grouped from r5) to serve files with custom settings "GET /moo/foo/foo/custom/static/router.go HTTP/1.1\r\n\r\n", } diff --git a/router.go b/router.go index 0a8bd34..b3a10bb 100644 --- a/router.go +++ b/router.go @@ -2,6 +2,7 @@ package router import ( "fmt" + "io/fs" "strings" "github.com/fasthttp/router/radix" @@ -15,8 +16,7 @@ import ( const MethodWild = "*" var ( - defaultContentType = []byte("text/plain; charset=utf-8") - questionMark = byte('?') + questionMark = byte('?') // MatchedRoutePathParam is the param name under which the path of the matched // route is stored, if Router.SaveMatchedRoutePath is set. @@ -164,7 +164,7 @@ func (r *Router) ANY(path string, handler fasthttp.RequestHandler) { r.Handle(MethodWild, path, handler) } -// ServeFiles serves files from the given file system root. +// ServeFiles serves files from the given file system root path. // The path must end with "/{filepath:*}", files are then served from the local // path /defined/root/dir/{filepath:*}. // For example if root is "/etc" and {filepath:*} is "passwd", the local file @@ -182,6 +182,27 @@ func (r *Router) ServeFiles(path string, rootPath string) { }) } +// ServeFS serves files from the given file system. +// The path must end with "/{filepath:*}", files are then served from the local +// path /defined/root/dir/{filepath:*}. +// For example if root is "/etc" and {filepath:*} is "passwd", the local file +// "/etc/passwd" would be served. +// Internally a fasthttp.FSHandler is used, therefore fasthttp.NotFound is used instead +// Use: +// +// router.ServeFS("/src/{filepath:*}", myFilesystem) +func (r *Router) ServeFS(path string, filesystem fs.FS) { + r.ServeFilesCustom(path, &fasthttp.FS{ + FS: filesystem, + Root: "", + AllowEmptyRoot: true, + GenerateIndexPages: true, + AcceptByteRange: true, + Compress: true, + CompressBrotli: true, + }) +} + // ServeFilesCustom serves files from the given file system settings. // The path must end with "/{filepath:*}", files are then served from the local // path /defined/root/dir/{filepath:*}. @@ -193,7 +214,7 @@ func (r *Router) ServeFiles(path string, rootPath string) { // // router.ServeFilesCustom("/src/{filepath:*}", *customFS) func (r *Router) ServeFilesCustom(path string, fs *fasthttp.FS) { - suffix := "/{filepath:*}" + const suffix = "/{filepath:*}" if !strings.HasSuffix(path, suffix) { panic("path must end with " + suffix + " in path '" + path + "'") diff --git a/router_test.go b/router_test.go index 1836bb1..66af8f5 100644 --- a/router_test.go +++ b/router_test.go @@ -3,6 +3,7 @@ package router import ( "bufio" "bytes" + "embed" "fmt" "io/ioutil" "math/rand" @@ -37,6 +38,9 @@ var httpMethods = []string{ "CUSTOM", } +//go:embed LICENSE +var fsTestFilesystem embed.FS + func randomHTTPMethod() string { method := httpMethods[rand.Intn(len(httpMethods)-1)] @@ -934,8 +938,11 @@ func TestRouterServeFiles(t *testing.T) { if recv == nil { t.Fatal("registering path not ending with '{filepath:*}' did not panic") } + body := []byte("fake ico") - ioutil.WriteFile(os.TempDir()+"/favicon.ico", body, 0644) + if err := os.WriteFile(os.TempDir()+"/favicon.ico", body, 0644); err != nil { + t.Fatal(err) + } r.ServeFiles("/{filepath:*}", os.TempDir()) @@ -954,6 +961,38 @@ func TestRouterServeFiles(t *testing.T) { }) } +func TestRouterServeFS(t *testing.T) { + r := New() + + recv := catchPanic(func() { + r.ServeFS("/noFilepath", fsTestFilesystem) + }) + if recv == nil { + t.Fatal("registering path not ending with '{filepath:*}' did not panic") + } + + body, err := os.ReadFile("LICENSE") + if err != nil { + t.Fatal(err) + } + + r.ServeFS("/{filepath:*}", fsTestFilesystem) + + assertWithTestServer(t, "GET /LICENSE HTTP/1.1\r\n\r\n", r.Handler, func(rw *readWriter) { + br := bufio.NewReader(&rw.w) + var resp fasthttp.Response + if err := resp.Read(br); err != nil { + t.Fatalf("Unexpected error when reading response: %s", err) + } + if resp.Header.StatusCode() != 200 { + t.Fatalf("Unexpected status code %d. Expected %d", resp.Header.StatusCode(), 200) + } + if !bytes.Equal(resp.Body(), body) { + t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), string(body)) + } + }) +} + func TestRouterServeFilesCustom(t *testing.T) { r := New()