Skip to content

Commit 1b1a68f

Browse files
committed
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
1 parent 7c41b93 commit 1b1a68f

15 files changed

+819
-97
lines changed

context.go

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import (
99
"net"
1010
"net/http"
1111
"net/url"
12-
"os"
13-
"path/filepath"
1412
"strings"
1513
"sync"
1614
)
@@ -569,29 +567,6 @@ func (c *context) Stream(code int, contentType string, r io.Reader) (err error)
569567
return
570568
}
571569

572-
func (c *context) File(file string) (err error) {
573-
f, err := os.Open(file)
574-
if err != nil {
575-
return NotFoundHandler(c)
576-
}
577-
defer f.Close()
578-
579-
fi, _ := f.Stat()
580-
if fi.IsDir() {
581-
file = filepath.Join(file, indexPage)
582-
f, err = os.Open(file)
583-
if err != nil {
584-
return NotFoundHandler(c)
585-
}
586-
defer f.Close()
587-
if fi, err = f.Stat(); err != nil {
588-
return
589-
}
590-
}
591-
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
592-
return
593-
}
594-
595570
func (c *context) Attachment(file, name string) error {
596571
return c.contentDisposition(file, name, "attachment")
597572
}

context_fs.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//go:build !go1.16
2+
// +build !go1.16
3+
4+
package echo
5+
6+
import (
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
)
11+
12+
func (c *context) File(file string) (err error) {
13+
f, err := os.Open(file)
14+
if err != nil {
15+
return NotFoundHandler(c)
16+
}
17+
defer f.Close()
18+
19+
fi, _ := f.Stat()
20+
if fi.IsDir() {
21+
file = filepath.Join(file, indexPage)
22+
f, err = os.Open(file)
23+
if err != nil {
24+
return NotFoundHandler(c)
25+
}
26+
defer f.Close()
27+
if fi, err = f.Stat(); err != nil {
28+
return
29+
}
30+
}
31+
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), f)
32+
return
33+
}

context_fs_go1.16.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//go:build go1.16
2+
// +build go1.16
3+
4+
package echo
5+
6+
import (
7+
"errors"
8+
"io"
9+
"io/fs"
10+
"net/http"
11+
"path/filepath"
12+
)
13+
14+
func (c *context) File(file string) error {
15+
return fsFile(c, file, c.echo.Filesystem)
16+
}
17+
18+
func (c *context) FileFS(file string, filesystem fs.FS) error {
19+
return fsFile(c, file, filesystem)
20+
}
21+
22+
func fsFile(c Context, file string, filesystem fs.FS) error {
23+
f, err := filesystem.Open(file)
24+
if err != nil {
25+
return ErrNotFound
26+
}
27+
defer f.Close()
28+
29+
fi, _ := f.Stat()
30+
if fi.IsDir() {
31+
file = filepath.Join(file, indexPage)
32+
f, err = filesystem.Open(file)
33+
if err != nil {
34+
return ErrNotFound
35+
}
36+
defer f.Close()
37+
if fi, err = f.Stat(); err != nil {
38+
return err
39+
}
40+
}
41+
ff, ok := f.(io.ReadSeeker)
42+
if !ok {
43+
return errors.New("file does not implement io.ReadSeeker")
44+
}
45+
http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff)
46+
return nil
47+
}

