mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
fix(cli): accept absolute paths in selective scan --target (#5947)
* fix(scanner): accept absolute paths in selective scan --target The scanner's fs.FS only accepts paths relative to the library root, so an absolute --target path (e.g. 2:/jukebox/collection) failed with an opaque "invalid argument" error. Rebase absolute targets onto the library root before scanning; relative paths are unchanged. Fixes #5943 * refactor(scanner): simplify libraryRelativePath with IsLocal and slice.ToMap * fix(scanner): make libraryRelativePath cross-platform Windows CI failed: the tests hardcoded Unix-style absolute paths, which are not absolute on Windows, and filepath.Rel yields backslash-separated paths that the io/fs-based scanner FS rejects. Build the test paths with filepath.Abs so they are absolute on every OS, and normalize the rebased result with filepath.ToSlash. * fix(scanner): resolve relative library root before rebasing target filepath.Rel cannot rebase an absolute target onto a relative library root (e.g. the default MusicFolder=./music), so an absolute --target was left unchanged and rejected by the rooted io/fs. Make the library root absolute first; it resolves against the same cwd as the scanner's fs.
This commit is contained in:
parent
752b38609c
commit
5a4a3099f1
@ -12,7 +12,7 @@ import (
|
||||
// NOTE: This struct is used as a map key, so it should only contain comparable types.
|
||||
type ScanTarget struct {
|
||||
LibraryID int
|
||||
FolderPath string // Relative path within the library, or "" for entire library
|
||||
FolderPath string // Path within the library (relative or absolute), or "" for entire library
|
||||
}
|
||||
|
||||
func (st ScanTarget) String() string {
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -51,6 +52,27 @@ func (s *scanState) sendError(err error) {
|
||||
s.sendProgress(&ProgressInfo{Error: err.Error()})
|
||||
}
|
||||
|
||||
// libraryRelativePath rebases an absolute scan target path onto the library root, since the
|
||||
// scanner's fs.FS only accepts paths relative to it. Relative paths, and absolute paths outside
|
||||
// the library root, are returned unchanged.
|
||||
func libraryRelativePath(libPath, folderPath string) string {
|
||||
if !filepath.IsAbs(folderPath) {
|
||||
return folderPath
|
||||
}
|
||||
// The library root may be relative (e.g. the default "./music"); it must be made absolute
|
||||
// to match against an absolute target, and it resolves against the same cwd as the scanner's fs.
|
||||
absLib, err := filepath.Abs(libPath)
|
||||
if err != nil {
|
||||
return folderPath
|
||||
}
|
||||
rel, err := filepath.Rel(absLib, folderPath)
|
||||
if err != nil || !filepath.IsLocal(rel) {
|
||||
return folderPath
|
||||
}
|
||||
// The scanner's fs.FS is an io/fs, which always uses forward slashes.
|
||||
return filepath.ToSlash(rel)
|
||||
}
|
||||
|
||||
func (s *scannerImpl) scanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget, progress chan<- *ProgressInfo) {
|
||||
startTime := time.Now()
|
||||
|
||||
@ -77,8 +99,12 @@ func (s *scannerImpl) scanFolders(ctx context.Context, fullScan bool, targets []
|
||||
// Selective scan: filter libraries and build targets map
|
||||
state.targets = make(map[int][]string)
|
||||
|
||||
libPaths := slice.ToMap(allLibs, func(lib model.Library) (int, string) {
|
||||
return lib.ID, lib.Path
|
||||
})
|
||||
|
||||
for _, target := range targets {
|
||||
folderPath := target.FolderPath
|
||||
folderPath := libraryRelativePath(libPaths[target.LibraryID], target.FolderPath)
|
||||
if folderPath == "" {
|
||||
folderPath = "."
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ package scanner
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
ppl "github.com/google/go-pipeline/pkg/pipeline"
|
||||
@ -11,6 +13,43 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("libraryRelativePath", func() {
|
||||
// Paths are built with filepath so the "absolute" cases stay absolute on every OS
|
||||
// (a Unix-style "/foo" is not absolute on Windows).
|
||||
libRoot, _ := filepath.Abs(filepath.Join("jukebox", "collection"))
|
||||
outside, _ := filepath.Abs(filepath.Join("somewhere", "else"))
|
||||
|
||||
It("returns a relative path unchanged", func() {
|
||||
Expect(libraryRelativePath(libRoot, "_Collection")).To(Equal("_Collection"))
|
||||
})
|
||||
|
||||
It("rebases an absolute target when the library root is relative", func() {
|
||||
cwd, err := os.Getwd()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(libraryRelativePath(filepath.Join("music", "library"), filepath.Join(cwd, "music", "library", "rock"))).To(Equal("rock"))
|
||||
})
|
||||
|
||||
It("rebases an absolute path that equals the library root to '.'", func() {
|
||||
Expect(libraryRelativePath(libRoot, libRoot)).To(Equal("."))
|
||||
})
|
||||
|
||||
It("rebases an absolute path under the library root", func() {
|
||||
Expect(libraryRelativePath(libRoot, filepath.Join(libRoot, "_Collection"))).To(Equal("_Collection"))
|
||||
})
|
||||
|
||||
It("handles a trailing slash on the library path", func() {
|
||||
Expect(libraryRelativePath(libRoot+string(filepath.Separator), filepath.Join(libRoot, "_Collection"))).To(Equal("_Collection"))
|
||||
})
|
||||
|
||||
It("leaves an absolute path outside the library root unchanged", func() {
|
||||
Expect(libraryRelativePath(libRoot, outside)).To(Equal(outside))
|
||||
})
|
||||
|
||||
It("returns an empty path unchanged", func() {
|
||||
Expect(libraryRelativePath(libRoot, "")).To(Equal(""))
|
||||
})
|
||||
})
|
||||
|
||||
type mockPhase struct {
|
||||
num int
|
||||
produceFunc func() ppl.Producer[int]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user