forked from influxdata/influxdb
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathstatic.go
214 lines (179 loc) · 6.3 KB
/
static.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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//go:generate env GO111MODULE=on go run github.com/kevinburke/go-bindata/go-bindata -o static_gen.go -ignore 'map|go' -tags assets -pkg static data/...
package static
import (
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
assetfs "github.com/elazarl/go-bindata-assetfs"
platform "github.com/influxdata/influxdb/v2"
)
const (
// defaultFile is the default UI asset file that will be served if no other
// static asset matches. This is particularly useful for serving content
// related to a SPA with client-side routing.
defaultFile = "index.html"
// embedBaseDir is the prefix for files in the bundle with the binary.
embedBaseDir = "data"
// uiBaseDir is the directory in embedBaseDir where the built UI assets
// reside.
uiBaseDir = "build"
// swaggerFile is the name of the swagger JSON.
swaggerFile = "swagger.json"
// fallbackPathSlug is the path to re-write on the request if the requested
// path does not match a file and the default file is served. For telemetry
// and metrics reporting purposes.
fallbackPathSlug = "/:fallback_path"
)
// NewAssetHandler returns an http.Handler to serve files from the provided
// path. If no --assets-path flag is used when starting influxd, the path will
// be empty and files are served from the embedded filesystem.
func NewAssetHandler(assetsPath string) http.Handler {
var fileOpener http.FileSystem
if assetsPath == "" {
fileOpener = &assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
AssetInfo: AssetInfo,
Prefix: filepath.Join(embedBaseDir, uiBaseDir),
}
} else {
fileOpener = http.FS(os.DirFS(assetsPath))
}
return mwSetCacheControl(assetHandler(fileOpener))
}
// NewSwaggerHandler returns an http.Handler to serve the swaggerFile from the
// embedBaseDir. If the swaggerFile is not found, returns a 404.
func NewSwaggerHandler() http.Handler {
fileOpener := &assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
AssetInfo: AssetInfo,
Prefix: embedBaseDir,
}
return mwSetCacheControl(swaggerHandler(fileOpener))
}
// mwSetCacheControl sets a default cache control header.
func mwSetCacheControl(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=3600")
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// swaggerHandler returns a handler that serves the swaggerFile or returns a 404
// if the swaggerFile is not present.
func swaggerHandler(fileOpener http.FileSystem) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
f, err := fileOpener.Open(swaggerFile)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
defer f.Close()
staticFileHandler(f).ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// assetHandler returns a handler that either serves the file at that path, or
// the default file if a file cannot be found at that path. If the default file
// is served, the request path is re-written to the root path to simplify
// metrics reporting.
func assetHandler(fileOpener http.FileSystem) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
// If the root directory is being requested, respond with the default file.
if name == "" {
name = defaultFile
r.URL.Path = "/" + defaultFile
}
// Try to open the file requested by name, falling back to the default file.
// If even the default file can't be found, the binary must not have been
// built with assets, so respond with not found.
f, fallback, err := openAsset(fileOpener, name)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
defer f.Close()
// If the default file will be served because the requested path didn't
// match any existing files, re-write the request path a placeholder value.
// This is to ensure that metrics do not get collected for an arbitrarily
// large range of incorrect paths.
if fallback {
r.URL.Path = fallbackPathSlug
}
staticFileHandler(f).ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// staticFileHandler sets the ETag header prior to calling http.ServeContent
// with the contents of the file.
func staticFileHandler(f fs.File) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
content, ok := f.(io.ReadSeeker)
if !ok {
err := fmt.Errorf("could not open file for reading")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
i, err := f.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
modTime, err := modTimeFromInfo(i, buildTime)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("ETag", etag(i.Size(), modTime))
// ServeContent will automatically set the content-type header for files
// from the extension of "name", and will also set the Last-Modified header
// from the provided time.
http.ServeContent(w, r, i.Name(), modTime, content)
}
return http.HandlerFunc(fn)
}
// openAsset attempts to open the asset by name in the given directory, falling
// back to the default file if the named asset can't be found. Returns an error
// if even the default asset can't be opened.
func openAsset(fileOpener http.FileSystem, name string) (fs.File, bool, error) {
var fallback bool
f, err := fileOpener.Open(name)
if err != nil {
if os.IsNotExist(err) {
fallback = true
f, err = fileOpener.Open(defaultFile)
}
if err != nil {
return nil, fallback, err
}
}
return f, fallback, nil
}
// modTimeFromInfo gets the modification time from an fs.FileInfo. If this
// modification time is time.Time{}, it falls back to the time returned by
// timeFunc. The modification time will only be time.Time{} if using assets
// embedded with go:embed.
func modTimeFromInfo(i fs.FileInfo, timeFunc func() (time.Time, error)) (time.Time, error) {
modTime := i.ModTime()
if modTime.IsZero() {
return timeFunc()
}
return modTime, nil
}
// etag calculates an etag string from the provided file size and modification
// time.
func etag(s int64, mt time.Time) string {
hour, minute, second := mt.Clock()
return fmt.Sprintf(`"%d%d%d%d%d"`, s, mt.Day(), hour, minute, second)
}
func buildTime() (time.Time, error) {
return time.Parse(time.RFC3339, platform.GetBuildInfo().Date)
}