From 69e017f817760c45d4ffb62d15d20dda4f788504 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 14 May 2024 15:50:37 +0300 Subject: [PATCH 01/22] middleware: add static middleware --- middleware/static/config.go | 83 ++++++ middleware/static/static.go | 129 ++++++++ middleware/static/static_test.go | 494 +++++++++++++++++++++++++++++++ router.go | 1 + 4 files changed, 707 insertions(+) create mode 100644 middleware/static/config.go create mode 100644 middleware/static/static.go create mode 100644 middleware/static/static_test.go diff --git a/middleware/static/config.go b/middleware/static/config.go new file mode 100644 index 0000000000..60f5d6a584 --- /dev/null +++ b/middleware/static/config.go @@ -0,0 +1,83 @@ +package static + +import ( + "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 + + Root string `json:"root"` + + // 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 fiber.Handler +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Index: "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.Index == "" { + cfg.Index = ConfigDefault.Index + } + + 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 0000000000..acb1d68119 --- /dev/null +++ b/middleware/static/static.go @@ -0,0 +1,129 @@ +package static + +import ( + "fmt" + "strconv" + "strings" + "sync" + + "github.com/gofiber/fiber/v3" + "github.com/valyala/fasthttp" +) + +func New(cfg ...Config) fiber.Handler { + config := configDefault(cfg...) + + var createFS sync.Once + var fileHandler fasthttp.RequestHandler + var cacheControlValue string + var modifyResponse fiber.Handler + + return func(c fiber.Ctx) error { + createFS.Do(func() { + prefix := c.Route().Path + isStar := 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] + } + + fmt.Printf("prefix: %s, prefixlen: %d, isStar: %t\n", prefix, prefixLen, isStar) + + fs := &fasthttp.FS{ + Root: config.Root, + AllowEmptyRoot: true, + GenerateIndexPages: false, + AcceptByteRange: false, + Compress: false, + CompressedFileSuffix: c.App().Config().CompressedFileSuffix, + CacheDuration: config.CacheDuration, + IndexNames: []string{"index.html"}, + PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { + path := fctx.Path() + fmt.Println(string(path)) + if len(path) >= prefixLen { + // TODO: All routes have to contain star we don't need this mechanishm anymore i think + /*if isStar && string(path[0:prefixLen]) == prefix { + path = append(path[0:0], '/') + fmt.Printf("istar %s", path) + } else {*/ + path = path[prefixLen:] + fmt.Printf("path2 %s\n", path) + if len(path) == 0 || path[len(path)-1] != '/' { + path = append(path, '/') + } + //} + } + if len(path) > 0 && path[0] != '/' { + path = append([]byte("/"), path...) + } + fmt.Printf("path %s\n", path) + return path + }, + PathNotFound: func(fctx *fasthttp.RequestCtx) { + fctx.Response.SetStatusCode(fiber.StatusNotFound) + }, + } + + maxAge := config.MaxAge + if maxAge > 0 { + cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) + } + + fs.CacheDuration = config.CacheDuration + fs.Compress = config.Compress + fs.AcceptByteRange = config.ByteRange + fs.GenerateIndexPages = config.Browse + if config.Index != "" { + fs.IndexNames = []string{config.Index} + } + modifyResponse = config.ModifyResponse + + fileHandler = fs.NewRequestHandler() + }) + + // Don't execute middleware if Next returns true + if config.Next != nil && config.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 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 modifyResponse != nil { + return modifyResponse(c) + } + return nil + } + + // Reset response to default + c.Context().SetContentType("") // Issue #420 + c.Context().Response.SetStatusCode(fiber.StatusOK) + c.Context().Response.SetBodyString("") + + // Next middleware + return c.Next() + } +} diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go new file mode 100644 index 0000000000..cbda291ae3 --- /dev/null +++ b/middleware/static/static_test.go @@ -0,0 +1,494 @@ +package static + +import ( + "io" + "net/http/httptest" + "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(Config{ + Root: "../../.github/workflows", + })) + + app.Get("", New(Config{ + Root: "../../.github/", + })) + + app.Get("test", New(Config{ + Index: "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(Config{ + Root: "/home/efectn/Devel/fiber-v3-constraint/.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.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(Config{ + Root: "../../.github", + 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(Config{ + Root: "../../.github", + 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") +} + +// go test -run Test_Static_Download +func Test_Static_Download(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Get("/fiber.png", New(Config{ + Root: "../../.github/testdata/fs/img/fiber.png", + 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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.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(Config{ + Root: "../../.github", + 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(Config{ + Root: dir, + 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(Config{ + Root: 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(Config{ + Root: dir, + 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(Config{ + Root: dir, + 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(Config{ + Root: 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(Config{ + Root: 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") +} diff --git a/router.go b/router.go index f65b4ece90..a6e5a7a74b 100644 --- a/router.go +++ b/router.go @@ -507,6 +507,7 @@ func (app *App) registerStatic(prefix, root string, config ...Static) { Path: prefix, Handlers: []Handler{handler}, } + // Increment global handler count atomic.AddUint32(&app.handlersCount, 1) // Add route to stack From 6cca6ffb8824c9695c63d8436155080d655908e5 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 14 May 2024 15:58:49 +0300 Subject: [PATCH 02/22] uncomment broken tests --- middleware/static/static_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index cbda291ae3..a734fa9575 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -140,7 +140,7 @@ func Test_Static_Download(t *testing.T) { } // go test -run Test_Static_Group -/*func Test_Static_Group(t *testing.T) { +func Test_Static_Group(t *testing.T) { t.Parallel() app := fiber.New() @@ -172,9 +172,9 @@ func Test_Static_Download(t *testing.T) { 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) { +func Test_Static_Wildcard(t *testing.T) { t.Parallel() app := fiber.New() @@ -192,9 +192,9 @@ func Test_Static_Download(t *testing.T) { 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) { +func Test_Static_Prefix_Wildcard(t *testing.T) { t.Parallel() app := fiber.New() @@ -222,7 +222,7 @@ func Test_Static_Download(t *testing.T) { 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() From e49298a63308c5c22367da3be4814f38a8479705 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 14 May 2024 16:04:20 +0300 Subject: [PATCH 03/22] introduce isfile config property to fix file issues --- middleware/static/config.go | 2 ++ middleware/static/static.go | 14 +++++++------- middleware/static/static_test.go | 15 ++++++++++----- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/middleware/static/config.go b/middleware/static/config.go index 60f5d6a584..2498979ef2 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -48,6 +48,8 @@ type Config struct { // Optional. Default value 0. MaxAge int `json:"max_age"` + IsFile bool `json:"is_file"` + // ModifyResponse defines a function that allows you to alter the response. // // Optional. Default: nil diff --git a/middleware/static/static.go b/middleware/static/static.go index acb1d68119..9aad82835a 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -54,16 +54,16 @@ func New(cfg ...Config) fiber.Handler { fmt.Println(string(path)) if len(path) >= prefixLen { // TODO: All routes have to contain star we don't need this mechanishm anymore i think - /*if isStar && string(path[0:prefixLen]) == prefix { + if config.IsFile { path = append(path[0:0], '/') fmt.Printf("istar %s", path) - } else {*/ - path = path[prefixLen:] - fmt.Printf("path2 %s\n", path) - if len(path) == 0 || path[len(path)-1] != '/' { - path = append(path, '/') + } else { + path = path[prefixLen:] + fmt.Printf("path2 %s\n", path) + if len(path) == 0 || path[len(path)-1] != '/' { + path = append(path, '/') + } } - //} } if len(path) > 0 && path[0] != '/' { path = append([]byte("/"), path...) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index a734fa9575..c78aac2555 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -150,7 +150,8 @@ func Test_Static_Group(t *testing.T) { }) grp.Get("/v2*", New(Config{ - Root: "../../.github/index.html", + Root: "../../.github/index.html", + IsFile: true, })) req := httptest.NewRequest(fiber.MethodGet, "/v1/v2", nil) @@ -163,7 +164,8 @@ func Test_Static_Group(t *testing.T) { grp = app.Group("/v2") grp.Get("/v3*", New(Config{ - Root: "../../.github/index.html", + Root: "../../.github/index.html", + IsFile: true, })) req = httptest.NewRequest(fiber.MethodGet, "/v2/v3/john/doe", nil) @@ -179,7 +181,8 @@ func Test_Static_Wildcard(t *testing.T) { app := fiber.New() app.Get("*", New(Config{ - Root: "../../.github/index.html", + Root: "../../.github/index.html", + IsFile: true, })) req := httptest.NewRequest(fiber.MethodGet, "/yesyes/john/doe", nil) @@ -199,7 +202,8 @@ func Test_Static_Prefix_Wildcard(t *testing.T) { app := fiber.New() app.Get("/test/*", New(Config{ - Root: "../../.github/index.html", + Root: "../../.github/index.html", + IsFile: true, })) req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil) @@ -210,7 +214,8 @@ func Test_Static_Prefix_Wildcard(t *testing.T) { require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) app.Get("/my/nameisjohn*", New(Config{ - Root: "../../.github/index.html", + Root: "../../.github/index.html", + IsFile: true, })) resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/my/nameisjohn/no/its/not", nil)) From c8cb416ff650b41ea9888077e5c041c97a83aee9 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 16 May 2024 13:28:24 +0300 Subject: [PATCH 04/22] test --- docs/middleware/static.md | 128 +++++++++++++++++++++++++++++++ middleware/static/config.go | 23 +++--- middleware/static/static.go | 81 +++++++++++-------- middleware/static/static_test.go | 102 +++++++----------------- 4 files changed, 217 insertions(+), 117 deletions(-) create mode 100644 docs/middleware/static.md diff --git a/docs/middleware/static.md b/docs/middleware/static.md new file mode 100644 index 0000000000..c6e4ae6179 --- /dev/null +++ b/docs/middleware/static.md @@ -0,0 +1,128 @@ +--- +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 + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) + +func main() { + app := fiber.New() + + app.Get("/*", static.New("./public")) + + app.Listen(":3000") +} +``` + +
+Test + +```sh +curl http://localhost:3000/hello.html +curl http://localhost:3000/css/style.css +``` + +
+ +```go +package main + +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) + +func main() { + app := fiber.New() + + app.Use("/", static.New("./public")) + + app.Listen(":3000") +} +``` + +
+Test + +```sh +curl http://localhost:3000/hello.html +curl http://localhost:3000/css/style.css +``` + +
+ +```go +package main + +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) + +func main() { + app := fiber.New() + + app.Use("/static", static.New("./public/hello.html")) + + app.Listen(":3000") +} +``` + +
+Test + +```sh +curl http://localhost:3000/static # will show hello.html +curl http://localhost:3000/static/john/doee # will show hello.html +``` + +
+ +:::caution +If you want to define static routes using `Get`, you need to use wildcard (`*`) operator in 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` | +| 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` | +| Index | `string` | The name of the index file for serving a directory. | `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` | + + +## Default Config + +```go +var ConfigDefault = Config{ + Index: "index.html", + CacheDuration: 10 * time.Second, +} +``` diff --git a/middleware/static/config.go b/middleware/static/config.go index 2498979ef2..e54489c0c5 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -13,43 +13,44 @@ type Config struct { // Optional. Default: nil Next func(c fiber.Ctx) bool - Root string `json:"root"` - // 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 + // + // Optional. Default: false Compress bool `json:"compress"` // When set to true, enables byte range requests. - // Optional. Default value false + // + // Optional. Default: false ByteRange bool `json:"byte_range"` // When set to true, enables directory browsing. - // Optional. Default value false. + // + // Optional. Default: false. Browse bool `json:"browse"` // When set to true, enables direct download. - // Optional. Default value false. + // + // Optional. Default: false. Download bool `json:"download"` // The name of the index file for serving a directory. - // Optional. Default value "index.html". + // + // Optional. Default: "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. + // 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 value 0. + // Optional. Default: 0. MaxAge int `json:"max_age"` - IsFile bool `json:"is_file"` - // ModifyResponse defines a function that allows you to alter the response. // // Optional. Default: nil diff --git a/middleware/static/static.go b/middleware/static/static.go index 9aad82835a..f5070bcdd2 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -1,7 +1,7 @@ package static import ( - "fmt" + "os" "strconv" "strings" "sync" @@ -10,65 +10,77 @@ import ( "github.com/valyala/fasthttp" ) -func New(cfg ...Config) fiber.Handler { +// 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 - var modifyResponse fiber.Handler 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 - isStar := 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] } - fmt.Printf("prefix: %s, prefixlen: %d, isStar: %t\n", prefix, prefixLen, isStar) - fs := &fasthttp.FS{ - Root: config.Root, + Root: root, AllowEmptyRoot: true, - GenerateIndexPages: false, - AcceptByteRange: false, - Compress: false, + GenerateIndexPages: config.Browse, + AcceptByteRange: config.ByteRange, + Compress: config.Compress, CompressedFileSuffix: c.App().Config().CompressedFileSuffix, CacheDuration: config.CacheDuration, IndexNames: []string{"index.html"}, PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { path := fctx.Path() - fmt.Println(string(path)) + if len(path) >= prefixLen { - // TODO: All routes have to contain star we don't need this mechanishm anymore i think - if config.IsFile { + checkFile, err := isFile(root) + if err != nil { + return path + } + + if checkFile { path = append(path[0:0], '/') - fmt.Printf("istar %s", path) } else { path = path[prefixLen:] - fmt.Printf("path2 %s\n", path) if len(path) == 0 || path[len(path)-1] != '/' { path = append(path, '/') } } } + if len(path) > 0 && path[0] != '/' { path = append([]byte("/"), path...) } - fmt.Printf("path %s\n", path) + return path }, PathNotFound: func(fctx *fasthttp.RequestCtx) { @@ -81,23 +93,13 @@ func New(cfg ...Config) fiber.Handler { cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) } - fs.CacheDuration = config.CacheDuration - fs.Compress = config.Compress - fs.AcceptByteRange = config.ByteRange - fs.GenerateIndexPages = config.Browse if config.Index != "" { fs.IndexNames = []string{config.Index} } - modifyResponse = config.ModifyResponse fileHandler = fs.NewRequestHandler() }) - // Don't execute middleware if Next returns true - if config.Next != nil && config.Next(c) { - return c.Next() - } - // Serve file fileHandler(c.Context()) @@ -112,9 +114,11 @@ func New(cfg ...Config) fiber.Handler { if len(cacheControlValue) > 0 { c.Context().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue) } - if modifyResponse != nil { - return modifyResponse(c) + + if config.ModifyResponse != nil { + return config.ModifyResponse(c) } + return nil } @@ -127,3 +131,18 @@ func New(cfg ...Config) fiber.Handler { return c.Next() } } + +// isFile checks if the root is a file. +func isFile(root string) (bool, error) { + file, err := os.Open(root) + if err != nil { + return false, err + } + + stat, err := file.Stat() + if err != nil { + return false, err + } + + return stat.Mode().IsRegular(), nil +} diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index c78aac2555..e62f31639b 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -15,15 +15,11 @@ func Test_Static_Index_Default(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/prefix", New(Config{ - Root: "../../.github/workflows", - })) + app.Get("/prefix", New("../../.github/workflows")) - app.Get("", New(Config{ - Root: "../../.github/", - })) + app.Get("", New("../../.github/")) - app.Get("test", New(Config{ + app.Get("test", New("", Config{ Index: "index.html", })) @@ -53,9 +49,7 @@ func Test_Static_Direct(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/*", New(Config{ - Root: "/home/efectn/Devel/fiber-v3-constraint/.github", - })) + app.Get("/*", New("/home/efectn/Devel/fiber-v3-constraint/.github")) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil)) require.NoError(t, err, "app.Test(req)") @@ -84,8 +78,7 @@ func Test_Static_MaxAge(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/*", New(Config{ - Root: "../../.github", + app.Get("/*", New("../../.github", Config{ MaxAge: 100, })) @@ -102,8 +95,7 @@ func Test_Static_Custom_CacheControl(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/*", New(Config{ - Root: "../../.github", + 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") @@ -126,8 +118,7 @@ func Test_Static_Download(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/fiber.png", New(Config{ - Root: "../../.github/testdata/fs/img/fiber.png", + app.Get("/fiber.png", New("../../.github/testdata/fs/img/fiber.png", Config{ Download: true, })) @@ -149,10 +140,7 @@ func Test_Static_Group(t *testing.T) { return c.Next() }) - grp.Get("/v2*", New(Config{ - Root: "../../.github/index.html", - IsFile: true, - })) + grp.Get("/v2*", New("../../.github/index.html")) req := httptest.NewRequest(fiber.MethodGet, "/v1/v2", nil) resp, err := app.Test(req) @@ -163,10 +151,7 @@ func Test_Static_Group(t *testing.T) { require.Equal(t, "123", resp.Header.Get("Test-Header")) grp = app.Group("/v2") - grp.Get("/v3*", New(Config{ - Root: "../../.github/index.html", - IsFile: true, - })) + grp.Get("/v3*", New("../../.github/index.html")) req = httptest.NewRequest(fiber.MethodGet, "/v2/v3/john/doe", nil) resp, err = app.Test(req) @@ -180,10 +165,7 @@ func Test_Static_Wildcard(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("*", New(Config{ - Root: "../../.github/index.html", - IsFile: true, - })) + app.Get("*", New("../../.github/index.html")) req := httptest.NewRequest(fiber.MethodGet, "/yesyes/john/doe", nil) resp, err := app.Test(req) @@ -201,10 +183,7 @@ func Test_Static_Prefix_Wildcard(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/test/*", New(Config{ - Root: "../../.github/index.html", - IsFile: true, - })) + app.Get("/test*", New("../../.github/index.html")) req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil) resp, err := app.Test(req) @@ -213,10 +192,7 @@ func Test_Static_Prefix_Wildcard(t *testing.T) { require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - app.Get("/my/nameisjohn*", New(Config{ - Root: "../../.github/index.html", - IsFile: true, - })) + 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)") @@ -232,9 +208,7 @@ func Test_Static_Prefix_Wildcard(t *testing.T) { func Test_Static_Prefix(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/john/*", New(Config{ - Root: "../../.github", - })) + app.Get("/john*", New("../../.github")) req := httptest.NewRequest(fiber.MethodGet, "/john/index.html", nil) resp, err := app.Test(req) @@ -243,9 +217,7 @@ func Test_Static_Prefix(t *testing.T) { require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - app.Get("/prefix/*", New(Config{ - Root: "../../.github/testdata", - })) + app.Get("/prefix*", New("../../.github/testdata")) req = httptest.NewRequest(fiber.MethodGet, "/prefix/index.html", nil) resp, err = app.Test(req) @@ -254,9 +226,7 @@ func Test_Static_Prefix(t *testing.T) { require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - app.Get("/single/*", New(Config{ - Root: "../../.github/testdata/testRoutes.json", - })) + app.Get("/single*", New("../../.github/testdata/testRoutes.json")) req = httptest.NewRequest(fiber.MethodGet, "/single", nil) resp, err = app.Test(req) @@ -269,9 +239,7 @@ func Test_Static_Prefix(t *testing.T) { func Test_Static_Trailing_Slash(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/john", New(Config{ - Root: "../../.github", - })) + app.Get("/john*", New("../../.github")) req := httptest.NewRequest(fiber.MethodGet, "/john/", nil) resp, err := app.Test(req) @@ -280,9 +248,7 @@ func Test_Static_Trailing_Slash(t *testing.T) { require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - app.Get("/john_without_index", New(Config{ - Root: "../../.github/testdata/fs/css", - })) + app.Get("/john_without_index*", New("../../.github/testdata/fs/css")) req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", nil) resp, err = app.Test(req) @@ -291,9 +257,7 @@ func Test_Static_Trailing_Slash(t *testing.T) { require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - app.Use("/john", New(Config{ - Root: "../../.github", - })) + app.Use("/john", New("../../.github")) req = httptest.NewRequest(fiber.MethodGet, "/john/", nil) resp, err = app.Test(req) @@ -309,9 +273,7 @@ func Test_Static_Trailing_Slash(t *testing.T) { require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength)) require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - app.Use("/john_without_index/", New(Config{ - Root: "../../.github/testdata/fs/css", - })) + app.Use("/john_without_index/", New("../../.github/testdata/fs/css")) req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", nil) resp, err = app.Test(req) @@ -325,14 +287,13 @@ func Test_Static_Next(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/", New(Config{ - Root: "../../.github", + 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 { + app.Get("/*", func(c fiber.Ctx) error { return c.SendString("You've skipped app.Static") }) @@ -372,8 +333,7 @@ func Test_Route_Static_Root(t *testing.T) { dir := "../../.github/testdata/fs/css" app := fiber.New() - app.Get("/*", New(Config{ - Root: dir, + app.Get("/*", New(dir, Config{ Browse: true, })) @@ -390,9 +350,7 @@ func Test_Route_Static_Root(t *testing.T) { require.Contains(t, string(body), "color") app = fiber.New() - app.Get("/*", New(Config{ - Root: dir, - })) + app.Get("/*", New(dir)) resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err, "app.Test(req)") @@ -412,8 +370,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { dir := "../../.github/testdata/fs/css" app := fiber.New() - app.Get("/static/*", New(Config{ - Root: dir, + app.Get("/static*", New(dir, Config{ Browse: true, })) @@ -434,8 +391,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { require.Contains(t, string(body), "color") app = fiber.New() - app.Get("/static/*", New(Config{ - Root: dir, + app.Get("/static/*", New(dir, Config{ Browse: true, })) @@ -456,9 +412,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { require.Contains(t, string(body), "color") app = fiber.New() - app.Get("/static/*", New(Config{ - Root: dir, - })) + app.Get("/static*", New(dir)) resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil)) require.NoError(t, err, "app.Test(req)") @@ -477,9 +431,7 @@ func Test_Route_Static_HasPrefix(t *testing.T) { require.Contains(t, string(body), "color") app = fiber.New() - app.Get("/static/*", New(Config{ - Root: dir, - })) + app.Get("/static*", New(dir)) resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil)) require.NoError(t, err, "app.Test(req)") From 4284ebcc5f20c548e80b86c86997f3960d4538be Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 18 May 2024 13:16:23 +0300 Subject: [PATCH 05/22] add io/fs support to static mw --- constants.go | 1 + go.mod | 6 ++- go.sum | 8 +-- middleware/static/config.go | 6 +++ middleware/static/static.go | 56 +++++++++++-------- middleware/static/static_test.go | 92 ++++++++++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 28 deletions(-) diff --git a/constants.go b/constants.go index c856183955..1fc2548bf8 100644 --- a/constants.go +++ b/constants.go @@ -32,6 +32,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/go.mod b/go.mod index f7fd9ad420..e2aba7d81d 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,12 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/klauspost/compress v1.17.6 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/valyala/fasthttp => /home/efectn/Devel/fasthttp diff --git a/go.sum b/go.sum index 451d4586c4..bdb737d783 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS4 github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -47,8 +47,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= diff --git a/middleware/static/config.go b/middleware/static/config.go index e54489c0c5..2f7bac2fef 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -1,6 +1,7 @@ package static import ( + "io/fs" "time" "github.com/gofiber/fiber/v3" @@ -13,6 +14,11 @@ type Config struct { // Optional. Default: nil Next func(c fiber.Ctx) bool + // FS is the file system to serve the static files from. + // + // 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. // diff --git a/middleware/static/static.go b/middleware/static/static.go index f5070bcdd2..ea10c41ffc 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -21,6 +21,15 @@ func New(root string, cfg ...Config) fiber.Handler { var fileHandler fasthttp.RequestHandler var cacheControlValue string + // adjustments for io/fs compatibility + if config.FS != nil && root != "" { + root = "." + } + + if root != "." && !strings.HasPrefix(root, "/") { + root = "./" + root + } + return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if config.Next != nil && config.Next(c) { @@ -51,6 +60,7 @@ func New(root string, cfg ...Config) fiber.Handler { fs := &fasthttp.FS{ Root: root, + FS: config.FS, AllowEmptyRoot: true, GenerateIndexPages: config.Browse, AcceptByteRange: config.ByteRange, @@ -58,34 +68,36 @@ func New(root string, cfg ...Config) fiber.Handler { CompressedFileSuffix: c.App().Config().CompressedFileSuffix, CacheDuration: config.CacheDuration, IndexNames: []string{"index.html"}, - PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { - path := fctx.Path() + PathNotFound: func(fctx *fasthttp.RequestCtx) { + fctx.Response.SetStatusCode(fiber.StatusNotFound) + }, + } - if len(path) >= prefixLen { - checkFile, err := isFile(root) - if err != nil { - return path - } + fs.PathRewrite = func(fctx *fasthttp.RequestCtx) []byte { + path := fctx.Path() - if checkFile { - path = append(path[0:0], '/') - } else { - path = path[prefixLen:] - if len(path) == 0 || path[len(path)-1] != '/' { - path = append(path, '/') - } - } + if len(path) >= prefixLen { + checkFile, err := isFile(root) + if err != nil { + return path } - if len(path) > 0 && path[0] != '/' { - path = append([]byte("/"), path...) + // If the root is a file, we need to reset the path to "/" always. + if checkFile { + path = append(path[0:0], '/') + } else { + path = path[prefixLen:] + if len(path) == 0 || path[len(path)-1] != '/' { + path = append(path, '/') + } } + } - return path - }, - PathNotFound: func(fctx *fasthttp.RequestCtx) { - fctx.Response.SetStatusCode(fiber.StatusNotFound) - }, + if len(path) > 0 && path[0] != '/' { + path = append([]byte("/"), path...) + } + + return path } maxAge := config.MaxAge diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index e62f31639b..274c7599a5 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -1,8 +1,10 @@ package static import ( + "embed" "io" "net/http/httptest" + "os" "strings" "testing" @@ -449,3 +451,93 @@ func Test_Route_Static_HasPrefix(t *testing.T) { 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") +} + +//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"), + })) + + 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") +} From a9d7f223dcca71abc8c7cb323ff93a3f5d915b84 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 18 May 2024 15:43:22 +0300 Subject: [PATCH 06/22] add io/fs support to static mw --- docs/middleware/static.md | 1 + middleware/static/config.go | 1 + middleware/static/static.go | 32 +++++++++----- middleware/static/static_test.go | 76 +++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index c6e4ae6179..de3c0041cf 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -108,6 +108,7 @@ If you want to define static routes using `Get`, you need to use wildcard (`*`) | 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` | diff --git a/middleware/static/config.go b/middleware/static/config.go index 2f7bac2fef..ed022867a4 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -15,6 +15,7 @@ type Config struct { 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 diff --git a/middleware/static/static.go b/middleware/static/static.go index ea10c41ffc..661969426a 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -1,12 +1,14 @@ package static import ( + "io/fs" "os" "strconv" "strings" "sync" "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" ) @@ -22,14 +24,10 @@ func New(root string, cfg ...Config) fiber.Handler { var cacheControlValue string // adjustments for io/fs compatibility - if config.FS != nil && root != "" { + if config.FS != nil && root == "" { root = "." } - if root != "." && !strings.HasPrefix(root, "/") { - root = "./" + root - } - return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if config.Next != nil && config.Next(c) { @@ -77,14 +75,16 @@ func New(root string, cfg ...Config) fiber.Handler { path := fctx.Path() if len(path) >= prefixLen { - checkFile, err := isFile(root) + 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. - if checkFile { + if checkFile && fs.FS == nil { path = append(path[0:0], '/') + } else if checkFile && fs.FS != nil { + path = utils.UnsafeBytes(root) } else { path = path[prefixLen:] if len(path) == 0 || path[len(path)-1] != '/' { @@ -145,10 +145,20 @@ func New(root string, cfg ...Config) fiber.Handler { } // isFile checks if the root is a file. -func isFile(root string) (bool, error) { - file, err := os.Open(root) - if err != nil { - return false, err +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, err + } + } else { + file, err = os.Open(root) + if err != nil { + return false, err + } } stat, err := file.Stat() diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index 274c7599a5..a3aad3c51b 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -3,6 +3,7 @@ package static import ( "embed" "io" + "io/fs" "net/http/httptest" "os" "strings" @@ -527,7 +528,8 @@ func Test_Static_FS_Prefix_Wildcard(t *testing.T) { app := fiber.New() app.Get("/test*", New("index.html", Config{ - FS: os.DirFS("../../.github"), + FS: os.DirFS("../../.github"), + Index: "not_index.html", })) req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil) @@ -541,3 +543,75 @@ func Test_Static_FS_Prefix_Wildcard(t *testing.T) { 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) { + actual, err := isFile(c.path, c.filesystem) + require.ErrorIs(t, err, c.gotError) + require.Equal(t, c.expected, actual) + }) + } +} From 3d2f41b7362b56838fa2a4598800922d5d081a21 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 18 May 2024 15:58:31 +0300 Subject: [PATCH 07/22] remove filesystem and app.Static --- .github/README.md | 10 +- app.go | 53 ---- app_test.go | 318 +--------------------- docs/api/app.md | 66 ----- docs/intro.md | 8 +- docs/middleware/filesystem.md | 300 --------------------- go.mod | 6 +- go.sum | 6 +- group.go | 10 - middleware/filesystem/filesystem.go | 323 ----------------------- middleware/filesystem/filesystem_test.go | 252 ------------------ middleware/filesystem/utils.go | 91 ------- register.go | 8 - router.go | 142 ---------- router_test.go | 122 --------- 15 files changed, 18 insertions(+), 1697 deletions(-) delete mode 100644 docs/middleware/filesystem.md delete mode 100644 middleware/filesystem/filesystem.go delete mode 100644 middleware/filesystem/filesystem_test.go delete mode 100644 middleware/filesystem/utils.go diff --git a/.github/README.md b/.github/README.md index ee33a86a08..34d40c80af 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("/", dtatic.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 5147e9e482..351b3a213b 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 39890ead48..6b493de1eb 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/docs/api/app.md b/docs/api/app.md index b9bd97723d..164b1edcba 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/intro.md b/docs/intro.md index 6dbc6ca764..d482df1092 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 01d50270ff..0000000000 --- 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/go.mod b/go.mod index e2aba7d81d..df87dfff55 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tinylib/msgp v1.1.8 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.52.0 + github.com/valyala/fasthttp v1.53.0 ) require ( @@ -22,6 +22,4 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/valyala/fasthttp => /home/efectn/Devel/fasthttp +) \ No newline at end of file diff --git a/go.sum b/go.sum index bdb737d783..1f2175341c 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= -github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= +github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4zc= +github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -64,4 +64,4 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/group.go b/group.go index 2b8001d5f8..fe2ac97acb 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 62d4f4f6bb..0000000000 --- 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 feccdd128f..0000000000 --- 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 3c11acf1e8..0000000000 --- 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/register.go b/register.go index 60bc19ba77..ab67447c5a 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 a6e5a7a74b..26a2483f09 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,145 +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 5d7c95c14c..57ce920921 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() From 183a17dc91a809b790c9773d1bd8f4b23e6b16ba Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 18 May 2024 16:07:11 +0300 Subject: [PATCH 08/22] fix linter --- middleware/static/static.go | 17 ++++++++++------- middleware/static/static_test.go | 3 +++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/middleware/static/static.go b/middleware/static/static.go index 661969426a..899c2c3e60 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -1,8 +1,10 @@ package static import ( + "fmt" "io/fs" "os" + "path/filepath" "strconv" "strings" "sync" @@ -81,11 +83,12 @@ func New(root string, cfg ...Config) fiber.Handler { } // If the root is a file, we need to reset the path to "/" always. - if checkFile && fs.FS == nil { + switch { + case checkFile && fs.FS == nil: path = append(path[0:0], '/') - } else if checkFile && fs.FS != nil { + case checkFile && fs.FS != nil: path = utils.UnsafeBytes(root) - } else { + default: path = path[prefixLen:] if len(path) == 0 || path[len(path)-1] != '/' { path = append(path, '/') @@ -152,18 +155,18 @@ func isFile(root string, filesystem fs.FS) (bool, error) { if filesystem != nil { file, err = filesystem.Open(root) if err != nil { - return false, err + return false, fmt.Errorf("static: %w", err) } } else { - file, err = os.Open(root) + file, err = os.Open(filepath.Clean(root)) if err != nil { - return false, err + return false, fmt.Errorf("static: %w", err) } } stat, err := file.Stat() if err != nil { - return false, err + 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 index a3aad3c51b..a5c4a66cae 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -609,6 +609,9 @@ func Test_isFile(t *testing.T) { 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) From ad70fe6c38c5e83dc5daae6438fea192f0a02493 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 18 May 2024 16:09:20 +0300 Subject: [PATCH 09/22] apply review --- docs/middleware/static.md | 2 +- middleware/static/static_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index de3c0041cf..23995a4ffe 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -100,7 +100,7 @@ curl http://localhost:3000/static/john/doee # will show hello.html :::caution -If you want to define static routes using `Get`, you need to use wildcard (`*`) operator in the end of the route. +If you want to define static routes using `Get`, you need to use the wildcard (`*`) operator at the end of the route. ::: ## Config diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index a5c4a66cae..fdddfb75ad 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -52,7 +52,7 @@ func Test_Static_Direct(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/*", New("/home/efectn/Devel/fiber-v3-constraint/.github")) + app.Get("/*", New("../../.github")) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil)) require.NoError(t, err, "app.Test(req)") From 545a5ebea32d77f15f79b1491c1c3cff8e01ab53 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sun, 19 May 2024 14:33:55 +0300 Subject: [PATCH 10/22] support disablecache --- middleware/static/static.go | 1 + middleware/static/static_test.go | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/middleware/static/static.go b/middleware/static/static.go index 899c2c3e60..96b9bd2dd9 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -67,6 +67,7 @@ func New(root string, cfg ...Config) fiber.Handler { Compress: config.Compress, CompressedFileSuffix: c.App().Config().CompressedFileSuffix, CacheDuration: config.CacheDuration, + SkipCache: config.CacheDuration < 0, IndexNames: []string{"index.html"}, PathNotFound: func(fctx *fasthttp.RequestCtx) { fctx.Response.SetStatusCode(fiber.StatusNotFound) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index fdddfb75ad..79f604e1f4 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -74,6 +74,7 @@ func Test_Static_Direct(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), "test_routes") + t.Fail() } // go test -run Test_Static_MaxAge @@ -116,6 +117,41 @@ func Test_Static_Custom_CacheControl(t *testing.T) { require.Equal(t, "", normalResp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") } +func Test_Static_Disable_Cache(t *testing.T) { + 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) + + // Remove the file even if the test fails + defer os.Remove("../../.github/test.txt") + + 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, string(body), "Cannot GET /test.txt") +} + // go test -run Test_Static_Download func Test_Static_Download(t *testing.T) { t.Parallel() From 75c40676388aeb2bc1cc4073fce96bda0c535f3b Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sun, 19 May 2024 23:48:13 +0300 Subject: [PATCH 11/22] support multi indexes --- docs/middleware/static.md | 6 +++--- middleware/static/config.go | 12 ++++++------ middleware/static/static.go | 6 +----- middleware/static/static_test.go | 6 +++--- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index 23995a4ffe..ce8e3e1f7a 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -100,7 +100,7 @@ curl http://localhost:3000/static/john/doee # will show hello.html :::caution -If you want to define static routes using `Get`, you need to use the wildcard (`*`) operator at the end of the route. +If you want to define static routes using `Get`, you need to put the wildcard (`*`) operator at the end of the route. ::: ## Config @@ -113,7 +113,7 @@ If you want to define static routes using `Get`, you need to use the wildcard (` | 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` | +| 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` | @@ -123,7 +123,7 @@ If you want to define static routes using `Get`, you need to use the wildcard (` ```go var ConfigDefault = Config{ - Index: "index.html", + Index: []string{"index.html"}, CacheDuration: 10 * time.Second, } ``` diff --git a/middleware/static/config.go b/middleware/static/config.go index ed022867a4..8094709b59 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -41,10 +41,10 @@ type Config struct { // Optional. Default: false. Download bool `json:"download"` - // The name of the index file for serving a directory. + // The names of the index files for serving a directory. // - // Optional. Default: "index.html". - Index string `json:"index"` + // Optional. Default: []string{"index.html"}. + IndexNames []string `json:"index"` // Expiration duration for inactive file handlers. // Use a negative time.Duration to disable it. @@ -66,7 +66,7 @@ type Config struct { // ConfigDefault is the default config var ConfigDefault = Config{ - Index: "index.html", + IndexNames: []string{"index.html"}, CacheDuration: 10 * time.Second, } @@ -81,8 +81,8 @@ func configDefault(config ...Config) Config { cfg := config[0] // Set default values - if cfg.Index == "" { - cfg.Index = ConfigDefault.Index + if cfg.IndexNames == nil { + cfg.IndexNames = ConfigDefault.IndexNames } if cfg.CacheDuration == 0 { diff --git a/middleware/static/static.go b/middleware/static/static.go index 96b9bd2dd9..d0d15f48a5 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -68,7 +68,7 @@ func New(root string, cfg ...Config) fiber.Handler { CompressedFileSuffix: c.App().Config().CompressedFileSuffix, CacheDuration: config.CacheDuration, SkipCache: config.CacheDuration < 0, - IndexNames: []string{"index.html"}, + IndexNames: config.IndexNames, PathNotFound: func(fctx *fasthttp.RequestCtx) { fctx.Response.SetStatusCode(fiber.StatusNotFound) }, @@ -109,10 +109,6 @@ func New(root string, cfg ...Config) fiber.Handler { cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) } - if config.Index != "" { - fs.IndexNames = []string{config.Index} - } - fileHandler = fs.NewRequestHandler() }) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index 79f604e1f4..a50b2189af 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -23,7 +23,7 @@ func Test_Static_Index_Default(t *testing.T) { app.Get("", New("../../.github/")) app.Get("test", New("", Config{ - Index: "index.html", + IndexNames: []string{"index.html"}, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -564,8 +564,8 @@ func Test_Static_FS_Prefix_Wildcard(t *testing.T) { app := fiber.New() app.Get("/test*", New("index.html", Config{ - FS: os.DirFS("../../.github"), - Index: "not_index.html", + FS: os.DirFS("../../.github"), + IndexNames: []string{"not_index.html"}, })) req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil) From 24ed820a806fa243d9f11f60a5273139bf2f772f Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Wed, 22 May 2024 12:28:07 +0300 Subject: [PATCH 12/22] add an example for io/fs --- docs/middleware/static.md | 30 ++++++++++++++++++++++++++++++ go.mod | 4 +++- go.sum | 4 +--- middleware/static/static_test.go | 30 +++++++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index ce8e3e1f7a..d3e842f5c1 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -99,6 +99,36 @@ curl http://localhost:3000/static/john/doee # will show hello.html +```go +package main + +import ( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) + +func main() { + app := fiber.New() + + app.Get("/files*", static.New("", static.Config{ + FS: os.DirFS("files"), + Browse: true, + })) + + app.Listen(":3000") +} +``` + +
+Test + +```sh +curl http://localhost:3000/files/css/style.css +curl http://localhost:3000/files/index.html +``` + +
+ :::caution If you want to define static routes using `Get`, you need to put the wildcard (`*`) operator at the end of the route. ::: diff --git a/go.mod b/go.mod index df87dfff55..dbf21802c1 100644 --- a/go.mod +++ b/go.mod @@ -22,4 +22,6 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) + +replace github.com/valyala/fasthttp => /home/efectn/Devel/fasthttp diff --git a/go.sum b/go.sum index 1f2175341c..2d64d95daf 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,6 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4zc= -github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -64,4 +62,4 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index a50b2189af..6368191151 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -74,7 +74,6 @@ func Test_Static_Direct(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), "test_routes") - t.Fail() } // go test -run Test_Static_MaxAge @@ -513,6 +512,35 @@ func Test_Static_FS(t *testing.T) { 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 From 40a7a87bda7f08839188f3d0b356ea54cb864d94 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Wed, 22 May 2024 12:50:54 +0300 Subject: [PATCH 13/22] update whats new & apply reviews --- docs/middleware/static.md | 3 +- docs/whats_new.md | 67 ++++++++++++++++++++++++++++++-- go.mod | 4 +- go.sum | 4 +- middleware/static/static_test.go | 6 ++- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index d3e842f5c1..eb3af3796c 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -130,7 +130,7 @@ curl http://localhost:3000/files/index.html :::caution -If you want to define static routes using `Get`, you need to put the wildcard (`*`) operator at the end of the route. +To define static routes using `Get`, append the wildcard (`*`) operator at the end of the route. ::: ## Config @@ -148,7 +148,6 @@ If you want to define static routes using `Get`, you need to put the wildcard (` | 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` | - ## Default Config ```go diff --git a/docs/whats_new.md b/docs/whats_new.md index fe19348504..4eb6918ea4 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/go.mod b/go.mod index dbf21802c1..df87dfff55 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,4 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/valyala/fasthttp => /home/efectn/Devel/fasthttp +) \ No newline at end of file diff --git a/go.sum b/go.sum index 2d64d95daf..1f2175341c 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4zc= +github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -62,4 +64,4 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index 6368191151..50726cf3c1 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -126,7 +126,9 @@ func Test_Static_Disable_Cache(t *testing.T) { require.NoError(t, err) // Remove the file even if the test fails - defer os.Remove("../../.github/test.txt") + defer func() { + require.NoError(t, os.Remove("../../.github/test.txt")) + }() app.Get("/*", New("../../.github/", Config{ CacheDuration: -1, @@ -148,7 +150,7 @@ func Test_Static_Disable_Cache(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) - require.Equal(t, string(body), "Cannot GET /test.txt") + require.Equal(t, "Cannot GET /test.txt", string(body)) } // go test -run Test_Static_Download From 92e739c8f47be56a6eb24ab6feaba723dfca24ff Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Wed, 22 May 2024 13:24:50 +0300 Subject: [PATCH 14/22] update --- middleware/static/static_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index 50726cf3c1..f61ae2b46a 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -127,7 +127,7 @@ func Test_Static_Disable_Cache(t *testing.T) { // Remove the file even if the test fails defer func() { - require.NoError(t, os.Remove("../../.github/test.txt")) + _ = os.Remove("../../.github/test.txt") //nolint:errcheck // not needed }() app.Get("/*", New("../../.github/", Config{ From 74014f9cf03622f5775da129239c0ae5176533e8 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Wed, 22 May 2024 13:32:38 +0300 Subject: [PATCH 15/22] use fasthttp from master --- go.mod | 4 ++-- go.sum | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index df87dfff55..ddc15b245e 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tinylib/msgp v1.1.8 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.53.0 + github.com/valyala/fasthttp v1.53.1-0.20240519131158-ee34656bec46 ) require ( @@ -22,4 +22,4 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index 1f2175341c..1b37ade0c2 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4zc= github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= +github.com/valyala/fasthttp v1.53.1-0.20240519131158-ee34656bec46 h1:cgs9x9vfiwgkfRYS9FVT4cC9sctCMGI8Vqw9YDHLHh0= +github.com/valyala/fasthttp v1.53.1-0.20240519131158-ee34656bec46/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -64,4 +66,4 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3bbfb23128061377869e7f329c7bde88ad535f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Thu, 23 May 2024 14:41:10 +0300 Subject: [PATCH 16/22] Update .github/README.md Co-authored-by: RW --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 34d40c80af..40dcf7b107 100644 --- a/.github/README.md +++ b/.github/README.md @@ -388,7 +388,7 @@ curl -H "Origin: http://example.com" --verbose http://localhost:3000 func main() { app := fiber.New() - app.Get("/", dtatic.New("./public")) + app.Get("/", static.New("./public")) app.Get("/demo", func(c fiber.Ctx) error { return c.SendString("This is a demo!") From 98a53db6d070f35be4f99693d39a78adbc874e76 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 24 May 2024 23:29:35 +0300 Subject: [PATCH 17/22] update1 --- middleware/static/static_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index f61ae2b46a..e454f2a7ae 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -64,6 +64,12 @@ func Test_Static_Direct(t *testing.T) { 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") From b2ac1f8dd883893e6fc0d3fe9e3354e1a7bf1b56 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 24 May 2024 23:43:08 +0300 Subject: [PATCH 18/22] apply reviews --- middleware/static/config.go | 5 +++++ middleware/static/static.go | 7 ++++++- middleware/static/static_test.go | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/middleware/static/config.go b/middleware/static/config.go index 8094709b59..2f1cf269ce 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -62,6 +62,11 @@ type Config struct { // // 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 diff --git a/middleware/static/static.go b/middleware/static/static.go index d0d15f48a5..1bb4e8cc4d 100644 --- a/middleware/static/static.go +++ b/middleware/static/static.go @@ -86,7 +86,7 @@ func New(root string, cfg ...Config) fiber.Handler { // If the root is a file, we need to reset the path to "/" always. switch { case checkFile && fs.FS == nil: - path = append(path[0:0], '/') + path = []byte("/") case checkFile && fs.FS != nil: path = utils.UnsafeBytes(root) default: @@ -134,6 +134,11 @@ func New(root string, cfg ...Config) fiber.Handler { 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) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index e454f2a7ae..c3852fab70 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -159,6 +159,27 @@ func Test_Static_Disable_Cache(t *testing.T) { 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() From 6db3d6384ca96dfcedbd2714355eb0d1a21c2023 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 24 May 2024 23:47:15 +0300 Subject: [PATCH 19/22] update --- constants.go | 1 + docs/api/constants.md | 27 +++++++++++++++------------ docs/middleware/static.md | 15 ++++++++++----- docs/whats_new.md | 6 +++--- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/constants.go b/constants.go index 1fc2548bf8..6144dc7626 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 diff --git a/docs/api/constants.md b/docs/api/constants.md index fce36d3694..a9ee6d5a05 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/middleware/static.md b/docs/middleware/static.md index eb3af3796c..07b3983759 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -111,9 +111,9 @@ func main() { app := fiber.New() app.Get("/files*", static.New("", static.Config{ - FS: os.DirFS("files"), - Browse: true, - })) + FS: os.DirFS("files"), + Browse: true, + })) app.Listen(":3000") } @@ -147,12 +147,17 @@ To define static routes using `Get`, append the wildcard (`*`) operator at the e | 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, + Index: []string{"index.html"}, + CacheDuration: 10 * time.Second, } ``` diff --git a/docs/whats_new.md b/docs/whats_new.md index 4eb6918ea4..ff8b834d46 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -272,7 +272,7 @@ DRAFT section ### Filesystem 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. +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 @@ -364,7 +364,7 @@ You need to move filesystem middleware to static middleware due to it has been r ```go // Before app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), + Root: http.Dir("./assets"), })) app.Use(filesystem.New(filesystem.Config{ @@ -378,7 +378,7 @@ app.Use(filesystem.New(filesystem.Config{ ```go // After app.Use(static.New("", static.Config{ - FS: os.DirFS("./assets"), + FS: os.DirFS("./assets"), })) app.Use(static.New("", static.Config{ From 6ee3aed22d0af1cfae84e8607b2cc703fc135cdb Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 24 May 2024 23:49:33 +0300 Subject: [PATCH 20/22] update --- middleware/static/static_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index c3852fab70..bc0585a293 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http/httptest" "os" + "runtime" "strings" "testing" @@ -123,13 +124,20 @@ func Test_Static_Custom_CacheControl(t *testing.T) { } 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() { From 502fbc711652cc4d6265f68ec83df77dda6e3eb9 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Mon, 27 May 2024 18:57:46 +0300 Subject: [PATCH 21/22] update examples --- docs/middleware/static.md | 36 ++++++++++++++++++++++++++++++++++++ middleware/static/config.go | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index 07b3983759..74c1b85d94 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -18,6 +18,8 @@ func New(root string, cfg ...Config) fiber.Handler ## Examples +### Serving files from a directory + ```go package main @@ -45,6 +47,8 @@ curl http://localhost:3000/css/style.css +### Serving files from a directory with Use + ```go package main @@ -72,6 +76,8 @@ curl http://localhost:3000/css/style.css +### Serving a file + ```go package main @@ -99,6 +105,8 @@ curl http://localhost:3000/static/john/doee # will show hello.html +### Serving files using os.DirFS + ```go package main @@ -129,6 +137,34 @@ curl http://localhost:3000/files/index.html +### SPA (Single Page Application) + +```go +func main() { + app := fiber.New() + 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") + }) + + app.Listen(":3000") +} +``` + +
+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. ::: diff --git a/middleware/static/config.go b/middleware/static/config.go index 2f1cf269ce..cc7a935744 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -86,7 +86,7 @@ func configDefault(config ...Config) Config { cfg := config[0] // Set default values - if cfg.IndexNames == nil { + if cfg.IndexNames == nil || len(cfg.IndexNames) == 0 { cfg.IndexNames = ConfigDefault.IndexNames } From 740c15ce3e76cc6a5265a6e53a47e2347da3c9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Tue, 28 May 2024 09:19:34 +0200 Subject: [PATCH 22/22] add more examples --- docs/middleware/static.md | 107 ++++++++++++++------------------------ 1 file changed, 40 insertions(+), 67 deletions(-) diff --git a/docs/middleware/static.md b/docs/middleware/static.md index 74c1b85d94..6e292b08bc 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -18,23 +18,18 @@ func New(root string, cfg ...Config) fiber.Handler ## Examples -### Serving files from a directory - +Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework ```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/static" +import( + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" ) +``` -func main() { - app := fiber.New() - - app.Get("/*", static.New("./public")) - - app.Listen(":3000") -} +### Serving files from a directory + +```go +app.Get("/*", static.New("./public")) ```
@@ -50,20 +45,7 @@ curl http://localhost:3000/css/style.css ### Serving files from a directory with Use ```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/static" -) - -func main() { - app := fiber.New() - - app.Use("/", static.New("./public")) - - app.Listen(":3000") -} +app.Use("/", static.New("./public")) ```
@@ -79,20 +61,7 @@ curl http://localhost:3000/css/style.css ### Serving a file ```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/static" -) - -func main() { - app := fiber.New() - - app.Use("/static", static.New("./public/hello.html")) - - app.Listen(":3000") -} +app.Use("/static", static.New("./public/hello.html")) ```
@@ -108,23 +77,32 @@ curl http://localhost:3000/static/john/doee # will show hello.html ### Serving files using os.DirFS ```go -package main +app.Get("/files*", static.New("", static.Config{ + FS: os.DirFS("files"), + Browse: true, +})) +``` -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/static" -) +
+Test -func main() { - app := fiber.New() - - app.Get("/files*", static.New("", static.Config{ - FS: os.DirFS("files"), +```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, - })) - - app.Listen(":3000") -} +})) ```
@@ -140,18 +118,13 @@ curl http://localhost:3000/files/index.html ### SPA (Single Page Application) ```go -func main() { - app := fiber.New() - app.Use("/web", static.New("", static.Config{ - FS: os.DirFS("dist"), - })) +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") - }) - - app.Listen(":3000") -} +app.Get("/web*", func(c fiber.Ctx) error { + return c.SendFile("dist/index.html") +}) ```