Skip to content

Commit

Permalink
inotify: add recursive watcher
Browse files Browse the repository at this point in the history
This adds a recursive watcher for inotify; per my suggestion in [1] it
uses the "/..." syntax in the path to indicate recursive watches,
similar to how Go tools work:

	w, _ := fsnotify.NewWatcher()
	w.Add("some/dir/...")

This will watch "some/dir" as well as any subdirectories of "some/dir".

The upshot of using a syntax like this is that we can:

1. With AddRecursive(path string) adding new Add* methods would become
   hard; for example AddMask("path", fsnotify.Create) to only get CREATE
   events; we'd then also have to add AddMaskRecursive(). Plus we'd
   have to add a RemoveRecursive() as well.

2. With Watcher.SetRecursive() like in #339 it's not possible to add
   some paths recursively and others non-recursively, which may be
   useful in some cases. Also, it makes it a bit easier to accept user
   input; in the CLI or config you can set "dir/..." and just pass that
   as-is to fsnotify, without applications having to write special code.

For other watchers it will return ErrRecursionUnsupported for now;
Windows support is already mostly finished in #339, and kqueue can be
added in much the same way as inotify in a future PR.

I also moved all test helpers to helper_test.go, and added a bunch of
"shell-like" functions so you're not forever typing error checks and
filepath.Join(). The new "eventCollector" is also useful in tests to
conveniently collect a slice of events.

TODO:

- Also support recursion in Remove(), and deal with paths added with
  "...". I just updated the documentation but didn't actually implement
  anything.

- A few test cases don't seem quite right; want to get #470 merged first
  as it really confuses things.

- Maybe think of a few more test cases.

[1]: #339 (comment)
  • Loading branch information
arp242 committed Aug 6, 2022
1 parent 5b87f50 commit 9e51476
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 44 deletions.
65 changes: 50 additions & 15 deletions fsnotify.go
Expand Up @@ -12,9 +12,28 @@ package fsnotify
import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"strings"
)

// These are the generalized file operations that can trigger a notification.
const (
Create Op = 1 << iota
Write
Remove
Rename
Chmod
)

// Common errors that can be reported by a watcher
var (
ErrNonExistentWatch = errors.New("can't remove non-existent watcher")
ErrEventOverflow = errors.New("fsnotify queue overflow")
ErrNotDirectory = errors.New("not a directory")
ErrRecursionUnsupported = errors.New("recursion not supported")
)

// Event represents a single file system notification.
type Event struct {
// Path to the file or directory.
Expand All @@ -34,21 +53,6 @@ type Event struct {
// Op describes a set of file operations.
type Op uint32

// These are the generalized file operations that can trigger a notification.
const (
Create Op = 1 << iota
Write
Remove
Rename
Chmod
)

// Common errors that can be reported by a watcher
var (
ErrNonExistentWatch = errors.New("can't remove non-existent watcher")
ErrEventOverflow = errors.New("fsnotify queue overflow")
)

func (op Op) String() string {
var b strings.Builder
if op.Has(Create) {
Expand Down Expand Up @@ -83,3 +87,34 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) }
func (e Event) String() string {
return fmt.Sprintf("%q: %s", e.Name, e.Op.String())
}

// findDirs finds all directories under path (return value *includes* path as
// the first entry).
func findDirs(path string) ([]string, error) {
dirs := make([]string, 0, 8)
err := filepath.WalkDir(path, func(root string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if root == path && !d.IsDir() {
return fmt.Errorf("%q: %w", path, ErrNotDirectory)
}
if d.IsDir() {
dirs = append(dirs, root)
}
return nil
})
if err != nil {
return nil, err
}
return dirs, nil
}

// Check if this path is recursive (ends with "/..."), and return the path with
// the /... stripped.
func recursivePath(path string) (string, bool) {
if filepath.Base(path) == "..." {
return filepath.Dir(path), true
}
return path, false
}
47 changes: 47 additions & 0 deletions fsnotify_test.go
Expand Up @@ -4,6 +4,8 @@
package fsnotify

import (
"path/filepath"
"strings"
"testing"
)

Expand Down Expand Up @@ -34,3 +36,48 @@ func TestEventString(t *testing.T) {
})
}
}

func TestFindDirs(t *testing.T) {
join := func(list ...string) string {
return "\n\t" + strings.Join(list, "\n\t")
}

t.Run("finds dirs", func(t *testing.T) {
tmp := t.TempDir()

mkdirAll(t, tmp, "/one/two/three/four")
cat(t, "asd", tmp, "one/two/file.txt")
symlink(t, "/", tmp, "link")

dirs, err := findDirs(tmp)
if err != nil {
t.Fatal(err)
}

have := join(dirs...)
want := join([]string{
tmp,
filepath.Join(tmp, "one"),
filepath.Join(tmp, "one/two"),
filepath.Join(tmp, "one/two/three"),
filepath.Join(tmp, "one/two/three/four"),
}...)

if have != want {
t.Errorf("\nhave: %s\nwant: %s", have, want)
}
})

t.Run("file", func(t *testing.T) {
tmp := t.TempDir()
cat(t, "asd", tmp, "file")

dirs, err := findDirs(filepath.Join(tmp, "file"))
if !errorContains(err, "not a directory") {
t.Errorf("wrong error: %s", err)
}
if len(dirs) > 0 {
t.Errorf("dirs contains entries: %s", dirs)
}
})
}
36 changes: 23 additions & 13 deletions helpers_test.go
Expand Up @@ -94,19 +94,19 @@ func mkdir(t *testing.T, path ...string) {
}

// mkdir -p
// func mkdirAll(t *testing.T, path ...string) {
// t.Helper()
// if len(path) < 1 {
// t.Fatalf("mkdirAll: path must have at least one element: %s", path)
// }
// err := os.MkdirAll(filepath.Join(path...), 0o0755)
// if err != nil {
// t.Fatalf("mkdirAll(%q): %s", filepath.Join(path...), err)
// }
// if shouldWait(path...) {
// eventSeparator()
// }
// }
func mkdirAll(t *testing.T, path ...string) {
t.Helper()
if len(path) < 1 {
t.Fatalf("mkdirAll: path must have at least one element: %s", path)
}
err := os.MkdirAll(filepath.Join(path...), 0o0755)
if err != nil {
t.Fatalf("mkdirAll(%q): %s", filepath.Join(path...), err)
}
if shouldWait(path...) {
eventSeparator()
}
}

// ln -s
func symlink(t *testing.T, target string, link ...string) {
Expand Down Expand Up @@ -448,3 +448,13 @@ func isCI() bool {
_, ok := os.LookupEnv("CI")
return ok
}

func errorContains(out error, want string) bool {
if out == nil {
return want == ""
}
if want == "" {
return false
}
return strings.Contains(out.Error(), want)
}

0 comments on commit 9e51476

Please sign in to comment.