Config: Fix fallback that loads defaults from config/defaults.yml #5325

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-11-20 18:42:06 +01:00
parent 2e85caa6b0
commit b45abbd0cd
4 changed files with 79 additions and 13 deletions

View File

@@ -10,6 +10,8 @@ import (
"strings"
"sync"
"github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@@ -264,17 +266,52 @@ func (c *Config) OptionsYaml() string {
return fs.Abs(c.options.OptionsYaml)
}
// DefaultsYaml resolves the default options YAML file. When
// PHOTOPRISM_DEFAULTS_YAML points to a readable file we use it; otherwise we
// fall back to `defaults.{yml,yaml}` inside the active config directory.
// This allows instances without `/etc/photoprism/defaults.yml` to
// load local defaults, e.g., in containerized environments.
func (c *Config) DefaultsYaml() string {
if !fs.FileExistsNotEmpty(c.options.DefaultsYaml) {
return fs.ConfigFilePath(c.ConfigPath(), "defaults", fs.ExtYml)
// configPath resolves the config path name from the CLI context.
func configPath(ctx *cli.Context) string {
if dir := ctx.String("config-path"); dir != "" {
return fs.Abs(dir)
}
return fs.Abs(c.options.DefaultsYaml)
storagePath := ctx.String("storage-path")
if storagePath == "" {
return ""
}
storagePath = fs.Abs(storagePath)
if fs.PathExists(filepath.Join(storagePath, fs.SettingsDir)) {
return filepath.Join(storagePath, fs.SettingsDir)
}
return filepath.Join(storagePath, fs.ConfigDir)
}
// defaultsYaml resolves the defaults file from CLI/env overrides and falls back
// to `defaults.{yml,yaml}` inside the active config directory when the override
// is missing or unreadable.
func defaultsYaml(ctx *cli.Context) string {
fileName := ctx.String("defaults-yaml")
if fileName != "" && fs.FileExistsNotEmpty(fileName) {
return fs.Abs(fileName)
}
fileName = fs.ConfigFilePath(configPath(ctx), "defaults", fs.ExtYml)
if fs.FileExistsNotEmpty(fileName) {
return fs.Abs(fileName)
}
return ""
}
// DefaultsYaml returns the defaults file path that was resolved during option
// initialization (CLI/env override first, then config-path fallback). Callers
// use this to locate the concrete defaults location without re-running the
// resolution logic.
func (c *Config) DefaultsYaml() string {
return c.options.DefaultsYaml
}
// HubConfigFile returns the backend API config filename, honoring either the

View File

@@ -9,6 +9,7 @@ import (
gc "github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@@ -572,3 +573,31 @@ func TestConfig_SettingsYamlDefaults(t *testing.T) {
assert.NotEqual(t, c.SettingsYaml(), name1)
assert.NotEqual(t, c.SettingsYaml(), name3)
}
func TestDefaultsYamlResolution(t *testing.T) {
t.Run("ExplicitFlag", func(t *testing.T) {
ctx := CliTestContext()
file := filepath.Join(t.TempDir(), "explicit-defaults.yml")
require.NoError(t, os.WriteFile(file, []byte("Test: true"), fs.ModeFile))
require.NoError(t, ctx.Set("defaults-yaml", file))
got := defaultsYaml(ctx)
require.Equal(t, fs.Abs(file), got)
})
t.Run("ConfigFallback", func(t *testing.T) {
ctx := CliTestContext()
configDir := filepath.Join(t.TempDir(), "cfg")
require.NoError(t, os.MkdirAll(configDir, fs.ModeDir))
file := filepath.Join(configDir, "defaults.yml")
require.NoError(t, os.WriteFile(file, []byte("SiteUrl: https://example.com"), fs.ModeFile))
require.NoError(t, ctx.Set("defaults-yaml", ""))
require.NoError(t, ctx.Set("config-path", configDir))
got := defaultsYaml(ctx)
require.Equal(t, fs.Abs(file), got)
})
t.Run("MissingReturnsEmpty", func(t *testing.T) {
ctx := CliTestContext()
require.NoError(t, ctx.Set("defaults-yaml", filepath.Join(t.TempDir(), "missing.yml")))
require.NoError(t, ctx.Set("config-path", t.TempDir()))
require.Equal(t, "", defaultsYaml(ctx))
})
}

View File

@@ -297,10 +297,8 @@ func NewOptions(ctx *cli.Context) *Options {
c.BackupAlbums = true
// Initialize options with the values from the "defaults.yml" file, if it exists.
if defaultsYaml := ctx.String("defaults-yaml"); defaultsYaml == "" {
log.Tracef("config: defaults file was not specified")
} else if c.DefaultsYaml = fs.Abs(defaultsYaml); !fs.FileExists(c.DefaultsYaml) {
log.Tracef("config: defaults file %s does not exist", clean.Log(c.DefaultsYaml))
if c.DefaultsYaml = defaultsYaml(ctx); !fs.FileExistsNotEmpty(c.DefaultsYaml) {
log.Tracef("config: defaults file is empty or missing")
} else if err := c.Load(c.DefaultsYaml); err != nil {
log.Warnf("config: failed loading defaults from %s (%s)", clean.Log(c.DefaultsYaml), err)
}

View File

@@ -410,6 +410,7 @@ func CliTestContext() *cli.Context {
globalSet.String("import-path", config.OriginalsPath, "doc")
globalSet.String("cache-path", config.OriginalsPath, "doc")
globalSet.String("temp-path", config.OriginalsPath, "doc")
globalSet.String("defaults-yaml", config.DefaultsYaml, "doc")
globalSet.String("cluster-uuid", config.ClusterUUID, "doc")
globalSet.String("backup-path", config.StoragePath, "doc")
globalSet.Int("backup-retain", config.BackupRetain, "doc")
@@ -446,6 +447,7 @@ func CliTestContext() *cli.Context {
LogErr(c.Set("import-path", config.ImportPath))
LogErr(c.Set("cache-path", config.CachePath))
LogErr(c.Set("temp-path", config.TempPath))
LogErr(c.Set("defaults-yaml", config.DefaultsYaml))
LogErr(c.Set("backup-path", config.BackupPath))
LogErr(c.Set("backup-retain", strconv.Itoa(config.BackupRetain)))
LogErr(c.Set("backup-schedule", config.BackupSchedule))