mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(watcher): implement path ignoring based on .ndignore patterns
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
b361837f50
commit
3cbbb440c2
@ -43,7 +43,7 @@ func loadSpecificFolders(ctx context.Context, job *scanJob, targetFolders []stri
|
||||
}
|
||||
|
||||
// Load ignore patterns from parent directories up to this folder
|
||||
ignorePatterns := loadIgnorePatternsForPath(ctx, job.fs, folderPath)
|
||||
ignorePatterns := loadIgnoredPatternsForPath(ctx, job.fs, folderPath)
|
||||
|
||||
// Load only this specific folder (no recursion)
|
||||
folder, _, err := loadDir(ctx, job, folderPath, ignorePatterns)
|
||||
@ -65,8 +65,8 @@ func loadSpecificFolders(ctx context.Context, job *scanJob, targetFolders []stri
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// loadIgnorePatternsForPath loads all .ndignore patterns from the root down to the specified path
|
||||
func loadIgnorePatternsForPath(ctx context.Context, fsys fs.FS, targetPath string) []string {
|
||||
// loadIgnoredPatternsForPath loads all .ndignore patterns from the root down to the specified path
|
||||
func loadIgnoredPatternsForPath(ctx context.Context, fsys fs.FS, targetPath string) []string {
|
||||
var patterns []string
|
||||
currentPath := "."
|
||||
|
||||
@ -90,33 +90,7 @@ func loadIgnorePatternsForPath(ctx context.Context, fsys fs.FS, targetPath strin
|
||||
return patterns
|
||||
}
|
||||
|
||||
func walkFolder(ctx context.Context, job *scanJob, currentFolder string, ignorePatterns []string, results chan<- *folderEntry) error {
|
||||
ignorePatterns = loadIgnoredPatterns(ctx, job.fs, currentFolder, ignorePatterns)
|
||||
|
||||
folder, children, err := loadDir(ctx, job, currentFolder, ignorePatterns)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error loading dir. Skipping", "path", currentFolder, err)
|
||||
return nil
|
||||
}
|
||||
for _, c := range children {
|
||||
err := walkFolder(ctx, job, c, ignorePatterns, results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dir := path.Clean(currentFolder)
|
||||
log.Trace(ctx, "Scanner: Found directory", " path", dir, "audioFiles", maps.Keys(folder.audioFiles),
|
||||
"images", maps.Keys(folder.imageFiles), "playlists", folder.numPlaylists, "imagesUpdatedAt", folder.imagesUpdatedAt,
|
||||
"updTime", folder.updTime, "modTime", folder.modTime, "numChildren", len(children))
|
||||
folder.path = dir
|
||||
folder.elapsed.Start()
|
||||
|
||||
results <- folder
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadIgnoredPatterns loads .ndignore patterns from the specified folder and combines them with currentPatterns
|
||||
func loadIgnoredPatterns(ctx context.Context, fsys fs.FS, currentFolder string, currentPatterns []string) []string {
|
||||
ignoreFilePath := path.Join(currentFolder, consts.ScanIgnoreFile)
|
||||
var newPatterns []string
|
||||
@ -153,6 +127,33 @@ func loadIgnoredPatterns(ctx context.Context, fsys fs.FS, currentFolder string,
|
||||
return append(combinedPatterns, newPatterns...)
|
||||
}
|
||||
|
||||
func walkFolder(ctx context.Context, job *scanJob, currentFolder string, ignorePatterns []string, results chan<- *folderEntry) error {
|
||||
ignorePatterns = loadIgnoredPatterns(ctx, job.fs, currentFolder, ignorePatterns)
|
||||
|
||||
folder, children, err := loadDir(ctx, job, currentFolder, ignorePatterns)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Scanner: Error loading dir. Skipping", "path", currentFolder, err)
|
||||
return nil
|
||||
}
|
||||
for _, c := range children {
|
||||
err := walkFolder(ctx, job, c, ignorePatterns, results)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dir := path.Clean(currentFolder)
|
||||
log.Trace(ctx, "Scanner: Found directory", " path", dir, "audioFiles", maps.Keys(folder.audioFiles),
|
||||
"images", maps.Keys(folder.imageFiles), "playlists", folder.numPlaylists, "imagesUpdatedAt", folder.imagesUpdatedAt,
|
||||
"updTime", folder.updTime, "modTime", folder.modTime, "numChildren", len(children))
|
||||
folder.path = dir
|
||||
folder.elapsed.Start()
|
||||
|
||||
results <- folder
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDir(ctx context.Context, job *scanJob, dirPath string, ignorePatterns []string) (folder *folderEntry, children []string, err error) {
|
||||
folder = newFolderEntry(job, dirPath)
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/singleton"
|
||||
ignore "github.com/sabhiram/go-gitignore"
|
||||
)
|
||||
|
||||
type Watcher interface {
|
||||
@ -241,6 +242,12 @@ func (w *watcher) watchLibrary(ctx context.Context, lib *model.Library) error {
|
||||
// Find the folder to scan - validate path exists as directory, walk up if needed
|
||||
folderPath := resolveFolderPath(fsys, path)
|
||||
|
||||
// Check if the folder should be ignored based on .ndignore patterns
|
||||
if shouldIgnorePath(ctx, fsys, folderPath) {
|
||||
log.Trace(ctx, "Ignoring change in folder matching .ndignore pattern", "libraryID", lib.ID, "folderPath", folderPath)
|
||||
continue
|
||||
}
|
||||
|
||||
// Notify the main watcher of changes
|
||||
select {
|
||||
case w.watcherNotify <- scanNotification{Library: lib, FolderPath: folderPath}:
|
||||
@ -297,3 +304,26 @@ func isIgnoredPath(_ context.Context, _ fs.FS, path string) bool {
|
||||
// But at this point, we can assume it's a directory. If it's a file, it would be ignored anyway
|
||||
return isDirIgnored(baseDir)
|
||||
}
|
||||
|
||||
// shouldIgnorePath checks if the given path should be ignored based on .ndignore patterns.
|
||||
// It loads all .ndignore files from the root down to the path and returns true if the path
|
||||
// matches any ignore pattern. This function is suitable for checking paths without recursion,
|
||||
// such as in the watcher.
|
||||
func shouldIgnorePath(ctx context.Context, fsys fs.FS, relPath string) bool {
|
||||
// Handle root/empty path - never ignore
|
||||
if relPath == "" || relPath == "." {
|
||||
return false
|
||||
}
|
||||
|
||||
// Load ignore patterns from root to the target path
|
||||
patterns := loadIgnoredPatternsForPath(ctx, fsys, relPath)
|
||||
|
||||
// If no patterns, nothing to ignore
|
||||
if len(patterns) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compile and check
|
||||
matcher := ignore.CompileIgnoreLines(patterns...)
|
||||
return isScanIgnored(ctx, matcher, relPath)
|
||||
}
|
||||
|
||||
@ -146,8 +146,8 @@ var _ = Describe("Watcher", func() {
|
||||
// Send first notification
|
||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
||||
|
||||
// Wait half the watcher wait time
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
// Wait a bit less than half the watcher wait time to ensure timer doesn't fire
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
// No scan should have been triggered yet
|
||||
Expect(mockScanner.GetScanFoldersCallCount()).To(Equal(0))
|
||||
@ -155,14 +155,14 @@ var _ = Describe("Watcher", func() {
|
||||
// Send another notification (resets timer)
|
||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
||||
|
||||
// Wait half the watcher wait time again
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
// Wait a bit less than half the watcher wait time again
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
// Still no scan
|
||||
Expect(mockScanner.GetScanFoldersCallCount()).To(Equal(0))
|
||||
|
||||
// Wait for full timer to expire after last notification
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
// Wait for full timer to expire after last notification (plus margin)
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
|
||||
// Now scan should have been triggered
|
||||
Eventually(func() int {
|
||||
@ -279,6 +279,107 @@ var _ = Describe("Watcher", func() {
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("shouldIgnorePath", func() {
|
||||
var ctx context.Context
|
||||
var mockFS fs.FS
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
|
||||
// Create a mock filesystem with .ndignore files
|
||||
mockFS = fstest.MapFS{
|
||||
// Root .ndignore ignoring "temp/*"
|
||||
".ndignore": &fstest.MapFile{Data: []byte("temp/*\n*.log\n")},
|
||||
|
||||
// Normal directories
|
||||
"music": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"music/artist1": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"music/artist1/song.mp3": &fstest.MapFile{Data: []byte("audio")},
|
||||
|
||||
// Temp directory (should be ignored)
|
||||
"temp": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"temp/cache": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"temp/cache/file.mp3": &fstest.MapFile{Data: []byte("audio")},
|
||||
|
||||
// Directory with hierarchical .ndignore
|
||||
"project": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"project/.ndignore": &fstest.MapFile{Data: []byte("drafts\n")},
|
||||
"project/final": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"project/final/album.mp3": &fstest.MapFile{Data: []byte("audio")},
|
||||
"project/drafts": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"project/drafts/test.mp3": &fstest.MapFile{Data: []byte("audio")},
|
||||
|
||||
// Directory with empty .ndignore (should ignore everything)
|
||||
"empty": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"empty/.ndignore": &fstest.MapFile{Data: []byte("")},
|
||||
"empty/subdir": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
|
||||
// Log file at root level (should be ignored by *.log pattern)
|
||||
"debug.log": &fstest.MapFile{Data: []byte("logs")},
|
||||
}
|
||||
})
|
||||
|
||||
It("does not ignore paths without .ndignore patterns", func() {
|
||||
result := shouldIgnorePath(ctx, mockFS, "music/artist1")
|
||||
Expect(result).To(BeFalse())
|
||||
})
|
||||
|
||||
It("ignores paths matching root .ndignore patterns", func() {
|
||||
result := shouldIgnorePath(ctx, mockFS, "temp/cache")
|
||||
Expect(result).To(BeTrue())
|
||||
})
|
||||
|
||||
It("ignores log files matching *.log pattern", func() {
|
||||
result := shouldIgnorePath(ctx, mockFS, "debug.log")
|
||||
Expect(result).To(BeTrue())
|
||||
})
|
||||
|
||||
It("applies hierarchical .ndignore patterns", func() {
|
||||
// project/drafts should be ignored by project/.ndignore
|
||||
result := shouldIgnorePath(ctx, mockFS, "project/drafts")
|
||||
Expect(result).To(BeTrue())
|
||||
|
||||
// project/final should NOT be ignored
|
||||
result = shouldIgnorePath(ctx, mockFS, "project/final")
|
||||
Expect(result).To(BeFalse())
|
||||
})
|
||||
|
||||
It("ignores directories with empty .ndignore file", func() {
|
||||
result := shouldIgnorePath(ctx, mockFS, "empty/subdir")
|
||||
Expect(result).To(BeTrue())
|
||||
})
|
||||
|
||||
It("does not ignore root or empty paths", func() {
|
||||
Expect(shouldIgnorePath(ctx, mockFS, "")).To(BeFalse())
|
||||
Expect(shouldIgnorePath(ctx, mockFS, ".")).To(BeFalse())
|
||||
})
|
||||
|
||||
It("combines patterns from multiple .ndignore files", func() {
|
||||
// Create a more complex hierarchy
|
||||
complexFS := fstest.MapFS{
|
||||
".ndignore": &fstest.MapFile{Data: []byte("*.tmp\n")},
|
||||
"parent": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"parent/.ndignore": &fstest.MapFile{Data: []byte("test\n")},
|
||||
"parent/test": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"parent/test/file.mp3": &fstest.MapFile{Data: []byte("audio")},
|
||||
"parent/prod": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
"parent/prod/cache.tmp": &fstest.MapFile{Data: []byte("tmp")},
|
||||
}
|
||||
|
||||
// parent/test should be ignored by parent/.ndignore
|
||||
result := shouldIgnorePath(ctx, complexFS, "parent/test")
|
||||
Expect(result).To(BeTrue())
|
||||
|
||||
// parent/prod/cache.tmp path should be ignored by root .ndignore (*.tmp)
|
||||
result = shouldIgnorePath(ctx, complexFS, "parent/prod/cache.tmp")
|
||||
Expect(result).To(BeTrue())
|
||||
|
||||
// parent/prod directory itself should NOT be ignored
|
||||
result = shouldIgnorePath(ctx, complexFS, "parent/prod")
|
||||
Expect(result).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("resolveFolderPath", func() {
|
||||
var mockFS fs.FS
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user