Skip to content

Commit b643e68

Browse files
Fix #1787: Add support for optional filesystem to the static middleware (#1797)
* Add optional filesystem to static middleware.
1 parent de3f87e commit b643e68

File tree

3 files changed

+191
-24
lines changed

3 files changed

+191
-24
lines changed

middleware/static.go

+56-24
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ type (
4242
// the filesystem path is not doubled
4343
// Optional. Default value false.
4444
IgnoreBase bool `yaml:"ignoreBase"`
45+
46+
// Filesystem provides access to the static content.
47+
// Optional. Defaults to http.Dir(config.Root)
48+
Filesystem http.FileSystem `yaml:"-"`
4549
}
4650
)
4751

@@ -146,6 +150,10 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
146150
if config.Index == "" {
147151
config.Index = DefaultStaticConfig.Index
148152
}
153+
if config.Filesystem == nil {
154+
config.Filesystem = http.Dir(config.Root)
155+
config.Root = "."
156+
}
149157

150158
// Index template
151159
t, err := template.New("index").Parse(html)
@@ -178,49 +186,73 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
178186
}
179187
}
180188

181-
fi, err := os.Stat(name)
189+
file, err := openFile(config.Filesystem, name)
182190
if err != nil {
183-
if os.IsNotExist(err) {
184-
if err = next(c); err != nil {
185-
if he, ok := err.(*echo.HTTPError); ok {
186-
if config.HTML5 && he.Code == http.StatusNotFound {
187-
return c.File(filepath.Join(config.Root, config.Index))
188-
}
189-
}
190-
return
191-
}
191+
if !os.IsNotExist(err) {
192+
return err
193+
}
194+
195+
if err = next(c); err == nil {
196+
return err
197+
}
198+
199+
he, ok := err.(*echo.HTTPError)
200+
if !(ok && config.HTML5 && he.Code == http.StatusNotFound) {
201+
return err
202+
}
203+
204+
file, err = openFile(config.Filesystem, filepath.Join(config.Root, config.Index))
205+
if err != nil {
206+
return err
192207
}
193-
return
194208
}
195209

196-
if fi.IsDir() {
197-
index := filepath.Join(name, config.Index)
198-
fi, err = os.Stat(index)
210+
defer file.Close()
211+
212+
info, err := file.Stat()
213+
if err != nil {
214+
return err
215+
}
199216

217+
if info.IsDir() {
218+
index, err := openFile(config.Filesystem, filepath.Join(name, config.Index))
200219
if err != nil {
201220
if config.Browse {
202-
return listDir(t, name, c.Response())
221+
return listDir(t, name, file, c.Response())
203222
}
223+
204224
if os.IsNotExist(err) {
205225
return next(c)
206226
}
207-
return
208227
}
209228

210-
return c.File(index)
229+
defer index.Close()
230+
231+
info, err = index.Stat()
232+
if err != nil {
233+
return err
234+
}
235+
236+
return serveFile(c, index, info)
211237
}
212238

213-
return c.File(name)
239+
return serveFile(c, file, info)
214240
}
215241
}
216242
}
217243

