AdGuardHome/internal/aghos/filewalker.go

126 lines
3.2 KiB
Go

package aghos
import (
"fmt"
"io"
"io/fs"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
)
// FileWalker is the signature of a function called for files in the file tree.
// As opposed to filepath.Walk it only walk the files (not directories) matching
// the provided pattern and those returned by function itself. All patterns
// should be valid for fs.Glob. If FileWalker returns false for cont then
// walking terminates. Prefer using bufio.Scanner to read the r since the input
// is not limited.
//
// TODO(e.burkov, a.garipov): Move into another package like aghfs.
//
// TODO(e.burkov): Think about passing filename or any additional data.
type FileWalker func(r io.Reader) (patterns []string, cont bool, err error)
// checkFile tries to open and process a single file located on sourcePath in
// the specified fsys. The path is skipped if it's a directory.
func checkFile(
fsys fs.FS,
c FileWalker,
sourcePath string,
) (patterns []string, cont bool, err error) {
var f fs.File
f, err = fsys.Open(sourcePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// Ignore non-existing files since this may only happen when the
// file was removed after filepath.Glob matched it.
return nil, true, nil
}
return nil, false, err
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
var fi fs.FileInfo
if fi, err = f.Stat(); err != nil {
return nil, true, err
} else if fi.IsDir() {
// Skip the directories.
return nil, true, nil
}
return c(f)
}
// handlePatterns parses the patterns in fsys and ignores duplicates using
// srcSet. srcSet must be non-nil.
func handlePatterns(
fsys fs.FS,
srcSet *container.MapSet[string],
patterns ...string,
) (sub []string, err error) {
sub = make([]string, 0, len(patterns))
for _, p := range patterns {
var matches []string
matches, err = fs.Glob(fsys, p)
if err != nil {
// Enrich error with the pattern because filepath.Glob
// doesn't do it.
return nil, fmt.Errorf("invalid pattern %q: %w", p, err)
}
for _, m := range matches {
if srcSet.Has(m) {
continue
}
srcSet.Add(m)
sub = append(sub, m)
}
}
return sub, nil
}
// Walk starts walking the files in fsys defined by patterns from initial.
// It only returns true if fw signed to stop walking.
func (fw FileWalker) Walk(fsys fs.FS, initial ...string) (ok bool, err error) {
// The slice of sources keeps the order in which the files are walked since
// srcSet.Values() returns strings in undefined order.
srcSet := container.NewMapSet[string]()
var src []string
src, err = handlePatterns(fsys, srcSet, initial...)
if err != nil {
return false, err
}
var filename string
defer func() { err = errors.Annotate(err, "checking %q: %w", filename) }()
// TODO(e.burkov): Redo this loop, as it modifies the very same slice it
// iterates over.
for i := 0; i < len(src); i++ {
var patterns []string
var cont bool
filename = src[i]
patterns, cont, err = checkFile(fsys, fw, src[i])
if err != nil {
return false, err
}
if !cont {
return true, nil
}
var subsrc []string
subsrc, err = handlePatterns(fsys, srcSet, patterns...)
if err != nil {
return false, err
}
src = append(src, subsrc...)
}
return false, nil
}