Files
navidrome/core/storage/interface.go
Deluan Quintão 7fa13761d7 fix(scanner): resolve file symlinks with the production local storage FS (#5755)
* fix(scanner): resolve file symlinks with the production local storage FS

The symlink classification added for GHSA-r5qr-m328-qcf4 relied on
fs.ReadLink, but the local storage FS wraps os.DirFS behind the fs.FS
interface, hiding its ReadLinkFS implementation. Every resolution failed
at the first hop, so the scanner silently skipped ALL file symlinks,
regardless of target or the FollowSymlinks setting. Libraries made of
symlinks (e.g. shared-pool setups) lost all their tracks after
upgrading to 0.63.

The local storage now exposes full OS-level resolution (EvalSymlinks)
through a new optional storage.SymlinkResolverFS interface, which the
scanner prefers over the fs.ReadLink hop loop. This also classifies a
chain by its FINAL target even when it passes through an audio-named
intermediate outside the library, closing a bypass the hop loop had.

Regular (non-symlink) entries keep the same early-return path, so scan
performance is unaffected for normal libraries.

Fixes #5752

* fix(test): keep watcher specs off the real local storage

The watcher specs spawn watchLibrary goroutines that are not joined on
spec teardown. Now that the scanner test binary registers the file://
storage, those leaked goroutines reached newLocalStorage, which reads
conf.Server on construction, racing with the configtest cleanup that
restores the config snapshot (caught by CI's race detector). Point the
mock libraries at a fake storage scheme, which never touches the config
and does not support watching, so the goroutine exits immediately.

* fix(storage): reject invalid fs paths in ResolveSymlink

Defense-in-depth for the SymlinkResolverFS contract: names must be valid
fs.FS paths. A lexical ".." in the name would otherwise escape the
library root via filepath.Join cleaning. No current caller can produce
such a name (they come from ReadDir walks), but the guard enforces the
documented contract at the boundary.
2026-07-10 10:52:05 -04:00

34 lines
1.0 KiB
Go

package storage
import (
"context"
"io/fs"
"github.com/navidrome/navidrome/model/metadata"
)
type Storage interface {
FS() (MusicFS, error)
}
// MusicFS is an interface that extends the fs.FS interface with the ability to read tags from files
type MusicFS interface {
fs.FS
ReadTags(path ...string) (map[string]metadata.Info, error)
}
// SymlinkResolverFS is an optional interface for MusicFS implementations backed by a real
// filesystem. ResolveSymlink resolves the whole symlink chain of the named entry at the OS
// level and returns the final target's path — including targets outside the FS root, which
// fs.ReadLink-based resolution cannot follow.
type SymlinkResolverFS interface {
ResolveSymlink(name string) (string, error)
}
// Watcher is a storage with the ability watch the FS and notify changes
type Watcher interface {
// Start starts a watcher on the whole FS and returns a channel to send detected changes.
// The watcher must be stopped when the context is done.
Start(context.Context) (<-chan string, error)
}