diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-09 18:28:47 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-09 18:28:47 -0700 |
| commit | 631b7f1f99c2ff6295152210640a2f9b2c4f5a88 (patch) | |
| tree | c9bc509610835ad5283928a4b317aa4ed228e13d /cmd | |
| parent | 4bd4c14daff4cb8807a91305239cae1e8544f276 (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 'cmd')
| -rw-r--r-- | cmd/mobius-hotline-server/main.go | 24 |
1 files changed, 22 insertions, 2 deletions
diff --git a/cmd/mobius-hotline-server/main.go b/cmd/mobius-hotline-server/main.go index 9e59fa8..52099ad 100644 --- a/cmd/mobius-hotline-server/main.go +++ b/cmd/mobius-hotline-server/main.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" "io" + "log/slog" "os" "os/signal" "path" @@ -102,7 +103,6 @@ func main() { hotline.WithInterface(*netInterface), hotline.WithLogger(slogger), hotline.WithPort(*basePort), - hotline.WithConfig(*config), } if tlsConfig != nil { opts = append(opts, hotline.WithTLS(tlsConfig, *tlsPort)) @@ -113,10 +113,17 @@ func main() { // the concrete FileStore and pass it via hotline.WithFileStore. switch *fileStoreBackend { case "os", "": - // Default OSFileStore is set by NewServer; nothing to do. + // Default OSFileStore is set by NewServer; nothing to do beyond resolving FileRoot, + // which is a host filesystem path for this backend only. Object-store backends treat + // FileRoot as a path within the store's own namespace and keep it as configured, so a + // relative FileRoot yields host-independent object keys. + if !filepath.IsAbs(config.FileRoot) { + config.FileRoot = filepath.Join(*configDir, config.FileRoot) + } case "memory": opts = append(opts, hotline.WithFileStore(hotline.NewMemFileStore())) slogger.Warn("Using in-memory file store; uploaded files are not persisted") + warnAbsoluteFileRoot(slogger, config.FileRoot) case "r2": r2Store, err := newR2FileStore(ctx) if err != nil { @@ -125,11 +132,16 @@ func main() { } opts = append(opts, hotline.WithFileStore(r2Store)) slogger.Info("Using Cloudflare R2 file store", "bucket", os.Getenv("R2_BUCKET")) + warnAbsoluteFileRoot(slogger, config.FileRoot) default: slogger.Error("Unknown file-store backend", "backend", *fileStoreBackend) os.Exit(1) } + // The config is passed by value, so this must come after the backend selection above, which + // resolves config.FileRoot for the chosen backend. + opts = append(opts, hotline.WithConfig(*config)) + srv, err := hotline.NewServer(opts...) if err != nil { slogger.Error("Error starting server", "err", err) @@ -287,6 +299,14 @@ type namedReloader struct { reloader mobius.Reloader } +// warnAbsoluteFileRoot flags an absolute FileRoot when an object-store backend is selected: the +// path is used verbatim as the key namespace, so host filesystem layout would leak into every key. +func warnAbsoluteFileRoot(logger *slog.Logger, fileRoot string) { + if filepath.IsAbs(fileRoot) { + logger.Warn("FileRoot is an absolute path; object keys will embed it verbatim. Use a relative FileRoot for host-independent keys.", "FileRoot", fileRoot) + } +} + // findConfigPath searches for an existing config directory from the predefined search order. // Returns the first directory that exists, or falls back to "config" as the default. func findConfigPath() string { |