Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
164 lines
5.2 KiB
Go
164 lines
5.2 KiB
Go
package scanner
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io/fs"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
ignore "github.com/sabhiram/go-gitignore"
|
|
)
|
|
|
|
// IgnoreChecker manages .ndignore patterns using a stack-based approach.
|
|
// Use Push() to add patterns when entering a folder, Pop() when leaving,
|
|
// and ShouldIgnore() to check if a path should be ignored.
|
|
type IgnoreChecker struct {
|
|
fsys fs.FS
|
|
patternStack [][]string // Stack of patterns for each folder level
|
|
currentPatterns []string // Flattened current patterns
|
|
matcher *ignore.GitIgnore // Compiled matcher for current patterns
|
|
}
|
|
|
|
// newIgnoreChecker creates a new IgnoreChecker for the given filesystem.
|
|
func newIgnoreChecker(fsys fs.FS) *IgnoreChecker {
|
|
return &IgnoreChecker{
|
|
fsys: fsys,
|
|
patternStack: make([][]string, 0),
|
|
}
|
|
}
|
|
|
|
// Push loads .ndignore patterns from the specified folder and adds them to the pattern stack.
|
|
// Use this when entering a folder during directory tree traversal.
|
|
func (ic *IgnoreChecker) Push(ctx context.Context, folder string) error {
|
|
patterns := ic.loadPatternsFromFolder(ctx, folder)
|
|
ic.patternStack = append(ic.patternStack, patterns)
|
|
ic.rebuildCurrentPatterns()
|
|
return nil
|
|
}
|
|
|
|
// Pop removes the most recent patterns from the stack.
|
|
// Use this when leaving a folder during directory tree traversal.
|
|
func (ic *IgnoreChecker) Pop() {
|
|
if len(ic.patternStack) > 0 {
|
|
ic.patternStack = ic.patternStack[:len(ic.patternStack)-1]
|
|
ic.rebuildCurrentPatterns()
|
|
}
|
|
}
|
|
|
|
// PushAllParents pushes patterns from root down to the target path.
|
|
// This is a convenience method for when you need to check a specific path
|
|
// without recursively walking the tree. It handles the common pattern of
|
|
// pushing all parent directories from root to the target.
|
|
// This method is optimized to compile patterns only once at the end.
|
|
func (ic *IgnoreChecker) PushAllParents(ctx context.Context, targetPath string) error {
|
|
if targetPath == "." || targetPath == "" {
|
|
// Simple case: just push root
|
|
return ic.Push(ctx, ".")
|
|
}
|
|
|
|
// Load patterns for root
|
|
patterns := ic.loadPatternsFromFolder(ctx, ".")
|
|
ic.patternStack = append(ic.patternStack, patterns)
|
|
|
|
// Load patterns for each parent directory
|
|
currentPath := "."
|
|
parts := strings.Split(path.Clean(targetPath), "/")
|
|
for _, part := range parts {
|
|
if part == "." || part == "" {
|
|
continue
|
|
}
|
|
currentPath = path.Join(currentPath, part)
|
|
patterns = ic.loadPatternsFromFolder(ctx, currentPath)
|
|
ic.patternStack = append(ic.patternStack, patterns)
|
|
}
|
|
|
|
// Rebuild and compile patterns only once at the end
|
|
ic.rebuildCurrentPatterns()
|
|
return nil
|
|
}
|
|
|
|
// ShouldIgnore checks if the given path should be ignored based on the current patterns.
|
|
// Returns true if the path matches any ignore pattern, false otherwise.
|
|
func (ic *IgnoreChecker) ShouldIgnore(ctx context.Context, relPath string) bool {
|
|
// Handle root/empty path - never ignore
|
|
if relPath == "" || relPath == "." {
|
|
return false
|
|
}
|
|
|
|
// If no patterns loaded, nothing to ignore
|
|
if ic.matcher == nil {
|
|
return false
|
|
}
|
|
|
|
matches := ic.matcher.MatchesPath(relPath)
|
|
if matches {
|
|
log.Trace(ctx, "Scanner: Ignoring entry matching .ndignore", "path", relPath)
|
|
}
|
|
return matches
|
|
}
|
|
|
|
// loadPatternsFromFolder reads the .ndignore file in the specified folder and returns the patterns.
|
|
// If the file doesn't exist, returns an empty slice.
|
|
// If the file exists but is empty, returns a pattern to ignore everything ("**/*").
|
|
func (ic *IgnoreChecker) loadPatternsFromFolder(ctx context.Context, folder string) []string {
|
|
ignoreFilePath := path.Join(folder, consts.ScanIgnoreFile)
|
|
var patterns []string
|
|
|
|
// Check if .ndignore file exists
|
|
if _, err := fs.Stat(ic.fsys, ignoreFilePath); err != nil {
|
|
// No .ndignore file in this folder
|
|
return patterns
|
|
}
|
|
|
|
// Read and parse the .ndignore file
|
|
ignoreFile, err := ic.fsys.Open(ignoreFilePath)
|
|
if err != nil {
|
|
log.Warn(ctx, "Scanner: Error opening .ndignore file", "path", ignoreFilePath, err)
|
|
return patterns
|
|
}
|
|
defer ignoreFile.Close()
|
|
|
|
lineScanner := bufio.NewScanner(ignoreFile)
|
|
for lineScanner.Scan() {
|
|
line := strings.TrimSpace(lineScanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue // Skip empty lines, whitespace-only lines, and comments
|
|
}
|
|
patterns = append(patterns, line)
|
|
}
|
|
|
|
if err := lineScanner.Err(); err != nil {
|
|
log.Warn(ctx, "Scanner: Error reading .ndignore file", "path", ignoreFilePath, err)
|
|
return patterns
|
|
}
|
|
|
|
// If the .ndignore file is empty, ignore everything
|
|
if len(patterns) == 0 {
|
|
log.Trace(ctx, "Scanner: .ndignore file is empty, ignoring everything", "path", folder)
|
|
patterns = []string{"**/*"}
|
|
}
|
|
|
|
return patterns
|
|
}
|
|
|
|
// rebuildCurrentPatterns flattens the pattern stack into currentPatterns and recompiles the matcher.
|
|
func (ic *IgnoreChecker) rebuildCurrentPatterns() {
|
|
ic.currentPatterns = make([]string, 0)
|
|
for _, patterns := range ic.patternStack {
|
|
ic.currentPatterns = append(ic.currentPatterns, patterns...)
|
|
}
|
|
ic.compilePatterns()
|
|
}
|
|
|
|
// compilePatterns compiles the current patterns into a GitIgnore matcher.
|
|
func (ic *IgnoreChecker) compilePatterns() {
|
|
if len(ic.currentPatterns) == 0 {
|
|
ic.matcher = nil
|
|
return
|
|
}
|
|
ic.matcher = ignore.CompileIgnoreLines(ic.currentPatterns...)
|
|
}
|