diff --git a/middleware/filesystem/README.md b/middleware/filesystem/README.md index dea14fa43e..e7f3cbbb5e 100644 --- a/middleware/filesystem/README.md +++ b/middleware/filesystem/README.md @@ -12,11 +12,6 @@ Filesystem middleware for [Fiber](https://github.com/gofiber/fiber) that enables - [Examples](#examples) - [Config](#config) - [embed](#embed) - - [pkger](#pkger) - - [packr](#packr) - - [go.rice](#gorice) - - [fileb0x](#fileb0x) - - [statik](#statik) - [Config](#config-1) - [Default Config](#default-config) @@ -44,12 +39,12 @@ Then create a Fiber app with `app := fiber.New()`. ```go // Provide a minimal config app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), + Root: os.DirFS("./assets"), })) // Or extend your config for customization app.Use(filesystem.New(filesystem.Config{ - Root: http.Dir("./assets"), + Root: os.DirFS("./assets"), Browse: true, Index: "index.html", NotFoundFile: "404.html", @@ -88,15 +83,14 @@ func main() { app := fiber.New() app.Use("/", filesystem.New(filesystem.Config{ - Root: http.FS(f), + Root: 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", + Root: embedDirStatic, Browse: true, })) @@ -104,137 +98,6 @@ func main() { } ``` -### pkger - -[Pkger](https://github.com/markbates/pkger) can be used to embed files in a Golang excecutable. - -```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 - -[Packr](https://github.com/gobuffalo/packr) can be used to embed files in a Golang excecutable. - -```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 - -```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 - -[Fileb0x](https://github.com/UnnoTed/fileb0x) can be used to embed files in a Golang excecutable. - -```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 - -[Statik](https://github.com/rakyll/statik) can be used to embed files in a Golang excecutable. - -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" - - "/statik" - fs "github.com/rakyll/statik/fs" -) - -func main() { - statik, 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 ```go @@ -249,14 +112,12 @@ type Config struct { // to a collection of files and directories. // // Required. Default: nil - Root http.FileSystem `json:"-"` + Root fs.FS `json:"-"` // PathPrefix defines a prefix to be added to a filepath when // reading a file from the FileSystem. // - // Use when using Go 1.16 embed.FS - // - // Optional. Default "" + // Optional. Default "." PathPrefix string `json:"path_prefix"` // Enable directory browsing. @@ -288,7 +149,7 @@ type Config struct { var ConfigDefault = Config{ Next: nil, Root: nil, - PathPrefix: "", + PathPrefix: ".", Browse: false, Index: "/index.html", MaxAge: 0, diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go index ded63874ce..c8f08b636f 100644 --- a/middleware/filesystem/filesystem.go +++ b/middleware/filesystem/filesystem.go @@ -1,8 +1,10 @@ package filesystem import ( + "io/fs" "net/http" "os" + "path/filepath" "strconv" "strings" "sync" @@ -21,14 +23,12 @@ type Config struct { // to a collection of files and directories. // // Required. Default: nil - Root http.FileSystem `json:"-"` + Root fs.FS `json:"-"` // PathPrefix defines a prefix to be added to a filepath when // reading a file from the FileSystem. // - // Use when using Go 1.16 embed.FS - // - // Optional. Default "" + // Optional. Default "." PathPrefix string `json:"path_prefix"` // Enable directory browsing. @@ -41,6 +41,11 @@ type Config struct { // 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. // @@ -57,7 +62,7 @@ type Config struct { var ConfigDefault = Config{ Next: nil, Root: nil, - PathPrefix: "", + PathPrefix: ".", Browse: false, Index: "/index.html", MaxAge: 0, @@ -76,6 +81,9 @@ func New(config ...Config) fiber.Handler { if cfg.Index == "" { cfg.Index = ConfigDefault.Index } + if cfg.PathPrefix == "" { + cfg.PathPrefix = ConfigDefault.PathPrefix + } if !strings.HasPrefix(cfg.Index, "/") { cfg.Index = "/" + cfg.Index } @@ -88,8 +96,13 @@ func New(config ...Config) fiber.Handler { panic("filesystem: Root cannot be nil") } - if cfg.PathPrefix != "" && !strings.HasPrefix(cfg.PathPrefix, "/") { - cfg.PathPrefix = "/" + cfg.PathPrefix + // 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 @@ -120,23 +133,26 @@ func New(config ...Config) fiber.Handler { if !strings.HasPrefix(path, "/") { path = "/" + path } - // Add PathPrefix - if cfg.PathPrefix != "" { - // PathPrefix already has a "/" prefix - path = cfg.PathPrefix + path - } var ( - file http.File + 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 = cfg.Root.Open(path) + + file, err = openFile(cfg.Root, path) + if err != nil && os.IsNotExist(err) && cfg.NotFoundFile != "" { - file, err = cfg.Root.Open(cfg.NotFoundFile) + file, err = openFile(cfg.Root, cfg.NotFoundFile) } if err != nil { @@ -153,7 +169,9 @@ func New(config ...Config) fiber.Handler { // Serve index if path is directory if stat.IsDir() { indexPath := strings.TrimRight(path, "/") + cfg.Index - index, err := cfg.Root.Open(indexPath) + indexPath = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+indexPath)) + + index, err := openFile(cfg.Root, indexPath) if err == nil { indexStat, err := index.Stat() if err == nil { @@ -168,6 +186,7 @@ func New(config ...Config) fiber.Handler { if cfg.Browse { return dirList(c, file) } + return fiber.ErrForbidden } @@ -182,6 +201,11 @@ func New(config ...Config) fiber.Handler { 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) @@ -205,13 +229,15 @@ func New(config ...Config) fiber.Handler { } // SendFile ... -func SendFile(c fiber.Ctx, fs http.FileSystem, path string) (err error) { +func SendFile(c fiber.Ctx, filesystem fs.FS, path string) (err error) { var ( - file http.File + file fs.File stat os.FileInfo ) - file, err = fs.Open(path) + path = filepath.Join(".", filepath.Clean("/"+path)) + + file, err = openFile(filesystem, path) if err != nil { if os.IsNotExist(err) { return fiber.ErrNotFound @@ -226,7 +252,7 @@ func SendFile(c fiber.Ctx, fs http.FileSystem, path string) (err error) { // Serve index if path is directory if stat.IsDir() { indexPath := strings.TrimRight(path, "/") + ConfigDefault.Index - index, err := fs.Open(indexPath) + index, err := openFile(filesystem, indexPath) if err == nil { indexStat, err := index.Stat() if err == nil { diff --git a/middleware/filesystem/filesystem_test.go b/middleware/filesystem/filesystem_test.go index 6cae378734..e48e4f0b31 100644 --- a/middleware/filesystem/filesystem_test.go +++ b/middleware/filesystem/filesystem_test.go @@ -3,6 +3,7 @@ package filesystem import ( "net/http" "net/http/httptest" + "os" "testing" "github.com/gofiber/fiber/v3" @@ -14,11 +15,11 @@ func Test_FileSystem(t *testing.T) { app := fiber.New() app.Use("/test", New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), })) app.Use("/dir", New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), Browse: true, })) @@ -27,13 +28,13 @@ func Test_FileSystem(t *testing.T) { }) app.Use("/spatest", New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), Index: "index.html", NotFoundFile: "index.html", })) app.Use("/prefix", New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), PathPrefix: "img", })) @@ -133,7 +134,7 @@ func Test_FileSystem(t *testing.T) { func Test_FileSystem_Next(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), Next: func(_ fiber.Ctx) bool { return true }, @@ -144,11 +145,27 @@ func Test_FileSystem_Next(t *testing.T) { utils.AssertEqual(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("GET", "/img/fiber.png", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(fiber.HeaderContentLength) == "") + utils.AssertEqual(t, "image/png", resp.Header.Get(fiber.HeaderContentType)) + utils.AssertEqual(t, `attachment`, resp.Header.Get(fiber.HeaderContentDisposition)) +} + func Test_FileSystem_NonGetAndHead(t *testing.T) { app := fiber.New() app.Use("/test", New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), })) resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil)) @@ -160,7 +177,7 @@ func Test_FileSystem_Head(t *testing.T) { app := fiber.New() app.Use("/test", New(Config{ - Root: http.Dir("../../.github/testdata/fs"), + Root: os.DirFS("../../.github/testdata/fs"), })) req, _ := http.NewRequest(fiber.MethodHead, "/test", nil) @@ -183,7 +200,7 @@ func Test_FileSystem_UsingParam(t *testing.T) { app := fiber.New() app.Use("/:path", func(c fiber.Ctx) error { - return SendFile(c, http.Dir("../../.github/testdata/fs"), c.Params("path")+".html") + return SendFile(c, os.DirFS("../../.github/testdata/fs"), c.Params("path")+".html") }) req, _ := http.NewRequest(fiber.MethodHead, "/index", nil) @@ -196,7 +213,7 @@ func Test_FileSystem_UsingParam_NonFile(t *testing.T) { app := fiber.New() app.Use("/:path", func(c fiber.Ctx) error { - return SendFile(c, http.Dir("../../.github/testdata/fs"), c.Params("path")+".html") + return SendFile(c, os.DirFS("../../.github/testdata/fs"), c.Params("path")+".html") }) req, _ := http.NewRequest(fiber.MethodHead, "/template", nil) diff --git a/middleware/filesystem/utils.go b/middleware/filesystem/utils.go index 7ec68cfde9..3561870a0a 100644 --- a/middleware/filesystem/utils.go +++ b/middleware/filesystem/utils.go @@ -3,9 +3,9 @@ package filesystem import ( "fmt" "html" - "net/http" - "os" + "io/fs" "path" + "path/filepath" "sort" "strings" @@ -20,17 +20,23 @@ func getFileExtension(path string) string { return path[n:] } -func dirList(c fiber.Ctx, f http.File) error { - fileinfos, err := f.Readdir(-1) +func dirList(c fiber.Ctx, f fs.File) error { + ff := f.(fs.ReadDirFile) + fileinfos, err := ff.ReadDir(-1) if err != nil { return err } - fm := make(map[string]os.FileInfo, len(fileinfos)) + fm := make(map[string]fs.FileInfo, len(fileinfos)) filenames := make([]string, 0, len(fileinfos)) for _, fi := range fileinfos { name := fi.Name() - fm[name] = fi + info, err := fi.Info() + if err != nil { + return err + } + + fm[name] = info filenames = append(filenames, name) } @@ -63,3 +69,9 @@ func dirList(c fiber.Ctx, f http.File) error { return nil } + +func openFile(fs fs.FS, name string) (fs.File, error) { + name = filepath.ToSlash(name) + + return fs.Open(name) +}