context_fs_go1.16_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//go:build go1.16
2+
// +build go1.16
3+
4+
package echo
5+
6+
import (
7+
testify "github.com/stretchr/testify/assert"
8+
"io/fs"
9+
"net/http"
10+
"net/http/httptest"
11+
"os"
12+
"testing"
13+
)
14+
15+
func TestContext_File(t *testing.T) {
16+
var testCases = []struct {
17+
name string
18+
whenFile string
19+
whenFS fs.FS
20+
expectStatus int
21+
expectStartsWith []byte
22+
expectError string
23+
}{
24+
{
25+
name: "ok, from default file system",
26+
whenFile: "_fixture/images/walle.png",
27+
whenFS: nil,
28+
expectStatus: http.StatusOK,
29+
expectStartsWith: []byte{0x89, 0x50, 0x4e},
30+
},
31+
{
32+
name: "ok, from custom file system",
33+
whenFile: "walle.png",
34+
whenFS: os.DirFS("_fixture/images"),
35+
expectStatus: http.StatusOK,
36+
expectStartsWith: []byte{0x89, 0x50, 0x4e},
37+
},
38+
{
39+
name: "nok, not existent file",
40+
whenFile: "not.png",
41+
whenFS: os.DirFS("_fixture/images"),
42+
expectStatus: http.StatusOK,
43+
expectStartsWith: nil,
44+
expectError: "code=404, message=Not Found",
45+
},
46+
}
47+
48+
for _, tc := range testCases {
49+
t.Run(tc.name, func(t *testing.T) {
50+
e := New()
51+
if tc.whenFS != nil {
52+
e.Filesystem = tc.whenFS
53+
}
54+
55+
handler := func(ec Context) error {
56+
return ec.(*context).File(tc.whenFile)
57+
}
58+
59+
req := httptest.NewRequest(http.MethodGet, "/match.png", nil)
60+
rec := httptest.NewRecorder()
61+
c := e.NewContext(req, rec)
62+
63+
err := handler(c)
64+
65+
testify.Equal(t, tc.expectStatus, rec.Code)
66+
if tc.expectError != "" {
67+
testify.EqualError(t, err, tc.expectError)
68+
} else {
69+
testify.NoError(t, err)
70+
}
71+
72+
body := rec.Body.Bytes()
73+
if len(body) > len(tc.expectStartsWith) {
74+
body = body[:len(tc.expectStartsWith)]
75+
}
76+
testify.Equal(t, tc.expectStartsWith, body)
77+
})
78+
}
79+
}
80+
81+
func TestContext_FileFS(t *testing.T) {
82+
var testCases = []struct {
83+
name string
84+
whenFile string
85+
whenFS fs.FS
86+
expectStatus int
87+
expectStartsWith []byte
88+
expectError string
89+
}{
90+
{
91+
name: "ok",
92+
whenFile: "walle.png",
93+
whenFS: os.DirFS("_fixture/images"),
94+
expectStatus: http.StatusOK,
95+
expectStartsWith: []byte{0x89, 0x50, 0x4e},
96+
},
97+
{
98+
name: "nok, not existent file",
99+
whenFile: "not.png",
100+
whenFS: os.DirFS("_fixture/images"),
101+
expectStatus: http.StatusOK,
102+
expectStartsWith: nil,
103+
expectError: "code=404, message=Not Found",
104+
},
105+
}
106+
107+
for _, tc := range testCases {
108+
t.Run(tc.name, func(t *testing.T) {
109+
e := New()
110+
111+
handler := func(ec Context) error {
112+
return ec.(*context).FileFS(tc.whenFile, tc.whenFS)
113+
}
114+
115+
req := httptest.NewRequest(http.MethodGet, "/match.png", nil)
116+
rec := httptest.NewRecorder()
117+
c := e.NewContext(req, rec)
118+
119+
err := handler(c)
120+
121+
testify.Equal(t, tc.expectStatus, rec.Code)
122+
if tc.expectError != "" {
123+
testify.EqualError(t, err, tc.expectError)
124+
} else {
125+
testify.NoError(t, err)
126+
}
127+
128+
body := rec.Body.Bytes()
129+
if len(body) > len(tc.expectStartsWith) {
130+
body = body[:len(tc.expectStartsWith)]
131+
}
132+
testify.Equal(t, tc.expectStartsWith, body)
133+
})
134+
}
135+
}

echo.go

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ import (
4747
stdLog "log"
4848
"net"
4949
"net/http"
50-
"net/url"
51-
"os"
52-
"path/filepath"
5350
"reflect"
5451
"runtime"
5552
"sync"
@@ -66,6 +63,7 @@ import (
6663
type (
6764
// Echo is the top-level framework instance.
6865
Echo struct {
66+
filesystem
6967
common
7068
// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
7169
// listener address info (on which interface/port was listener binded) without having data races.
@@ -320,8 +318,9 @@ var (
320318
// New creates an instance of Echo.
321319
func New() (e *Echo) {
322320
e = &Echo{
323-
Server: new(http.Server),
324-
TLSServer: new(http.Server),
321+
filesystem: createFilesystem(),
322+
Server: new(http.Server),
323+
TLSServer: new(http.Server),
325324
AutoTLSManager: autocert.Manager{
326325
Prompt: autocert.AcceptTOS,
327326
},
@@ -500,50 +499,6 @@ func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middlew
500499
return routes
501500
}
502501

503-
// Static registers a new route with path prefix to serve static files from the
504-
// provided root directory.
505-
func (e *Echo) Static(prefix, root string) *Route {
506-
if root == "" {
507-
root = "." // For security we want to restrict to CWD.
508-
}
509-
return e.static(prefix, root, e.GET)
510-
}
511-
512-
func (common) static(prefix, root string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route) *Route {
513-
h := func(c Context) error {
514-
p, err := url.PathUnescape(c.Param("*"))
515-
if err != nil {
516-
return err
517-
}
518-
519-
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
520-
fi, err := os.Stat(name)
521-
if err != nil {
522-
// The access path does not exist
523-
return NotFoundHandler(c)
524-
}
525-
526-
// If the request is for a directory and does not end with "/"
527-
p = c.Request().URL.Path // path must not be empty.
528-
if fi.IsDir() && p[len(p)-1] != '/' {
529-
// Redirect to ends with "/"
530-
return c.Redirect(http.StatusMovedPermanently, p+"/")
531-
}
532-
return c.File(name)
533-
}
534-
// Handle added routes based on trailing slash:
535-
// /prefix => exact route "/prefix" + any route "/prefix/*"
536-
// /prefix/ => only any route "/prefix/*"
537-
if prefix != "" {
538-
if prefix[len(prefix)-1] == '/' {
539-
// Only add any route for intentional trailing slash
540-
return get(prefix+"*", h)
541-
}
542-
get(prefix, h)
543-
}
544-
return get(prefix+"/*", h)
545-
}
546-
547502
func (common) file(path, file string, get func(string, HandlerFunc, ...MiddlewareFunc) *Route,
548503
m ...MiddlewareFunc) *Route {
549504
return get(path, func(c Context) error {

0 commit comments

Comments
 (0)