* 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.
* feat(scanner): add Scanner.IgnoreDotFolders to allow scanning dot folders
Adds a new Scanner.IgnoreDotFolders option (default true, preserving current
behavior) that, when disabled, lets the scanner traverse folders whose names
start with a dot, such as albums like ".Hack Sign Original Soundtrack".
Previously every dot-prefixed entry was skipped unconditionally before the
directory check, so such album folders were never indexed. The walk loop now
determines whether an entry is a directory first, then skips dot-prefixed files
always and dot-prefixed folders only when IgnoreDotFolders is enabled. Special
system directories are still ignored in all cases via the ignoredDirs blocklist,
which now also lists .git explicitly (it was previously caught only by the
generic dot-prefix rule). isDirIgnored is reduced to a pure blocklist check and
the name-only predicate is renamed from isEntryIgnored to isDotEntry.
* refactor(scanner): centralize entry ignore policy in isIgnoredEntry
Consolidates the directory-entry ignore decision into a single isIgnoredEntry
helper so the walk loop reads as pure dispatch (recurse into directories,
classify files) instead of interleaving ignore policy with traversal.
The dot-prefix rule and the ignoredDirs blocklist were previously checked in two
separate places inside loadDir's loop. They are now combined behind one helper
that takes the entry name and whether it is a directory. isDirIgnored remains a
standalone blocklist predicate because the file watcher (isIgnoredPath) calls it
directly. Adds focused unit tests for isIgnoredEntry covering both states of
Scanner.IgnoreDotFolders. No behavior change.
* fix(scanner): stop watcher from scanning ignored dot folders
A filesystem change inside a dot-prefixed folder (e.g. ".Hidden Album/track.mp3")
previously triggered a targeted scan of that folder, because isIgnoredPath let
all media files through and only checked the changed path's parent against the
ignore list (which never matched for nested paths due to the trailing separator).
With Scanner.IgnoreDotFolders enabled this caused the folder to be indexed even
though a full scan would skip it.
The watcher now ignores any change located inside an ignored directory via a new
isUnderIgnoredDir helper that reuses the same isIgnoredEntry policy as the scan
walk, and checks the entry itself with isIgnoredEntry instead of the parent dir.
This keeps the watcher and the scanner in agreement for both dot-folders (gated
by the flag) and the ignoredDirs blocklist. Adds direct table tests for
isIgnoredPath covering both states of the option.
* fix(scanner): exclude '.' from isDotEntry and ignore dot media files in watcher
Addresses code review feedback:
- isDotEntry now excludes the literal "." reference, matching its documentation.
Previously isDotEntry(".") returned true, which could mark a path component as
a dot-entry in the watcher.
- isIgnoredPath now ignores dot-prefixed media files (e.g. ".hidden.mp3") so the
watcher matches the scanner, which always skips dot files. Non-media leaves
still fall through to the directory-assumption check, so dot-folders continue
to follow Scanner.IgnoreDotFolders.
Adds unit tests for isDotEntry and watcher coverage for dot-prefixed media files.
* docs(scanner): clarify isDotEntry multi-dot exclusion and add test
Expand the isDotEntry comment to explain why names with two or more
leading dots (".."/"..foo"/"...Album") are not treated as hidden, which
surprised a reviewer testing dot-folder scanning. Add a "..foo" test case
to make the two-leading-dots behavior explicit.
Claude-Session: https://claude.ai/code/session_012STiDTyhZAdH8JNtdNe8L1
The scanner classified a file by the name of the directory entry, so a
symlink was treated as audio/image/playlist based on the link name rather
than what it actually points to. Now symlinks are fully resolved (following
the whole chain) and classified by the resolved target's extension, so a
symlink to a non-audio file is no longer imported as a track.
This also makes Scanner.FollowSymlinks apply to file symlinks, not just
directory symlinks as before. The default stays true, so following symlinks
to real audio files (second drives, shared folders, etc.) keeps working.
Adds trace logging for symlink resolution decisions and real-fs regression
tests covering multi-level symlink chains.
* feat: Add selective folder scanning capability
Implement targeted scanning of specific library/folder pairs without
full recursion. This enables efficient rescanning of individual folders
when changes are detected, significantly reducing scan time for large
libraries.
Key changes:
- Add ScanTarget struct and ScanFolders API to Scanner interface
- Implement CLI flag --targets for specifying libraryID:folderPath pairs
- Add FolderRepository.GetByPaths() for batch folder info retrieval
- Create loadSpecificFolders() for non-recursive directory loading
- Scope GC operations to affected libraries only (with TODO for full impl)
- Add comprehensive tests for selective scanning behavior
The selective scan:
- Only processes specified folders (no subdirectory recursion)
- Maintains library isolation
- Runs full maintenance pipeline scoped to affected libraries
- Supports both full and quick scan modes
Examples:
navidrome scan --targets "1:Music/Rock,1:Music/Jazz"
navidrome scan --full --targets "2:Classical"
* feat(folder): replace GetByPaths with GetFolderUpdateInfo for improved folder updates retrieval
Signed-off-by: Deluan <deluan@navidrome.org>
* test: update parseTargets test to handle folder names with spaces
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(folder): remove unused LibraryPath struct and update GC logging message
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(folder): enhance external scanner to support target-specific scanning
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): simplify scanner methods
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(watcher): implement folder scanning notifications with deduplication
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(watcher): add resolveFolderPath function for testability
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(watcher): implement path ignoring based on .ndignore patterns
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): implement IgnoreChecker for managing .ndignore patterns
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(ignore_checker): rename scanner to lineScanner for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): enhance ScanTarget struct with String method for better target representation
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): validate library ID to prevent negative values
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): simplify GC method by removing library ID parameter
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scanner): update folder scanning to include all descendants of specified folders
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(subsonic): allow selective scan in the /startScan endpoint
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): update CallScan to handle specific library/folder pairs
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): streamline scanning logic by removing scanAll method
Signed-off-by: Deluan <deluan@navidrome.org>
* test: enhance mockScanner for thread safety and improve test reliability
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): move scanner.ScanTarget to model.ScanTarget
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: move scanner types to model,implement MockScanner
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): update scanner interface and implementations to use model.Scanner
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(folder_repository): normalize target path handling by using filepath.Clean
Signed-off-by: Deluan <deluan@navidrome.org>
* test(folder_repository): add comprehensive tests for folder retrieval and child exclusion
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): simplify selective scan logic using slice.Filter
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): streamline phase folder and album creation by removing unnecessary library parameter
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): move initialization logic from phase_1 to the scanner itself
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(tests): rename selective scan test file to scanner_selective_test.go
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(configuration): add DevSelectiveWatcher configuration option
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(watcher): enhance .ndignore handling for folder deletions and file changes
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(scanner): comments
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(scanner): enhance walkDirTree to support target folder scanning
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner, watcher): handle errors when pushing ignore patterns for folders
Signed-off-by: Deluan <deluan@navidrome.org>
* Update scanner/phase_1_folders.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* refactor(scanner): replace parseTargets function with direct call to scanner.ParseTargets
Signed-off-by: Deluan <deluan@navidrome.org>
* test(scanner): add tests for ScanBegin and ScanEnd functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(library): update PRAGMA optimize to check table sizes without ANALYZE
Signed-off-by: Deluan <deluan@navidrome.org>
* test(scanner): refactor tests
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add selective scan options and update translations
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add quick and full scan options for individual libraries
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(ui): add Scan buttonsto the LibraryList
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scan): update scanning parameters from 'path' to 'target' for selective scans.
* refactor(scan): move ParseTargets function to model package
* test(scan): suppress unused return value from SetUserLibraries in tests
* feat(gc): enhance garbage collection to support selective library purging
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(scanner): prevent race condition when scanning deleted folders
When the watcher detects changes in a folder that gets deleted before
the scanner runs (due to the 10-second delay), the scanner was
prematurely removing these folders from the tracking map, preventing
them from being marked as missing.
The issue occurred because `newFolderEntry` was calling `popLastUpdate`
before verifying the folder actually exists on the filesystem.
Changes:
- Move fs.Stat check before newFolderEntry creation in loadDir to
ensure deleted folders remain in lastUpdates for finalize() to handle
- Add early existence check in walkDirTree to skip non-existent target
folders with a warning log
- Add unit test verifying non-existent folders aren't removed from
lastUpdates prematurely
- Add integration test for deleted folder scenario with ScanFolders
Fixes the issue where deleting entire folders (e.g., /music/AC_DC)
wouldn't mark tracks as missing when using selective folder scanning.
* refactor(scan): streamline folder entry creation and update handling
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(scan): add '@Recycle' (QNAP) to ignored directories list
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(log): improve thread safety in logging level management
* test(scan): move unit tests for ParseTargets function
Signed-off-by: Deluan <deluan@navidrome.org>
* review
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: deluan <deluan.quintao@mechanical-orchard.com>
* fix(scanner): Always refresh folder image time when adding first image
Currently, the `images_updated_at` field is only set to the image modification time.
However, in cases where a new image is added _and_ said image is older than the folder mod time, the field is not updated properly.
In this the case where `images_updated_at` is null (no images were ever added) and a new images is found, use the folder modification time instead of image modification time.
**Note**, this doesn't handle cases such as replacing a newer image with an older one.
* simplify image update at
* we don't want to set imagesUpdatedAt when there's no images in the folder
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* Handle subdirectories without rx permission correctly
Allow ogg files w/o metadata, having taglib behave more like ffmpeg
* Fix test for walk_dir_tree, fix full reading of files in permission-
constrained directories, allow directories with leading ellipses
* Sorted directory traversal is preferred, and cleanup tests
* Small refactoring to clean-up `loadDir` function and to remove some "warnings" from IntelliJ
Co-authored-by: Deluan <deluan@navidrome.org>