From f66ab4550a4adbb74bb280b3a4296c54e58207be Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 21 Feb 2022 15:46:21 +0100 Subject: [PATCH] Implement asset serving --- app/servers.go | 65 ++++++++++++++++++++++++++++++++++++ assets/embed.go | 10 ++++++ assets/src/favicon32x32.png | Bin 0 -> 2262 bytes assets/src/logo.png | Bin 0 -> 11189 bytes 4 files changed, 75 insertions(+) create mode 100644 assets/embed.go create mode 100644 assets/src/favicon32x32.png create mode 100644 assets/src/logo.png diff --git a/app/servers.go b/app/servers.go index 47a5c6775..12cd2808f 100644 --- a/app/servers.go +++ b/app/servers.go @@ -1,11 +1,15 @@ package app import ( + "fmt" + "io/fs" "net/http" + "os" "path" "regexp" "strings" + "github.com/cortezaproject/corteza-server/assets" automationRest "github.com/cortezaproject/corteza-server/automation/rest" composeRest "github.com/cortezaproject/corteza-server/compose/rest" "github.com/cortezaproject/corteza-server/docs" @@ -24,6 +28,40 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { ho = app.Opt.HTTPServer ) + func() { + // asset serving has some overlap with auth assets, web-console and webapp serving + // and might be joined with one or more of them in the later version + + var ( + url = options.CleanBase(ho.BaseUrl, "assets") + aPath = ho.AssetsPath + files fs.FS + err error + ) + + if len(aPath) > 0 { + if files, err = loadAssetsFromPath(aPath); err != nil { + // log warning but fallback to embedded assets + app.Log.Warn( + fmt.Sprintf("failed to use custom assets path (HTTP_SERVER_ASSETS_PATH=%s)", aPath), + zap.Error(err), + ) + } + } + + if files == nil { + aPath = "embedded" + files, err = fs.Sub(assets.Embedded, "src") + if err != nil { + // if this is off, we might as well panic + panic(err) + } + } + + r.Handle(url+"/*", http.StripPrefix(url+"/", http.FileServer(http.FS(files)))) + app.Log.Info("web assets mounted", zap.String("url", url), zap.String("path", aPath)) + }() + func() { if ho.WebappEnabled && ho.ApiEnabled && ho.ApiBaseUrl == ho.WebappBaseUrl { app.Log. @@ -133,3 +171,30 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { r.Handle("/.well-known/openid-configuration", app.AuthService.WellKnownOpenIDConfiguration()) }() } + +func loadAssetsFromPath(path string) (assets fs.FS, err error) { + // at least favicon file should exist in the custom asset path + // otherwise we default to embedded files + const check = "favicon32x32.png" + + var ( + fi os.FileInfo + ) + + if fi, err = os.Stat(path); err != nil { + return + + } + + if !fi.IsDir() { + return nil, fmt.Errorf("expecting directory") + + } + + assets = os.DirFS(path) + if _, err = assets.Open(check); err != nil { + return nil, err + } + + return +} diff --git a/assets/embed.go b/assets/embed.go new file mode 100644 index 000000000..49a2a281e --- /dev/null +++ b/assets/embed.go @@ -0,0 +1,10 @@ +package assets + +import ( + "embed" +) + +var ( + //go:embed src/* + Embedded embed.FS +) diff --git a/assets/src/favicon32x32.png b/assets/src/favicon32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..a0549aa54e7d8d12476e3e4e218deda91ef94d45 GIT binary patch literal 2262 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 literal 0 HcmV?d00001