瀏覽代碼

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.
Kailash Nadh 4 年之前
父節點
當前提交
82735bba69
共有 4 個文件被更改,包括 95 次插入48 次删除
  1. 2 2
      Makefile
  2. 83 44
      cmd/init.go
  3. 1 1
      cmd/install.go
  4. 9 1
      cmd/main.go

+ 2 - 2
Makefile

@@ -19,7 +19,6 @@ STATIC := config.toml.sample \
 	schema.sql queries.sql \
 	schema.sql queries.sql \
 	static/public:/public \
 	static/public:/public \
 	static/email-templates \
 	static/email-templates \
-	frontend/dist/favicon.png:/frontend/favicon.png \
 	frontend/dist/frontend:/frontend \
 	frontend/dist/frontend:/frontend \
 	i18n:/i18n
 	i18n:/i18n
 
 
@@ -44,9 +43,10 @@ run: $(BIN)
 
 
 # Build the JS frontend into frontend/dist.
 # Build the JS frontend into frontend/dist.
 $(FRONTEND_DIST): $(FRONTEND_DEPS)
 $(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)
 	touch --no-create $(FRONTEND_DIST)
 
 
+
 .PHONY: build-frontend
 .PHONY: build-frontend
 build-frontend: $(FRONTEND_DIST)
 build-frontend: $(FRONTEND_DIST)
 
 

+ 83 - 44
cmd/init.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"fmt"
 	"html/template"
 	"html/template"
 	"os"
 	"os"
+	"path"
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
@@ -108,74 +109,100 @@ func initConfigFiles(files []string, ko *koanf.Koanf) {
 
 
 // initFileSystem initializes the stuffbin FileSystem to provide
 // initFileSystem initializes the stuffbin FileSystem to provide
 // access to bunded static assets to the app.
 // 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.
 	// Get the executable's path.
 	path, err := os.Executable()
 	path, err := os.Executable()
 	if err != nil {
 	if err != nil {
 		lo.Fatalf("error getting executable path: %v", err)
 		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)
 	fs, err := stuffbin.UnStuff(path)
 	if err != nil {
 	if err != nil {
+		hasEmbed = false
+
 		// Running in local mode. Load local assets into
 		// Running in local mode. Load local assets into
 		// the in-memory stuffbin.FileSystem.
 		// 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 {
 		if err != nil {
 			lo.Fatalf("failed to initialize local file for assets: %v", err)
 			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
 	return fs
 }
 }
 
 
@@ -553,3 +580,15 @@ func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) ch
 
 
 	return out
 	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
+}

+ 1 - 1
cmd/install.go

@@ -167,7 +167,7 @@ func newConfigFile(path string) error {
 
 
 	// Initialize the static file system into which all
 	// Initialize the static file system into which all
 	// required static assets (.sql, .js files etc.) are loaded.
 	// required static assets (.sql, .js files etc.) are loaded.
-	fs := initFS("", "")
+	fs := initFS(appDir, "", "", "")
 	b, err := fs.Read("config.toml.sample")
 	b, err := fs.Read("config.toml.sample")
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
 		return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)

+ 9 - 1
cmd/main.go

@@ -2,6 +2,7 @@ package main
 
 
 import (
 import (
 	"context"
 	"context"
+	_ "embed"
 	"fmt"
 	"fmt"
 	"html/template"
 	"html/template"
 	"io"
 	"io"
@@ -68,8 +69,15 @@ var (
 	db      *sqlx.DB
 	db      *sqlx.DB
 	queries *Queries
 	queries *Queries
 
 
+	// Compile-time variables.
 	buildString   string
 	buildString   string
 	versionString 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() {
 func init() {
@@ -107,7 +115,7 @@ func init() {
 
 
 	// Connect to the database, load the filesystem to read SQL queries.
 	// Connect to the database, load the filesystem to read SQL queries.
 	db = initDB()
 	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
 	// Installer mode? This runs before the SQL queries are loaded and prepared
 	// as the installer needs to work on an empty DB.
 	// as the installer needs to work on an empty DB.