1package middleware 2 3import ( 4 "fmt" 5 "html/template" 6 "net/http" 7 "net/url" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 13 "github.com/labstack/echo/v4" 14 "github.com/labstack/gommon/bytes" 15) 16 17type ( 18 // StaticConfig defines the config for Static middleware. 19 StaticConfig struct { 20 // Skipper defines a function to skip middleware. 21 Skipper Skipper 22 23 // Root directory from where the static content is served. 24 // Required. 25 Root string `yaml:"root"` 26 27 // Index file for serving a directory. 28 // Optional. Default value "index.html". 29 Index string `yaml:"index"` 30 31 // Enable HTML5 mode by forwarding all not-found requests to root so that 32 // SPA (single-page application) can handle the routing. 33 // Optional. Default value false. 34 HTML5 bool `yaml:"html5"` 35 36 // Enable directory browsing. 37 // Optional. Default value false. 38 Browse bool `yaml:"browse"` 39 40 // Enable ignoring of the base of the URL path. 41 // Example: when assigning a static middleware to a non root path group, 42 // the filesystem path is not doubled 43 // Optional. Default value false. 44 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:"-"` 49 } 50) 51 52const html = ` 53<!DOCTYPE html> 54<html lang="en"> 55<head> 56 <meta charset="UTF-8"> 57 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 58 <meta http-equiv="X-UA-Compatible" content="ie=edge"> 59 <title>{{ .Name }}</title> 60 <style> 61 body { 62 font-family: Menlo, Consolas, monospace; 63 padding: 48px; 64 } 65 header { 66 padding: 4px 16px; 67 font-size: 24px; 68 } 69 ul { 70 list-style-type: none; 71 margin: 0; 72 padding: 20px 0 0 0; 73 display: flex; 74 flex-wrap: wrap; 75 } 76 li { 77 width: 300px; 78 padding: 16px; 79 } 80 li a { 81 display: block; 82 overflow: hidden; 83 white-space: nowrap; 84 text-overflow: ellipsis; 85 text-decoration: none; 86 transition: opacity 0.25s; 87 } 88 li span { 89 color: #707070; 90 font-size: 12px; 91 } 92 li a:hover { 93 opacity: 0.50; 94 } 95 .dir { 96 color: #E91E63; 97 } 98 .file { 99 color: #673AB7; 100 } 101 </style> 102</head> 103<body> 104 <header> 105 {{ .Name }} 106 </header> 107 <ul> 108 {{ range .Files }} 109 <li> 110 {{ if .Dir }} 111 {{ $name := print .Name "/" }} 112 <a class="dir" href="{{ $name }}">{{ $name }}</a> 113 {{ else }} 114 <a class="file" href="{{ .Name }}">{{ .Name }}</a> 115 <span>{{ .Size }}</span> 116 {{ end }} 117 </li> 118 {{ end }} 119 </ul> 120</body> 121</html> 122` 123 124var ( 125 // DefaultStaticConfig is the default Static middleware config. 126 DefaultStaticConfig = StaticConfig{ 127 Skipper: DefaultSkipper, 128 Index: "index.html", 129 } 130) 131 132// Static returns a Static middleware to serves static content from the provided 133// root directory. 134func Static(root string) echo.MiddlewareFunc { 135 c := DefaultStaticConfig 136 c.Root = root 137 return StaticWithConfig(c) 138} 139 140// StaticWithConfig returns a Static middleware with config. 141// See `Static()`. 142func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { 143 // Defaults 144 if config.Root == "" { 145 config.Root = "." // For security we want to restrict to CWD. 146 } 147 if config.Skipper == nil { 148 config.Skipper = DefaultStaticConfig.Skipper 149 } 150 if config.Index == "" { 151 config.Index = DefaultStaticConfig.Index 152 } 153 if config.Filesystem == nil { 154 config.Filesystem = http.Dir(config.Root) 155 config.Root = "." 156 } 157 158 // Index template 159 t, err := template.New("index").Parse(html) 160 if err != nil { 161 panic(fmt.Sprintf("echo: %v", err)) 162 } 163 164 return func(next echo.HandlerFunc) echo.HandlerFunc { 165 return func(c echo.Context) (err error) { 166 if config.Skipper(c) { 167 return next(c) 168 } 169 170 p := c.Request().URL.Path 171 if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`. 172 p = c.Param("*") 173 } 174 p, err = url.PathUnescape(p) 175 if err != nil { 176 return 177 } 178 name := filepath.Join(config.Root, filepath.Clean("/"+p)) // "/"+ for security 179 180 if config.IgnoreBase { 181 routePath := path.Base(strings.TrimRight(c.Path(), "/*")) 182 baseURLPath := path.Base(p) 183 if baseURLPath == routePath { 184 i := strings.LastIndex(name, routePath) 185 name = name[:i] + strings.Replace(name[i:], routePath, "", 1) 186 } 187 } 188 189 file, err := openFile(config.Filesystem, name) 190 if err != nil { 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 207 } 208 } 209 210 defer file.Close() 211 212 info, err := file.Stat() 213 if err != nil { 214 return err 215 } 216 217 if info.IsDir() { 218 index, err := openFile(config.Filesystem, filepath.Join(name, config.Index)) 219 if err != nil { 220 if config.Browse { 221 return listDir(t, name, file, c.Response()) 222 } 223 224 if os.IsNotExist(err) { 225 return next(c) 226 } 227 } 228 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) 237 } 238 239 return serveFile(c, file, info) 240 } 241 } 242} 243 244func openFile(fs http.FileSystem, name string) (http.File, error) { 245 pathWithSlashes := filepath.ToSlash(name) 246 return fs.Open(pathWithSlashes) 247} 248 249func 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 254func listDir(t *template.Template, name string, dir http.File, res *echo.Response) (err error) { 255 files, err := dir.Readdir(-1) 256 if err != nil { 257 return 258 } 259 260 // Create directory index 261 res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) 262 data := struct { 263 Name string 264 Files []interface{} 265 }{ 266 Name: name, 267 } 268 for _, f := range files { 269 data.Files = append(data.Files, struct { 270 Name string 271 Dir bool 272 Size string 273 }{f.Name(), f.IsDir(), bytes.Format(f.Size())}) 274 } 275 return t.Execute(res, data) 276} 277