1
0
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:
toimtoimtoim 2022-01-08 22:41:34 +02:00 committed by Martti T
parent 7c41b93f0c
commit 1b1a68fd4f
15 changed files with 819 additions and 97 deletions

View File

@ -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
View 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
View 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
View 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
View File

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

View File

@ -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
View File

@ -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
View File

@ -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=

View File

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