From f5360772ca4c9e5857f9e78cc32567f38862e8a0 Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Thu, 5 Mar 2026 17:29:18 -0700 Subject: [PATCH] allow us to specify paths and files in the configuration relative to the directory the configuration file is in --- config/config.go | 29 ++++++++++++++++++++++------- email/sender.go | 11 ++++++----- ui/dialog.go | 11 ++++++----- ui/menus.go | 9 +++++---- ui/messagebox.go | 9 +++++---- ui/static.go | 11 ++++++----- ui/templates.go | 11 ++++++----- 7 files changed, 56 insertions(+), 35 deletions(-) diff --git a/config/config.go b/config/config.go index 812e9ed..c43e8fb 100644 --- a/config/config.go +++ b/config/config.go @@ -156,6 +156,15 @@ type AmConfig struct { UserProps int `yaml:"userProps"` } `yaml:"caches"` } `yaml:"tuning"` + baseDir string // the base directory to evaluate relative paths to. +} + +// ExPath expands a path relative to the baseDir (the location of the config file). +func (c *AmConfig) ExPath(path string) string { + if path == "" || c.baseDir == "" || filepath.IsAbs(path) { + return path + } + return filepath.Join(c.baseDir, path) } // AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig. @@ -213,6 +222,10 @@ func locateConfigFile() (string, *os.File) { func overlayStructValue(dest, loaded, defaults reflect.Value) { typ := dest.Type() for i := 0; i < dest.NumField(); i++ { + structField := typ.Field(i) + if !structField.IsExported() { + continue + } fldDest := dest.Field(i) fldLoaded := loaded.Field(i) fldDefaults := defaults.Field(i) @@ -257,8 +270,7 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) { } } else { // if we see this message, this function needs more work - errField := typ.Field(i) - log.Errorf("*** unable to deal with field %s of type %s", errField.Name, typ.Name()) + log.Errorf("*** unable to deal with field %s of type %s", structField.Name, typ.Name()) } } } @@ -290,19 +302,20 @@ func parseDataSize(s string) (int32, error) { // AmOpenExternalContentPath opens the "external content path" specified in the configuration as a root filesystem. func AmOpenExternalContentPath() (fs.FS, error) { - if GlobalConfig.Resources.ExternalContentPath == "" { + path := GlobalConfig.ExPath(GlobalConfig.Resources.ExternalContentPath) + if path == "" { return nil, nil } - finfo, err := os.Stat(GlobalConfig.Resources.ExternalContentPath) + finfo, err := os.Stat(path) if err != nil { - log.Errorf("external content path \"%s\" is not valid, ignored (%v)", GlobalConfig.Resources.ExternalContentPath, err) + log.Errorf("external content path \"%s\" is not valid, ignored (%v)", path, err) return nil, nil } if !finfo.IsDir() { - log.Errorf("external content path \"%s\" is not a directory, ignored", GlobalConfig.Resources.ExternalContentPath) + log.Errorf("external content path \"%s\" is not a directory, ignored", path) return nil, nil } - root, err := os.OpenRoot(GlobalConfig.Resources.ExternalContentPath) + root, err := os.OpenRoot(path) if err != nil { return nil, err } @@ -331,9 +344,11 @@ func SetupConfig() { panic(fmt.Sprintf("unable to load configuration file %s: %v", name, err)) } overlayStructValue(reflect.ValueOf(&GlobalConfig).Elem(), reflect.ValueOf(&loadedConfig).Elem(), reflect.ValueOf(&defaultConfig).Elem()) + GlobalConfig.baseDir = filepath.Dir(name) } else { log.Info("SetupConfig(): using default configs only") GlobalConfig = defaultConfig // just copy over the defaults + GlobalConfig.baseDir = "" } // Compute additional values. diff --git a/email/sender.go b/email/sender.go index dcba50c..f11f31b 100644 --- a/email/sender.go +++ b/email/sender.go @@ -213,16 +213,17 @@ func SetupMailSender() func() { // Locate the external template directory and build the loaders. templateLoaders := make([]jet.Loader, 0, 2) - if config.GlobalConfig.Resources.EmailTemplateDir != "" { - finfo, err := os.Stat(config.GlobalConfig.Resources.EmailTemplateDir) + etDir := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.EmailTemplateDir) + if etDir != "" { + finfo, err := os.Stat(etDir) if err == nil { if finfo.IsDir() { - templateLoaders = append(templateLoaders, jet.NewOSFileSystemLoader(config.GlobalConfig.Resources.EmailTemplateDir)) + templateLoaders = append(templateLoaders, jet.NewOSFileSystemLoader(etDir)) } else { - log.Errorf("email template directory %s is not a directory, ignored", config.GlobalConfig.Resources.EmailTemplateDir) + log.Errorf("email template directory %s is not a directory, ignored", etDir) } } else { - log.Errorf("email template directory %s is not valid, ignored (%v)", config.GlobalConfig.Resources.EmailTemplateDir, err) + log.Errorf("email template directory %s is not valid, ignored (%v)", etDir, err) } } templateLoaders = append(templateLoaders, embedfs.NewLoader("templates/", emailTemplates)) diff --git a/ui/dialog.go b/ui/dialog.go index a281bd1..9ac6a1d 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -88,20 +88,21 @@ var extDialogs fs.FS = nil // setupDialogs sets up the external dialog filesystem. func setupDialogs() { // Open the external dialog path. - if config.GlobalConfig.Resources.DialogTemplateDir != "" { - finfo, err := os.Stat(config.GlobalConfig.Resources.DialogTemplateDir) + dtDir := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.DialogTemplateDir) + if dtDir != "" { + finfo, err := os.Stat(dtDir) if err == nil { if finfo.IsDir() { - root, err := os.OpenRoot(config.GlobalConfig.Resources.DialogTemplateDir) + root, err := os.OpenRoot(dtDir) if err != nil { panic(err) } extDialogs = root.FS() } else { - log.Errorf("external resource path \"%s\" is not a directory, ignored", config.GlobalConfig.Resources.DialogTemplateDir) + log.Errorf("external resource path \"%s\" is not a directory, ignored", dtDir) } } else { - log.Errorf("external resource path \"%s\" is not valid, ignored (%v)", config.GlobalConfig.Resources.DialogTemplateDir, err) + log.Errorf("external resource path \"%s\" is not valid, ignored (%v)", dtDir, err) } } } diff --git a/ui/menus.go b/ui/menus.go index cef082d..ffa0c35 100644 --- a/ui/menus.go +++ b/ui/menus.go @@ -164,8 +164,9 @@ func setupMenus() { if menuCache, err = lru.New(config.GlobalConfig.Tuning.Caches.Menus); err != nil { panic(err) } - if config.GlobalConfig.Resources.ExternalMenuDefinitions != "" { - b, err := os.ReadFile(config.GlobalConfig.Resources.ExternalMenuDefinitions) + mfile := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.ExternalMenuDefinitions) + if mfile != "" { + b, err := os.ReadFile(mfile) if err == nil { md := new(MenuDefs) err = yaml.Unmarshal(b, md) @@ -177,10 +178,10 @@ func setupMenus() { } } } else { - log.Errorf("cannot parse external menu definition file %s, ignored (%v)", config.GlobalConfig.Resources.ExternalMenuDefinitions, err) + log.Errorf("cannot parse external menu definition file %s, ignored (%v)", mfile, err) } } else { - log.Errorf("cannot read external menu definition file %s, ignored (%v)", config.GlobalConfig.Resources.ExternalMenuDefinitions, err) + log.Errorf("cannot read external menu definition file %s, ignored (%v)", mfile, err) } } } diff --git a/ui/messagebox.go b/ui/messagebox.go index 456c2c2..984c6b9 100644 --- a/ui/messagebox.go +++ b/ui/messagebox.go @@ -85,8 +85,9 @@ func init() { // setupMessageBoxes loads external message box definitions. func setupMessageBoxes() { - if config.GlobalConfig.Resources.ExternalMessageDefinitions != "" { - b, err := os.ReadFile(config.GlobalConfig.Resources.ExternalMessageDefinitions) + mbfile := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.ExternalMessageDefinitions) + if mbfile != "" { + b, err := os.ReadFile(mbfile) if err == nil { mb := new(MessageBoxDefs) err = yaml.Unmarshal(b, mb) @@ -102,10 +103,10 @@ func setupMessageBoxes() { } } } else { - log.Errorf("cannot parse external message definition file %s, ignored (%v)", config.GlobalConfig.Resources.ExternalMessageDefinitions, err) + log.Errorf("cannot parse external message definition file %s, ignored (%v)", mbfile, err) } } else { - log.Errorf("cannot read external message definition file %s, ignored (%v)", config.GlobalConfig.Resources.ExternalMessageDefinitions, err) + log.Errorf("cannot read external message definition file %s, ignored (%v)", mbfile, err) } } } diff --git a/ui/static.go b/ui/static.go index a37a9b9..5782007 100644 --- a/ui/static.go +++ b/ui/static.go @@ -37,20 +37,21 @@ var external_resources fs.FS = nil func setupResources() { // Open the external resource path. - if config.GlobalConfig.Resources.ExternalResourcePath != "" { - finfo, err := os.Stat(config.GlobalConfig.Resources.ExternalResourcePath) + rpath := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.ExternalResourcePath) + if rpath != "" { + finfo, err := os.Stat(rpath) if err == nil { if finfo.IsDir() { - root, err := os.OpenRoot(config.GlobalConfig.Resources.ExternalResourcePath) + root, err := os.OpenRoot(rpath) if err != nil { panic(err) } external_resources = root.FS() } else { - log.Errorf("external resource path \"%s\" is not a directory, ignored", config.GlobalConfig.Resources.ExternalResourcePath) + log.Errorf("external resource path \"%s\" is not a directory, ignored", rpath) } } else { - log.Errorf("external resource path \"%s\" is not valid, ignored (%v)", config.GlobalConfig.Resources.ExternalResourcePath, err) + log.Errorf("external resource path \"%s\" is not valid, ignored (%v)", rpath, err) } } } diff --git a/ui/templates.go b/ui/templates.go index 404ce69..2c39ce8 100644 --- a/ui/templates.go +++ b/ui/templates.go @@ -287,16 +287,17 @@ func postRewrite(a jet.Arguments) reflect.Value { func setupTemplates() { // Set up the template loaders: the optional filesystem loader, then the embedded loader. templateLoaders := make([]jet.Loader, 0, 2) - if config.GlobalConfig.Resources.ViewTemplateDir != "" { - finfo, err := os.Stat(config.GlobalConfig.Resources.ViewTemplateDir) + vtDir := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.ViewTemplateDir) + if vtDir != "" { + finfo, err := os.Stat(vtDir) if err == nil { if finfo.IsDir() { - templateLoaders = append(templateLoaders, jet.NewOSFileSystemLoader(config.GlobalConfig.Resources.ViewTemplateDir)) + templateLoaders = append(templateLoaders, jet.NewOSFileSystemLoader(vtDir)) } else { - log.Errorf("view template directory %s is not a directory, ignored", config.GlobalConfig.Resources.ViewTemplateDir) + log.Errorf("view template directory %s is not a directory, ignored", vtDir) } } else { - log.Errorf("view template directory %s is not valid, ignored (%v)", config.GlobalConfig.Resources.ViewTemplateDir, err) + log.Errorf("view template directory %s is not valid, ignored (%v)", vtDir, err) } } templateLoaders = append(templateLoaders, embedfs.NewLoader("views/", static_views))