From d6f0e808eb1fed8ef25a25d865b8bae8648afa83 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sun, 2 Nov 2025 11:33:40 +0100 Subject: [PATCH] Config: Support YAML filenames with alternative extensions #5304 Signed-off-by: Michael Mayer --- AGENTS.md | 3 +- CODEMAP.md | 3 +- internal/config/config_storage.go | 24 ++++-- internal/config/config_vision.go | 10 +-- internal/config/flags.go | 14 ++-- internal/config/test.go | 4 +- pkg/fs/config.go | 93 ++++++++++++++++++++++ pkg/fs/config_test.go | 117 ++++++++++++++++++++++++++++ pkg/fs/directories_test.go | 2 +- pkg/fs/file_ext.go | 103 +++++++++++++----------- pkg/fs/testdata/config/.foo.local | 1 + pkg/fs/testdata/config/settings.yml | 20 +++++ pkg/fs/walk_test.go | 6 +- pkg/fs/yaml.go | 25 ------ pkg/fs/yaml_test.go | 63 --------------- 15 files changed, 329 insertions(+), 159 deletions(-) create mode 100644 pkg/fs/config.go create mode 100644 pkg/fs/config_test.go create mode 100644 pkg/fs/testdata/config/.foo.local create mode 100644 pkg/fs/testdata/config/settings.yml delete mode 100644 pkg/fs/yaml.go delete mode 100644 pkg/fs/yaml_test.go diff --git a/AGENTS.md b/AGENTS.md index cde998add..884e8a57b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # PhotoPrism® Repository Guidelines -**Last Updated:** October 28, 2025 +**Last Updated:** November 2, 2025 ## Purpose @@ -331,6 +331,7 @@ Note: Across our public documentation, official images, and in production, the c ### API & Config Changes - Respect precedence: `options.yml` overrides CLI/env values, which override defaults. When adding a new option, update `internal/config/options.go` (yaml/flag tags), register it in `internal/config/flags.go`, expose a getter, surface it in `*config.Report()`, and write generated values back to `options.yml` by setting `c.options.OptionsYaml` before persisting. Use `CliTestContext` in `internal/config/test.go` to exercise new flags. +- Use `pkg/fs.ConfigFilePath` when you need a config filename so existing `.yml` files remain valid and new installs can adopt `.yaml` transparently (the helper also covers other paired extensions such as `.toml`/`.tml`). - When touching configuration in Go code, use the public accessors on `*config.Config` (e.g. `Config.JWKSUrl()`, `Config.SetJWKSUrl()`, `Config.ClusterUUID()`) instead of mutating `Config.Options()` directly; reserve raw option tweaks for test fixtures only. - When introducing new metadata sources (e.g., `SrcOllama`, `SrcOpenAI`), define them in both `internal/entity/src.go` and the frontend lookup tables (`frontend/src/common/util.js`) so UI badges and server priorities stay aligned. - Vision worker scheduling is controlled via `VisionSchedule` / `VisionFilter` and the `Run` property set in `vision.yml`. Utilities like `vision.FilterModels` and `entity.Photo.ShouldGenerateLabels/Caption` help decide when work is required before loading media files. diff --git a/CODEMAP.md b/CODEMAP.md index 1028d11fc..78de2f12d 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1,6 +1,6 @@ PhotoPrism — Backend CODEMAP -**Last Updated:** October 29, 2025 +**Last Updated:** November 2, 2025 Purpose - Give agents and contributors a fast, reliable map of where things live and how they fit together, so you can add features, fix bugs, and write tests without spelunking. @@ -142,6 +142,7 @@ Common How‑Tos - Expose a getter (e.g., in `config_server.go` or topic file) - Append to `rows` in `*config.Report()` after the same option as in `options.go` - If value must persist, write back to `options.yml` and reload into memory + - When you need the path to defaults/options/settings files, call `pkg/fs.ConfigFilePath` so `.yml` and `.yaml` stay interchangeable. - Tests: cover CLI/env/file precedence (see `internal/config/test.go` helpers) - Touch the DB schema diff --git a/internal/config/config_storage.go b/internal/config/config_storage.go index d728fb297..a18abfb73 100644 --- a/internal/config/config_storage.go +++ b/internal/config/config_storage.go @@ -253,12 +253,14 @@ func (c *Config) ConfigPath() string { return fs.Abs(c.options.ConfigPath) } -// OptionsYaml returns the config options YAML filename. +// OptionsYaml returns the absolute path to the options configuration file. +// It relies on fs.ConfigFilePath so legacy `.yml` files keep working while +// newly created instances may use `.yaml` without additional wiring. func (c *Config) OptionsYaml() string { configPath := c.ConfigPath() if c.options.OptionsYaml == "" { - return filepath.Join(configPath, "options.yml") + return fs.ConfigFilePath(configPath, "options", fs.ExtYml) } return fs.Abs(c.options.OptionsYaml) @@ -269,17 +271,23 @@ func (c *Config) DefaultsYaml() string { return fs.Abs(c.options.DefaultsYaml) } -// HubConfigFile returns the backend api config file name. +// HubConfigFile returns the backend API config filename, honoring either the +// traditional `.yml` suffix or an existing `.yaml` variant in the config +// directory. func (c *Config) HubConfigFile() string { - return filepath.Join(c.ConfigPath(), "hub.yml") + return fs.ConfigFilePath(c.ConfigPath(), "hub", fs.ExtYml) } -// SettingsYaml returns the settings YAML filename. +// SettingsYaml returns the path to the UI settings file. Like other helpers it +// defers to fs.ConfigFilePath so administrators can store the file as +// `settings.yml` or `settings.yaml`. func (c *Config) SettingsYaml() string { - return filepath.Join(c.ConfigPath(), "settings.yml") + return fs.ConfigFilePath(c.ConfigPath(), "settings", fs.ExtYml) } -// SettingsYamlDefaults returns the default settings YAML filename. +// SettingsYamlDefaults returns the defaults file that should seed new settings +// files. When both `.yml` and `.yaml` exist, the helper mirrors +// SettingsYaml()'s selection logic to keep behavior consistent. func (c *Config) SettingsYamlDefaults(settingsYml string) string { if settingsYml != "" && fs.FileExists(settingsYml) { // Use regular settings YAML file. @@ -287,7 +295,7 @@ func (c *Config) SettingsYamlDefaults(settingsYml string) string { // Use regular settings YAML file. } else if dir := filepath.Dir(defaultsYml); dir == "" || dir == "." { // Use regular settings YAML file. - } else if fileName := filepath.Join(dir, "settings.yml"); settingsYml == "" || fs.FileExistsNotEmpty(fileName) { + } else if fileName := fs.ConfigFilePath(dir, "settings", fs.ExtYml); settingsYml == "" || fs.FileExistsNotEmpty(fileName) { // Use default settings YAML file. return fileName } diff --git a/internal/config/config_vision.go b/internal/config/config_vision.go index 2bf9b764f..0c1629976 100644 --- a/internal/config/config_vision.go +++ b/internal/config/config_vision.go @@ -10,11 +10,9 @@ import ( "github.com/photoprism/photoprism/pkg/fs" ) -// VisionYaml returns the vision config YAML filename. -// -// TODO: Call fs.YamlFilePath to use ".yaml" extension for new YAML files, unless a .yml" file already exists. -// -// return fs.YamlFilePath("vision", c.ConfigPath(), c.options.VisionYaml) +// VisionYaml returns the path to the computer-vision configuration file, +// preferring an explicit override and otherwise letting fs.ConfigFilePath pick +// the right `.yml`/`.yaml` variant in the config directory. func (c *Config) VisionYaml() string { if c == nil { return "" @@ -23,7 +21,7 @@ func (c *Config) VisionYaml() string { if c.options.VisionYaml != "" { return fs.Abs(c.options.VisionYaml) } else { - return filepath.Join(c.ConfigPath(), "vision.yml") + return fs.ConfigFilePath(c.ConfigPath(), "vision", fs.ExtYml) } } diff --git a/internal/config/flags.go b/internal/config/flags.go index 8c330c30e..e09d8d925 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -198,16 +198,20 @@ var Flags = CliFlags{ }}, { Flag: &cli.PathFlag{ Name: "config-path", - Aliases: []string{"c"}, + Aliases: []string{"config", "c"}, Usage: "config storage `PATH` or options.yml filename, values in this file override CLI flags and environment variables if present", EnvVars: EnvVars("CONFIG_PATH"), TakesFile: true, }}, { Flag: &cli.StringFlag{ - Name: "defaults-yaml", - Aliases: []string{"y"}, - Usage: "loads default config values from `FILENAME` if it exists, does not override CLI flags or environment variables", - Value: "/etc/photoprism/defaults.yml", + Name: "defaults-yaml", + // Alias was changed from "y" to "defaults" since "y" is a reserved alias for "yes". + // Since our examples and end-user docs for this flag don't include any aliases, the change should be safe. + Aliases: []string{"defaults"}, + Usage: "loads default config values from `FILENAME` if it exists, does not override CLI flags or environment variables", + // fs.ConfigFilePath lets existing installations keep a defaults.yml file + // while new deployments may drop in defaults.yaml without updating the flag. + Value: fs.ConfigFilePath("/etc/photoprism", "defaults", fs.ExtYml), EnvVars: EnvVars("DEFAULTS_YAML"), TakesFile: true, }}, { diff --git a/internal/config/test.go b/internal/config/test.go index 6a35f3563..e7cd48bb0 100644 --- a/internal/config/test.go +++ b/internal/config/test.go @@ -331,7 +331,9 @@ func NewTestConfig(dbName string) *Config { log.Fatalf("config: %s", err.Error()) } - if err := s.Save(filepath.Join(c.ConfigPath(), "settings.yml")); err != nil { + // Save settings next to the test config path, reusing any existing + // `.yaml`/`.yml` variant so the tests mirror production behavior. + if err := s.Save(fs.ConfigFilePath(c.ConfigPath(), "settings", fs.ExtYml)); err != nil { log.Fatalf("config: %s", err.Error()) } diff --git a/pkg/fs/config.go b/pkg/fs/config.go new file mode 100644 index 000000000..41cf11f2a --- /dev/null +++ b/pkg/fs/config.go @@ -0,0 +1,93 @@ +package fs + +import ( + "os" + "path/filepath" +) + +// ConfigFilePath builds an absolute path for a configuration file using the +// provided directory, base name, and preferred extension. If a file with the +// preferred extension already exists, that path is returned. Otherwise the +// helper searches for known sibling extensions (for example `.yaml` vs +// `.yml`) so callers transparently reuse whichever variant an admin created. +// When no matching file exists, the preferred extension is appended. +func ConfigFilePath(configPath, baseName, defaultExt string) string { + // Return empty file path is no file name was specified. + if baseName == "" { + return "" + } + + // Search file in current directory if configPath is emtpy. + if configPath == "" { + if dir, err := os.Getwd(); err == nil && dir != "" { + configPath = dir + } + } + + defaultPath := filepath.Join(configPath, baseName+defaultExt) + + // If the default file exists, return its file path and look no further. + if FileExists(defaultPath) { + return defaultPath + } + + // If the default file does not exist, check for a file + // with an alternative extension that already exists. + switch defaultExt { + case ExtNone: + if altPath := filepath.Join(configPath, baseName+ExtLocal); FileExists(altPath) { + return altPath + } + case ExtYml: + if altPath := filepath.Join(configPath, baseName+ExtYaml); FileExists(altPath) { + return altPath + } + case ExtYaml: + if altPath := filepath.Join(configPath, baseName+ExtYml); FileExists(altPath) { + return altPath + } + case ExtGeoJson: + if altPath := filepath.Join(configPath, baseName+ExtJson); FileExists(altPath) { + return altPath + } + case ExtTml: + if altPath := filepath.Join(configPath, baseName+ExtToml); FileExists(altPath) { + return altPath + } + case ExtToml: + if altPath := filepath.Join(configPath, baseName+ExtTml); FileExists(altPath) { + return altPath + } + case ExtMd: + if altPath := filepath.Join(configPath, baseName+ExtMarkdown); FileExists(altPath) { + return altPath + } + case ExtMarkdown: + if altPath := filepath.Join(configPath, baseName+ExtMd); FileExists(altPath) { + return altPath + } + case ExtHTML: + if altPath := filepath.Join(configPath, baseName+ExtHTM); FileExists(altPath) { + return altPath + } else if altPath = filepath.Join(configPath, baseName+ExtXHTML); FileExists(altPath) { + return altPath + } + case ExtHTM: + if altPath := filepath.Join(configPath, baseName+ExtHTML); FileExists(altPath) { + return altPath + } else if altPath = filepath.Join(configPath, baseName+ExtXHTML); FileExists(altPath) { + return altPath + } + case ExtPb: + if altPath := filepath.Join(configPath, baseName+ExtProto); FileExists(altPath) { + return altPath + } + case ExtProto: + if altPath := filepath.Join(configPath, baseName+ExtPb); FileExists(altPath) { + return altPath + } + } + + // Return default config file path. + return defaultPath +} diff --git a/pkg/fs/config_test.go b/pkg/fs/config_test.go new file mode 100644 index 000000000..cb76e5769 --- /dev/null +++ b/pkg/fs/config_test.go @@ -0,0 +1,117 @@ +package fs + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigFilePath(t *testing.T) { + pwd, _ := os.Getwd() + + t.Run("EmptyName", func(t *testing.T) { + assert.Equal(t, "", ConfigFilePath("", "", "")) + assert.Equal(t, "", ConfigFilePath("", "", ExtYml)) + assert.Equal(t, "", ConfigFilePath("./testdata", "", ExtYml)) + }) + t.Run("EmptyPath", func(t *testing.T) { + assert.Equal(t, filepath.Join(pwd, "example.json"), ConfigFilePath("", "example", ExtJson)) + assert.Equal(t, filepath.Join(pwd, "example.yml"), ConfigFilePath("", "example", ExtYml)) + assert.Equal(t, filepath.Join(pwd, "example.yaml"), ConfigFilePath("", "example", ExtYaml)) + }) + t.Run("ExtNone", func(t *testing.T) { + configPath := "testdata/config" + envPath := filepath.Join(configPath, ".env") + fooPath := filepath.Join(configPath, ".foo") + fooPathLocal := fooPath + ExtLocal + + assert.Equal(t, envPath, ConfigFilePath(configPath, ".env", ExtNone)) + assert.Equal(t, fooPathLocal, ConfigFilePath(configPath, ".foo", ExtNone)) + }) + t.Run("YmlFileExists", func(t *testing.T) { + dir := t.TempDir() + name := "app-config" + + // Create .yml file + ymlPath := filepath.Join(dir, name+ExtYml) + err := os.WriteFile(ymlPath, []byte("foo: bar\n"), ModeFile) + if err != nil { + t.Fatalf("write %s: %v", ymlPath, err) + } + + assert.Equal(t, ymlPath, ConfigFilePath(dir, name, ExtYml)) + assert.Equal(t, ymlPath, ConfigFilePath(dir, name, ExtYaml)) + }) + t.Run("YamlFilesMissing", func(t *testing.T) { + dir := t.TempDir() + name := "settings" + + // Ensure .yml does not exist; do not create it. + ymlPath := filepath.Join(dir, name+ExtYml) + yamlPath := filepath.Join(dir, name+ExtYaml) + + assert.Equal(t, ymlPath, ConfigFilePath(dir, name, ExtYml)) + assert.Equal(t, yamlPath, ConfigFilePath(dir, name, ExtYaml)) + }) + t.Run("BothYamlFilesExist", func(t *testing.T) { + dir := t.TempDir() + name := "prefs" + + // Create both files. + ymlPath := filepath.Join(dir, name+ExtYml) + yamlPath := filepath.Join(dir, name+ExtYaml) + + if err := os.WriteFile(ymlPath, []byte("a: 1\n"), ModeFile); err != nil { + t.Fatalf("write %s: %v", ymlPath, err) + } + + if err := os.WriteFile(yamlPath, []byte("a: 2\n"), ModeFile); err != nil { + t.Fatalf("write %s: %v", yamlPath, err) + } + + assert.Equal(t, ymlPath, ConfigFilePath(dir, name, ExtYml)) + assert.Equal(t, yamlPath, ConfigFilePath(dir, name, ExtYaml)) + }) + t.Run("AlternateExtensions", func(t *testing.T) { + tests := []struct { + name string + defaultExt string + altExts []string + expectPathIdx int + }{ + {name: "geo", defaultExt: ExtGeoJson, altExts: []string{ExtJson}}, + {name: "tml", defaultExt: ExtTml, altExts: []string{ExtToml}}, + {name: "toml", defaultExt: ExtToml, altExts: []string{ExtTml}}, + {name: "md", defaultExt: ExtMd, altExts: []string{ExtMarkdown}}, + {name: "markdown", defaultExt: ExtMarkdown, altExts: []string{ExtMd}}, + {name: "html", defaultExt: ExtHTML, altExts: []string{ExtHTM, ExtXHTML}}, + {name: "html-xhtml", defaultExt: ExtHTML, altExts: []string{ExtXHTML}, expectPathIdx: 0}, + {name: "htm", defaultExt: ExtHTM, altExts: []string{ExtHTML, ExtXHTML}}, + {name: "htm-xhtml", defaultExt: ExtHTM, altExts: []string{ExtXHTML}, expectPathIdx: 0}, + {name: "pb", defaultExt: ExtPb, altExts: []string{ExtProto}}, + {name: "proto", defaultExt: ExtProto, altExts: []string{ExtPb}}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + base := "config-" + tc.name + + var paths []string + for _, ext := range tc.altExts { + path := filepath.Join(dir, base+ext) + if err := os.WriteFile(path, []byte(ext+" file"), ModeFile); err != nil { + t.Fatalf("write %s: %v", path, err) + } + paths = append(paths, path) + } + + expected := paths[tc.expectPathIdx] + got := ConfigFilePath(dir, base, tc.defaultExt) + assert.Equal(t, expected, got) + }) + } + }) +} diff --git a/pkg/fs/directories_test.go b/pkg/fs/directories_test.go index 9e6940a75..c7abf9c2c 100644 --- a/pkg/fs/directories_test.go +++ b/pkg/fs/directories_test.go @@ -15,7 +15,7 @@ func TestDirs(t *testing.T) { t.Fatal(err) } - assert.Len(t, result, 8) + assert.Len(t, result, 9) assert.Contains(t, result, "/directory") assert.Contains(t, result, "/directory/subdirectory") assert.Contains(t, result, "/directory/subdirectory/animals") diff --git a/pkg/fs/file_ext.go b/pkg/fs/file_ext.go index 191e37c38..1ad82c5cd 100644 --- a/pkg/fs/file_ext.go +++ b/pkg/fs/file_ext.go @@ -6,52 +6,63 @@ import ( ) const ( - ExtPDF = ".pdf" - ExtJpeg = ".jpg" - ExtPng = ".png" - ExtDng = ".dng" - ExtThm = ".thm" - ExtH264 = ".h264" - ExtAvc = ".avc" - ExtAvc1 = ".avc1" - ExtDva = ".dva" - ExtDva1 = ".dva1" - ExtAvc2 = ".avc2" - ExtAvc3 = ".avc3" - ExtDvav = ".dvav" - ExtAvc10 = ".avc10" - ExtH265 = ".h265" - ExtHvc = ".hvc" - ExtHvc1 = ".hvc1" - ExtDvh = ".dvh" - ExtDvh1 = ".dvh1" - ExtHvc2 = ".hvc2" - ExtHvc3 = ".hvc3" - ExtHvc10 = ".hvc10" - ExtHevc = ".hevc" - ExtHevc10 = ".hevc10" - ExtHev = ".hev" - ExtDvhe = ".dvhe" - ExtHev1 = ".hev1" - ExtHev2 = ".hev2" - ExtHev3 = ".hev3" - ExtHev10 = ".hev10" - ExtH266 = ".h266" - ExtVvc = ".vvc" - ExtVvc1 = ".vvc1" - ExtEvc = ".evc" - ExtEvc1 = ".evc1" - ExtMp4 = ".mp4" - ExtMov = ".mov" - ExtQT = ".qt" - ExtYml = ".yml" - ExtYaml = ".yaml" - ExtJson = ".json" - ExtXml = ".xml" - ExtXMP = ".xmp" - ExtTxt = ".txt" - ExtMd = ".md" - ExtZip = ".zip" + ExtNone = "" + ExtLocal = ".local" + ExtPDF = ".pdf" + ExtJpeg = ".jpg" + ExtPng = ".png" + ExtDng = ".dng" + ExtThm = ".thm" + ExtH264 = ".h264" + ExtAvc = ".avc" + ExtAvc1 = ".avc1" + ExtDva = ".dva" + ExtDva1 = ".dva1" + ExtAvc2 = ".avc2" + ExtAvc3 = ".avc3" + ExtDvav = ".dvav" + ExtAvc10 = ".avc10" + ExtH265 = ".h265" + ExtHvc = ".hvc" + ExtHvc1 = ".hvc1" + ExtDvh = ".dvh" + ExtDvh1 = ".dvh1" + ExtHvc2 = ".hvc2" + ExtHvc3 = ".hvc3" + ExtHvc10 = ".hvc10" + ExtHevc = ".hevc" + ExtHevc10 = ".hevc10" + ExtHev = ".hev" + ExtDvhe = ".dvhe" + ExtHev1 = ".hev1" + ExtHev2 = ".hev2" + ExtHev3 = ".hev3" + ExtHev10 = ".hev10" + ExtH266 = ".h266" + ExtVvc = ".vvc" + ExtVvc1 = ".vvc1" + ExtEvc = ".evc" + ExtEvc1 = ".evc1" + ExtMp4 = ".mp4" + ExtMov = ".mov" + ExtQT = ".qt" + ExtYml = ".yml" + ExtYaml = ".yaml" + ExtTml = ".tml" + ExtToml = ".toml" + ExtJson = ".json" + ExtGeoJson = ".geojson" + ExtXml = ".xml" + ExtXMP = ".xmp" + ExtHTM = ".htm" + ExtHTML = ".html" + ExtXHTML = ".xhtml" + ExtTxt = ".txt" + ExtMd = ".md" + ExtMarkdown = ".markdown" + ExtPb = ".pb" + ExtProto = ".proto" + ExtZip = ".zip" ) // Ext returns all extension of a file name including the dots. diff --git a/pkg/fs/testdata/config/.foo.local b/pkg/fs/testdata/config/.foo.local new file mode 100644 index 000000000..9a02a5bf5 --- /dev/null +++ b/pkg/fs/testdata/config/.foo.local @@ -0,0 +1 @@ +TEST=bar \ No newline at end of file diff --git a/pkg/fs/testdata/config/settings.yml b/pkg/fs/testdata/config/settings.yml new file mode 100644 index 000000000..86f405a92 --- /dev/null +++ b/pkg/fs/testdata/config/settings.yml @@ -0,0 +1,20 @@ +UI: + Scrollbar: true + Zoom: false + Theme: custom + Language: en + TimeZone: Europe/Berlin + StartPage: default +Albums: + Download: + Name: share + Disabled: false + Originals: true + MediaRaw: false + MediaSidecar: false + Order: + Album: oldest + Folder: added + Moment: oldest + State: newest + Month: oldest diff --git a/pkg/fs/walk_test.go b/pkg/fs/walk_test.go index 728cf6775..183b85c17 100644 --- a/pkg/fs/walk_test.go +++ b/pkg/fs/walk_test.go @@ -108,8 +108,10 @@ func TestSkipWalk(t *testing.T) { assert.NoError(t, err) assert.Contains(t, dirs, "testdata/directory/subdirectory/.hiddendir") - expected := []string{ + mustSkip := []string{ "testdata", + "testdata/config", + "testdata/config/.foo.local", "testdata/directory", "testdata/directory/.ppignore", "testdata/directory/bar.txt", @@ -131,6 +133,6 @@ func TestSkipWalk(t *testing.T) { "testdata/originals", } - assert.Equal(t, expected, skipped) + assert.Equal(t, mustSkip, skipped) }) } diff --git a/pkg/fs/yaml.go b/pkg/fs/yaml.go deleted file mode 100644 index 6edbe5230..000000000 --- a/pkg/fs/yaml.go +++ /dev/null @@ -1,25 +0,0 @@ -package fs - -import ( - "path/filepath" -) - -// YamlFilePath returns the appropriate YAML file name to use. This can be either -// the absolute path of the custom file name passed as the first argument, the default -// name with a ".yml" extension if it already exists, or the default name with a ".yaml" -// extension if a ".yml" file does not exist. This facilitates the transition from ".yml" -// to the new default YAML file extension, ".yaml". -func YamlFilePath(yamlName, yamlDir, customFileName string) string { - // Return custom file name with absolute path. - if customFileName != "" { - return Abs(customFileName) - } - - // If the file already exists, return the file path with the legacy "*.yml" extension. - if filePathYml := filepath.Join(yamlDir, yamlName+ExtYml); FileExists(filePathYml) { - return filePathYml - } - - // Return file path with ".yaml" extension. - return filepath.Join(yamlDir, yamlName+ExtYaml) -} diff --git a/pkg/fs/yaml_test.go b/pkg/fs/yaml_test.go deleted file mode 100644 index 8812685b6..000000000 --- a/pkg/fs/yaml_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package fs - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Tests for YamlFilePath in yaml.go using subtests. -func TestYamlFilePath(t *testing.T) { - t.Run("CustomPath", func(t *testing.T) { - tmp := t.TempDir() - rel := filepath.Join(tmp, "custom", "config.yaml") - // Do not create the file; function should simply return Abs(customFileName). - expected := Abs(rel) - got := YamlFilePath("", "", rel) - assert.Equal(t, expected, got) - }) - t.Run("PreferYmlIfExists", func(t *testing.T) { - dir := t.TempDir() - name := "app-config" - - // Create .yml file - ymlPath := filepath.Join(dir, name+ExtYml) - err := os.WriteFile(ymlPath, []byte("foo: bar\n"), ModeFile) - if err != nil { - t.Fatalf("write %s: %v", ymlPath, err) - } - - got := YamlFilePath(name, dir, "") - assert.Equal(t, ymlPath, got) - }) - t.Run("DefaultYamlWhenYmlMissing", func(t *testing.T) { - dir := t.TempDir() - name := "settings" - - // Ensure .yml does not exist; do not create it. - expected := filepath.Join(dir, name+ExtYaml) - got := YamlFilePath(name, dir, "") - assert.Equal(t, expected, got) - }) - t.Run("BothExistReturnsYml", func(t *testing.T) { - dir := t.TempDir() - name := "prefs" - - // Create both files - ymlPath := filepath.Join(dir, name+ExtYml) - yamlPath := filepath.Join(dir, name+ExtYaml) - - if err := os.WriteFile(ymlPath, []byte("a: 1\n"), ModeFile); err != nil { - t.Fatalf("write %s: %v", ymlPath, err) - } - - if err := os.WriteFile(yamlPath, []byte("a: 2\n"), ModeFile); err != nil { - t.Fatalf("write %s: %v", yamlPath, err) - } - - got := YamlFilePath(name, dir, "") - assert.Equal(t, ymlPath, got) - }) -}