influxdb/static/static.go

215 lines
6.3 KiB
Go

//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)
}