-
Notifications
You must be signed in to change notification settings - Fork 326
/
file_server.go
152 lines (123 loc) · 3.12 KB
/
file_server.go
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package http
import (
"io/fs"
"net/http"
"path"
"strings"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type (
// special file-server meant to be used for serving single page applications
// or anything that needs a bit of extra attention like fallback handling
// 404 handler, error handler and URL prefixing
fileServer struct {
// fileServer files
files http.FileSystem
urlPrefix string
fallbacks []string
// Final not-found handler
notFound http.HandlerFunc
// how errors are handled
errHandler func(w http.ResponseWriter, error string, code int)
logger *zap.Logger
}
configurator func(*fileServer) error
)
// MountSPA helper function, preconfigures FileServer for SPA serving
// and mounts it to chi Router
func MountSPA(r chi.Router, path string, root fs.FS, cc ...configurator) error {
path = "/" + strings.Trim(strings.TrimRight(path, "*"), "/") + "/"
cc = append(
[]configurator{UrlPrefix(path), Fallbacks("index.html")},
// appnd all configurators at the end and allow override of prefix & fallbacks
cc...
)
handler, err := FileServer(root, cc...)
if err != nil {
return err
}
r.Handle(
strings.TrimRight(path, "/"),
http.RedirectHandler("."+path, http.StatusTemporaryRedirect),
)
r.Handle(path+"*", handler)
return nil
}
func FileServer(files fs.FS, cc ...configurator) (h *fileServer, err error) {
h = &fileServer{
files: http.FS(files),
notFound: http.NotFound,
errHandler: http.Error,
logger: zap.NewNop(),
}
for _, configure := range cc {
if err = configure(h); err != nil {
return
}
}
return
}
func UrlPrefix(prefix string) configurator {
return func(s *fileServer) error {
s.urlPrefix = prefix
return nil
}
}
func Fallbacks(ff ...string) configurator {
return func(s *fileServer) error {
s.fallbacks = ff
return nil
}
}
func NotFound(h http.HandlerFunc) configurator {
return func(s *fileServer) error {
s.notFound = h
return nil
}
}
func Logger(l *zap.Logger) configurator {
return func(s *fileServer) error {
s.logger = l
return nil
}
}
// Serves the single-page-application
//
// This is file-server with some special logic for handling missing
// files (404s) and directories.
// In both cases we serve index file directly
func (h *fileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// catch requests for non-existing files and redirect to index.html
if h.files == nil {
h.errHandler(w, "configured without files", http.StatusInternalServerError)
return
}
trimmed := path.Clean(strings.TrimPrefix(r.URL.Path, h.urlPrefix))
h.logger.Debug(r.URL.Path, zap.String("trimmed", trimmed), zap.String("urlPrefix", h.urlPrefix))
r.URL.Path = trimmed
var (
err error
fh http.File
st fs.FileInfo
)
for _, candidate := range append([]string{r.URL.Path}, h.fallbacks...) {
if len(candidate) == 0 {
continue
}
if fh, err = h.files.Open(candidate); err != nil {
continue
} else if st, err = fh.Stat(); err != nil {
continue
} else if st.IsDir() {
// index
continue
}
break
}
if fh == nil || st == nil {
h.notFound(w, r)
return
}
http.ServeContent(w, r, st.Name(), st.ModTime(), fh)
}