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" 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) 41 42const html = ` 43<!DOCTYPE html> 44<html lang="en"> 45<head> 46 <meta charset="UTF-8"> 47 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 48 <meta http-equiv="X-UA-Compatible" content="ie=edge"> 49 <title>{{ .Name }}</title> 50 <style> 51 body { 52 font-family: Menlo, Consolas, monospace; 53 padding: 48px; 54 } 55 header { 56 padding: 4px 16px; 57 font-size: 24px; 58 } 59 ul { 60 list-style-type: none; 61 margin: 0; 62 padding: 20px 0 0 0; 63 display: flex; 64 flex-wrap: wrap; 65 } 66 li { 67 width: 300px; 68 padding: 16px; 69 } 70 li a { 71 display: block; 72 overflow: hidden; 73 white-space: nowrap; 74 text-overflow: ellipsis; 75 text-decoration: none; 76 transition: opacity 0.25s; 77 } 78 li span { 79 color: #707070; 80 font-size: 12px; 81 } 82 li a:hover { 83 opacity: 0.50; 84 } 85 .dir { 86 color: #E91E63; 87 } 88 .file { 89 color: #673AB7; 90 } 91 </style> 92</head> 93<body> 94 <header> 95 {{ .Name }} 96 </header> 97 <ul> 98 {{ range .Files }} 99 <li> 100 {{ if .Dir }} 101 {{ $name := print .Name "/" }} 102 <a class="dir" href="{{ $name }}">{{ $name }}</a> 103 {{ else }} 104 <a class="file" href="{{ .Name }}">{{ .Name }}</a> 105 <span>{{ .Size }}</span> 106 {{ end }} 107 </li> 108 {{ end }} 109 </ul> 110</body> 111</html> 112` 113 114var ( 115 // DefaultStaticConfig is the default Static middleware config. 116 DefaultStaticConfig = StaticConfig{ 117 Skipper: DefaultSkipper, 118 Index: "index.html", 119 } 120) 121 122// Static returns a Static middleware to serves static content from the provided 123// root directory. 124func Static(root string) echo.MiddlewareFunc { 125 c := DefaultStaticConfig 126 c.Root = root 127 return StaticWithConfig(c) 128} 129 130// StaticWithConfig returns a Static middleware with config. 131// See `Static()`. 132func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { 133 // Defaults 134 if config.Root == "" { 135 config.Root = "." // For security we want to restrict to CWD. 136 } 137 if config.Skipper == nil { 138 config.Skipper = DefaultStaticConfig.Skipper 139 } 140 if config.Index == "" { 141 config.Index = DefaultStaticConfig.Index 142 } 143 144 // Index template 145 t, err := template.New("index").Parse(html) 146 if err != nil { 147 panic(fmt.Sprintf("echo: %v", err)) 148 } 149 150 return func(next echo.HandlerFunc) echo.HandlerFunc { 151 return func(c echo.Context) (err error) { 152 if config.Skipper(c) { 153 return next(c) 154 } 155 156 p := c.Request().URL.Path 157 if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`. 158 p = c.Param("*") 159 } 160 p, err = url.PathUnescape(p) 161 if err != nil { 162 return 163 } 164 name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security 165 166 fi, err := os.Stat(name) 167 if err != nil { 168 if os.IsNotExist(err) { 169 if err = next(c); err != nil { 170 if he, ok := err.(*echo.HTTPError); ok { 171 if config.HTML5 && he.Code == http.StatusNotFound { 172 return c.File(filepath.Join(config.Root, config.Index)) 173 } 174 } 175 return 176 } 177 } 178 return 179 } 180 181 if fi.IsDir() { 182 index := filepath.Join(name, config.Index) 183 fi, err = os.Stat(index) 184 185 if err != nil { 186 if config.Browse { 187 return listDir(t, name, c.Response()) 188 } 189 if os.IsNotExist(err) { 190 return next(c) 191 } 192 return 193 } 194 195 return c.File(index) 196 } 197 198 return c.File(name) 199 } 200 } 201} 202 203func listDir(t *template.Template, name string, res *echo.Response) (err error) { 204 file, err := os.Open(name) 205 if err != nil { 206 return 207 } 208 files, err := file.Readdir(-1) 209 if err != nil { 210 return 211 } 212 213 // Create directory index 214 res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) 215 data := struct { 216 Name string 217 Files []interface{} 218 }{ 219 Name: name, 220 } 221 for _, f := range files { 222 data.Files = append(data.Files, struct { 223 Name string 224 Dir bool 225 Size string 226 }{f.Name(), f.IsDir(), bytes.Format(f.Size())}) 227 } 228 return t.Execute(res, data) 229} 230