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