From b45a8e94fe71ba8bab0e882ae16acb41fcc72190 Mon Sep 17 00:00:00 2001 From: Max Claus Nunes Date: Fri, 22 Jun 2018 21:44:26 -0300 Subject: [PATCH] Improve watcher search --- watcher.go | 84 +++++++++++++++++++++++++++++++++++++------------ watcher_test.go | 15 +++++++-- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/watcher.go b/watcher.go index d3bde0e..89acf76 100644 --- a/watcher.go +++ b/watcher.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" zglob "github.com/mattn/go-zglob" @@ -13,8 +14,8 @@ import ( // Watcher is a interface for the watch process type Watcher struct { PollInterval int - WatchItems []string - IgnoreItems []string + WatchItems map[string]bool + IgnoreItems map[string]bool AllowedExtensions map[string]bool Events chan string Errors chan error @@ -35,22 +36,24 @@ func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, ext allowedExts["."+ext] = true } - watchMatches, err := resolveGlobMatches(watchItems) + watchPaths, err := resolvePaths(watchItems, allowedExts) if err != nil { return nil, err } - ignoreMatches, err := resolveGlobMatches(ignoreItems) + ignorePaths, err := resolvePaths(ignoreItems, allowedExts) if err != nil { return nil, err } + logger.Debugf("Resolved watch paths: %v", watchPaths) + logger.Debugf("Resolved ignore paths: %v", ignorePaths) return &Watcher{ Events: make(chan string), Errors: make(chan error), PollInterval: pollInterval, - WatchItems: watchMatches, - IgnoreItems: ignoreMatches, + WatchItems: watchPaths, + IgnoreItems: ignorePaths, AllowedExtensions: allowedExts, }, nil } @@ -61,8 +64,8 @@ var errDetectedChange = errors.New("done") // Watch starts watching for file changes func (w *Watcher) Watch() { for { - for i := range w.WatchItems { - fileChanged, err := w.scanChange(w.WatchItems[i]) + for watchPath := range w.WatchItems { + fileChanged, err := w.scanChange(watchPath) if err != nil { w.Errors <- err return @@ -84,15 +87,13 @@ func (w *Watcher) scanChange(watchPath string) (string, error) { var fileChanged string err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error { - // ignore hidden files and directories + // always ignore hidden files and directories if filepath.Base(path)[0] == '.' { return nil } - for _, x := range w.IgnoreItems { - if x == path { - return filepath.SkipDir - } + if _, ignored := w.IgnoreItems[path]; ignored { + return filepath.SkipDir } ext := filepath.Ext(path) @@ -111,18 +112,61 @@ func (w *Watcher) scanChange(watchPath string) (string, error) { return fileChanged, nil } -func resolveGlobMatches(paths []string) ([]string, error) { - var result []string +func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) { + result := map[string]bool{} for _, path := range paths { - matches, err := zglob.Glob(path) - if err != nil { - return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err) + matches := []string{path} + + isGlob := strings.Contains(path, "*") + if isGlob { + var err error + matches, err = zglob.Glob(path) + if err != nil { + return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err) + } } - logger.Debugf("Resolved glob path %s: %v", path, matches) - result = append(result, matches...) + for _, match := range matches { + // don't care for extension filter right now for non glob paths + // since they could be a directory + if isGlob { + if _, ok := extensions[filepath.Ext(path)]; !ok { + continue + } + } + + if _, ok := result[match]; !ok { + result[match] = true + } + } } + removeOverlappedPaths(result) + return result, nil } + +// remove overlapped paths so it makes the scan for changes later faster and simpler +func removeOverlappedPaths(mapPaths map[string]bool) { + for p1 := range mapPaths { + for p2 := range mapPaths { + if p1 == p2 { + continue + } + + if strings.HasPrefix(p2, p1) { + mapPaths[p2] = false + } else if strings.HasPrefix(p1, p2) { + mapPaths[p1] = false + } + } + } + + // cleanup path list + for p := range mapPaths { + if !mapPaths[p] { + delete(mapPaths, p) + } + } +} diff --git a/watcher_test.go b/watcher_test.go index 90c176a..13b49bc 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -19,7 +19,7 @@ func TestWatcherDefaultValues(t *testing.T) { assert.Nil(t, err, "wacher error") assert.Equal(t, 500, w.PollInterval) - assert.Equal(t, watchItems, w.WatchItems) + assert.Equal(t, map[string]bool{"testdata/server": true}, w.WatchItems) assert.Len(t, w.IgnoreItems, 0) assert.Equal(t, map[string]bool{".go": true}, w.AllowedExtensions) } @@ -32,7 +32,18 @@ func TestWatcherGlobPath(t *testing.T) { w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) assert.Nil(t, err, "wacher error") - assert.Equal(t, []string{"testdata/server/main_test.go"}, w.IgnoreItems) + assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.IgnoreItems) +} + +func TestWatcherRemoveOverlapdPaths(t *testing.T) { + pollInterval := 0 + watchItems := []string{filepath.Join("testdata", "server")} + ignoreItems := []string{"./testdata/**/*", "./testdata/server"} + var extensions []string + + w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) + assert.Nil(t, err, "wacher error") + assert.Equal(t, map[string]bool{"./testdata/server": true}, w.IgnoreItems) } func TestWatcherWatchChange(t *testing.T) {