diff --git a/_fixture/dist/private.txt b/_fixture/dist/private.txt
new file mode 100644
index 000000000..0f9d2435b
--- /dev/null
+++ b/_fixture/dist/private.txt
@@ -0,0 +1 @@
+private file
diff --git a/_fixture/dist/public/assets/readme.md b/_fixture/dist/public/assets/readme.md
new file mode 100644
index 000000000..50590f554
--- /dev/null
+++ b/_fixture/dist/public/assets/readme.md
@@ -0,0 +1 @@
+readme in assets
diff --git a/_fixture/dist/public/assets/subfolder/subfolder.md b/_fixture/dist/public/assets/subfolder/subfolder.md
new file mode 100644
index 000000000..74c928b2f
--- /dev/null
+++ b/_fixture/dist/public/assets/subfolder/subfolder.md
@@ -0,0 +1 @@
+file inside subfolder
diff --git a/_fixture/dist/public/index.html b/_fixture/dist/public/index.html
new file mode 100644
index 000000000..df6d9015a
--- /dev/null
+++ b/_fixture/dist/public/index.html
@@ -0,0 +1 @@
+
Hello from index
diff --git a/middleware/static.go b/middleware/static.go
index 77cbb4edb..ee1c8bee9 100644
--- a/middleware/static.go
+++ b/middleware/static.go
@@ -118,12 +118,13 @@ const directoryListHTMLTemplate = `
{{ range .Files }}
+ {{ $href := .Name }}{{ if ne $.Name "/" }}{{ $href = print $.Name "/" .Name }}{{ end }}
-
{{ if .Dir }}
{{ $name := print .Name "/" }}
- {{ $name }}
+ {{ $name }}
{{ else }}
- {{ .Name }}
+ {{ .Name }}
{{ .Size }}
{{ end }}
@@ -196,14 +197,15 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
// 3. The "/" prefix forces absolute path interpretation, removing ".." components
// 4. Backslashes are treated as literal characters (not path separators), preventing traversal
// See static_windows.go for Go 1.20+ filepath.Clean compatibility notes
- name := path.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
+ requestedPath := path.Clean("/" + p) // "/"+ for security
+ filePath := path.Join(config.Root, requestedPath)
if config.IgnoreBase {
routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
baseURLPath := path.Base(p)
if baseURLPath == routePath {
- i := strings.LastIndex(name, routePath)
- name = name[:i] + strings.Replace(name[i:], routePath, "", 1)
+ i := strings.LastIndex(filePath, routePath)
+ filePath = filePath[:i] + strings.Replace(filePath[i:], routePath, "", 1)
}
}
@@ -212,7 +214,7 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
currentFS = c.Echo().Filesystem
}
- file, err := currentFS.Open(name)
+ file, err := currentFS.Open(filePath)
if err != nil {
if !isIgnorableOpenFileError(err) {
return err
@@ -243,10 +245,10 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
}
if info.IsDir() {
- index, err := currentFS.Open(path.Join(name, config.Index))
+ index, err := currentFS.Open(path.Join(filePath, config.Index))
if err != nil {
if config.Browse {
- return listDir(dirListTemplate, name, currentFS, c.Response())
+ return listDir(dirListTemplate, requestedPath, filePath, currentFS, c.Response())
}
return next(c)
@@ -276,34 +278,36 @@ func serveFile(c *echo.Context, file fs.File, info os.FileInfo) error {
return nil
}
-func listDir(t *template.Template, name string, filesystem fs.FS, res http.ResponseWriter) error {
+func listDir(t *template.Template, requestedPath string, pathInFs string, filesystem fs.FS, res http.ResponseWriter) error {
+ files, err := fs.ReadDir(filesystem, pathInFs)
+ if err != nil {
+ return fmt.Errorf("static middleware failed to read directory for listing: %w", err)
+ }
+
// Create directory index
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
data := struct {
Name string
Files []any
}{
- Name: name,
+ Name: requestedPath,
}
- err := fs.WalkDir(filesystem, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- info, infoErr := d.Info()
- if infoErr != nil {
- return fmt.Errorf("static middleware list dir error when getting file info: %w", infoErr)
+ for _, f := range files {
+ var size int64
+ if !f.IsDir() {
+ info, err := f.Info()
+ if err != nil {
+ return fmt.Errorf("static middleware failed to get file info for listing: %w", err)
+ }
+ size = info.Size()
}
+
data.Files = append(data.Files, struct {
Name string
Dir bool
Size string
- }{d.Name(), d.IsDir(), format(info.Size())})
-
- return nil
- })
- if err != nil {
- return err
+ }{f.Name(), f.IsDir(), format(size)})
}
return t.Execute(res, data)
diff --git a/middleware/static_test.go b/middleware/static_test.go
index 15d62875f..d0f323531 100644
--- a/middleware/static_test.go
+++ b/middleware/static_test.go
@@ -577,3 +577,67 @@ func TestStatic_CustomFS(t *testing.T) {
})
}
}
+
+func TestStatic_DirectoryBrowsing(t *testing.T) {
+ var testCases = []struct {
+ name string
+ givenConfig StaticConfig
+ whenURL string
+ expectContains string
+ expectNotContains []string
+ expectCode int
+ }{
+ {
+ name: "ok, should return index.html contents from Root=public folder",
+ givenConfig: StaticConfig{
+ Root: "public",
+ Filesystem: os.DirFS("../_fixture/dist"),
+ Browse: true,
+ },
+ whenURL: "/",
+ expectCode: http.StatusOK,
+ expectContains: `Hello from index
`,
+ },
+ {
+ name: "ok, should return only subfolder folder listing from Root=public/assets",
+ givenConfig: StaticConfig{
+ Root: "public",
+ Filesystem: os.DirFS("../_fixture/dist"),
+ Browse: true,
+ },
+ whenURL: "/assets",
+ expectCode: http.StatusOK,
+ expectContains: `readme.md`,
+ expectNotContains: []string{
+ `Hello from index
`, // should see the listing, not index.html contents
+ `private.txt`, // file from the parent folder
+ `subfolder.md`, // file from subfolder
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ e := echo.New()
+
+ middlewareFunc, err := tc.givenConfig.ToMiddleware()
+ assert.NoError(t, err)
+
+ e.Use(middlewareFunc)
+
+ req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
+ rec := httptest.NewRecorder()
+
+ e.ServeHTTP(rec, req)
+
+ assert.Equal(t, tc.expectCode, rec.Code)
+
+ responseBody := rec.Body.String()
+ if tc.expectContains != "" {
+ assert.Contains(t, responseBody, tc.expectContains, "body should contain: "+tc.expectContains)
+ }
+ for _, notContains := range tc.expectNotContains {
+ assert.NotContains(t, responseBody, notContains, "body should NOT contain: "+notContains)
+ }
+ })
+ }
+}