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