v3 (enhancement): refactor filesystem middleware with `io/fs` (#2027)

*  v3 (feature): refactor filesystem middleware with `io/fs`

* update docs

* attachment support.

* fix
pull/2130/head
M. Efe Çetin 2022-09-29 09:28:52 +03:00 committed by GitHub
parent 01cfc64f1c
commit 668b0c85f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 181 deletions

View File

@ -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://<server>/static/image.png`.
// Without `PathPrefix`, you have to access it via URL:
// `http://<server>/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"
"<Your go module>/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"
"<Your go module>/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,

View File

@ -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
}
var (
file fs.File
stat os.FileInfo
)
// Add PathPrefix
if cfg.PathPrefix != "" {
// PathPrefix already has a "/" prefix
path = cfg.PathPrefix + path
path = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+path))
}
var (
file http.File
stat os.FileInfo
)
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 {

View File

@ -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) {
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("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)

View File

@ -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)
}