diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b023c5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + + diff --git a/common_test.go b/common_test.go new file mode 100644 index 0000000..ae1696d --- /dev/null +++ b/common_test.go @@ -0,0 +1,11 @@ +package fsmonitor + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + TestingT(t) +} diff --git a/fsmonitor.go b/fsmonitor.go index 90c5f66..1c4e996 100644 --- a/fsmonitor.go +++ b/fsmonitor.go @@ -1,11 +1,14 @@ package fsmonitor import ( - "code.google.com/p/go.exp/fsnotify" "os" "path/filepath" + + "gopkg.in/fsnotify.v1" ) +// NewWatcher initializes a new watcher which is able to recursively scan +// the directory structure. func NewWatcher() (*Watcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -15,6 +18,8 @@ func NewWatcher() (*Watcher, error) { return monitorWatcher, nil } +// NewWatcherWithSkipFolders initializes a new watcher capable of watching +// for file system events of a recursive directory structure. func NewWatcherWithSkipFolders(skipFolders []string) (*Watcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -24,16 +29,24 @@ func NewWatcherWithSkipFolders(skipFolders []string) (*Watcher, error) { return monitorWatcher, nil } +// initWatcher starts the underlying watcher interface. func initWatcher(watcher *fsnotify.Watcher, skipFolders []string) *Watcher { - event := make(chan *fsnotify.FileEvent) + event := make(chan *fsnotify.Event) watcherError := make(chan error) - monitorWatcher := &Watcher{Event: event, Error: watcherError, watcher: watcher, SkipFolders: skipFolders} + closeChannel := make(chan struct{}) + monitorWatcher := &Watcher{ + Events: event, + Error: watcherError, + watcher: watcher, + SkipFolders: skipFolders, + closeChannel: closeChannel, + isClosed: false, + } go func() { for { select { - case ev := <-watcher.Event: - event <- ev - if ev.IsCreate() { + case ev := <-watcher.Events: + if ev.Op&fsnotify.Create == fsnotify.Create { go func() { if f, err := os.Stat(ev.Name); err == nil { if f.IsDir() { @@ -43,39 +56,45 @@ func initWatcher(watcher *fsnotify.Watcher, skipFolders []string) *Watcher { }() } - if ev.IsDelete() { + if ev.Op&fsnotify.Remove == fsnotify.Remove { go func() { - watcher.RemoveWatch(ev.Name) + watcher.Remove(ev.Name) }() } - case e := <-watcher.Error: - watcherError <- e + monitorWatcher.Events <- &ev + case chanErr := <-watcher.Errors: + watcherError <- chanErr + case <-monitorWatcher.closeChannel: + return } } }() return monitorWatcher } +// Watcher is the struct handling the watching of file system events over a recursive +// directory tree. type Watcher struct { - Event chan *fsnotify.FileEvent - Error chan error - SkipFolders []string - watcher *fsnotify.Watcher + Events chan *fsnotify.Event + Error chan error + SkipFolders []string + watcher *fsnotify.Watcher + closeChannel chan struct{} + isClosed bool } -func (self *Watcher) Watch(path string) error { - err := self.watchAllFolders(path) - if err != nil { - return err - } - return nil +// Watch starts watching the given path and all it's subdirectories for file system +// changes. +func (w *Watcher) Watch(path string) error { + return w.watchAllFolders(path) } -func (self *Watcher) watchAllFolders(path string) (err error) { +// watchAllFolders starts watching all subdirectories via the underlying fsnotify watchers. +func (w *Watcher) watchAllFolders(path string) (err error) { err = filepath.Walk(path, func(path string, f os.FileInfo, err error) error { if f != nil && f.IsDir() { filename := f.Name() - for _, skipFolder := range self.SkipFolders { + for _, skipFolder := range w.SkipFolders { match, err := filepath.Match(skipFolder, filename) if err != nil { return err @@ -84,7 +103,7 @@ func (self *Watcher) watchAllFolders(path string) (err error) { return filepath.SkipDir } } - err := self.addWatcher(path) + err := w.addWatcher(path) if err != nil { return err } @@ -94,7 +113,21 @@ func (self *Watcher) watchAllFolders(path string) (err error) { return } -func (self *Watcher) addWatcher(path string) (err error) { - err = self.watcher.Watch(path) - return +// addWatcher adds the given path to the underyling fsnotify watcher. +func (w *Watcher) addWatcher(path string) error { + return w.watcher.Add(path) +} + +// Close stops the internal watcher. +func (w *Watcher) Close() error { + if(!w.IsClosed()) { + close(w.closeChannel) + w.isClosed = true + } + return w.watcher.Close() +} + +// IsClose returns if the watcher has been closed. +func (w *Watcher) IsClosed() bool { + return w.isClosed } diff --git a/fsmonitor_test.go b/fsmonitor_test.go new file mode 100644 index 0000000..fa59abf --- /dev/null +++ b/fsmonitor_test.go @@ -0,0 +1,201 @@ +package fsmonitor + +import ( + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/fsnotify.v1" + + . "gopkg.in/check.v1" +) + +const ( + // default permissions + defaultFilePerms = 0600 + defaultDirPerms = 0700 +) + +type WatcherTests struct { + dir string + watcher *Watcher +} + +var _ = Suite(&WatcherTests{}) + +func (t *WatcherTests) SetUpTest(c *C) { + t.dir = c.MkDir() + watcher, err := NewWatcher() + c.Assert(err, IsNil) + t.watcher = watcher +} + +func (t *WatcherTests) TearDownTest(c *C) { + err := t.watcher.Close() + c.Assert(err, IsNil) +} + +func (t *WatcherTests) TestFileCreation(c *C) { + err := t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + name := filepath.Join(t.dir, "test.txt") + err = ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Create) +} + +func (t *WatcherTests) TestFileDeletion(c *C) { + name := filepath.Join(t.dir, "test.txt") + err := ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + err = t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + err = os.Remove(name) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Remove) +} + +func (t *WatcherTests) TestFileModification(c *C) { + name := filepath.Join(t.dir, "test.txt") + err := ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + err = t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(name, []byte("asdfefadsjklvafh7öafdsü+38"), defaultFilePerms) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Write) +} + +func (t *WatcherTests) TestFileRename(c *C) { + name := filepath.Join(t.dir, "test.txt") + err := ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + err = t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + newName := filepath.Join(t.dir, "test2.txt") + err = os.Rename(name, newName) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Rename) +} + +func (t *WatcherTests) TestCreateEventInDir(c *C) { + dirName := filepath.Join(t.dir, "test") + err := os.Mkdir(dirName, defaultDirPerms) + c.Assert(err, IsNil) + + err = t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + name := filepath.Join(dirName, "test.txt") + err = ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Create) +} + +func (t *WatcherTests) TestCreateEventInCreatedDir(c *C) { + err := t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + dirName := filepath.Join(t.dir, "test") + err = os.Mkdir(dirName, defaultDirPerms) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, dirName) + c.Assert(event.Op, Equals, fsnotify.Create) + + name := filepath.Join(dirName, "test.txt") + err = ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + event = <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Create) +} + +func (t *WatcherTests) TestDeleteInDir(c *C) { + dirName := filepath.Join(t.dir, "test") + err := os.Mkdir(dirName, defaultDirPerms) + c.Assert(err, IsNil) + + name := filepath.Join(dirName, "test.txt") + err = ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + err = t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + err = os.Remove(name) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Remove) +} + +func (t *WatcherTests) TestDeleteInCreatedDir(c *C) { + err := t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + dirName := filepath.Join(t.dir, "test") + err = os.Mkdir(dirName, defaultDirPerms) + c.Assert(err, IsNil) + + event := <-t.watcher.Events + c.Assert(event.Name, Equals, dirName) + c.Assert(event.Op, Equals, fsnotify.Create) + + name := filepath.Join(dirName, "test.txt") + err = ioutil.WriteFile(name, []byte("asdf"), defaultFilePerms) + c.Assert(err, IsNil) + + event = <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Create) + event = <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Write) + + err = os.Remove(name) + c.Assert(err, IsNil) + + event = <-t.watcher.Events + c.Assert(event.Name, Equals, name) + c.Assert(event.Op, Equals, fsnotify.Remove) +} + +func (t *WatcherTests) TestClose(c *C) { + err := t.watcher.Watch(t.dir) + c.Assert(err, IsNil) + + c.Assert(t.watcher.IsClosed(), Equals, false) + err = t.watcher.Close() + c.Assert(err, IsNil) + c.Assert(t.watcher.IsClosed(), Equals, true) + + watcher, err := NewWatcher() + c.Assert(err, IsNil) + t.watcher = watcher +}