mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-09 17:18:45 +00:00
Dir embedded sync.Once directly and exposed a value-receiver GoString so
that pretty.Sprintf("%# v", Server) could render the path. That meant
every pretty-print copied the entire Dir along with its Once, and a
goroutine concurrently using the original (or any copy) for Path() could
hit a "sync: unlock of unlocked mutex" runtime fatal error. The failure
was reproduced deterministically on Windows CI when test-suite shuffle
ordering raced cache initialization (utils/cache/file_caches.go's
NewFileCache.func1 -> conf.CacheFolder.MustPath) against the
configuration-dump pretty.Sprintf in Load().
Drop the sync.Once entirely. Dir is now a plain {path, perm} value type,
and Path() calls os.MkdirAll on every invocation. MkdirAll is
idempotent, so repeated calls on an existing directory cost one stat
syscall — negligible for the few config paths read at startup and during
cache init.
This removes the entire class of bug:
- No Mutex, so copies (via reflection, pretty-print, etc.) are safe.
- No state pointer, so no nil-state defensive checks scattered across
methods, and no risk of two copies seeing different lifecycle state.
- go vet is happy with the value receivers — the //nolint:govet
suppression on GoString is gone.
Adds two regression tests in conf/dir_test.go:
- GoString renders Dir as a quoted path under pretty.Sprintf (and
does not leak the internal struct fields).
- Concurrent copy + Path() stress test, locking in the copy-safety
property in case the type ever grows non-trivial state again.
77 lines
2.2 KiB
Go
77 lines
2.2 KiB
Go
package conf
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// Dir wraps a directory path and creates the directory on demand. Dir is a
|
|
// plain value type — safe to copy, compare, and print via reflection-based
|
|
// formatters (pretty.Sprintf("%# v", ...)) without any concurrency hazards.
|
|
// Directory creation is delegated to os.MkdirAll on every Path() call;
|
|
// MkdirAll is idempotent, so repeated calls cost one stat syscall when the
|
|
// directory already exists.
|
|
type Dir struct {
|
|
path string
|
|
perm os.FileMode
|
|
}
|
|
|
|
// NewDir creates a new Dir with the given path and default permissions (os.ModePerm).
|
|
func NewDir(path string) Dir {
|
|
return Dir{path: path, perm: os.ModePerm}
|
|
}
|
|
|
|
// NewDirWithPerm creates a new Dir with the given path and permissions.
|
|
// A perm of 0 is treated as "default" and resolves to os.ModePerm at
|
|
// directory-creation time; pass an explicit non-zero mode to constrain the
|
|
// permissions.
|
|
func NewDirWithPerm(path string, perm os.FileMode) Dir {
|
|
return Dir{path: path, perm: perm}
|
|
}
|
|
|
|
// String returns the raw path without creating the directory. Satisfies fmt.Stringer.
|
|
func (d Dir) String() string {
|
|
return d.path
|
|
}
|
|
|
|
// Path ensures the directory exists and returns its path. Safe to call
|
|
// repeatedly; an empty path is returned as-is with no error.
|
|
func (d Dir) Path() (string, error) {
|
|
if d.path == "" {
|
|
return "", nil
|
|
}
|
|
if err := os.MkdirAll(d.path, cmp.Or(d.perm, os.ModePerm)); err != nil {
|
|
return d.path, fmt.Errorf("creating directory %q: %w", d.path, err)
|
|
}
|
|
return d.path, nil
|
|
}
|
|
|
|
// MustPath calls Path() and calls logFatal on error.
|
|
func (d Dir) MustPath() string {
|
|
path, err := d.Path()
|
|
if err != nil {
|
|
logFatal("creating directory:", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// GoString implements fmt.GoStringer so that %#v (used by pretty.Sprintf)
|
|
// prints the path string instead of the internal struct fields.
|
|
func (d Dir) GoString() string {
|
|
return fmt.Sprintf("%q", d.path)
|
|
}
|
|
|
|
// MarshalText returns the raw path bytes. No side effects.
|
|
func (d Dir) MarshalText() ([]byte, error) {
|
|
return []byte(d.path), nil
|
|
}
|
|
|
|
// UnmarshalText sets the path from bytes. No side effects.
|
|
func (d *Dir) UnmarshalText(text []byte) error {
|
|
d.path = string(text)
|
|
if d.perm == 0 {
|
|
d.perm = os.ModePerm
|
|
}
|
|
return nil
|
|
}
|