mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Config: Support YAML filenames with alternative extensions #5304
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}}, {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
93
pkg/fs/config.go
Normal file
93
pkg/fs/config.go
Normal file
@@ -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
|
||||
}
|
||||
117
pkg/fs/config_test.go
Normal file
117
pkg/fs/config_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
1
pkg/fs/testdata/config/.foo.local
vendored
Normal file
1
pkg/fs/testdata/config/.foo.local
vendored
Normal file
@@ -0,0 +1 @@
|
||||
TEST=bar
|
||||
20
pkg/fs/testdata/config/settings.yml
vendored
Normal file
20
pkg/fs/testdata/config/settings.yml
vendored
Normal file
@@ -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
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user