Skip to content

Commit

Permalink
Refactor behaviour of loading static files from disk vs. embedding.
Browse files Browse the repository at this point in the history
Ref: #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.
  • Loading branch information
knadh committed Jul 11, 2021
1 parent c8826d0 commit 82735bb
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 48 deletions.
4 changes: 2 additions & 2 deletions Makefile
Expand Up @@ -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

Expand All @@ -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)

Expand Down
127 changes: 83 additions & 44 deletions cmd/init.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"html/template"
"os"
"path"
"path/filepath"
"strings"
"syscall"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion cmd/install.go
Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion cmd/main.go
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
_ "embed"
"fmt"
"html/template"
"io"
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 82735bb

Please sign in to comment.