aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-09 18:28:47 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-09 18:28:47 -0700
commit631b7f1f99c2ff6295152210640a2f9b2c4f5a88 (patch)
treec9bc509610835ad5283928a4b317aa4ed228e13d /internal
parent4bd4c14daff4cb8807a91305239cae1e8544f276 (diff)
Resolve FileRoot per storage backend to keep object keys host-independent
LoadConfig unconditionally rewrote a relative FileRoot to an absolute path under the config dir. R2FileStore.key() then embedded that local path in every object key (e.g. Users/jhalter/.../config/Files/...), so bucket contents were tied to the host's directory layout and orphaned by moving the config dir or pointing another server at the bucket. FileRoot is a path within the selected file store's namespace, so only the backend selection in main knows how to resolve it: the os backend resolves relative values against the config dir as before, while the memory and r2 backends keep the configured value verbatim, yielding portable keys like Files/foo.txt. An absolute FileRoot combined with an object-store backend now logs a warning. WithConfig copies the config struct, so the option is appended after the backend selection mutates FileRoot rather than before.
Diffstat (limited to 'internal')
-rw-r--r--internal/mobius/config.go7
-rw-r--r--internal/mobius/config_test.go39
2 files changed, 41 insertions, 5 deletions
diff --git a/internal/mobius/config.go b/internal/mobius/config.go
index 6e5fb9f..171f6c7 100644
--- a/internal/mobius/config.go
+++ b/internal/mobius/config.go
@@ -52,10 +52,7 @@ func LoadConfig(path string) (*hotline.Config, error) {
return nil, fmt.Errorf("validate config: %v", err)
}
- // If the FileRoot is an absolute path, use it, otherwise treat as a relative path to the config dir.
- if !filepath.IsAbs(config.FileRoot) {
- config.FileRoot = filepath.Join(path, "../", config.FileRoot)
- }
-
+ // FileRoot is returned verbatim: it is a path within the selected file store's namespace, so
+ // only the caller knows how to resolve it (e.g. against the config dir for the OS backend).
return &config, nil
}
diff --git a/internal/mobius/config_test.go b/internal/mobius/config_test.go
index b76b16d..bed739d 100644
--- a/internal/mobius/config_test.go
+++ b/internal/mobius/config_test.go
@@ -41,6 +41,45 @@ FileRoot: "files"
}
}
+// TestLoadConfig_FileRootKeptVerbatim guards against LoadConfig resolving FileRoot to a host
+// filesystem path. FileRoot is a path within the selected file store's namespace; rewriting it to
+// a local absolute path here would leak the host's directory layout into object-store keys.
+func TestLoadConfig_FileRootKeptVerbatim(t *testing.T) {
+ tests := []struct {
+ name string
+ fileRoot string
+ }{
+ {"relative path", "files"},
+ {"nested relative path", "library/files"},
+ {"absolute path", "/srv/hotline/files"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ configContent := `
+Name: "Test Server"
+Description: "Test Description"
+FileRoot: "` + tt.fileRoot + `"
+`
+ configPath := filepath.Join(tmpDir, "config.yaml")
+ if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
+ t.Fatalf("Failed to write config file: %v", err)
+ }
+
+ config, err := LoadConfig(configPath)
+ if err != nil {
+ t.Fatalf("Expected no error, got: %v", err)
+ }
+
+ if config.FileRoot != tt.fileRoot {
+ t.Errorf("Expected FileRoot to be %q, got %q", tt.fileRoot, config.FileRoot)
+ }
+ })
+ }
+}
+
func TestLoadConfig_ValidBannerFileExtensions(t *testing.T) {
tests := []struct {
name string