From 993f4357c4d9c7396cabda31c81b89066925b6cc Mon Sep 17 00:00:00 2001 From: Pavel Zavora Date: Tue, 5 Apr 2022 12:54:02 +0200 Subject: [PATCH] fix(dist): load UI resources using embed --- Makefile | 6 +-- dist/dist.go | 95 +---------------------------------- server/assets.go | 13 +---- ui/dist.go | 126 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 109 deletions(-) create mode 100644 ui/dist.go diff --git a/Makefile b/Makefile index 4177f176c..72de1e0c7 100644 --- a/Makefile +++ b/Makefile @@ -58,12 +58,9 @@ docker: dep assets docker-${BINARY} assets: .jssrc .bindata -.bindata: server/swagger.json canned/*.json protoboards/*.json dist/dist_gen.go +.bindata: server/swagger.json canned/*.json protoboards/*.json $(UISOURCES) @touch .bindata -dist/dist_gen.go: $(UISOURCES) - go generate -x ./dist - .jssrc: $(UISOURCES) cd ui && yarn run clean && yarn run build @touch .jssrc @@ -142,7 +139,6 @@ clean: cd ui && yarn run clean rm -rf node_modules cd ui && rm -rf node_modules - rm -f dist/dist_gen.go @rm -f .godep .jsdep .jssrc .bindata ctags: diff --git a/dist/dist.go b/dist/dist.go index 7aefdad22..69d95d162 100644 --- a/dist/dist.go +++ b/dist/dist.go @@ -1,15 +1,9 @@ package dist -//go:generate go-bindata -o dist_gen.go -ignore '\.map|\.go' -pkg dist ../ui/build/... ../ui/package.json -//go:generate go fmt dist_gen.go - import ( - "fmt" "net/http" - "regexp" - "strings" - assetfs "github.com/elazarl/go-bindata-assetfs" + "github.com/influxdata/chronograf/ui" ) // DebugAssets serves assets via a specified directory @@ -23,92 +17,7 @@ func (d *DebugAssets) Handler() http.Handler { return http.FileServer(NewDir(d.Dir, d.Default)) } -// BindataAssets serves assets from go-bindata, but, also serves Default if assent doesn't exist -// This is to support single-page react-apps with its own router. -type BindataAssets struct { - Prefix string // Prefix is prepended to the http file request - Default string // Default is the file to serve if the file is not found - DefaultContentType string // DefaultContentType is the content type of the default file -} - -// Handler serves go-bindata using a go-bindata-assetfs façade -func (b *BindataAssets) Handler() http.Handler { - return b -} - -// addCacheHeaders requests an hour of Cache-Control and sets an ETag based on file size and modtime -func (b *BindataAssets) addCacheHeaders(filename string, w http.ResponseWriter) error { - w.Header().Add("Cache-Control", "public, max-age=3600") - - w.Header().Add("X-Frame-Options", "SAMEORIGIN") - w.Header().Add("X-XSS-Protection", "1; mode=block") - w.Header().Add("X-Content-Type-Options", "nosniff") - w.Header().Add("Content-Security-Policy", "script-src 'self'; object-src 'self'") - - fi, err := AssetInfo(filename) - if err != nil { - return err - } - - hour, minute, second := fi.ModTime().Clock() - etag := fmt.Sprintf(`"%d%d%d%d%d"`, fi.Size(), fi.ModTime().Day(), hour, minute, second) - - w.Header().Set("ETag", etag) - return nil -} - -// ServeHTTP wraps http.FileServer by returning a default asset if the asset -// doesn't exist. This supports single-page react-apps with its own -// built-in router. Additionally, we override the content-type if the -// Default file is used. -func (b *BindataAssets) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // def wraps the assets to return the default file if the file doesn't exist - def := func(name string) ([]byte, error) { - // If the named asset exists, then return it directly. - octets, err := Asset(name) - if err != nil { - // If this is at / then we just error out so we can return a Directory - // This directory will then be redirected by go to the /index.html - if name == b.Prefix { - return nil, err - } - // If this is anything other than slash, we just return the default - // asset. This default asset will handle the routing. - // Additionally, because we know we are returning the default asset, - // we need to set the default asset's content-type. - w.Header().Set("Content-Type", b.DefaultContentType) - if err := b.addCacheHeaders(b.Default, w); err != nil { - return nil, err - } - return Asset(b.Default) - } - if err := b.addCacheHeaders(name, w); err != nil { - return nil, err - } - // https://github.com/influxdata/chronograf/issues/5565 - // workaround wrong .js content-type on windows - if strings.HasSuffix(name, ".js") { - w.Header().Set("Content-Type", "text/javascript") - } - return octets, nil - } - var dir http.FileSystem = &assetfs.AssetFS{ - Asset: def, - AssetDir: AssetDir, - AssetInfo: AssetInfo, - Prefix: b.Prefix, - } - http.FileServer(dir).ServeHTTP(w, r) -} - -var re = regexp.MustCompile(`"version"\s*:\s*"(.*)"`) - // GetVersion returns version of the packed assets func GetVersion() string { - if data, err := Asset("../ui/package.json"); err == nil { - if matches := re.FindStringSubmatch(string(data)); matches != nil { - return matches[1] - } - } - return "" + return ui.GetVersion() } diff --git a/server/assets.go b/server/assets.go index cd5598358..5cd22ac48 100644 --- a/server/assets.go +++ b/server/assets.go @@ -5,19 +5,14 @@ import ( "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/dist" + "github.com/influxdata/chronograf/ui" ) const ( - // Dir is prefix of the assets in the bindata - Dir = "../ui/build" - // Default is the default item to load if 404 - Default = "../ui/build/index.html" // DebugDir is the prefix of the assets in development mode DebugDir = "ui/build" // DebugDefault is the default item to load if 404 DebugDefault = "ui/build/index.html" - // DefaultContentType is the content-type to return for the Default file - DefaultContentType = "text/html; charset=utf-8" ) // AssetsOpts configures the asset middleware @@ -37,11 +32,7 @@ func Assets(opts AssetsOpts) http.Handler { Default: DebugDefault, } } else { - assets = &dist.BindataAssets{ - Prefix: Dir, - Default: Default, - DefaultContentType: DefaultContentType, - } + assets = &ui.BindataAssets{} } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/ui/dist.go b/ui/dist.go new file mode 100644 index 000000000..a80e398d5 --- /dev/null +++ b/ui/dist.go @@ -0,0 +1,126 @@ +package ui + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "regexp" + "strings" +) + +const ( + // Default page to load (upon a miss) + DefaultPage = "index.html" + // DefaultPageContentType is the content-type of the DefaultPage + DefaultPageContentType = "text/html; charset=utf-8" +) + +//go:embed build/* +var embeddedFS embed.FS +var buildDir fs.FS + +//go:embed package.json +var packageJson string +var version string + +// init initializes version and buildDir file system +func init() { + // parse version + version = "" + re := regexp.MustCompile(`"version"\s*:\s*"(.*)"`) + if matches := re.FindStringSubmatch(packageJson); matches != nil { + version = matches[1] + } + // initialize buildDir and default file + var err error + if buildDir, err = fs.Sub(embeddedFS, "build"); err != nil { + panic("no ui/build directory found!") + } +} + +// BindataAssets serves embedded ui assets and also serves its index.html by default +// in order to support single-page react-apps with its own router. +type BindataAssets struct { +} + +// fsImpl is a s imple fs.FS implementation that uses the supplied OpenFn function +type fsImpl struct { + openFn func(path string) (fs.File, error) +} + +func (fs *fsImpl) Open(path string) (fs.File, error) { + return fs.openFn(path) +} + +// Handler returns HTTP handler that serves embedded data +func (b *BindataAssets) Handler() http.Handler { + return b +} + +// addCacheHeaders requests an hour of Cache-Control and sets an ETag based on file size and modtime +func addCacheHeaders(file fs.File, headers http.Header) error { + fi, err := file.Stat() + if err == nil { + headers.Add("Cache-Control", "public, max-age=3600") + + headers.Add("X-Frame-Options", "SAMEORIGIN") + headers.Add("X-XSS-Protection", "1; mode=block") + headers.Add("X-Content-Type-Options", "nosniff") + headers.Add("Content-Security-Policy", "script-src 'self'; object-src 'self'") + + hour, minute, second := fi.ModTime().Clock() + etag := fmt.Sprintf(`"%d%d%d%d%d"`, fi.Size(), fi.ModTime().Day(), hour, minute, second) + + headers.Set("ETag", etag) + } + return err +} + +// ServeHTTP wraps http.FileServer by returning a default asset if the asset +// doesn't exist. This supports single-page react-apps with its own +// built-in router. Additionally, we override the content-type if the +// Default file is used. +func (b *BindataAssets) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // openFn wraps buidDir.Open in order to setup default HTTP headers + // it also returns the default file if the file doesn't exist + openFn := func(name string) (fs.File, error) { + // If the named asset exists, then return it directly. + file, err := buildDir.Open(name) + if err != nil { + // If this is at / then we just error out so we can return a Directory + // This directory will then be redirected by go to the /index.html + if name == "/" || name == "." { + return nil, err + } + // If this is anything other than slash, we just return the default + // asset. This default asset will handle the routing. + // Additionally, because we know we are returning the default asset, + // we need to set the default asset's content-type. + w.Header().Set("Content-Type", DefaultPageContentType) + defaultFile, err := buildDir.Open(DefaultPage) + if err != nil { + return nil, err + } + file = defaultFile + } + if err := addCacheHeaders(file, w.Header()); err != nil { + return nil, err + } + // https://github.com/influxdata/chronograf/issues/5565 + // workaround wrong .js content-type on windows + if strings.HasSuffix(name, ".js") { + w.Header().Set("Content-Type", "text/javascript") + } + return file, nil + } + var fs fs.FS = &fsImpl{ + openFn: openFn, + } + http.FileServer(http.FS(fs)).ServeHTTP(w, r) +} + +// GetVersion returns version of the packed assets +func GetVersion() string { + return version +}