218-
func listDir(t *template.Template, name string, res *echo.Response) (err error) {
219-
file, err := os.Open(name)
220-
if err != nil {
221-
return
222-
}
223-
files, err := file.Readdir(-1)
244+
func openFile(fs http.FileSystem, name string) (http.File, error) {
245+
pathWithSlashes := filepath.ToSlash(name)
246+
return fs.Open(pathWithSlashes)
247+
}
248+
249+
func serveFile(c echo.Context, file http.File, info os.FileInfo) error {
250+
http.ServeContent(c.Response(), c.Request(), info.Name(), info.ModTime(), file)
251+
return nil
252+
}
253+
254+
func listDir(t *template.Template, name string, dir http.File, res *echo.Response) (err error) {
255+
files, err := dir.Readdir(-1)
224256
if err != nil {
225257
return
226258
}

middleware/static_1_16_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// +build go1.16
2+
3+
package middleware
4+
5+
import (
6+
"io/fs"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"testing"
11+
"testing/fstest"
12+
13+
"github.com/labstack/echo/v4"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestStatic_CustomFS(t *testing.T) {
18+
var testCases = []struct {
19+
name string
20+
filesystem fs.FS
21+
root string
22+
whenURL string
23+
expectContains string
24+
expectCode int
25+
}{
26+
{
27+
name: "ok, serve index with Echo message",
28+
whenURL: "/",
29+
filesystem: os.DirFS("../_fixture"),
30+
expectCode: http.StatusOK,
31+
expectContains: "<title>Echo</title>",
32+
},
33+
34+
{
35+
name: "ok, serve index with Echo message",
36+
whenURL: "/_fixture/",
37+
filesystem: os.DirFS(".."),
38+
expectCode: http.StatusOK,
39+
expectContains: "<title>Echo</title>",
40+
},
41+
{
42+
name: "ok, serve file from map fs",
43+
whenURL: "/file.txt",
44+
filesystem: fstest.MapFS{
45+
"file.txt": &fstest.MapFile{Data: []byte("file.txt is ok")},
46+
},
47+
expectCode: http.StatusOK,
48+
expectContains: "file.txt is ok",
49+
},
50+
{
51+
name: "nok, missing file in map fs",
52+
whenURL: "/file.txt",
53+
expectCode: http.StatusNotFound,
54+
filesystem: fstest.MapFS{
55+
"file2.txt": &fstest.MapFile{Data: []byte("file2.txt is ok")},
56+
},
57+
},
58+
{
59+
name: "nok, file is not a subpath of root",
60+
whenURL: `/../../secret.txt`,
61+
root: "/nested/folder",
62+
filesystem: fstest.MapFS{
63+
"secret.txt": &fstest.MapFile{Data: []byte("this is a secret")},
64+
},
65+
expectCode: http.StatusNotFound,
66+
},
67+
{
68+
name: "nok, backslash is forbidden",
69+
whenURL: `/..\..\secret.txt`,
70+
expectCode: http.StatusNotFound,
71+
root: "/nested/folder",
72+
filesystem: fstest.MapFS{
73+
"secret.txt": &fstest.MapFile{Data: []byte("this is a secret")},
74+
},
75+
},
76+
}
77+
78+
for _, tc := range testCases {
79+
t.Run(tc.name, func(t *testing.T) {
80+
e := echo.New()
81+
82+
config := StaticConfig{
83+
Root: ".",
84+
Filesystem: http.FS(tc.filesystem),
85+
}
86+
87+
if tc.root != "" {
88+
config.Root = tc.root
89+
}
90+
91+
middlewareFunc := StaticWithConfig(config)
92+
e.Use(middlewareFunc)
93+
94+
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
95+
rec := httptest.NewRecorder()
96+
97+
e.ServeHTTP(rec, req)
98+
99+
assert.Equal(t, tc.expectCode, rec.Code)
100+
if tc.expectContains != "" {
101+
responseBody := rec.Body.String()
102+
assert.Contains(t, responseBody, tc.expectContains)
103+
}
104+
})
105+
}
106+
}

middleware/static_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,32 @@ func TestStatic(t *testing.T) {
9494
expectCode: http.StatusNotFound,
9595
expectContains: "{\"message\":\"Not Found\"}\n",
9696
},
97+
{
98+
name: "ok, do not serve file, when a handler took care of the request",
99+
whenURL: "/regular-handler",
100+
expectCode: http.StatusOK,
101+
expectContains: "ok",
102+
},
103+
{
104+
name: "nok, when html5 fail if the index file does not exist",
105+
givenConfig: &StaticConfig{
106+
Root: "../_fixture",
107+
HTML5: true,
108+
Index: "missing.html",
109+
},
110+
whenURL: "/random",
111+
expectCode: http.StatusInternalServerError,
112+
},
113+
{
114+
name: "ok, serve from http.FileSystem",
115+
givenConfig: &StaticConfig{
116+
Root: "_fixture",
117+
Filesystem: http.Dir(".."),
118+
},
119+
whenURL: "/",
120+
expectCode: http.StatusOK,
121+
expectContains: "<title>Echo</title>",
122+
},
97123
}
98124

99125
for _, tc := range testCases {
@@ -115,6 +141,9 @@ func TestStatic(t *testing.T) {
115141
} else {
116142
// middleware is on root level
117143
e.Use(middlewareFunc)
144+
e.GET("/regular-handler", func(c echo.Context) error {
145+
return c.String(http.StatusOK, "ok")
146+
})
118147
}
119148

120149
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)

0 commit comments

Comments
 (0)