mirror of
https://github.com/labstack/echo.git
synced 2024-11-28 08:38:39 +02:00
Improve filesystem support (Go 1.16+). Add field echo.Filesystem, methods: echo.FileFS, echo.StaticFS, group.FileFS, group.StaticFS. Following methods will use echo.Filesystem to server files: echo.File, echo.Static, group.File, group.Static, Context.File
This commit is contained in:
parent
7c41b93f0c
commit
1b1a68fd4f
25
context.go
25
context.go
@ -9,8 +9,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@ -569,29 +567,6 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *context) File(file string) (err error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, _ := f.Stat()
|
||||
if fi.IsDir() {
|
||||
file = filepath.Join(file, indexPage)
|
||||
f, err = os.Open(file)
|
||||
if err != nil {
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
defer f.Close()
|
||||
if fi, err = f.Stat(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *context) Attachment(file, name string) error {
|
||||
return c.contentDisposition(file, name, "attachment")
|
||||
}
|
||||
|
33
context_fs.go
Normal file
33
context_fs.go
Normal file
@ -0,0 +1,33 @@
|
||||
//go:build !go1.16
|
||||
// +build !go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (c *context) File(file string) (err error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, _ := f.Stat()
|
||||
if fi.IsDir() {
|
||||
file = filepath.Join(file, indexPage)
|
||||
f, err = os.Open(file)
|
||||
if err != nil {
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
defer f.Close()
|
||||
if fi, err = f.Stat(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
|
||||
return
|
||||
}
|
47
context_fs_go1.16.go
Normal file
47
context_fs_go1.16.go
Normal file
@ -0,0 +1,47 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (c *context) File(file string) error {
|
||||
return fsFile(c, file, c.echo.Filesystem)
|
||||
}
|
||||
|
||||
func (c *context) FileFS(file string, filesystem fs.FS) error {
|
||||
return fsFile(c, file, filesystem)
|
||||
}
|
||||
|
||||
func fsFile(c Context, file string, filesystem fs.FS) error {
|
||||
f, err := filesystem.Open(file)
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, _ := f.Stat()
|
||||
if fi.IsDir() {
|
||||
file = filepath.Join(file, indexPage)
|
||||
f, err = filesystem.Open(file)
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
defer f.Close()
|
||||
if fi, err = f.Stat(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ff, ok := f.(io.ReadSeeker)
|
||||
if !ok {
|
||||
return errors.New("file does not implement io.ReadSeeker")
|
||||
}
|
||||
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff)
|
||||
return nil
|
||||
}
|
135
context_fs_go1.16_test.go
Normal file
135
context_fs_go1.16_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContext_File(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
whenFile string
|
||||
whenFS fs.FS
|
||||
expectStatus int
|
||||
expectStartsWith []byte
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "ok, from default file system",
|
||||
whenFile: "_fixture/images/walle.png",
|
||||
whenFS: nil,
|
||||
expectStatus: http.StatusOK,
|
||||
expectStartsWith: []byte{0x89, 0x50, 0x4e},
|
||||
},
|
||||
{
|
||||
name: "ok, from custom file system",
|
||||
whenFile: "walle.png",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
expectStatus: http.StatusOK,
|
||||
expectStartsWith: []byte{0x89, 0x50, 0x4e},
|
||||
},
|
||||
{
|
||||
name: "nok, not existent file",
|
||||
whenFile: "not.png",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
expectStatus: http.StatusOK,
|
||||
expectStartsWith: nil,
|
||||
expectError: "code=404, message=Not Found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
if tc.whenFS != nil {
|
||||
e.Filesystem = tc.whenFS
|
||||
}
|
||||
|
||||
handler := func(ec Context) error {
|
||||
return ec.(*context).File(tc.whenFile)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/match.png", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
err := handler(c)
|
||||
|
||||
testify.Equal(t, tc.expectStatus, rec.Code)
|
||||
if tc.expectError != "" {
|
||||
testify.EqualError(t, err, tc.expectError)
|
||||
} else {
|
||||
testify.NoError(t, err)
|
||||
}
|
||||
|
||||
body := rec.Body.Bytes()
|
||||
if len(body) > len(tc.expectStartsWith) {
|
||||
body = body[:len(tc.expectStartsWith)]
|
||||
}
|
||||
testify.Equal(t, tc.expectStartsWith, body)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_FileFS(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
whenFile string
|
||||
whenFS fs.FS
|
||||
expectStatus int
|
||||
expectStartsWith []byte
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
whenFile: "walle.png",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
expectStatus: http.StatusOK,
|
||||
expectStartsWith: []byte{0x89, 0x50, 0x4e},
|
||||
},
|
||||
{
|
||||
name: "nok, not existent file",
|
||||
whenFile: "not.png",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
expectStatus: http.StatusOK,
|
||||
expectStartsWith: nil,
|
||||
expectError: "code=404, message=Not Found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
|
||||
handler := func(ec Context) error {
|
||||
return ec.(*context).FileFS(tc.whenFile, tc.whenFS)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/match.png", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
err := handler(c)
|
||||
|
||||
testify.Equal(t, tc.expectStatus, rec.Code)
|
||||
if tc.expectError != "" {
|
||||
testify.EqualError(t, err, tc.expectError)
|
||||
} else {
|
||||
testify.NoError(t, err)
|
||||
}
|
||||
|
||||
body := rec.Body.Bytes()
|
||||
if len(body) > len(tc.expectStartsWith) {
|
||||
body = body[:len(tc.expectStartsWith)]
|
||||
}
|
||||
testify.Equal(t, tc.expectStartsWith, body)
|
||||
})
|
||||
}
|
||||
}
|
53
echo.go
53
echo.go
@ -47,9 +47,6 @@ import (
|
||||
stdLog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
@ -66,6 +63,7 @@ import (
|
||||
type (
|
||||
// Echo is the top-level framework instance.
|
||||
Echo struct {
|
||||
filesystem
|
||||
common
|
||||
// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
|
||||
// listener address info (on which interface/port was listener binded) without having data races.
|
||||
@ -320,8 +318,9 @@ var (
|
||||
// New creates an instance of Echo.
|
||||
func New() (e *Echo) {
|
||||
e = &Echo{
|
||||
Server: new(http.Server),
|
||||
TLSServer: new(http.Server),
|
||||
filesystem: createFilesystem(),
|
||||
Server: new(http.Server),
|
||||
TLSServer: new(http.Server),
|
||||
AutoTLSManager: autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
},
|
||||
@ -500,50 +499,6 @@ func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middlew
|
||||
return routes
|
||||
}
|
||||
|
||||
// Static registers a new route with path prefix to serve static files from the
|
||||
// provided root directory.
|
||||
func (e *Echo) Static(prefix, root string) *Route {
|
||||
if root == "" {
|
||||
root = "." // For security we want to restrict to CWD.
|
||||
}
|
||||
return e.static(prefix, root, e.GET)
|
||||
}
|
||||
|
||||
func (common) static(prefix, root string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route) *Route {
|
||||
h := func(c Context) error {
|
||||
p, err := url.PathUnescape(c.Param("*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
// The access path does not exist
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
|
||||
// If the request is for a directory and does not end with "/"
|
||||
p = c.Request().URL.Path // path must not be empty.
|
||||
if fi.IsDir() && p[len(p)-1] != '/' {
|
||||
// Redirect to ends with "/"
|
||||
return c.Redirect(http.StatusMovedPermanently, p+"/")
|
||||
}
|
||||
return c.File(name)
|
||||
}
|
||||
// Handle added routes based on trailing slash:
|
||||
// /prefix => exact route "/prefix" + any route "/prefix/*"
|
||||
// /prefix/ => only any route "/prefix/*"
|
||||
if prefix != "" {
|
||||
if prefix[len(prefix)-1] == '/' {
|
||||
// Only add any route for intentional trailing slash
|
||||
return get(prefix+"*", h)
|
||||
}
|
||||
get(prefix, h)
|
||||
}
|
||||
return get(prefix+"/*", h)
|
||||
}
|
||||
|
||||
func (common) file(path, file string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route,
|
||||
m ...MiddlewareFunc) *Route {
|
||||
return get(path, func(c Context) error {
|
||||
|
62
echo_fs.go
Normal file
62
echo_fs.go
Normal file
@ -0,0 +1,62 @@
|
||||
//go:build !go1.16
|
||||
// +build !go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type filesystem struct {
|
||||
}
|
||||
|
||||
func createFilesystem() filesystem {
|
||||
return filesystem{}
|
||||
}
|
||||
|
||||
// Static registers a new route with path prefix to serve static files from the
|
||||
// provided root directory.
|
||||
func (e *Echo) Static(prefix, root string) *Route {
|
||||
if root == "" {
|
||||
root = "." // For security we want to restrict to CWD.
|
||||
}
|
||||
return e.static(prefix, root, e.GET)
|
||||
}
|
||||
|
||||
func (common) static(prefix, root string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route) *Route {
|
||||
h := func(c Context) error {
|
||||
p, err := url.PathUnescape(c.Param("*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
// The access path does not exist
|
||||
return NotFoundHandler(c)
|
||||
}
|
||||
|
||||
// If the request is for a directory and does not end with "/"
|
||||
p = c.Request().URL.Path // path must not be empty.
|
||||
if fi.IsDir() && p[len(p)-1] != '/' {
|
||||
// Redirect to ends with "/"
|
||||
return c.Redirect(http.StatusMovedPermanently, p+"/")
|
||||
}
|
||||
return c.File(name)
|
||||
}
|
||||
// Handle added routes based on trailing slash:
|
||||
// /prefix => exact route "/prefix" + any route "/prefix/*"
|
||||
// /prefix/ => only any route "/prefix/*"
|
||||
if prefix != "" {
|
||||
if prefix[len(prefix)-1] == '/' {
|
||||
// Only add any route for intentional trailing slash
|
||||
return get(prefix+"*", h)
|
||||
}
|
||||
get(prefix, h)
|
||||
}
|
||||
return get(prefix+"/*", h)
|
||||
}
|
126
echo_fs_go1.16.go
Normal file
126
echo_fs_go1.16.go
Normal file
@ -0,0 +1,126 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type filesystem struct {
|
||||
// Filesystem is file system used by Static and File handlers to access files.
|
||||
// Defaults to os.DirFS(".")
|
||||
Filesystem fs.FS
|
||||
}
|
||||
|
||||
func createFilesystem() filesystem {
|
||||
return filesystem{
|
||||
Filesystem: newDefaultFS(),
|
||||
}
|
||||
}
|
||||
|
||||
// Static registers a new route with path prefix to serve static files from the provided root directory.
|
||||
func (e *Echo) Static(pathPrefix, root string) *Route {
|
||||
subFs, err := subFS(e.Filesystem, root)
|
||||
if err != nil {
|
||||
// happens when `root` contains invalid path according to `fs.ValidPath` rules and we are unable to create FS
|
||||
panic(fmt.Errorf("invalid root given to echo.Static, err %w", err))
|
||||
}
|
||||
return e.Add(
|
||||
http.MethodGet,
|
||||
pathPrefix+"*",
|
||||
StaticDirectoryHandler(subFs, false),
|
||||
)
|
||||
}
|
||||
|
||||
// StaticFS registers a new route with path prefix to serve static files from the provided file system.
|
||||
func (e *Echo) StaticFS(pathPrefix string, fileSystem fs.FS) *Route {
|
||||
return e.Add(
|
||||
http.MethodGet,
|
||||
pathPrefix+"*",
|
||||
StaticDirectoryHandler(fileSystem, false),
|
||||
)
|
||||
}
|
||||
|
||||
// StaticDirectoryHandler creates handler function to serve files from provided file system
|
||||
// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.
|
||||
func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {
|
||||
return func(c Context) error {
|
||||
p := c.Param("*")
|
||||
if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice
|
||||
tmpPath, err := url.PathUnescape(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unescape path variable: %w", err)
|
||||
}
|
||||
p = tmpPath
|
||||
}
|
||||
|
||||
// fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid
|
||||
name := filepath.Clean(strings.TrimPrefix(p, "/"))
|
||||
fi, err := fs.Stat(fileSystem, name)
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
// If the request is for a directory and does not end with "/"
|
||||
p = c.Request().URL.Path // path must not be empty.
|
||||
if fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {
|
||||
// Redirect to ends with "/"
|
||||
return c.Redirect(http.StatusMovedPermanently, p+"/")
|
||||
}
|
||||
return fsFile(c, name, fileSystem)
|
||||
}
|
||||
}
|
||||
|
||||
// FileFS registers a new route with path to serve file from the provided file system.
|
||||
func (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
|
||||
return e.GET(path, StaticFileHandler(file, filesystem), m...)
|
||||
}
|
||||
|
||||
// StaticFileHandler creates handler function to serve file from provided file system
|
||||
func StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {
|
||||
return func(c Context) error {
|
||||
return fsFile(c, file, filesystem)
|
||||
}
|
||||
}
|
||||
|
||||
// defaultFS emulates os.Open behaviour with filesystem opened by `os.DirFs`. Difference between `os.Open` and `fs.Open`
|
||||
// is that FS does not allow to open path that start with `..` or `/` etc. For example previously you could have `../images`
|
||||
// in your application but `fs := os.DirFS("./")` would not allow you to use `fs.Open("../images")` and this would break
|
||||
// all old applications that rely on being able to traverse up from current executable run path.
|
||||
// NB: private because you really should use fs.FS implementation instances
|
||||
type defaultFS struct {
|
||||
prefix string
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
func newDefaultFS() *defaultFS {
|
||||
dir, _ := os.Getwd()
|
||||
return &defaultFS{
|
||||
prefix: dir,
|
||||
fs: os.DirFS(dir),
|
||||
}
|
||||
}
|
||||
|
||||
func (fs defaultFS) Open(name string) (fs.File, error) {
|
||||
return fs.fs.Open(name)
|
||||
}
|
||||
|
||||
func subFS(currentFs fs.FS, root string) (fs.FS, error) {
|
||||
if dFS, ok := currentFs.(*defaultFS); ok {
|
||||
// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS to
|
||||
// allow cases when root is given as `../somepath` which is not valid for fs.FS
|
||||
root = filepath.Join(dFS.prefix, root)
|
||||
return &defaultFS{
|
||||
prefix: root,
|
||||
fs: os.DirFS(root),
|
||||
}, nil
|
||||
}
|
||||
return fs.Sub(currentFs, filepath.Clean(root))
|
||||
}
|
251
echo_fs_go1.16_test.go
Normal file
251
echo_fs_go1.16_test.go
Normal file
@ -0,0 +1,251 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEcho_StaticFS(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
givenPrefix string
|
||||
givenFs fs.FS
|
||||
whenURL string
|
||||
expectStatus int
|
||||
expectHeaderLocation string
|
||||
expectBodyStartsWith string
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
givenPrefix: "/images",
|
||||
givenFs: os.DirFS("./_fixture/images"),
|
||||
whenURL: "/images/walle.png",
|
||||
expectStatus: http.StatusOK,
|
||||
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
|
||||
},
|
||||
{
|
||||
name: "No file",
|
||||
givenPrefix: "/images",
|
||||
givenFs: os.DirFS("_fixture/scripts"),
|
||||
whenURL: "/images/bolt.png",
|
||||
expectStatus: http.StatusNotFound,
|
||||
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
givenPrefix: "/images",
|
||||
givenFs: os.DirFS("_fixture/images"),
|
||||
whenURL: "/images/",
|
||||
expectStatus: http.StatusNotFound,
|
||||
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||
},
|
||||
{
|
||||
name: "Directory Redirect",
|
||||
givenPrefix: "/",
|
||||
givenFs: os.DirFS("_fixture/"),
|
||||
whenURL: "/folder",
|
||||
expectStatus: http.StatusMovedPermanently,
|
||||
expectHeaderLocation: "/folder/",
|
||||
expectBodyStartsWith: "",
|
||||
},
|
||||
{
|
||||
name: "Directory Redirect with non-root path",
|
||||
givenPrefix: "/static",
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/static",
|
||||
expectStatus: http.StatusMovedPermanently,
|
||||
expectHeaderLocation: "/static/",
|
||||
expectBodyStartsWith: "",
|
||||
},
|
||||
{
|
||||
name: "Prefixed directory 404 (request URL without slash)",
|
||||
givenPrefix: "/folder/", // trailing slash will intentionally not match "/folder"
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/folder", // no trailing slash
|
||||
expectStatus: http.StatusNotFound,
|
||||
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||
},
|
||||
{
|
||||
name: "Prefixed directory redirect (without slash redirect to slash)",
|
||||
givenPrefix: "/folder", // no trailing slash shall match /folder and /folder/*
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/folder", // no trailing slash
|
||||
expectStatus: http.StatusMovedPermanently,
|
||||
expectHeaderLocation: "/folder/",
|
||||
expectBodyStartsWith: "",
|
||||
},
|
||||
{
|
||||
name: "Directory with index.html",
|
||||
givenPrefix: "/",
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/",
|
||||
expectStatus: http.StatusOK,
|
||||
expectBodyStartsWith: "<!doctype html>",
|
||||
},
|
||||
{
|
||||
name: "Prefixed directory with index.html (prefix ending with slash)",
|
||||
givenPrefix: "/assets/",
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/assets/",
|
||||
expectStatus: http.StatusOK,
|
||||
expectBodyStartsWith: "<!doctype html>",
|
||||
},
|
||||
{
|
||||
name: "Prefixed directory with index.html (prefix ending without slash)",
|
||||
givenPrefix: "/assets",
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/assets/",
|
||||
expectStatus: http.StatusOK,
|
||||
expectBodyStartsWith: "<!doctype html>",
|
||||
},
|
||||
{
|
||||
name: "Sub-directory with index.html",
|
||||
givenPrefix: "/",
|
||||
givenFs: os.DirFS("_fixture"),
|
||||
whenURL: "/folder/",
|
||||
expectStatus: http.StatusOK,
|
||||
expectBodyStartsWith: "<!doctype html>",
|
||||
},
|
||||
{
|
||||
name: "do not allow directory traversal (backslash - windows separator)",
|
||||
givenPrefix: "/",
|
||||
givenFs: os.DirFS("_fixture/"),
|
||||
whenURL: `/..\\middleware/basic_auth.go`,
|
||||
expectStatus: http.StatusNotFound,
|
||||
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||
},
|
||||
{
|
||||
name: "do not allow directory traversal (slash - unix separator)",
|
||||
givenPrefix: "/",
|
||||
givenFs: os.DirFS("_fixture/"),
|
||||
whenURL: `/../middleware/basic_auth.go`,
|
||||
expectStatus: http.StatusNotFound,
|
||||
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
e.StaticFS(tc.givenPrefix, tc.givenFs)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, tc.expectStatus, rec.Code)
|
||||
body := rec.Body.String()
|
||||
if tc.expectBodyStartsWith != "" {
|
||||
assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
|
||||
} else {
|
||||
assert.Equal(t, "", body)
|
||||
}
|
||||
|
||||
if tc.expectHeaderLocation != "" {
|
||||
assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0])
|
||||
} else {
|
||||
_, ok := rec.Result().Header["Location"]
|
||||
assert.False(t, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcho_FileFS(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
whenPath string
|
||||
whenFile string
|
||||
whenFS fs.FS
|
||||
givenURL string
|
||||
expectCode int
|
||||
expectStartsWith []byte
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
whenPath: "/walle",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
whenFile: "walle.png",
|
||||
givenURL: "/walle",
|
||||
expectCode: http.StatusOK,
|
||||
expectStartsWith: []byte{0x89, 0x50, 0x4e},
|
||||
},
|
||||
{
|
||||
name: "nok, requesting invalid path",
|
||||
whenPath: "/walle",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
whenFile: "walle.png",
|
||||
givenURL: "/walle.png",
|
||||
expectCode: http.StatusNotFound,
|
||||
expectStartsWith: []byte(`{"message":"Not Found"}`),
|
||||
},
|
||||
{
|
||||
name: "nok, serving not existent file from filesystem",
|
||||
whenPath: "/walle",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
whenFile: "not-existent.png",
|
||||
givenURL: "/walle",
|
||||
expectCode: http.StatusNotFound,
|
||||
expectStartsWith: []byte(`{"message":"Not Found"}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
e.FileFS(tc.whenPath, tc.whenFile, tc.whenFS)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, tc.expectCode, rec.Code)
|
||||
|
||||
body := rec.Body.Bytes()
|
||||
if len(body) > len(tc.expectStartsWith) {
|
||||
body = body[:len(tc.expectStartsWith)]
|
||||
}
|
||||
assert.Equal(t, tc.expectStartsWith, body)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcho_StaticPanic(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
givenRoot string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "panics for ../",
|
||||
givenRoot: "../assets",
|
||||
expectError: "invalid root given to echo.Static, err sub ../assets: invalid name",
|
||||
},
|
||||
{
|
||||
name: "panics for /",
|
||||
givenRoot: "/assets",
|
||||
expectError: "invalid root given to echo.Static, err sub /assets: invalid name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
e.Filesystem = os.DirFS("./")
|
||||
|
||||
assert.PanicsWithError(t, tc.expectError, func() {
|
||||
e.Static("/assets", tc.givenRoot)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
17
echo_test.go
17
echo_test.go
@ -211,7 +211,6 @@ func TestEchoStatic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEchoStaticRedirectIndex(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
e := New()
|
||||
|
||||
// HandlerFunc
|
||||
@ -220,23 +219,25 @@ func TestEchoStaticRedirectIndex(t *testing.T) {
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
errCh <- e.Start("127.0.0.1:1323")
|
||||
errCh <- e.Start(":0")
|
||||
}()
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
err := waitForServerStart(e, errCh, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if resp, err := http.Get("http://127.0.0.1:1323/static"); err == nil {
|
||||
addr := e.ListenerAddr().String()
|
||||
if resp, err := http.Get("http://" + addr + "/static"); err == nil { // http.Get follows redirects by default
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
if body, err := ioutil.ReadAll(resp.Body); err == nil {
|
||||
assert.Equal(true, strings.HasPrefix(string(body), "<!doctype html>"))
|
||||
assert.Equal(t, true, strings.HasPrefix(string(body), "<!doctype html>"))
|
||||
} else {
|
||||
assert.Fail(err.Error())
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
|
||||
} else {
|
||||
assert.Fail(err.Error())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if err := e.Close(); err != nil {
|
||||
|
2
go.mod
2
go.mod
@ -8,7 +8,7 @@ require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/valyala/fasttemplate v1.2.1
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
|
||||
)
|
||||
|
11
go.sum
11
go.sum
@ -20,18 +20,13 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211104170005-ce137452f963 h1:8gJUadZl+kWvZBqG/LautX0X6qe5qTC2VI/3V3NBRAY=
|
||||
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -42,8 +37,6 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
|
5
group.go
5
group.go
@ -102,11 +102,6 @@ func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) (sg *Group) {
|
||||
return
|
||||
}
|
||||
|
||||
// Static implements `Echo#Static()` for sub-routes within the Group.
|
||||
func (g *Group) Static(prefix, root string) {
|
||||
g.static(prefix, root, g.GET)
|
||||
}
|
||||
|
||||
// File implements `Echo#File()` for sub-routes within the Group.
|
||||
func (g *Group) File(path, file string) {
|
||||
g.file(path, file, g.GET)
|
||||
|
9
group_fs.go
Normal file
9
group_fs.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build !go1.16
|
||||
// +build !go1.16
|
||||
|
||||
package echo
|
||||
|
||||
// Static implements `Echo#Static()` for sub-routes within the Group.
|
||||
func (g *Group) Static(prefix, root string) {
|
||||
g.static(prefix, root, g.GET)
|
||||
}
|
34
group_fs_go1.16.go
Normal file
34
group_fs_go1.16.go
Normal file
@ -0,0 +1,34 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Static implements `Echo#Static()` for sub-routes within the Group.
|
||||
func (g *Group) Static(pathPrefix, root string) {
|
||||
subFs, err := subFS(g.echo.Filesystem, root)
|
||||
if err != nil {
|
||||
// happens when `root` contains invalid path according to `fs.ValidPath` rules and we are unable to create FS
|
||||
panic(fmt.Errorf("invalid root given to group.Static, err %w", err))
|
||||
}
|
||||
g.StaticFS(pathPrefix, subFs)
|
||||
}
|
||||
|
||||
// StaticFS implements `Echo#StaticFS()` for sub-routes within the Group.
|
||||
func (g *Group) StaticFS(pathPrefix string, fileSystem fs.FS) {
|
||||
g.Add(
|
||||
http.MethodGet,
|
||||
pathPrefix+"*",
|
||||
StaticDirectoryHandler(fileSystem, false),
|
||||
)
|
||||
}
|
||||
|
||||
// FileFS implements `Echo#FileFS()` for sub-routes within the Group.
|
||||
func (g *Group) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route {
|
||||
return g.GET(path, StaticFileHandler(file, filesystem), m...)
|
||||
}
|
106
group_fs_go1.16_test.go
Normal file
106
group_fs_go1.16_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package echo
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGroup_FileFS(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
whenPath string
|
||||
whenFile string
|
||||
whenFS fs.FS
|
||||
givenURL string
|
||||
expectCode int
|
||||
expectStartsWith []byte
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
whenPath: "/walle",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
whenFile: "walle.png",
|
||||
givenURL: "/assets/walle",
|
||||
expectCode: http.StatusOK,
|
||||
expectStartsWith: []byte{0x89, 0x50, 0x4e},
|
||||
},
|
||||
{
|
||||
name: "nok, requesting invalid path",
|
||||
whenPath: "/walle",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
whenFile: "walle.png",
|
||||
givenURL: "/assets/walle.png",
|
||||
expectCode: http.StatusNotFound,
|
||||
expectStartsWith: []byte(`{"message":"Not Found"}`),
|
||||
},
|
||||
{
|
||||
name: "nok, serving not existent file from filesystem",
|
||||
whenPath: "/walle",
|
||||
whenFS: os.DirFS("_fixture/images"),
|
||||
whenFile: "not-existent.png",
|
||||
givenURL: "/assets/walle",
|
||||
expectCode: http.StatusNotFound,
|
||||
expectStartsWith: []byte(`{"message":"Not Found"}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
g := e.Group("/assets")
|
||||
g.FileFS(tc.whenPath, tc.whenFile, tc.whenFS)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, tc.expectCode, rec.Code)
|
||||
|
||||
body := rec.Body.Bytes()
|
||||
if len(body) > len(tc.expectStartsWith) {
|
||||
body = body[:len(tc.expectStartsWith)]
|
||||
}
|
||||
assert.Equal(t, tc.expectStartsWith, body)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroup_StaticPanic(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
givenRoot string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "panics for ../",
|
||||
givenRoot: "../images",
|
||||
expectError: "invalid root given to group.Static, err sub ../images: invalid name",
|
||||
},
|
||||
{
|
||||
name: "panics for /",
|
||||
givenRoot: "/images",
|
||||
expectError: "invalid root given to group.Static, err sub /images: invalid name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
e.Filesystem = os.DirFS("./")
|
||||
|
||||
g := e.Group("/assets")
|
||||
|
||||
assert.PanicsWithError(t, tc.expectError, func() {
|
||||
g.Static("/images", tc.givenRoot)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user