aboutsummaryrefslogtreecommitdiff
path: root/echo/static_file.go
blob: 258c8327252b4f8f7a18f85abed03c53ddb02995 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package echo

import (
	"crypto/sha256"
	"encoding/base64"
	"io"
	"io/fs"
	"net/http"
	"net/url"
	"path/filepath"

	"github.com/labstack/echo/v4"
)

type routeFunc func(string, echo.HandlerFunc, ...echo.MiddlewareFunc) *echo.Route

func StaticFSSha256Etags(get routeFunc, f fs.FS, prefix, root string, m ...echo.MiddlewareFunc) *echo.Route {
	return staticFS(get, f, prefix, root, true, m...)
}

func StaticFS(get routeFunc, f fs.FS, prefix, root string, m ...echo.MiddlewareFunc) *echo.Route {
	return staticFS(get, f, prefix, root, false, m...)
}

// TODO: This should support HEAD requests
func staticFS(get routeFunc, f fs.FS, prefix, root string, addEtags bool, m ...echo.MiddlewareFunc) *echo.Route {
	if root == "" {
		root = "." // For security we want to restrict to CWD.
	}

	h := func(c echo.Context) error {
		p, err := url.PathUnescape(c.Param("*"))
		if err != nil {
			return err
		}

		name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
		fp, err := f.Open(name)
		if err != nil {
			// The access path does not exist
			return echo.NotFoundHandler(c)
		}
		defer fp.Close()

		fi, err := fp.Stat()
		if err != nil {
			// The access path does not exist
			return echo.NotFoundHandler(c)
		}

		// If the request is for a directory and does not end with "/"
		p = c.Request().URL.Path // path must not be empty.
		if fi.IsDir() && p[len(p)-1] != '/' {
			// Redirect to ends with "/"
			// return c.Redirect(http.StatusMovedPermanently, p+"/")
			// TODO: Serve an index.html if there is one for this dir
			return echo.NotFoundHandler(c)
		}

		fs, ok := fp.(io.ReadSeeker)
		if !ok {
			c.Logger().Errorf("File %s is not a io.ReadSeeker", p)
			return echo.ErrInternalServerError
		}

		// Only do this if the consumer requests it since it could be expensive
		// for high traffic sites as it requires a full read and SHA256
		// computation
		// TODO: Cache this?
		if addEtags {
			h := sha256.New()
			if _, err := io.Copy(h, fs); err != nil {
				c.Logger().Errorf("Error checksumming file %s: %s", p, err)
				return echo.ErrInternalServerError
			}
			etag := base64.RawStdEncoding.EncodeToString(h.Sum(nil))
			c.Response().Header().Add("ETag", etag)

			if _, err := fs.Seek(0, io.SeekStart); err != nil {
				c.Logger().Errorf("Error seeking 0 in file %s: %s", p, err)
				return echo.ErrInternalServerError
			}
		}

		http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), fs)
		return nil
	}

	// Handle added routes based on trailing slash:
	// 	/prefix  => exact route "/prefix" + any route "/prefix/*"
	// 	/prefix/ => only any route "/prefix/*"
	if prefix != "" {
		if prefix[len(prefix)-1] == '/' {
			// Only add any route for intentional trailing slash
			return get(prefix+"*", h, m...)
		}
		get(prefix, h, m...)
	}

	return get(prefix+"/*", h, m...)
}