static.go 4.6 KB

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