From 82735bba69a4b526de341fee0ae6c0e7b5ff0f6b Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sun, 11 Jul 2021 10:33:00 +0530 Subject: [PATCH] Refactor behaviour of loading static files from disk vs. embedding. Ref: https://github.com/knadh/listmonk/issues/409 - Introduce `main.appDir` and `main.fronendDir` Go compile-time flags to hardcode custom paths for loading frontend assets (frontend/dist/frontend in the repo after build) and app assets (queries.sql, schema.sql, config.toml.sample) in environments where embedding files in the binary is not feasible. These default to CWD unless explicitly set during compilation. - Fix the Vue favicon path oddity by copying the icon into the built frontend dir in the `make-frontend` step. --- Makefile | 4 +- cmd/init.go | 127 ++++++++++++++++++++++++++++++++----------------- cmd/install.go | 2 +- cmd/main.go | 10 +++- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/Makefile b/Makefile index dffc42fe1..b37a3088c 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,6 @@ STATIC := config.toml.sample \ schema.sql queries.sql \ static/public:/public \ static/email-templates \ - frontend/dist/favicon.png:/frontend/favicon.png \ frontend/dist/frontend:/frontend \ i18n:/i18n @@ -44,9 +43,10 @@ run: $(BIN) # Build the JS frontend into frontend/dist. $(FRONTEND_DIST): $(FRONTEND_DEPS) - export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) build + export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) build && mv dist/favicon.png dist/frontend/favicon.png touch --no-create $(FRONTEND_DIST) + .PHONY: build-frontend build-frontend: $(FRONTEND_DIST) diff --git a/cmd/init.go b/cmd/init.go index a51f20df5..994e75616 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "os" + "path" "path/filepath" "strings" "syscall" @@ -108,74 +109,100 @@ func initConfigFiles(files []string, ko *koanf.Koanf) { // initFileSystem initializes the stuffbin FileSystem to provide // access to bunded static assets to the app. -func initFS(staticDir, i18nDir string) stuffbin.FileSystem { +func initFS(appDir, frontendDir, staticDir, i18nDir string) stuffbin.FileSystem { + var ( + // stuffbin real_path:virtual_alias paths to map local assets on disk + // when there an embedded filestystem is not found. + + // These paths are joined with appDir. + appFiles = []string{ + "./config.toml.sample:config.toml.sample", + "./queries.sql:queries.sql", + "./schema.sql:schema.sql", + } + + frontendFiles = []string{ + // The app's frontend assets are accessible at /frontend/js/* during runtime. + // These paths are joined with frontendDir. + "./:/frontend", + } + + staticFiles = []string{ + // These paths are joined with staticDir. + "./email-templates:static/email-templates", + "./public:/public", + } + + i18nFiles = []string{ + // These paths are joined with i18nDir. + "./:/i18n", + } + ) + // Get the executable's path. path, err := os.Executable() if err != nil { lo.Fatalf("error getting executable path: %v", err) } - // Load the static files stuffed in the binary. + // Load embedded files in the executable. + hasEmbed := true fs, err := stuffbin.UnStuff(path) if err != nil { + hasEmbed = false + // Running in local mode. Load local assets into // the in-memory stuffbin.FileSystem. - lo.Printf("unable to initialize embedded filesystem: %v", err) - lo.Printf("using local filesystem for static assets") - files := []string{ - "config.toml.sample", - "queries.sql", - "schema.sql", - - // The frontend app's static assets are aliased to /frontend - // so that they are accessible at /frontend/js/* etc. - // Alias all files inside dist/ and dist/frontend to frontend/*. - "frontend/dist/favicon.png:/frontend/favicon.png", - "frontend/dist/frontend:/frontend", - "i18n:/i18n", - } - - // If no external static dir is provided, try to load from the working dir. - if staticDir == "" { - files = append(files, "static/email-templates", "static/public:/public") - } + lo.Printf("unable to initialize embedded filesystem (%v). Using local filesystem", err) - fs, err = stuffbin.NewLocalFS("/", files...) + fs, err = stuffbin.NewLocalFS("/") if err != nil { lo.Fatalf("failed to initialize local file for assets: %v", err) } } - // Optional static directory to override static files. - if staticDir != "" { - lo.Printf("loading static files from: %v", staticDir) - fStatic, err := stuffbin.NewLocalFS("/", []string{ - filepath.Join(staticDir, "/email-templates") + ":/static/email-templates", + // If the embed failed, load app and frontend files from the compile-time paths. + files := []string{} + if !hasEmbed { + files = append(files, joinFSPaths(appDir, appFiles)...) + files = append(files, joinFSPaths(frontendDir, frontendFiles)...) + } - // Alias /static/public to /public for the HTTP fileserver. - filepath.Join(staticDir, "/public") + ":/public", - }...) - if err != nil { - lo.Fatalf("failed reading static directory: %s: %v", staticDir, err) + // Irrespective of the embeds, if there are user specified static or i18n paths, + // load files from there and override default files (embedded or picked up from CWD). + if !hasEmbed || i18nDir != "" { + if i18nDir == "" { + // Default dir in cwd. + i18nDir = "i18n" } + lo.Printf("will load i18n files from: %v", i18nDir) + files = append(files, joinFSPaths(i18nDir, i18nFiles)...) + } - if err := fs.Merge(fStatic); err != nil { - lo.Fatalf("error merging static directory: %s: %v", staticDir, err) + if !hasEmbed || staticDir != "" { + if staticDir == "" { + // Default dir in cwd. + staticDir = "static" } + lo.Printf("will load static files from: %v", staticDir) + files = append(files, joinFSPaths(staticDir, staticFiles)...) } - // Optional static directory to override i18n language files. - if i18nDir != "" { - lo.Printf("loading i18n language files from: %v", i18nDir) - fi18n, err := stuffbin.NewLocalFS("/", []string{i18nDir + ":/i18n"}...) - if err != nil { - lo.Fatalf("failed reading i18n directory: %s: %v", i18nDir, err) - } + // No additional files to load. + if len(files) == 0 { + return fs + } - if err := fs.Merge(fi18n); err != nil { - lo.Fatalf("error merging i18n directory: %s: %v", i18nDir, err) - } + // Load files from disk and overlay into the FS. + fStatic, err := stuffbin.NewLocalFS("/", files...) + if err != nil { + lo.Fatalf("failed reading static files from disk: '%s': %v", staticDir, err) + } + + if err := fs.Merge(fStatic); err != nil { + lo.Fatalf("error merging static files: '%s': %v", staticDir, err) } + return fs } @@ -553,3 +580,15 @@ func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) ch return out } + +func joinFSPaths(root string, paths []string) []string { + out := make([]string, 0, len(paths)) + for _, p := range paths { + // real_path:stuffbin_alias + f := strings.Split(p, ":") + + out = append(out, path.Join(root, f[0])+":"+f[1]) + } + + return out +} diff --git a/cmd/install.go b/cmd/install.go index 97c5831b7..2699aad58 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -167,7 +167,7 @@ func newConfigFile(path string) error { // Initialize the static file system into which all // required static assets (.sql, .js files etc.) are loaded. - fs := initFS("", "") + fs := initFS(appDir, "", "", "") b, err := fs.Read("config.toml.sample") if err != nil { return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err) diff --git a/cmd/main.go b/cmd/main.go index 5ece04ed6..ff1a1c754 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( "context" + _ "embed" "fmt" "html/template" "io" @@ -68,8 +69,15 @@ var ( db *sqlx.DB queries *Queries + // Compile-time variables. buildString string versionString string + + // If these are set in build ldflags and static assets (*.sql, config.toml.sample. ./frontend) + // are not embedded (in make dist), these paths are looked up. The default values before, when not + // overridden by build flags, are relative to the CWD at runtime. + appDir string = "." + frontendDir string = "frontend" ) func init() { @@ -107,7 +115,7 @@ func init() { // Connect to the database, load the filesystem to read SQL queries. db = initDB() - fs = initFS(ko.String("static-dir"), ko.String("i18n-dir")) + fs = initFS(appDir, frontendDir, ko.String("static-dir"), ko.String("i18n-dir")) // Installer mode? This runs before the SQL queries are loaded and prepared // as the installer needs to work on an empty DB.