diff --git a/README.md b/README.md index b4f99e82..1d4b8739 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ Middleware | Description [Gzip](https://labstack.com/echo/guide/middleware/#gzip-middleware:37ab2f15ff048f67959bcac0a6032f32) | Send gzip HTTP response [BasicAuth](https://labstack.com/echo/guide/middleware/#basicauth-middleware:37ab2f15ff048f67959bcac0a6032f32) | HTTP basic authentication [CORS](https://labstack.com/echo/guide/middleware/#cors-middleware:37ab2f15ff048f67959bcac0a6032f32) | Cross-Origin Resource Sharing +[Static](https://labstack.com/echo/guide/static-files/#using-static-middleware:123f9d1043075fe4874616541b409e4d) | Serve static files [AddTrailingSlash](https://labstack.com/echo/guide/middleware/#addtrailingslash-middleware:37ab2f15ff048f67959bcac0a6032f32) | Add trailing slash to the request URI [RemoveTrailingSlash](https://labstack.com/echo/guide/middleware/#removetrailingslash-middleware:37ab2f15ff048f67959bcac0a6032f32) | Remove trailing slash from the request URI diff --git a/context.go b/context.go index c58309c3..b5c8f789 100644 --- a/context.go +++ b/context.go @@ -8,7 +8,6 @@ import ( "mime/multipart" "net/http" "os" - "path" "path/filepath" "time" @@ -367,12 +366,14 @@ func (c *context) File(file string) error { fi, _ := f.Stat() if fi.IsDir() { - file = path.Join(file, "index.html") + file = filepath.Join(file, "index.html") f, err = os.Open(file) if err != nil { return ErrNotFound } - fi, _ = f.Stat() + if fi, err = f.Stat(); err != nil { + return err + } } return c.ServeContent(f, fi.Name(), fi.ModTime()) } diff --git a/echo.go b/echo.go index 1196d07d..866f713d 100644 --- a/echo.go +++ b/echo.go @@ -45,6 +45,7 @@ import ( "fmt" "io" "net/http" + "path" "reflect" "runtime" "strings" @@ -374,19 +375,14 @@ func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middlew } } -// Static serves static files from provided `root` directory for `/*` URL. -func (e *Echo) Static(path, root string) { - e.StaticWithConfig(path, StaticConfig{ - Root: root, +// Static serves static files from provided root directory with URL path prefix. +func (e *Echo) Static(prefix, root string) { + e.Get(prefix+"*", func(c Context) error { + return c.File(path.Join(root, c.P(0))) }) } -// StaticWithConfig serves static files with provided config for `/*` URL. -func (e *Echo) StaticWithConfig(path string, config StaticConfig) { - e.Get(path+"*", StaticWithConfig(config)) -} - -// File serve provided file for `/` HTTP path. +// File serves provided file for URL path. func (e *Echo) File(path, file string) { e.Get(path, func(c Context) error { return c.File(file) diff --git a/group.go b/group.go index c2ec575e..5c615939 100644 --- a/group.go +++ b/group.go @@ -5,9 +5,10 @@ type ( // routes that share a common middlware or functionality that should be separate // from the parent echo instance while still inheriting from it. Group struct { - prefix string - middleware []MiddlewareFunc - echo *Echo + prefix string + middleware []MiddlewareFunc + initialized bool + echo *Echo } ) @@ -82,26 +83,24 @@ func (g *Group) Group(prefix string, m ...MiddlewareFunc) *Group { } // Static implements `Echo#Static()` for sub-routes within the Group. -func (g *Group) Static(path, root string) { - g.StaticWithConfig(path, StaticConfig{ - Root: root, - }) -} - -// StaticWithConfig implements `Echo#StaticWithConfig()` for sub-routes within the -// Group. -func (g *Group) StaticWithConfig(path string, config StaticConfig) { - g.Get(path+"*", StaticWithConfig(config)) +func (g *Group) Static(prefix, root string) { + g.echo.Static(g.prefix+prefix, root) } // File implements `Echo#File()` for sub-routes within the Group. func (g *Group) File(path, file string) { - g.Get(path, func(c Context) error { - return c.File(file) - }) + g.echo.File(g.prefix+path, file) } func (g *Group) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { middleware = append(g.middleware, middleware...) + if !g.initialized { + // Allow all requests to reach the group as they might get dropped if router + // doesn't find a match, making none of the group middleware process. + g.echo.Any(g.prefix+"*", func(c Context) error { + return ErrNotFound + }, middleware...) + g.initialized = true + } g.echo.add(method, g.prefix+path, handler, middleware...) } diff --git a/middleware/static.go b/middleware/static.go new file mode 100644 index 00000000..7916fc42 --- /dev/null +++ b/middleware/static.go @@ -0,0 +1,119 @@ +package middleware + +import ( + "fmt" + "net/http" + "path" + "strings" + + "github.com/labstack/echo" +) + +type ( + // StaticConfig defines the config for static middleware. + StaticConfig struct { + // Root is the directory from where the static content is served. + // Required. + Root string `json:"root"` + + // Index is the list of index files to be searched and used when serving + // a directory. + // Optional with default value as []string{"index.html"}. + Index []string `json:"index"` + + // Browse is a flag to enable/disable directory browsing. + // Optional with default value as false. + Browse bool `json:"browse"` + } +) + +var ( + // DefaultStaticConfig is the default static middleware config. + DefaultStaticConfig = StaticConfig{ + Index: []string{"index.html"}, + Browse: false, + } +) + +// Static returns a static middleware to serves static content from the provided +// root directory. +func Static(root string) echo.MiddlewareFunc { + c := DefaultStaticConfig + c.Root = root + return StaticWithConfig(c) +} + +// StaticWithConfig returns a static middleware from config. +// See `Static()`. +func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { + // Defaults + if config.Index == nil { + config.Index = DefaultStaticConfig.Index + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + fs := http.Dir(config.Root) + p := c.Request().URL().Path() + if strings.Contains(c.Path(), "*") { // If serving from a group, e.g. `/static*`. + p = c.P(0) + } + file := path.Clean(p) + f, err := fs.Open(file) + if err != nil { + return next(c) + } + defer f.Close() + fi, err := f.Stat() + if err != nil { + return err + } + + if fi.IsDir() { + /* NOTE: + Not checking the Last-Modified header as it caches the response `304` when + changing different directories for the same path. + */ + d := f + + // Index file + // TODO: search all files + file = path.Join(file, config.Index[0]) + f, err = fs.Open(file) + if err != nil { + return next(c) + } + if config.Browse { + dirs, err := d.Readdir(-1) + if err != nil { + return err + } + + // Create a directory index + rs := c.Response() + rs.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) + if _, err = fmt.Fprintf(rs, "
\n"); err != nil {
+						return err
+					}
+					for _, d := range dirs {
+						name := d.Name()
+						color := "#212121"
+						if d.IsDir() {
+							color = "#e91e63"
+							name += "/"
+						}
+						if _, err = fmt.Fprintf(rs, "%s\n", name, color, name); err != nil {
+							return err
+						}
+					}
+					_, err = fmt.Fprintf(rs, "
\n") + return err + } + if fi, err = f.Stat(); err != nil { // Index file + return err + } + } + return c.ServeContent(f, fi.Name(), fi.ModTime()) + } + } +} diff --git a/static.go b/static.go deleted file mode 100644 index 10334f9b..00000000 --- a/static.go +++ /dev/null @@ -1,110 +0,0 @@ -package echo - -import ( - "fmt" - "net/http" - "path" -) - -type ( - // StaticConfig defines the config for static handler. - StaticConfig struct { - // Root is the directory from where the static content is served. - // Required. - Root string `json:"root"` - - // Index is the list of index files to be searched and used when serving - // a directory. - // Optional with default value as []string{"index.html"}. - Index []string `json:"index"` - - // Browse is a flag to enable/disable directory browsing. - // Optional with default value as false. - Browse bool `json:"browse"` - } -) - -var ( - // DefaultStaticConfig is the default static handler config. - DefaultStaticConfig = StaticConfig{ - Index: []string{"index.html"}, - Browse: false, - } -) - -// Static returns a static handler to serves static content from the provided -// root directory. -func Static(root string) HandlerFunc { - c := DefaultStaticConfig - c.Root = root - return StaticWithConfig(c) -} - -// StaticWithConfig returns a static handler from config. -// See `Static()`. -func StaticWithConfig(config StaticConfig) HandlerFunc { - // Defaults - if config.Index == nil { - config.Index = DefaultStaticConfig.Index - } - - return func(c Context) error { - fs := http.Dir(config.Root) - file := path.Clean(c.P(0)) - f, err := fs.Open(file) - if err != nil { - return ErrNotFound - } - defer f.Close() - fi, err := f.Stat() - if err != nil { - return ErrNotFound - } - - if fi.IsDir() { - /* NOTE: - Not checking the Last-Modified header as it caches the response `304` when - changing different directories for the same path. - */ - d := f - - // Index file - // TODO: search all files - file = path.Join(file, config.Index[0]) - f, err = fs.Open(file) - if err != nil { - return ErrNotFound - } - if config.Browse { - dirs, err := d.Readdir(-1) - if err != nil { - return err - } - - // Create a directory index - rs := c.Response() - rs.Header().Set(HeaderContentType, MIMETextHTMLCharsetUTF8) - if _, err = fmt.Fprintf(rs, "
\n"); err != nil {
-					return err
-				}
-				for _, d := range dirs {
-					name := d.Name()
-					color := "#212121"
-					if d.IsDir() {
-						color = "#e91e63"
-						name += "/"
-					}
-					if _, err = fmt.Fprintf(rs, "%s\n", name, color, name); err != nil {
-						return err
-					}
-				}
-				_, err = fmt.Fprintf(rs, "
\n") - return err - } - if fi, err = f.Stat(); err != nil { // Index file - return ErrNotFound - } - } - return c.ServeContent(f, fi.Name(), fi.ModTime()) - } -}