From 8b26069813da2ab2109a47a775925425dec280a0 Mon Sep 17 00:00:00 2001 From: plastikfan Date: Fri, 23 Feb 2024 14:04:40 +0000 Subject: [PATCH] fix(proxy): use muesli for config paths (#162) --- go.mod | 2 + go.sum | 4 + src/app/cfg/config-runner.go | 50 +++++++++- src/app/cfg/config-runner_test.go | 99 +++++++++++++++++++ src/app/command/bootstrap.go | 31 +++--- src/app/plog/new-logger.go | 36 +++++-- src/app/proxy/common/config-defs.go | 19 ++++ src/app/proxy/common/const-defs.go | 8 +- src/app/proxy/pixa_test.go | 80 +++++++++++---- .../configuration/pixa-test-no-logger.yml | 49 +++++++++ 10 files changed, 334 insertions(+), 44 deletions(-) create mode 100644 test/data/configuration/pixa-test-no-logger.yml diff --git a/go.mod b/go.mod index 66c9d00..aa50fe4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.9.1 + github.com/muesli/go-app-paths v0.2.2 github.com/onsi/ginkgo/v2 v2.15.0 github.com/onsi/gomega v1.31.1 github.com/pkg/errors v0.9.1 @@ -32,6 +33,7 @@ require ( github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect diff --git a/go.sum b/go.sum index 23c83c3..21c7149 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= @@ -78,6 +80,8 @@ github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTd github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI= +github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= diff --git a/src/app/cfg/config-runner.go b/src/app/cfg/config-runner.go index 3388342..e91e5b5 100644 --- a/src/app/cfg/config-runner.go +++ b/src/app/cfg/config-runner.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + gap "github.com/muesli/go-app-paths" "github.com/samber/lo" "github.com/snivilised/cobrass/src/assistant/configuration" ci18n "github.com/snivilised/cobrass/src/assistant/i18n" @@ -45,6 +46,7 @@ func New( applicationName: applicationName, home: home, vfs: vfs, + useXDG: common.IsUsingXDG(ci.Viper), }, err } @@ -55,6 +57,7 @@ type configRunner struct { applicationName string home string vfs storage.VirtualFS + useXDG bool } func (c *configRunner) DefaultPath() string { @@ -65,7 +68,6 @@ func (c *configRunner) Run() error { c.vc.SetConfigName(c.ci.Name) c.vc.SetConfigType(c.ci.ConfigType) c.vc.AutomaticEnv() - c.vc.AddConfigPath(c.path()) err := c.read() @@ -93,6 +95,9 @@ func (c *configRunner) read() error { var ( err error ) + + c.vc.AddConfigPath(c.path()) + // the returned error from vc.ReadInConfig() does not support standard // golang error identity via errors.Is, so we are forced to assume // that if we get an error, it is viper.ConfigFileNotFoundError @@ -111,6 +116,49 @@ func (c *configRunner) read() error { return nil }, + func() error { + // try standard or XDG + // + if c.useXDG { + // manual XDG: ["~/.local/share/app", "/usr/local/share/app", "/usr/share/app"] + // https://github.com/muesli/go-app-paths?tab=readme-ov-file#directories + // + paths := []string{ + filepath.Join(c.home, ".local", "share"), + filepath.Join(string(filepath.Separator), "usr", "local", "share"), + filepath.Join(string(filepath.Separator), "usr", "share"), + } + + for _, dir := range paths { + c.vc.AddConfigPath(filepath.Join(dir, common.Definitions.Pixa.AppName)) + } + } else { + // use standard muesli; ie platform specific + // + scope := lo.TernaryF(c.ci.Scope != nil, + func() common.ConfigScope { + return c.ci.Scope + }, + func() common.ConfigScope { + return gap.NewVendorScope(gap.User, + common.Definitions.Pixa.Org, common.Definitions.Pixa.AppName, + ) + }, + ) + + paths, e := scope.ConfigDirs() + + if e == nil { + for _, dir := range paths { + c.vc.AddConfigPath(dir) + } + } else { + return e + } + } + + return nil + }, func() error { // not found in home, therefore export default to // home path, which has already been added in previous diff --git a/src/app/cfg/config-runner_test.go b/src/app/cfg/config-runner_test.go index 54b0f67..b788b71 100644 --- a/src/app/cfg/config-runner_test.go +++ b/src/app/cfg/config-runner_test.go @@ -1,6 +1,7 @@ package cfg_test import ( + "errors" "fmt" "path/filepath" @@ -19,12 +20,39 @@ import ( var ( sourceID = "github.com/snivilised/pixa" environment = "PIXA_HOME" + useXDG = "" ) +type testScope struct { +} + +func (f *testScope) ConfigDirs() ([]string, error) { + return []string{ + filepath.Join(string(filepath.Separator), "foo"), + filepath.Join(string(filepath.Separator), "bar"), + }, nil +} + +func (f *testScope) LogPath(filename string) (string, error) { + return filename, nil +} + +type errorScope struct { +} + +func (f *errorScope) ConfigDirs() ([]string, error) { + return []string{}, errors.New("fake could not get config dirs") +} + +func (f *errorScope) LogPath(filename string) (string, error) { + return filename, nil +} + type runnerTE struct { given string should string path string + scope common.ConfigScope arrange func(entry *runnerTE, path string) created func(entry *runnerTE, runner common.ConfigRunner) assert func(entry *runnerTE, runner common.ConfigRunner, err error) @@ -59,6 +87,7 @@ var _ = Describe("ConfigRunner", func() { ConfigType: common.Definitions.Pixa.ConfigType, ConfigPath: entry.path, Viper: mock, + Scope: entry.scope, } // this is why I hate mocking, requires too much // knowledge of the implementation, making the tests @@ -94,6 +123,10 @@ var _ = Describe("ConfigRunner", func() { given: "config file present at PIXA_HOME", should: "use config at PIXA_HOME", arrange: func(_ *runnerTE, path string) { + mock.EXPECT().Get(gomock.Eq(common.Definitions.Environment.UseXDG)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + mock.EXPECT().ReadInConfig().Times(1) mock.EXPECT().AddConfigPath(path).AnyTimes() mock.EXPECT().Get(gomock.Eq(environment)).DoAndReturn(func(_ string) string { @@ -110,6 +143,10 @@ var _ = Describe("ConfigRunner", func() { given: "config file present as configured by client, PIXA_HOME not defined", should: "use config at specified path", arrange: func(_ *runnerTE, _ string) { + mock.EXPECT().Get(gomock.Eq(common.Definitions.Environment.UseXDG)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + mock.EXPECT().ReadInConfig().Times(1) mock.EXPECT().AddConfigPath(gomock.Any()).AnyTimes() mock.EXPECT().Get(gomock.Eq(environment)).DoAndReturn(func(_ string) string { @@ -126,6 +163,10 @@ var _ = Describe("ConfigRunner", func() { given: "config file missing, but at default location, PIXA_HOME not defined", should: "use config at default location", arrange: func(_ *runnerTE, _ string) { + mock.EXPECT().Get(gomock.Eq(common.Definitions.Environment.UseXDG)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + mock.EXPECT().Get(gomock.Eq(environment)).DoAndReturn(func(_ string) string { return "" }).AnyTimes() @@ -158,6 +199,64 @@ var _ = Describe("ConfigRunner", func() { given: "config file completely missing", should: "use default exported config", arrange: func(_ *runnerTE, _ string) { + mock.EXPECT().Get(gomock.Eq(common.Definitions.Environment.UseXDG)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + + mock.EXPECT().Get(gomock.Eq(environment)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + + mock.EXPECT().ReadInConfig().Times(2).DoAndReturn(func() error { + return viper.ConfigFileNotFoundError{} + }) + mock.EXPECT().AddConfigPath(gomock.Any()).AnyTimes() + mock.EXPECT().ReadInConfig().Times(1).DoAndReturn(func() error { + return nil + }) + }, + assert: func(_ *runnerTE, runner common.ConfigRunner, err error) { + Expect(err).Error().To(BeNil()) + Expect(runner).NotTo(BeNil()) + }, + }), + + Entry(nil, &runnerTE{ + given: "use XDG, config file completely missing", + should: "use default exported config", + scope: &testScope{}, + arrange: func(_ *runnerTE, _ string) { + mock.EXPECT().Get(gomock.Eq(common.Definitions.Environment.UseXDG)).DoAndReturn(func(_ string) string { + return "true" + }).AnyTimes() + + mock.EXPECT().Get(gomock.Eq(environment)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + + mock.EXPECT().ReadInConfig().Times(2).DoAndReturn(func() error { + return viper.ConfigFileNotFoundError{} + }) + mock.EXPECT().AddConfigPath(gomock.Any()).AnyTimes() + mock.EXPECT().ReadInConfig().Times(1).DoAndReturn(func() error { + return nil + }) + }, + assert: func(_ *runnerTE, runner common.ConfigRunner, err error) { + Expect(err).Error().To(BeNil()) + Expect(runner).NotTo(BeNil()) + }, + }), + + Entry(nil, &runnerTE{ + given: "scope returns error, config file completely missing", + should: "use default exported config", + scope: &errorScope{}, + arrange: func(_ *runnerTE, _ string) { + mock.EXPECT().Get(gomock.Eq(common.Definitions.Environment.UseXDG)).DoAndReturn(func(_ string) string { + return "" + }).AnyTimes() + mock.EXPECT().Get(gomock.Eq(environment)).DoAndReturn(func(_ string) string { return "" }).AnyTimes() diff --git a/src/app/command/bootstrap.go b/src/app/command/bootstrap.go index 68dfff7..6c7651c 100644 --- a/src/app/command/bootstrap.go +++ b/src/app/command/bootstrap.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/viper" "golang.org/x/text/language" + gap "github.com/muesli/go-app-paths" "github.com/snivilised/cobrass/src/assistant" "github.com/snivilised/cobrass/src/assistant/configuration" ci18n "github.com/snivilised/cobrass/src/assistant/i18n" @@ -71,7 +72,7 @@ type Bootstrap struct { type ConfigureOptionsInfo struct { Detector LocaleDetector - Config common.ConfigInfo + Config *common.ConfigInfo Runner common.ConfigRunner } @@ -81,14 +82,26 @@ type ConfigureOptionFn func(*ConfigureOptionsInfo) // to be executed. func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { vc := &configuration.GlobalViperConfig{} - ci := common.ConfigInfo{ + ci := &common.ConfigInfo{ Name: common.Definitions.Pixa.AppName, ConfigType: common.Definitions.Pixa.ConfigType, Viper: vc, + Scope: gap.NewVendorScope(gap.User, + common.Definitions.Pixa.Org, common.Definitions.Pixa.AppName, + ), + } + + b.OptionsInfo = ConfigureOptionsInfo{ + Detector: &Jabber{}, + Config: ci, + } + + for _, fo := range options { + fo(&b.OptionsInfo) } runner, err := cfg.New( - &ci, + ci, common.Definitions.Pixa.SourceID, common.Definitions.Pixa.AppName, b.Vfs, @@ -102,19 +115,11 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { os.Exit(1) } - b.OptionsInfo = ConfigureOptionsInfo{ - Detector: &Jabber{}, - Config: ci, - Runner: runner, - } - - for _, fo := range options { - fo(&b.OptionsInfo) - } + b.OptionsInfo.Runner = runner b.configure() b.viper() - b.Logger = plog.New(b.Configs.Logging, b.Vfs) + b.Logger = plog.New(b.Configs.Logging, b.Vfs, ci.Scope, vc) b.Container = assistant.NewCobraContainer( &cobra.Command{ diff --git a/src/app/plog/new-logger.go b/src/app/plog/new-logger.go index e0ebdb5..8e6cb0a 100644 --- a/src/app/plog/new-logger.go +++ b/src/app/plog/new-logger.go @@ -2,8 +2,11 @@ package plog import ( "log/slog" + "path/filepath" "github.com/natefinch/lumberjack" + "github.com/samber/lo" + "github.com/snivilised/cobrass/src/assistant/configuration" "github.com/snivilised/extendio/xfs/storage" "github.com/snivilised/extendio/xfs/utils" "github.com/snivilised/pixa/src/app/proxy/common" @@ -12,18 +15,33 @@ import ( "go.uber.org/zap/zapcore" ) -func New(lc common.LoggingConfig, vfs storage.VirtualFS) *slog.Logger { - noc := slog.New(zapslog.NewHandler( - zapcore.NewNopCore(), nil), - ) +func New(lc common.LoggingConfig, + vfs storage.VirtualFS, + scope common.ConfigScope, + vc configuration.ViperConfig, +) *slog.Logger { + logPath := lo.TernaryF(common.IsUsingXDG(vc), + func() string { + // manual XDG: ~/.local/share/app/filename.log + // + return utils.ResolvePath(filepath.Join( + "~", ".local", "share", + common.Definitions.Pixa.AppName, + common.Definitions.Defaults.Logging.LogFilename, + )) + }, + func() string { + lp := lc.Path() + if lp != "" { + return utils.ResolvePath(lp) + } - logPath := lc.Path() + dir, _ := scope.LogPath(common.Definitions.Defaults.Logging.LogFilename) - if logPath == "" { - return noc - } + return dir + }, + ) - logPath = utils.ResolvePath(logPath) logPath, _ = utils.EnsurePathAt( logPath, common.Definitions.Defaults.Logging.LogFilename, diff --git a/src/app/proxy/common/config-defs.go b/src/app/proxy/common/config-defs.go index 3466f1b..9c07faa 100644 --- a/src/app/proxy/common/config-defs.go +++ b/src/app/proxy/common/config-defs.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "slices" "time" "github.com/snivilised/cobrass/src/assistant/configuration" @@ -38,6 +39,7 @@ type ( ConfigType string ConfigPath string Viper configuration.ViperConfig + Scope ConfigScope } ) @@ -105,4 +107,21 @@ type ( Level() string TimeFormat() string } + + ConfigScope interface { + ConfigDirs() ([]string, error) + LogPath(filename string) (string, error) + } ) + +func IsUsingXDG(vc configuration.ViperConfig) bool { + val, ok := vc.Get(Definitions.Environment.UseXDG).(string) + + if !ok { + val = "" + } + + return !slices.Contains([]string{"", "0", "false", "no", "off"}, + val, + ) +} diff --git a/src/app/proxy/common/const-defs.go b/src/app/proxy/common/const-defs.go index 65ad350..1da21af 100644 --- a/src/app/proxy/common/const-defs.go +++ b/src/app/proxy/common/const-defs.go @@ -23,6 +23,7 @@ type ( ConfigTestFilename string ConfigType string SubPath string + Org string } thirdPartyDefs struct { @@ -45,7 +46,8 @@ type ( } environmentDefs struct { - Home string + Home string + UseXDG string } filingDefs struct { @@ -79,6 +81,7 @@ var Definitions = definitions{ ConfigTestFilename: fmt.Sprintf("%v-test", appName), ConfigType: yml, SubPath: filepath.Join(org, appName), + Org: org, }, ThirdParty: thirdPartyDefs{ Magick: "magick", @@ -97,7 +100,8 @@ var Definitions = definitions{ }, }, Environment: environmentDefs{ - Home: "PIXA_HOME", + Home: "PIXA_HOME", + UseXDG: "PIXA_XDG", }, Filing: filingDefs{ JournalExt: ".txt", diff --git a/src/app/proxy/pixa_test.go b/src/app/proxy/pixa_test.go index ac31742..3f4b95e 100644 --- a/src/app/proxy/pixa_test.go +++ b/src/app/proxy/pixa_test.go @@ -70,24 +70,25 @@ func assertResultItemFile(name string, } type pixaTE struct { - given string - should string - reasons reasons - arranger arrange - asserters asserters - exists bool - args []string - isTui bool - dry bool - intermediate string - output string - trash string - profile string - scheme string - relative string - mandatory []string - supplements supplements - inputs []string + given string + should string + reasons reasons + arranger arrange + asserters asserters + exists bool + args []string + isTui bool + dry bool + intermediate string + output string + trash string + profile string + scheme string + relative string + mandatory []string + supplements supplements + inputs []string + configTestFilename string } func because(reason string, extras ...string) string { @@ -176,11 +177,16 @@ func (t *coreTest) run() { }, } + configTestFilename := common.Definitions.Pixa.ConfigTestFilename + if t.entry.configTestFilename != "" { + configTestFilename = t.entry.configTestFilename + } + tester := helpers.CommandTester{ Args: args, Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &helpers.DetectorStub{} - co.Config.Name = common.Definitions.Pixa.ConfigTestFilename + co.Config.Name = configTestFilename co.Config.ConfigPath = t.configPath co.Config.Viper = &configuration.GlobalViperConfig{} }), @@ -452,5 +458,41 @@ var _ = Describe("pixa", Ordered, func() { }, }, }), + + // + // === MISC + // + + // + // === NO LOGGER IN CONFIG (TRANSPARENT / PROFILE) + // + Entry(nil, &pixaTE{ + given: "no-log (🎯 @TID-CORE-1/2:_TBD__TR-PR-NC_TR)", + should: "use log with default scope", + relative: BackyardWorldsPlanet9Scan01, + reasons: reasons{ + folder: "transparency, result should take place of input in same folder", + file: "file should be moved out of the way and not cuddled", + }, + configTestFilename: "pixa-test-no-logger", + profile: "blur", + args: []string{ + "--files-rx", "Backyard-Worlds", + "--gaussian-blur", "0.51", + "--interlace", "line", + }, + intermediate: "nasa/exo/Backyard Worlds - Planet 9/sessions/scan-01", + supplements: supplements{ + file: "$TRASH$.blur", + folder: filepath.Join("$TRASH$", "blur"), + }, + inputs: helpers.BackyardWorldsPlanet9Scan01First6, + asserters: asserters{ + transfer: func(name string, entry *pixaTE, origin string, pa *pathAssertion, vfs storage.VirtualFS) { + }, + result: func(name string, entry *pixaTE, origin string, pa *pathAssertion, vfs storage.VirtualFS) { + }, + }, + }), ) }) diff --git a/test/data/configuration/pixa-test-no-logger.yml b/test/data/configuration/pixa-test-no-logger.yml new file mode 100644 index 0000000..c7eb4fc --- /dev/null +++ b/test/data/configuration/pixa-test-no-logger.yml @@ -0,0 +1,49 @@ +profiles: + blur: + strip: true + interlace: "plane" + gaussian-blur: "0.05" + sf: + strip: true + interlace: "plane" + sampling-factor: "4:2:0" + adaptive: + strip: true + interlace: "plane" + gaussian-blur: "0.25" + adaptive-resize: "60" +schemes: + blur-sf: ["blur", "sf"] + adaptive-sf: ["adaptive", "sf"] + adaptive-blur: ["adaptive", "blur"] + singleton: ["adaptive"] +sampler: + files: 2 + folders: 1 +interaction: + tui: + per-item-delay: "1ms" +advanced: + abort-on-error: true + overwrite-on-collision: false + labels: + adhoc: ADHOC + legacy: .LEGACY + journal-suffix: journal + trash: TRASH + fake: .FAKE + supplement: SUPP + extensions: + suffixes-csv: "jpg,jpeg,png" + transforms-csv: lower + map: + executable: + program-name: dummy + timeout: "20s" + no-retries: 0 +logging: + max-size-in-mb: 10 + max-backups: 3 + max-age-in-days: 30 + level: info + time-format: "2006-01-02 15:04:05"