diff --git a/.github/README.md b/.github/README.md index ee33a86a..40dcf7b1 100644 --- a/.github/README.md +++ b/.github/README.md @@ -203,15 +203,15 @@ func main() { func main() { app := fiber.New() - app.Static("/", "./public") + app.Get("/*", static.New("./public")) // => http://localhost:3000/js/script.js // => http://localhost:3000/css/style.css - app.Static("/prefix", "./public") + app.Get("/prefix*", static.New("./public")) // => http://localhost:3000/prefix/js/script.js // => http://localhost:3000/prefix/css/style.css - app.Static("*", "./public/index.html") + app.Get("*", static.New("./public/index.html")) // => http://localhost:3000/any/path/shows/index/html log.Fatal(app.Listen(":3000")) @@ -388,7 +388,7 @@ curl -H "Origin: http://example.com" --verbose http://localhost:3000 func main() { app := fiber.New() - app.Static("/", "./public") + app.Get("/", static.New("./public")) app.Get("/demo", func(c fiber.Ctx) error { return c.SendString("This is a demo!") @@ -586,7 +586,6 @@ Here is a list of middleware that are included within the Fiber framework. | [etag](https://github.com/gofiber/fiber/tree/main/middleware/etag) | Allows for caches to be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | | [expvar](https://github.com/gofiber/fiber/tree/main/middleware/expvar) | Serves via its HTTP server runtime exposed variants in the JSON format. | | [favicon](https://github.com/gofiber/fiber/tree/main/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | -| [filesystem](https://github.com/gofiber/fiber/tree/main/middleware/filesystem) | FileSystem middleware for Fiber. | | [healthcheck](https://github.com/gofiber/fiber/tree/main/middleware/healthcheck) | Liveness and Readiness probes for Fiber. | | [helmet](https://github.com/gofiber/fiber/tree/main/middleware/helmet) | Helps secure your apps by setting various HTTP headers. | | [idempotency](https://github.com/gofiber/fiber/tree/main/middleware/idempotency) | Allows for fault-tolerant APIs where duplicate requests do not erroneously cause the same action performed multiple times on the server-side. | @@ -601,6 +600,7 @@ Here is a list of middleware that are included within the Fiber framework. | [rewrite](https://github.com/gofiber/fiber/tree/main/middleware/rewrite) | Rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [session](https://github.com/gofiber/fiber/tree/main/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/main/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. | +| [static](https://github.com/gofiber/fiber/tree/main/middleware/static) | Static middleware for Fiber that serves static files such as **images**, **CSS,** and **JavaScript**. | | [timeout](https://github.com/gofiber/fiber/tree/main/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | ## 🧬 External Middleware diff --git a/app.go b/app.go index 5147e9e4..351b3a21 100644 --- a/app.go +++ b/app.go @@ -381,52 +381,6 @@ type Config struct { EnableSplittingOnParsers bool `json:"enable_splitting_on_parsers"` } -// Static defines configuration options when defining static assets. -type Static struct { - // When set to true, the server tries minimizing CPU usage by caching compressed files. - // This works differently than the github.com/gofiber/compression middleware. - // Optional. Default value false - Compress bool `json:"compress"` - - // When set to true, enables byte range requests. - // Optional. Default value false - ByteRange bool `json:"byte_range"` - - // When set to true, enables directory browsing. - // Optional. Default value false. - Browse bool `json:"browse"` - - // When set to true, enables direct download. - // Optional. Default value false. - Download bool `json:"download"` - - // The name of the index file for serving a directory. - // Optional. Default value "index.html". - Index string `json:"index"` - - // Expiration duration for inactive file handlers. - // Use a negative time.Duration to disable it. - // - // Optional. Default value 10 * time.Second. - CacheDuration time.Duration `json:"cache_duration"` - - // The value for the Cache-Control HTTP-header - // that is set on the file response. MaxAge is defined in seconds. - // - // Optional. Default value 0. - MaxAge int `json:"max_age"` - - // ModifyResponse defines a function that allows you to alter the response. - // - // Optional. Default: nil - ModifyResponse Handler - - // Next defines a function to skip this middleware when returned true. - // - // Optional. Default: nil - Next func(c Ctx) bool -} - // RouteMessage is some message need to be print when server starts type RouteMessage struct { name string @@ -780,13 +734,6 @@ func (app *App) Add(methods []string, path string, handler Handler, middleware . return app } -// Static will create a file server serving static files -func (app *App) Static(prefix, root string, config ...Static) Router { - app.registerStatic(prefix, root, config...) - - return app -} - // All will register the handler on all HTTP methods func (app *App) All(path string, handler Handler, middleware ...Handler) Router { return app.Add(app.config.RequestMethods, path, handler, middleware...) diff --git a/app_test.go b/app_test.go index 39890ead..6b493de1 100644 --- a/app_test.go +++ b/app_test.go @@ -901,314 +901,6 @@ func Test_App_ShutdownWithContext(t *testing.T) { } } -// go test -run Test_App_Static_Index_Default -func Test_App_Static_Index_Default(t *testing.T) { - t.Parallel() - app := New() - - app.Static("/prefix", "./.github/workflows") - app.Static("", "./.github/") - app.Static("test", "", Static{Index: "index.html"}) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "Hello, World!") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/not-found", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err = io.ReadAll(resp.Body) - require.NoError(t, err) - require.Equal(t, "Cannot GET /not-found", string(body)) -} - -// go test -run Test_App_Static_Index -func Test_App_Static_Direct(t *testing.T) { - t.Parallel() - app := New() - - app.Static("/", "./.github") - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "Hello, World!") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/testdata/testRoutes.json", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMEApplicationJSON, resp.Header.Get("Content-Type")) - require.Equal(t, "", resp.Header.Get(HeaderCacheControl), "CacheControl Control") - - body, err = io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "test_routes") -} - -// go test -run Test_App_Static_MaxAge -func Test_App_Static_MaxAge(t *testing.T) { - t.Parallel() - app := New() - - app.Static("/", "./.github", Static{MaxAge: 100}) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(HeaderContentType)) - require.Equal(t, "public, max-age=100", resp.Header.Get(HeaderCacheControl), "CacheControl Control") -} - -// go test -run Test_App_Static_Custom_CacheControl -func Test_App_Static_Custom_CacheControl(t *testing.T) { - t.Parallel() - app := New() - - app.Static("/", "./.github", Static{ModifyResponse: func(c Ctx) error { - if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") { - c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") - } - return nil - }}) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get(HeaderCacheControl), "CacheControl Control") - - normalResp, normalErr := app.Test(httptest.NewRequest(MethodGet, "/config.yml", nil)) - require.NoError(t, normalErr, "app.Test(req)") - require.Equal(t, "", normalResp.Header.Get(HeaderCacheControl), "CacheControl Control") -} - -// go test -run Test_App_Static_Download -func Test_App_Static_Download(t *testing.T) { - t.Parallel() - app := New() - - app.Static("/fiber.png", "./.github/testdata/fs/img/fiber.png", Static{Download: true}) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/fiber.png", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, "image/png", resp.Header.Get(HeaderContentType)) - require.Equal(t, `attachment`, resp.Header.Get(HeaderContentDisposition)) -} - -// go test -run Test_App_Static_Group -func Test_App_Static_Group(t *testing.T) { - t.Parallel() - app := New() - - grp := app.Group("/v1", func(c Ctx) error { - c.Set("Test-Header", "123") - return c.Next() - }) - - grp.Static("/v2", "./.github/index.html") - - req := httptest.NewRequest(MethodGet, "/v1/v2", nil) - resp, err := app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - require.Equal(t, "123", resp.Header.Get("Test-Header")) - - grp = app.Group("/v2") - grp.Static("/v3*", "./.github/index.html") - - req = httptest.NewRequest(MethodGet, "/v2/v3/john/doe", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) -} - -func Test_App_Static_Wildcard(t *testing.T) { - t.Parallel() - app := New() - - app.Static("*", "./.github/index.html") - - req := httptest.NewRequest(MethodGet, "/yesyes/john/doe", nil) - resp, err := app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "Test file") -} - -func Test_App_Static_Prefix_Wildcard(t *testing.T) { - t.Parallel() - app := New() - - app.Static("/test/*", "./.github/index.html") - - req := httptest.NewRequest(MethodGet, "/test/john/doe", nil) - resp, err := app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - app.Static("/my/nameisjohn*", "./.github/index.html") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/my/nameisjohn/no/its/not", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "Test file") -} - -func Test_App_Static_Prefix(t *testing.T) { - t.Parallel() - app := New() - app.Static("/john", "./.github") - - req := httptest.NewRequest(MethodGet, "/john/index.html", nil) - resp, err := app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - app.Static("/prefix", "./.github/testdata") - - req = httptest.NewRequest(MethodGet, "/prefix/index.html", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - app.Static("/single", "./.github/testdata/testRoutes.json") - - req = httptest.NewRequest(MethodGet, "/single", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMEApplicationJSON, resp.Header.Get(HeaderContentType)) -} - -func Test_App_Static_Trailing_Slash(t *testing.T) { - t.Parallel() - app := New() - app.Static("/john", "./.github") - - req := httptest.NewRequest(MethodGet, "/john/", nil) - resp, err := app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - app.Static("/john_without_index", "./.github/testdata/fs/css") - - req = httptest.NewRequest(MethodGet, "/john_without_index/", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) - - app.Static("/john/", "./.github") - - req = httptest.NewRequest(MethodGet, "/john/", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - req = httptest.NewRequest(MethodGet, "/john", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - app.Static("/john_without_index/", "./.github/testdata/fs/css") - - req = httptest.NewRequest(MethodGet, "/john_without_index/", nil) - resp, err = app.Test(req) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) -} - -func Test_App_Static_Next(t *testing.T) { - t.Parallel() - app := New() - app.Static("/", ".github", Static{ - Next: func(c Ctx) bool { - // If value of the header is any other from "skip" - // c.Next() will be invoked - return c.Get("X-Custom-Header") == "skip" - }, - }) - app.Get("/", func(c Ctx) error { - return c.SendString("You've skipped app.Static") - }) - - t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) { - t.Parallel() - req := httptest.NewRequest(MethodGet, "/", nil) - req.Header.Set("X-Custom-Header", "skip") - resp, err := app.Test(req) - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "You've skipped app.Static") - }) - - t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) { - t.Parallel() - req := httptest.NewRequest(MethodGet, "/", nil) - req.Header.Set("X-Custom-Header", "don't skip") - resp, err := app.Test(req) - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) - require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) - require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(body), "Hello, World!") - }) -} - // go test -run Test_App_Mixed_Routes_WithSameLen func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { t.Parallel() @@ -1220,7 +912,10 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { return c.Next() }) // routes with the same length - app.Static("/tesbar", "./.github") + app.Get("/tesbar", func(c Ctx) error { + c.Type("html") + return c.Send([]byte("TEST_BAR")) + }) app.Get("/foobar", func(c Ctx) error { c.Type("html") return c.Send([]byte("FOO_BAR")) @@ -1246,12 +941,11 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { require.Equal(t, 200, resp.StatusCode, "Status code") require.NotEmpty(t, resp.Header.Get(HeaderContentLength)) require.Equal(t, "TestValue", resp.Header.Get("TestHeader")) - require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(HeaderContentType)) + require.Equal(t, "text/html", resp.Header.Get(HeaderContentType)) body, err = io.ReadAll(resp.Body) require.NoError(t, err) - require.Contains(t, string(body), "Hello, World!") - require.True(t, strings.HasPrefix(string(body), ""), "Response: "+string(body)) + require.Contains(t, string(body), "TEST_BAR") } func Test_App_Group_Invalid(t *testing.T) { diff --git a/constants.go b/constants.go index c8561839..6144dc76 100644 --- a/constants.go +++ b/constants.go @@ -20,6 +20,7 @@ const ( MIMETextHTML = "text/html" MIMETextPlain = "text/plain" MIMETextJavaScript = "text/javascript" + MIMETextCSS = "text/css" MIMEApplicationXML = "application/xml" MIMEApplicationJSON = "application/json" // Deprecated: use MIMETextJavaScript instead @@ -32,6 +33,7 @@ const ( MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" + MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8" MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" // Deprecated: use MIMETextJavaScriptCharsetUTF8 instead diff --git a/docs/api/app.md b/docs/api/app.md index b9bd9772..164b1edc 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -11,70 +11,6 @@ import Reference from '@site/src/components/reference'; import RoutingHandler from './../partials/routing/handler.md'; -### Static - -Use the **Static** method to serve static files such as **images**, **CSS,** and **JavaScript**. - -:::info -By default, **Static** will serve `index.html` files in response to a request on a directory. -::: - -```go title="Signature" -func (app *App) Static(prefix, root string, config ...Static) Router -``` - -Use the following code to serve files in a directory named `./public` - -```go title="Examples" -// Serve files from multiple directories -app.Static("/", "./public") - -// => http://localhost:3000/hello.html -// => http://localhost:3000/js/jquery.js -// => http://localhost:3000/css/style.css - -// Serve files from "./files" directory: -app.Static("/", "./files") -``` - -You can use any virtual path prefix \(_where the path does not actually exist in the file system_\) for files that are served by the **Static** method, specify a prefix path for the static directory, as shown below: - -```go title="Examples" -app.Static("/static", "./public") - -// => http://localhost:3000/static/hello.html -// => http://localhost:3000/static/js/jquery.js -// => http://localhost:3000/static/css/style.css -``` - -#### Config - -If you want to have a little bit more control regarding the settings for serving static files. You could use the `fiber.Static` struct to enable specific settings. - -| Property | Type | Description | Default | -|------------------------------------------------------------|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| -| Compress | `bool` | When set to true, the server tries minimizing CPU usage by caching compressed files. This works differently than the [compress](../middleware/compress.md) middleware. | false | -| ByteRange | `bool` | When set to true, enables byte range requests. | false | -| Browse | `bool` | When set to true, enables directory browsing. | false | -| Download | `bool` | When set to true, enables direct download. | false | -| Index | `string` | The name of the index file for serving a directory. | "index.html" | -| CacheDuration | `time.Duration` | Expiration duration for inactive file handlers. Use a negative `time.Duration` to disable it. | 10 * time.Second | -| MaxAge | `int` | The value for the `Cache-Control` HTTP-header that is set on the file response. MaxAge is defined in seconds. | 0 | -| ModifyResponse | `Handler` | ModifyResponse defines a function that allows you to alter the response. | nil | -| Next | `func(c Ctx) bool` | Next defines a function to skip this middleware when returned true. | nil | - -```go title="Example" -// Custom config -app.Static("/", "./public", fiber.Static{ - Compress: true, - ByteRange: true, - Browse: true, - Index: "john.html", - CacheDuration: 10 * time.Second, - MaxAge: 3600, -}) -``` - ### Route Handlers @@ -181,8 +117,6 @@ type Register interface { Add(methods []string, handler Handler, middleware ...Handler) Register - Static(root string, config ...Static) Register - Route(path string) Register } ``` diff --git a/docs/api/constants.md b/docs/api/constants.md index fce36d36..a9ee6d5a 100644 --- a/docs/api/constants.md +++ b/docs/api/constants.md @@ -26,24 +26,27 @@ const ( ```go const ( - MIMETextXML = "text/xml" - MIMETextHTML = "text/html" - MIMETextPlain = "text/plain" - MIMEApplicationXML = "application/xml" - MIMEApplicationJSON = "application/json" + MIMETextXML = "text/xml" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + MIMETextJavaScript = "text/javascript" + MIMETextCSS = "text/css" + MIMEApplicationXML = "application/xml" + MIMEApplicationJSON = "application/json" MIMEApplicationJavaScript = "application/javascript" MIMEApplicationForm = "application/x-www-form-urlencoded" MIMEOctetStream = "application/octet-stream" MIMEMultipartForm = "multipart/form-data" - MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" - MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" - MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" - MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" - MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" + MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" + MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" + MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" + MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" + MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8" + MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" + MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" -) -``` +)``` ### HTTP status codes were copied from net/http. diff --git a/docs/intro.md b/docs/intro.md index 6dbc6ca7..d482df10 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -164,19 +164,15 @@ app.Get("/api/*", func(c fiber.Ctx) error { ### Static files To serve static files such as **images**, **CSS**, and **JavaScript** files, replace your function handler with a file or directory string. - +You can check out [static middleware](./middleware/static.md) for more information. Function signature: -```go -app.Static(prefix, root string, config ...Static) -``` - Use the following code to serve files in a directory named `./public`: ```go app := fiber.New() -app.Static("/", "./public") +app.Get("/*", static.New("./public")) app.Listen(":3000") ``` diff --git a/docs/middleware/filesystem.md b/docs/middleware/filesystem.md deleted file mode 100644 index 01d50270..00000000 --- a/docs/middleware/filesystem.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -id: filesystem ---- - -# FileSystem - -Filesystem middleware for [Fiber](https://github.com/gofiber/fiber) that enables you to serve files from a directory. - -:::caution -**`:params` & `:optionals?` within the prefix path are not supported!** - -**To handle paths with spaces (or other url encoded values) make sure to set `fiber.Config{ UnescapePath: true }`** -::: - -## Signatures - -```go -func New(config Config) fiber.Handler -``` - -## Examples - -Import the middleware package that is part of the Fiber web framework - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: - -```go -// Provide a minimal config -app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), -})) - -// Or extend your config for customization -app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), - Browse: true, - Index: "index.html", - NotFoundFile: "404.html", - MaxAge: 3600, -})) -``` - - -> If your environment (Go 1.16+) supports it, we recommend using Go Embed instead of the other solutions listed as this one is native to Go and the easiest to use. - -## embed - -[Embed](https://golang.org/pkg/embed/) is the native method to embed files in a Golang excecutable. Introduced in Go 1.16. - -```go -package main - -import ( - "embed" - "io/fs" - "log" - "net/http" - - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" -) - -// Embed a single file -//go:embed index.html -var f embed.FS - -// Embed a directory -//go:embed static/* -var embedDirStatic embed.FS - -func main() { - app := fiber.New() - - app.Use("/", filesystem.New(filesystem.Config{ - Root: http.FS(f), - })) - - // Access file "image.png" under `static/` directory via URL: `http:///static/image.png`. - // Without `PathPrefix`, you have to access it via URL: - // `http:///static/static/image.png`. - app.Use("/static", filesystem.New(filesystem.Config{ - Root: http.FS(embedDirStatic), - PathPrefix: "static", - Browse: true, - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## pkger - -[https://github.com/markbates/pkger](https://github.com/markbates/pkger) - -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" - - "github.com/markbates/pkger" -) - -func main() { - app := fiber.New() - - app.Use("/assets", filesystem.New(filesystem.Config{ - Root: pkger.Dir("/assets"), - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## packr - -[https://github.com/gobuffalo/packr](https://github.com/gobuffalo/packr) - -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" - - "github.com/gobuffalo/packr/v2" -) - -func main() { - app := fiber.New() - - app.Use("/assets", filesystem.New(filesystem.Config{ - Root: packr.New("Assets Box", "/assets"), - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## go.rice - -[https://github.com/GeertJohan/go.rice](https://github.com/GeertJohan/go.rice) - -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" - - "github.com/GeertJohan/go.rice" -) - -func main() { - app := fiber.New() - - app.Use("/assets", filesystem.New(filesystem.Config{ - Root: rice.MustFindBox("assets").HTTPBox(), - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## fileb0x - -[https://github.com/UnnoTed/fileb0x](https://github.com/UnnoTed/fileb0x) - -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" - - "/myEmbeddedFiles" -) - -func main() { - app := fiber.New() - - app.Use("/assets", filesystem.New(filesystem.Config{ - Root: myEmbeddedFiles.HTTP, - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## statik - -[https://github.com/rakyll/statik](https://github.com/rakyll/statik) - -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" - - // Use blank to invoke init function and register data to statik - _ "/statik" - "github.com/rakyll/statik/fs" -) - -func main() { - statikFS, err := fs.New() - if err != nil { - panic(err) - } - - app := fiber.New() - - app.Use("/", filesystem.New(filesystem.Config{ - Root: statikFS, - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## Config - -| Property | Type | Description | Default | -|:-------------------|:------------------------|:------------------------------------------------------------------------------------------------------------|:-------------| -| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Root | `http.FileSystem` | Root is a FileSystem that provides access to a collection of files and directories. | `nil` | -| PathPrefix | `string` | PathPrefix defines a prefix to be added to a filepath when reading a file from the FileSystem. | "" | -| Browse | `bool` | Enable directory browsing. | `false` | -| Index | `string` | Index file for serving a directory. | "index.html" | -| MaxAge | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds. | 0 | -| NotFoundFile | `string` | File to return if the path is not found. Useful for SPA's. | "" | -| ContentTypeCharset | `string` | The value for the Content-Type HTTP-header that is set on the file response. | "" | - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - Root: nil, - PathPrefix: "", - Browse: false, - Index: "/index.html", - MaxAge: 0, - ContentTypeCharset: "", -} -``` - -## Utils - -### SendFile - -Serves a file from an [HTTP file system](https://pkg.go.dev/net/http#FileSystem) at the specified path. - -```go title="Signature" title="Signature" -func SendFile(c fiber.Ctx, filesystem http.FileSystem, path string) error -``` -Import the middleware package that is part of the Fiber web framework - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" -) -``` - -```go title="Example" -// Define a route to serve a specific file -app.Get("/download", func(c fiber.Ctx) error { - // Serve the file using SendFile function - err := filesystem.SendFile(c, http.Dir("your/filesystem/root"), "path/to/your/file.txt") - if err != nil { - // Handle the error, e.g., return a 404 Not Found response - return c.Status(fiber.StatusNotFound).SendString("File not found") - } - - return nil -}) -``` - -```go title="Example" -// Serve static files from the "build" directory using Fiber's built-in middleware. -app.Use("/", filesystem.New(filesystem.Config{ - Root: http.FS(f), // Specify the root directory for static files. - PathPrefix: "build", // Define the path prefix where static files are served. -})) - -// For all other routes (wildcard "*"), serve the "index.html" file from the "build" directory. -app.Use("*", func(ctx fiber.Ctx) error { - return filesystem.SendFile(ctx, http.FS(f), "build/index.html") -}) -``` diff --git a/docs/middleware/static.md b/docs/middleware/static.md new file mode 100644 index 00000000..6e292b08 --- /dev/null +++ b/docs/middleware/static.md @@ -0,0 +1,172 @@ +--- +id: static +--- + +# Static + +Static middleware for Fiber that serves static files such as **images**, **CSS,** and **JavaScript**. + +:::info +By default, **Static** will serve `index.html` files in response to a request on a directory. You can change it from [Config](#config)` +::: + +## Signatures + +```go +func New(root string, cfg ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework +```go +import( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) +``` + +### Serving files from a directory + +```go +app.Get("/*", static.New("./public")) +``` + +
+Test + +```sh +curl http://localhost:3000/hello.html +curl http://localhost:3000/css/style.css +``` + +
+ +### Serving files from a directory with Use + +```go +app.Use("/", static.New("./public")) +``` + +
+Test + +```sh +curl http://localhost:3000/hello.html +curl http://localhost:3000/css/style.css +``` + +
+ +### Serving a file + +```go +app.Use("/static", static.New("./public/hello.html")) +``` + +
+Test + +```sh +curl http://localhost:3000/static # will show hello.html +curl http://localhost:3000/static/john/doee # will show hello.html +``` + +
+ +### Serving files using os.DirFS + +```go +app.Get("/files*", static.New("", static.Config{ + FS: os.DirFS("files"), + Browse: true, +})) +``` + +
+Test + +```sh +curl http://localhost:3000/files/css/style.css +curl http://localhost:3000/files/index.html +``` + +
+ +### Serving files using embed.FS + +```go +//go:embed path/to/files +var myfiles embed.FS + +app.Get("/files*", static.New("", static.Config{ + FS: myfiles, + Browse: true, +})) +``` + +
+Test + +```sh +curl http://localhost:3000/files/css/style.css +curl http://localhost:3000/files/index.html +``` + +
+ +### SPA (Single Page Application) + +```go +app.Use("/web", static.New("", static.Config{ + FS: os.DirFS("dist"), +})) + +app.Get("/web*", func(c fiber.Ctx) error { + return c.SendFile("dist/index.html") +}) +``` + +
+Test + +```sh +curl http://localhost:3000/web/css/style.css +curl http://localhost:3000/web/index.html +curl http://localhost:3000/web +``` + +
+ +:::caution +To define static routes using `Get`, append the wildcard (`*`) operator at the end of the route. +::: + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:---------------------------------------------------------------------------------------------------------------------------|:-----------------------| +| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| FS | `fs.FS` | FS is the file system to serve the static files from.

You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc. | `nil` | +| Compress | `bool` | When set to true, the server tries minimizing CPU usage by caching compressed files.

This works differently than the github.com/gofiber/compression middleware. | `false` | +| ByteRange | `bool` | When set to true, enables byte range requests. | `false` | +| Browse | `bool` | When set to true, enables directory browsing. | `false` | +| Download | `bool` | When set to true, enables direct download. | `false` | +| IndexNames | `[]string` | The names of the index files for serving a directory. | `[]string{"index.html"}` | +| CacheDuration | `string` | Expiration duration for inactive file handlers.

Use a negative time.Duration to disable it. | `10 * time.Second` | +| MaxAge | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds. | `0` | +| ModifyResponse | `fiber.Handler` | ModifyResponse defines a function that allows you to alter the response. | `nil` | +| NotFoundHandler | `fiber.Handler` | NotFoundHandler defines a function to handle when the path is not found. | `nil` | + +:::info +You can set `CacheDuration` config property to `-1` to disable caching. +::: + +## Default Config + +```go +var ConfigDefault = Config{ + Index: []string{"index.html"}, + CacheDuration: 10 * time.Second, +} +``` diff --git a/docs/whats_new.md b/docs/whats_new.md index fe193485..ff8b834d 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -47,6 +47,7 @@ DRAFT section We have made several changes to the Fiber app, including: * Listen -> unified with config +* Static -> has been removed and moved to [static middleware](./middleware/static.md) * app.Config properties moved to listen config * DisableStartupMessage * EnablePrefork -> previously Prefork @@ -270,9 +271,8 @@ DRAFT section ### Filesystem -:::caution -DRAFT section -::: +We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. +Now, static middleware can do everything that filesystem middleware and static do. You can check out [static middleware](./middleware/static.md) or [migration guide](#-migration-guide) to see what has been changed. ### Monitor @@ -295,6 +295,34 @@ Monitor middleware is now in Contrib package. ### 🚀 App +#### Static + +Since we've removed `app.Static()`, you need to move methods to static middleware like the example below: + +```go +// Before +app.Static("/", "./public") +app.Static("/prefix", "./public") +app.Static("/prefix", "./public", Static{ + Index: "index.htm", +}) +app.Static("*", "./public/index.html") +``` + +```go +// After +app.Get("/*", static.New("./public")) +app.Get("/prefix*", static.New("./public")) +app.Get("/prefix*", static.New("./public", static.Config{ + IndexNames: []string{"index.htm", "index.html"}, +})) +app.Get("*", static.New("./public/index.html")) +``` + +:::caution +You have to put `*` to the end of the route if you don't define static route with `app.Use`. +::: + ### 🗺 Router ### 🧠 Context @@ -328,4 +356,35 @@ app.Use(cors.New(cors.Config{ ExposeHeaders: []string{"Content-Length"}, })) ``` -... + +#### Filesystem + +You need to move filesystem middleware to static middleware due to it has been removed from the core. + +```go +// Before +app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir("./assets"), +})) + +app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir("./assets"), + Browse: true, + Index: "index.html", + MaxAge: 3600, +})) +``` + +```go +// After +app.Use(static.New("", static.Config{ + FS: os.DirFS("./assets"), +})) + +app.Use(static.New("", static.Config{ + FS: os.DirFS("./assets"), + Browse: true, + IndexNames: []string{"index.html"}, + MaxAge: 3600, +})) +``` \ No newline at end of file diff --git a/group.go b/group.go index 2b8001d5..fe2ac97a 100644 --- a/group.go +++ b/group.go @@ -170,16 +170,6 @@ func (grp *Group) Add(methods []string, path string, handler Handler, middleware return grp } -// Static will create a file server serving static files -func (grp *Group) Static(prefix, root string, config ...Static) Router { - grp.app.registerStatic(getGroupPath(grp.Prefix, prefix), root, config...) - if !grp.anyRouteDefined { - grp.anyRouteDefined = true - } - - return grp -} - // All will register the handler on all HTTP methods func (grp *Group) All(path string, handler Handler, middleware ...Handler) Router { _ = grp.Add(grp.app.config.RequestMethods, path, handler, middleware...) diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go deleted file mode 100644 index 62d4f4f6..00000000 --- a/middleware/filesystem/filesystem.go +++ /dev/null @@ -1,323 +0,0 @@ -package filesystem - -import ( - "errors" - "fmt" - "io/fs" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - - "github.com/gofiber/fiber/v3" -) - -// Config defines the config for middleware. -type Config struct { - // Next defines a function to skip this middleware when returned true. - // - // Optional. Default: nil - Next func(c fiber.Ctx) bool - - // Root is a FileSystem that provides access - // to a collection of files and directories. - // - // Required. Default: nil - Root fs.FS `json:"-"` - - // PathPrefix defines a prefix to be added to a filepath when - // reading a file from the FileSystem. - // - // Optional. Default "." - PathPrefix string `json:"path_prefix"` - - // Enable directory browsing. - // - // Optional. Default: false - Browse bool `json:"browse"` - - // Index file for serving a directory. - // - // Optional. Default: "index.html" - Index string `json:"index"` - - // When set to true, enables direct download for files. - // - // Optional. Default: false. - Download bool `json:"download"` - - // The value for the Cache-Control HTTP-header - // that is set on the file response. MaxAge is defined in seconds. - // - // Optional. Default value 0. - MaxAge int `json:"max_age"` - - // File to return if path is not found. Useful for SPA's. - // - // Optional. Default: "" - NotFoundFile string `json:"not_found_file"` - - // The value for the Content-Type HTTP-header - // that is set on the file response - // - // Optional. Default: "" - ContentTypeCharset string `json:"content_type_charset"` -} - -// ConfigDefault is the default config -var ConfigDefault = Config{ - Next: nil, - Root: nil, - PathPrefix: ".", - Browse: false, - Index: "/index.html", - MaxAge: 0, - ContentTypeCharset: "", -} - -// New creates a new middleware handler. -// -// filesystem does not handle url encoded values (for example spaces) -// on it's own. If you need that functionality, set "UnescapePath" -// in fiber.Config -func New(config ...Config) fiber.Handler { - // Set default config - cfg := ConfigDefault - - // Override config if provided - if len(config) > 0 { - cfg = config[0] - - // Set default values - if cfg.Index == "" { - cfg.Index = ConfigDefault.Index - } - if cfg.PathPrefix == "" { - cfg.PathPrefix = ConfigDefault.PathPrefix - } - if !strings.HasPrefix(cfg.Index, "/") { - cfg.Index = "/" + cfg.Index - } - if cfg.NotFoundFile != "" && !strings.HasPrefix(cfg.NotFoundFile, "/") { - cfg.NotFoundFile = "/" + cfg.NotFoundFile - } - } - - if cfg.Root == nil { - panic("filesystem: Root cannot be nil") - } - - // PathPrefix configurations for io/fs compatibility. - if cfg.PathPrefix != "." && !strings.HasPrefix(cfg.PathPrefix, "/") { - cfg.PathPrefix = "./" + cfg.PathPrefix - } - - if cfg.NotFoundFile != "" { - cfg.NotFoundFile = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+cfg.NotFoundFile)) - } - - var once sync.Once - var prefix string - cacheControlStr := "public, max-age=" + strconv.Itoa(cfg.MaxAge) - - // Return new handler - return func(c fiber.Ctx) error { - // Don't execute middleware if Next returns true - if cfg.Next != nil && cfg.Next(c) { - return c.Next() - } - - method := c.Method() - - // We only serve static assets on GET or HEAD methods - if method != fiber.MethodGet && method != fiber.MethodHead { - return c.Next() - } - - // Set prefix once - once.Do(func() { - prefix = c.Route().Path - }) - - // Strip prefix - path := strings.TrimPrefix(c.Path(), prefix) - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - - var ( - file fs.File - stat os.FileInfo - ) - - // Add PathPrefix - if cfg.PathPrefix != "" { - // PathPrefix already has a "/" prefix - path = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+path)) - } - - if len(path) > 1 { - path = strings.TrimRight(path, "/") - } - - file, err := openFile(cfg.Root, path) - - if err != nil && errors.Is(err, fs.ErrNotExist) && cfg.NotFoundFile != "" { - file, err = openFile(cfg.Root, cfg.NotFoundFile) - } - - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return c.Status(fiber.StatusNotFound).Next() - } - return fmt.Errorf("failed to open: %w", err) - } - - stat, err = file.Stat() - if err != nil { - return fmt.Errorf("failed to stat: %w", err) - } - - // Serve index if path is directory - if stat.IsDir() { - indexPath := strings.TrimRight(path, "/") + cfg.Index - indexPath = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+indexPath)) - - index, err := openFile(cfg.Root, indexPath) - if err == nil { - indexStat, err := index.Stat() - if err == nil { - file = index - stat = indexStat - } - } - } - - // Browse directory if no index found and browsing is enabled - if stat.IsDir() { - if cfg.Browse { - return dirList(c, file) - } - - return fiber.ErrForbidden - } - - c.Status(fiber.StatusOK) - - modTime := stat.ModTime() - contentLength := int(stat.Size()) - - // Set Content Type header - if cfg.ContentTypeCharset == "" { - c.Type(getFileExtension(stat.Name())) - } else { - c.Type(getFileExtension(stat.Name()), cfg.ContentTypeCharset) - } - - // Set Last Modified header - if !modTime.IsZero() { - c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat)) - } - - // Sets the response Content-Disposition header to attachment if the Download option is true and if it's a file - if cfg.Download && !stat.IsDir() { - c.Attachment() - } - - if method == fiber.MethodGet { - if cfg.MaxAge > 0 { - c.Set(fiber.HeaderCacheControl, cacheControlStr) - } - c.Response().SetBodyStream(file, contentLength) - return nil - } - if method == fiber.MethodHead { - c.Request().ResetBody() - // Fasthttp should skipbody by default if HEAD? - c.Response().SkipBody = true - c.Response().Header.SetContentLength(contentLength) - if err := file.Close(); err != nil { - return fmt.Errorf("failed to close: %w", err) - } - return nil - } - - return c.Next() - } -} - -// SendFile serves a file from an fs.FS filesystem at the specified path. -// It handles content serving, sets appropriate headers, and returns errors when needed. -// Usage: err := SendFile(ctx, fs, "/path/to/file.txt") -func SendFile(c fiber.Ctx, filesystem fs.FS, path string) error { - var ( - file fs.File - stat os.FileInfo - ) - - path = filepath.Join(".", filepath.Clean("/"+path)) - - file, err := openFile(filesystem, path) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return fiber.ErrNotFound - } - return fmt.Errorf("failed to open: %w", err) - } - - stat, err = file.Stat() - if err != nil { - return fmt.Errorf("failed to stat: %w", err) - } - - // Serve index if path is directory - if stat.IsDir() { - indexPath := strings.TrimRight(path, "/") + ConfigDefault.Index - index, err := openFile(filesystem, indexPath) - if err == nil { - indexStat, err := index.Stat() - if err == nil { - file = index - stat = indexStat - } - } - } - - // Return forbidden if no index found - if stat.IsDir() { - return fiber.ErrForbidden - } - - c.Status(fiber.StatusOK) - - modTime := stat.ModTime() - contentLength := int(stat.Size()) - - // Set Content Type header - c.Type(getFileExtension(stat.Name())) - - // Set Last Modified header - if !modTime.IsZero() { - c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat)) - } - - method := c.Method() - if method == fiber.MethodGet { - c.Response().SetBodyStream(file, contentLength) - return nil - } - if method == fiber.MethodHead { - c.Request().ResetBody() - // Fasthttp should skipbody by default if HEAD? - c.Response().SkipBody = true - c.Response().Header.SetContentLength(contentLength) - if err := file.Close(); err != nil { - return fmt.Errorf("failed to close: %w", err) - } - return nil - } - - return nil -} diff --git a/middleware/filesystem/filesystem_test.go b/middleware/filesystem/filesystem_test.go deleted file mode 100644 index feccdd12..00000000 --- a/middleware/filesystem/filesystem_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package filesystem - -import ( - "context" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/gofiber/fiber/v3" - "github.com/stretchr/testify/require" -) - -// go test -run Test_FileSystem -func Test_FileSystem(t *testing.T) { - t.Parallel() - app := fiber.New() - - app.Use("/test", New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - })) - - app.Use("/dir", New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - Browse: true, - })) - - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) - - app.Use("/spatest", New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - Index: "index.html", - NotFoundFile: "index.html", - })) - - app.Use("/prefix", New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - PathPrefix: "img", - })) - - tests := []struct { - name string - url string - statusCode int - contentType string - modifiedTime string - }{ - { - name: "Should be returns status 200 with suitable content-type", - url: "/test/index.html", - statusCode: 200, - contentType: "text/html", - }, - { - name: "Should be returns status 200 with suitable content-type", - url: "/test", - statusCode: 200, - contentType: "text/html", - }, - { - name: "Should be returns status 200 with suitable content-type", - url: "/test/css/style.css", - statusCode: 200, - contentType: "text/css", - }, - { - name: "Should be returns status 404", - url: "/test/nofile.js", - statusCode: 404, - }, - { - name: "Should be returns status 404", - url: "/test/nofile", - statusCode: 404, - }, - { - name: "Should be returns status 200", - url: "/", - statusCode: 200, - contentType: "text/plain; charset=utf-8", - }, - { - name: "Should be returns status 403", - url: "/test/img", - statusCode: 403, - }, - { - name: "Should list the directory contents", - url: "/dir/img", - statusCode: 200, - contentType: "text/html", - }, - { - name: "Should list the directory contents", - url: "/dir/img/", - statusCode: 200, - contentType: "text/html", - }, - { - name: "Should be returns status 200", - url: "/dir/img/fiber.png", - statusCode: 200, - contentType: "image/png", - }, - { - name: "Should be return status 200", - url: "/spatest/doesnotexist", - statusCode: 200, - contentType: "text/html", - }, - { - name: "PathPrefix should be applied", - url: "/prefix/fiber.png", - statusCode: 200, - contentType: "image/png", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tt.url, nil)) - require.NoError(t, err) - require.Equal(t, tt.statusCode, resp.StatusCode) - - if tt.contentType != "" { - ct := resp.Header.Get("Content-Type") - require.Equal(t, tt.contentType, ct) - } - }) - } -} - -// go test -run Test_FileSystem_Next -func Test_FileSystem_Next(t *testing.T) { - t.Parallel() - app := fiber.New() - app.Use(New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - Next: func(_ fiber.Ctx) bool { - return true - }, - })) - - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) - require.NoError(t, err) - require.Equal(t, fiber.StatusNotFound, resp.StatusCode) -} - -// go test -run Test_FileSystem_Download -func Test_FileSystem_Download(t *testing.T) { - app := fiber.New() - app.Use(New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - Download: true, - })) - - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/img/fiber.png", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) - require.Equal(t, "image/png", resp.Header.Get(fiber.HeaderContentType)) - require.Equal(t, "attachment", resp.Header.Get(fiber.HeaderContentDisposition)) -} - -func Test_FileSystem_NonGetAndHead(t *testing.T) { - t.Parallel() - app := fiber.New() - - app.Use("/test", New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - })) - - resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil)) - require.NoError(t, err) - require.Equal(t, 404, resp.StatusCode) -} - -func Test_FileSystem_Head(t *testing.T) { - t.Parallel() - app := fiber.New() - - app.Use("/test", New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - })) - - req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/test", nil) - require.NoError(t, err) - resp, err := app.Test(req) - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) -} - -func Test_FileSystem_NoRoot(t *testing.T) { - t.Parallel() - defer func() { - require.Equal(t, "filesystem: Root cannot be nil", recover()) - }() - - app := fiber.New() - app.Use(New()) - _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) - require.NoError(t, err) -} - -func Test_FileSystem_UsingParam(t *testing.T) { - t.Parallel() - app := fiber.New() - - app.Use("/:path", func(c fiber.Ctx) error { - return SendFile(c, os.DirFS("../../.github/testdata/fs"), c.Params("path")+".html") - }) - - req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/index", nil) - require.NoError(t, err) - resp, err := app.Test(req) - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) -} - -func Test_FileSystem_UsingParam_NonFile(t *testing.T) { - t.Parallel() - app := fiber.New() - - app.Use("/:path", func(c fiber.Ctx) error { - return SendFile(c, os.DirFS("../../.github/testdata/fs"), c.Params("path")+".html") - }) - - req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/template", nil) - require.NoError(t, err) - resp, err := app.Test(req) - require.NoError(t, err) - require.Equal(t, 404, resp.StatusCode) -} - -func Test_FileSystem_UsingContentTypeCharset(t *testing.T) { - t.Parallel() - app := fiber.New() - app.Use(New(Config{ - Root: os.DirFS("../../.github/testdata/fs"), - Index: "index.html", - ContentTypeCharset: "UTF-8", - })) - - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) - require.Equal(t, "text/html; charset=UTF-8", resp.Header.Get("Content-Type")) -} diff --git a/middleware/filesystem/utils.go b/middleware/filesystem/utils.go deleted file mode 100644 index 3c11acf1..00000000 --- a/middleware/filesystem/utils.go +++ /dev/null @@ -1,91 +0,0 @@ -package filesystem - -import ( - "errors" - "fmt" - "html" - "io/fs" - "path" - "path/filepath" - "sort" - "strings" - - "github.com/gofiber/fiber/v3" -) - -// ErrDirListingNotSupported is returned from the filesystem middleware handler if -// the given fs.FS does not support directory listing. This is uncommon and may -// indicate an issue with the FS implementation. -var ErrDirListingNotSupported = errors.New("failed to type-assert to fs.ReadDirFile") - -func getFileExtension(p string) string { - n := strings.LastIndexByte(p, '.') - if n < 0 { - return "" - } - return p[n:] -} - -func dirList(c fiber.Ctx, f fs.File) error { - ff, ok := f.(fs.ReadDirFile) - if !ok { - return ErrDirListingNotSupported - } - fileinfos, err := ff.ReadDir(-1) - if err != nil { - return fmt.Errorf("failed to read dir: %w", err) - } - - fm := make(map[string]fs.FileInfo, len(fileinfos)) - filenames := make([]string, 0, len(fileinfos)) - for _, fi := range fileinfos { - name := fi.Name() - info, err := fi.Info() - if err != nil { - return fmt.Errorf("failed to get file info: %w", err) - } - - fm[name] = info - filenames = append(filenames, name) - } - - basePathEscaped := html.EscapeString(c.Path()) - _, _ = fmt.Fprintf(c, "%s", basePathEscaped) - _, _ = fmt.Fprintf(c, "

%s

", basePathEscaped) - _, _ = fmt.Fprint(c, "
    ") - - if len(basePathEscaped) > 1 { - parentPathEscaped := html.EscapeString(strings.TrimRight(c.Path(), "/") + "/..") - _, _ = fmt.Fprintf(c, `
  • ..
  • `, parentPathEscaped) - } - - sort.Strings(filenames) - for _, name := range filenames { - pathEscaped := html.EscapeString(path.Join(c.Path() + "/" + name)) - fi := fm[name] - auxStr := "dir" - className := "dir" - if !fi.IsDir() { - auxStr = fmt.Sprintf("file, %d bytes", fi.Size()) - className = "file" - } - _, _ = fmt.Fprintf(c, `
  • %s, %s, last modified %s
  • `, - pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime()) - } - _, _ = fmt.Fprint(c, "
") - - c.Type("html") - - return nil -} - -func openFile(filesystem fs.FS, name string) (fs.File, error) { - name = filepath.ToSlash(name) - - file, err := filesystem.Open(name) - if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) - } - - return file, nil -} diff --git a/middleware/static/config.go b/middleware/static/config.go new file mode 100644 index 00000000..cc7a9357 --- /dev/null +++ b/middleware/static/config.go @@ -0,0 +1,98 @@ +package static + +import ( + "io/fs" + "time" + + "github.com/gofiber/fiber/v3" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c fiber.Ctx) bool + + // FS is the file system to serve the static files from. + // You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc. + // + // Optional. Default: nil + FS fs.FS + + // When set to true, the server tries minimizing CPU usage by caching compressed files. + // This works differently than the github.com/gofiber/compression middleware. + // + // Optional. Default: false + Compress bool `json:"compress"` + + // When set to true, enables byte range requests. + // + // Optional. Default: false + ByteRange bool `json:"byte_range"` + + // When set to true, enables directory browsing. + // + // Optional. Default: false. + Browse bool `json:"browse"` + + // When set to true, enables direct download. + // + // Optional. Default: false. + Download bool `json:"download"` + + // The names of the index files for serving a directory. + // + // Optional. Default: []string{"index.html"}. + IndexNames []string `json:"index"` + + // Expiration duration for inactive file handlers. + // Use a negative time.Duration to disable it. + // + // Optional. Default: 10 * time.Second. + CacheDuration time.Duration `json:"cache_duration"` + + // The value for the Cache-Control HTTP-header + // that is set on the file response. MaxAge is defined in seconds. + // + // Optional. Default: 0. + MaxAge int `json:"max_age"` + + // ModifyResponse defines a function that allows you to alter the response. + // + // Optional. Default: nil + ModifyResponse fiber.Handler + + // NotFoundHandler defines a function to handle when the path is not found. + // + // Optional. Default: nil + NotFoundHandler fiber.Handler +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + IndexNames: []string{"index.html"}, + CacheDuration: 10 * time.Second, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.IndexNames == nil || len(cfg.IndexNames) == 0 { + cfg.IndexNames = ConfigDefault.IndexNames + } + + if cfg.CacheDuration == 0 { + cfg.CacheDuration = ConfigDefault.CacheDuration + } + + return cfg +} diff --git a/middleware/static/static.go b/middleware/static/static.go new file mode 100644 index 00000000..1bb4e8cc --- /dev/null +++ b/middleware/static/static.go @@ -0,0 +1,175 @@ +package static + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" + "github.com/valyala/fasthttp" +) + +// New creates a new middleware handler. +// The root argument specifies the root directory from which to serve static assets. +// +// Note: Root has to be string or fs.FS, otherwise it will panic. +func New(root string, cfg ...Config) fiber.Handler { + config := configDefault(cfg...) + + var createFS sync.Once + var fileHandler fasthttp.RequestHandler + var cacheControlValue string + + // adjustments for io/fs compatibility + if config.FS != nil && root == "" { + root = "." + } + + return func(c fiber.Ctx) error { + // Don't execute middleware if Next returns true + if config.Next != nil && config.Next(c) { + return c.Next() + } + + // We only serve static assets on GET or HEAD methods + method := c.Method() + if method != fiber.MethodGet && method != fiber.MethodHead { + return c.Next() + } + + // Initialize FS + createFS.Do(func() { + prefix := c.Route().Path + + // Is prefix a partial wildcard? + if strings.Contains(prefix, "*") { + // /john* -> /john + prefix = strings.Split(prefix, "*")[0] + } + + prefixLen := len(prefix) + if prefixLen > 1 && prefix[prefixLen-1:] == "/" { + // /john/ -> /john + prefixLen-- + } + + fs := &fasthttp.FS{ + Root: root, + FS: config.FS, + AllowEmptyRoot: true, + GenerateIndexPages: config.Browse, + AcceptByteRange: config.ByteRange, + Compress: config.Compress, + CompressedFileSuffix: c.App().Config().CompressedFileSuffix, + CacheDuration: config.CacheDuration, + SkipCache: config.CacheDuration < 0, + IndexNames: config.IndexNames, + PathNotFound: func(fctx *fasthttp.RequestCtx) { + fctx.Response.SetStatusCode(fiber.StatusNotFound) + }, + } + + fs.PathRewrite = func(fctx *fasthttp.RequestCtx) []byte { + path := fctx.Path() + + if len(path) >= prefixLen { + checkFile, err := isFile(root, fs.FS) + if err != nil { + return path + } + + // If the root is a file, we need to reset the path to "/" always. + switch { + case checkFile && fs.FS == nil: + path = []byte("/") + case checkFile && fs.FS != nil: + path = utils.UnsafeBytes(root) + default: + path = path[prefixLen:] + if len(path) == 0 || path[len(path)-1] != '/' { + path = append(path, '/') + } + } + } + + if len(path) > 0 && path[0] != '/' { + path = append([]byte("/"), path...) + } + + return path + } + + maxAge := config.MaxAge + if maxAge > 0 { + cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) + } + + fileHandler = fs.NewRequestHandler() + }) + + // Serve file + fileHandler(c.Context()) + + // Sets the response Content-Disposition header to attachment if the Download option is true + if config.Download { + c.Attachment() + } + + // Return request if found and not forbidden + status := c.Context().Response.StatusCode() + if status != fiber.StatusNotFound && status != fiber.StatusForbidden { + if len(cacheControlValue) > 0 { + c.Context().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue) + } + + if config.ModifyResponse != nil { + return config.ModifyResponse(c) + } + + return nil + } + + // Return custom 404 handler if provided. + if config.NotFoundHandler != nil { + return config.NotFoundHandler(c) + } + + // Reset response to default + c.Context().SetContentType("") // Issue #420 + c.Context().Response.SetStatusCode(fiber.StatusOK) + c.Context().Response.SetBodyString("") + + // Next middleware + return c.Next() + } +} + +// isFile checks if the root is a file. +func isFile(root string, filesystem fs.FS) (bool, error) { + var file fs.File + var err error + + if filesystem != nil { + file, err = filesystem.Open(root) + if err != nil { + return false, fmt.Errorf("static: %w", err) + } + } else { + file, err = os.Open(filepath.Clean(root)) + if err != nil { + return false, fmt.Errorf("static: %w", err) + } + } + + stat, err := file.Stat() + if err != nil { + return false, fmt.Errorf("static: %w", err) + } + + return stat.Mode().IsRegular(), nil +} diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go new file mode 100644 index 00000000..bc0585a2 --- /dev/null +++ b/middleware/static/static_test.go @@ -0,0 +1,721 @@ +package static + +import ( + "embed" + "io" + "io/fs" + "net/http/httptest" + "os" + "runtime" + "strings" + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" +) + +// go test -run Test_Static_Index_Default +func Test_Static_Index_Default(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/prefix", New("../../.github/workflows")) + + app.Get("", New("../../.github/")) + + app.Get("test", New("", Config{ + IndexNames: []string{"index.html"}, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Hello, World!") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/not-found", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "Cannot GET /not-found", string(body)) +} + +// go test -run Test_Static_Index +func Test_Static_Direct(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/*", New("../../.github")) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Hello, World!") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/index.html", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 405, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/testdata/testRoutes.json", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get("Content-Type")) + require.Equal(t, "", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "test_routes") +} + +// go test -run Test_Static_MaxAge +func Test_Static_MaxAge(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/*", New("../../.github", Config{ + MaxAge: 100, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(fiber.HeaderContentType)) + require.Equal(t, "public, max-age=100", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + +// go test -run Test_Static_Custom_CacheControl +func Test_Static_Custom_CacheControl(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/*", New("../../.github", Config{ + ModifyResponse: func(c fiber.Ctx) error { + if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") { + c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + } + return nil + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") + + normalResp, normalErr := app.Test(httptest.NewRequest(fiber.MethodGet, "/config.yml", nil)) + require.NoError(t, normalErr, "app.Test(req)") + require.Equal(t, "", normalResp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + +func Test_Static_Disable_Cache(t *testing.T) { + // Skip on Windows. It's not possible to delete a file that is in use. + if runtime.GOOS == "windows" { + t.SkipNow() + } + + t.Parallel() + + app := fiber.New() + + file, err := os.Create("../../.github/test.txt") + require.NoError(t, err) + _, err = file.WriteString("Hello, World!") + require.NoError(t, err) + require.NoError(t, file.Close()) + + // Remove the file even if the test fails + defer func() { + _ = os.Remove("../../.github/test.txt") //nolint:errcheck // not needed + }() + + app.Get("/*", New("../../.github/", Config{ + CacheDuration: -1, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test.txt", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, "", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Hello, World!") + + require.NoError(t, os.Remove("../../.github/test.txt")) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/test.txt", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, "", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "Cannot GET /test.txt", string(body)) +} + +func Test_Static_NotFoundHandler(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/*", New("../../.github", Config{ + NotFoundHandler: func(c fiber.Ctx) error { + return c.SendString("Custom 404") + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/not-found", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "Custom 404", string(body)) +} + +// go test -run Test_Static_Download +func Test_Static_Download(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/fiber.png", New("../../.github/testdata/fs/img/fiber.png", Config{ + Download: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fiber.png", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, "image/png", resp.Header.Get(fiber.HeaderContentType)) + require.Equal(t, `attachment`, resp.Header.Get(fiber.HeaderContentDisposition)) +} + +// go test -run Test_Static_Group +func Test_Static_Group(t *testing.T) { + t.Parallel() + app := fiber.New() + + grp := app.Group("/v1", func(c fiber.Ctx) error { + c.Set("Test-Header", "123") + return c.Next() + }) + + grp.Get("/v2*", New("../../.github/index.html")) + + req := httptest.NewRequest(fiber.MethodGet, "/v1/v2", nil) + resp, err := app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + require.Equal(t, "123", resp.Header.Get("Test-Header")) + + grp = app.Group("/v2") + grp.Get("/v3*", New("../../.github/index.html")) + + req = httptest.NewRequest(fiber.MethodGet, "/v2/v3/john/doe", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) +} + +func Test_Static_Wildcard(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("*", New("../../.github/index.html")) + + req := httptest.NewRequest(fiber.MethodGet, "/yesyes/john/doe", nil) + resp, err := app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Test file") +} + +func Test_Static_Prefix_Wildcard(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/test*", New("../../.github/index.html")) + + req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil) + resp, err := app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + app.Get("/my/nameisjohn*", New("../../.github/index.html")) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/my/nameisjohn/no/its/not", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Test file") +} + +func Test_Static_Prefix(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/john*", New("../../.github")) + + req := httptest.NewRequest(fiber.MethodGet, "/john/index.html", nil) + resp, err := app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + app.Get("/prefix*", New("../../.github/testdata")) + + req = httptest.NewRequest(fiber.MethodGet, "/prefix/index.html", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + app.Get("/single*", New("../../.github/testdata/testRoutes.json")) + + req = httptest.NewRequest(fiber.MethodGet, "/single", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType)) +} + +func Test_Static_Trailing_Slash(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/john*", New("../../.github")) + + req := httptest.NewRequest(fiber.MethodGet, "/john/", nil) + resp, err := app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + app.Get("/john_without_index*", New("../../.github/testdata/fs/css")) + + req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + app.Use("/john", New("../../.github")) + + req = httptest.NewRequest(fiber.MethodGet, "/john/", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + req = httptest.NewRequest(fiber.MethodGet, "/john", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + app.Use("/john_without_index/", New("../../.github/testdata/fs/css")) + + req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", nil) + resp, err = app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) +} + +func Test_Static_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/*", New("../../.github", Config{ + Next: func(c fiber.Ctx) bool { + return c.Get("X-Custom-Header") == "skip" + }, + })) + + app.Get("/*", func(c fiber.Ctx) error { + return c.SendString("You've skipped app.Static") + }) + + t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("X-Custom-Header", "skip") + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "You've skipped app.Static") + }) + + t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("X-Custom-Header", "don't skip") + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Hello, World!") + }) +} + +func Test_Route_Static_Root(t *testing.T) { + t.Parallel() + + dir := "../../.github/testdata/fs/css" + app := fiber.New() + app.Get("/*", New(dir, Config{ + Browse: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") + + app = fiber.New() + app.Get("/*", New(dir)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") +} + +func Test_Route_Static_HasPrefix(t *testing.T) { + t.Parallel() + + dir := "../../.github/testdata/fs/css" + app := fiber.New() + app.Get("/static*", New(dir, Config{ + Browse: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") + + app = fiber.New() + app.Get("/static/*", New(dir, Config{ + Browse: true, + })) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") + + app = fiber.New() + app.Get("/static*", New(dir)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") + + app = fiber.New() + app.Get("/static*", New(dir)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") +} + +func Test_Static_FS(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Get("/*", New("", Config{ + FS: os.DirFS("../../.github/testdata/fs"), + Browse: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/css/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") +} + +/*func Test_Static_FS_DifferentRoot(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Get("/*", New("fs", Config{ + FS: os.DirFS("../../.github/testdata"), + IndexNames: []string{"index2.html"}, + Browse: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "

Hello, World!

") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/css/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") +}*/ + +//go:embed static.go config.go +var fsTestFilesystem embed.FS + +func Test_Static_FS_Browse(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Get("/embed*", New("", Config{ + FS: fsTestFilesystem, + Browse: true, + })) + + app.Get("/dirfs*", New("", Config{ + FS: os.DirFS("../../.github/testdata/fs/css"), + Browse: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "style.css") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs/style.css", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "color") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/embed", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err, "app.Test(req)") + require.Contains(t, string(body), "static.go") +} + +func Test_Static_FS_Prefix_Wildcard(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/test*", New("index.html", Config{ + FS: os.DirFS("../../.github"), + IndexNames: []string{"not_index.html"}, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil) + resp, err := app.Test(req) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, 200, resp.StatusCode, "Status code") + require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) + require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Test file") +} + +func Test_isFile(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + path string + filesystem fs.FS + expected bool + gotError error + }{ + { + name: "file", + path: "index.html", + filesystem: os.DirFS("../../.github"), + expected: true, + }, + { + name: "file", + path: "index2.html", + filesystem: os.DirFS("../../.github"), + expected: false, + gotError: fs.ErrNotExist, + }, + { + name: "directory", + path: ".", + filesystem: os.DirFS("../../.github"), + expected: false, + }, + { + name: "directory", + path: "not_exists", + filesystem: os.DirFS("../../.github"), + expected: false, + gotError: fs.ErrNotExist, + }, + { + name: "directory", + path: ".", + filesystem: os.DirFS("../../.github/testdata/fs/css"), + expected: false, + }, + { + name: "file", + path: "../../.github/testdata/fs/css/style.css", + filesystem: nil, + expected: true, + }, + { + name: "file", + path: "../../.github/testdata/fs/css/style2.css", + filesystem: nil, + expected: false, + gotError: fs.ErrNotExist, + }, + { + name: "directory", + path: "../../.github/testdata/fs/css", + filesystem: nil, + expected: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + c := c + t.Parallel() + + actual, err := isFile(c.path, c.filesystem) + require.ErrorIs(t, err, c.gotError) + require.Equal(t, c.expected, actual) + }) + } +} diff --git a/register.go b/register.go index 60bc19ba..ab67447c 100644 --- a/register.go +++ b/register.go @@ -19,8 +19,6 @@ type Register interface { Add(methods []string, handler Handler, middleware ...Handler) Register - Static(root string, config ...Static) Register - Route(path string) Register } @@ -112,12 +110,6 @@ func (r *Registering) Add(methods []string, handler Handler, middleware ...Handl return r } -// Static will create a file server serving static files -func (r *Registering) Static(root string, config ...Static) Register { - r.app.registerStatic(r.path, root, config...) - return r -} - // Route returns a new Register instance whose route path takes // the path in the current instance as its prefix. func (r *Registering) Route(path string) Register { diff --git a/router.go b/router.go index f65b4ece..26a2483f 100644 --- a/router.go +++ b/router.go @@ -9,10 +9,8 @@ import ( "fmt" "html" "sort" - "strconv" "strings" "sync/atomic" - "time" "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" @@ -33,7 +31,6 @@ type Router interface { Patch(path string, handler Handler, middleware ...Handler) Router Add(methods []string, path string, handler Handler, middleware ...Handler) Router - Static(prefix, root string, config ...Static) Router All(path string, handler Handler, middleware ...Handler) Router Group(prefix string, handlers ...Handler) Router @@ -377,144 +374,6 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler } } -func (app *App) registerStatic(prefix, root string, config ...Static) { - // For security, we want to restrict to the current work directory. - if root == "" { - root = "." - } - // Cannot have an empty prefix - if prefix == "" { - prefix = "/" - } - // Prefix always start with a '/' or '*' - if prefix[0] != '/' { - prefix = "/" + prefix - } - // in case-sensitive routing, all to lowercase - if !app.config.CaseSensitive { - prefix = utils.ToLower(prefix) - } - // Strip trailing slashes from the root path - if len(root) > 0 && root[len(root)-1] == '/' { - root = root[:len(root)-1] - } - // Is prefix a direct wildcard? - isStar := prefix == "/*" - // Is prefix a root slash? - isRoot := prefix == "/" - // Is prefix a partial wildcard? - if strings.Contains(prefix, "*") { - // /john* -> /john - isStar = true - prefix = strings.Split(prefix, "*")[0] - // Fix this later - } - prefixLen := len(prefix) - if prefixLen > 1 && prefix[prefixLen-1:] == "/" { - // /john/ -> /john - prefixLen-- - prefix = prefix[:prefixLen] - } - const cacheDuration = 10 * time.Second - // Fileserver settings - fs := &fasthttp.FS{ - Root: root, - AllowEmptyRoot: true, - GenerateIndexPages: false, - AcceptByteRange: false, - Compress: false, - CompressedFileSuffix: app.config.CompressedFileSuffix, - CacheDuration: cacheDuration, - IndexNames: []string{"index.html"}, - PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { - path := fctx.Path() - if len(path) >= prefixLen { - if isStar && app.getString(path[0:prefixLen]) == prefix { - path = append(path[0:0], '/') - } else { - path = path[prefixLen:] - if len(path) == 0 || path[len(path)-1] != '/' { - path = append(path, '/') - } - } - } - if len(path) > 0 && path[0] != '/' { - path = append([]byte("/"), path...) - } - return path - }, - PathNotFound: func(fctx *fasthttp.RequestCtx) { - fctx.Response.SetStatusCode(StatusNotFound) - }, - } - - // Set config if provided - var cacheControlValue string - var modifyResponse Handler - if len(config) > 0 { - maxAge := config[0].MaxAge - if maxAge > 0 { - cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) - } - fs.CacheDuration = config[0].CacheDuration - fs.Compress = config[0].Compress - fs.AcceptByteRange = config[0].ByteRange - fs.GenerateIndexPages = config[0].Browse - if config[0].Index != "" { - fs.IndexNames = []string{config[0].Index} - } - modifyResponse = config[0].ModifyResponse - } - fileHandler := fs.NewRequestHandler() - handler := func(c Ctx) error { - // Don't execute middleware if Next returns true - if len(config) != 0 && config[0].Next != nil && config[0].Next(c) { - return c.Next() - } - // Serve file - fileHandler(c.Context()) - // Sets the response Content-Disposition header to attachment if the Download option is true - if len(config) > 0 && config[0].Download { - c.Attachment() - } - // Return request if found and not forbidden - status := c.Context().Response.StatusCode() - if status != StatusNotFound && status != StatusForbidden { - if len(cacheControlValue) > 0 { - c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue) - } - if modifyResponse != nil { - return modifyResponse(c) - } - return nil - } - // Reset response to default - c.Context().SetContentType("") // Issue #420 - c.Context().Response.SetStatusCode(StatusOK) - c.Context().Response.SetBodyString("") - // Next middleware - return c.Next() - } - - // Create route metadata without pointer - route := Route{ - // Router booleans - use: true, - root: isRoot, - path: prefix, - // Public data - Method: MethodGet, - Path: prefix, - Handlers: []Handler{handler}, - } - // Increment global handler count - atomic.AddUint32(&app.handlersCount, 1) - // Add route to stack - app.addRoute(MethodGet, &route) - // Add HEAD route - app.addRoute(MethodHead, &route) -} - func (app *App) addRoute(method string, route *Route, isMounted ...bool) { // Check mounted routes var mounted bool diff --git a/router_test.go b/router_test.go index 5d7c95c1..57ce9209 100644 --- a/router_test.go +++ b/router_test.go @@ -332,128 +332,6 @@ func Test_Router_Handler_Catch_Error(t *testing.T) { require.Equal(t, StatusInternalServerError, c.Response.Header.StatusCode()) } -func Test_Route_Static_Root(t *testing.T) { - t.Parallel() - - dir := "./.github/testdata/fs/css" - app := New() - app.Static("/", dir, Static{ - Browse: true, - }) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/style.css", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err, "app.Test(req)") - require.Contains(t, app.getString(body), "color") - - app = New() - app.Static("/", dir) - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/style.css", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - body, err = io.ReadAll(resp.Body) - require.NoError(t, err, "app.Test(req)") - require.Contains(t, app.getString(body), "color") -} - -func Test_Route_Static_HasPrefix(t *testing.T) { - t.Parallel() - - dir := "./.github/testdata/fs/css" - app := New() - app.Static("/static", dir, Static{ - Browse: true, - }) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/static", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err, "app.Test(req)") - require.Contains(t, app.getString(body), "color") - - app = New() - app.Static("/static/", dir, Static{ - Browse: true, - }) - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - body, err = io.ReadAll(resp.Body) - require.NoError(t, err, "app.Test(req)") - require.Contains(t, app.getString(body), "color") - - app = New() - app.Static("/static", dir) - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - body, err = io.ReadAll(resp.Body) - require.NoError(t, err, "app.Test(req)") - require.Contains(t, app.getString(body), "color") - - app = New() - app.Static("/static/", dir) - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 404, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) - require.NoError(t, err, "app.Test(req)") - require.Equal(t, 200, resp.StatusCode, "Status code") - - body, err = io.ReadAll(resp.Body) - require.NoError(t, err, "app.Test(req)") - require.Contains(t, app.getString(body), "color") -} - func Test_Router_NotFound(t *testing.T) { t.Parallel() app := New()