├── .travis.yml ├── LICENSE ├── README.md ├── cmd └── watcher │ ├── README.md │ └── main.go ├── example ├── basics │ └── main.go ├── close_watcher │ └── main.go ├── filter_events │ └── main.go ├── ignore_files │ └── main.go ├── ignore_hidden_files │ └── main.go └── test_folder │ ├── .dotfile │ ├── file.txt │ └── test_folder_recursive │ └── file_recursive.txt ├── ishidden.go ├── ishidden_windows.go ├── samefile.go ├── samefile_windows.go ├── watcher.go └── watcher_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - tip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Benjamin Radovsky. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of watcher nor the names of its contributors may be used to 15 | endorse or promote products derived from this software without specific prior 16 | written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watcher 2 | 3 | [![Build Status](https://travis-ci.org/radovskyb/watcher.svg?branch=master)](https://travis-ci.org/radovskyb/watcher) 4 | 5 | `watcher` is a Go package for watching for files or directory changes (recursively or non recursively) without using filesystem events, which allows it to work cross platform consistently. 6 | 7 | `watcher` watches for changes and notifies over channels either anytime an event or an error has occurred. 8 | 9 | Events contain the `os.FileInfo` of the file or directory that the event is based on and the type of event and file or directory path. 10 | 11 | [Installation](#installation) 12 | [Features](#features) 13 | [Example](#example) 14 | [Contributing](#contributing) 15 | [Watcher Command](#command) 16 | 17 | # Update 18 | - Event.OldPath has been added [Aug 17, 2019] 19 | - Added new file filter hooks (Including a built in regexp filtering hook) [Dec 12, 2018] 20 | - Event.Path for Rename and Move events is now returned in the format of `fromPath -> toPath` 21 | 22 | #### Chmod event is not supported under windows. 23 | 24 | # Installation 25 | 26 | ```shell 27 | go get -u github.com/radovskyb/watcher/... 28 | ``` 29 | 30 | # Features 31 | 32 | - Customizable polling interval. 33 | - Filter Events. 34 | - Watch folders recursively or non-recursively. 35 | - Choose to ignore hidden files. 36 | - Choose to ignore specified files and folders. 37 | - Notifies the `os.FileInfo` of the file that the event is based on. e.g `Name`, `ModTime`, `IsDir`, etc. 38 | - Notifies the full path of the file that the event is based on or the old and new paths if the event was a `Rename` or `Move` event. 39 | - Limit amount of events that can be received per watching cycle. 40 | - List the files being watched. 41 | - Trigger custom events. 42 | 43 | # Todo 44 | 45 | - Write more tests. 46 | - Write benchmarks. 47 | 48 | # Example 49 | 50 | ```go 51 | package main 52 | 53 | import ( 54 | "fmt" 55 | "log" 56 | "time" 57 | 58 | "github.com/radovskyb/watcher" 59 | ) 60 | 61 | func main() { 62 | w := watcher.New() 63 | 64 | // SetMaxEvents to 1 to allow at most 1 event's to be received 65 | // on the Event channel per watching cycle. 66 | // 67 | // If SetMaxEvents is not set, the default is to send all events. 68 | w.SetMaxEvents(1) 69 | 70 | // Only notify rename and move events. 71 | w.FilterOps(watcher.Rename, watcher.Move) 72 | 73 | // Only files that match the regular expression during file listings 74 | // will be watched. 75 | r := regexp.MustCompile("^abc$") 76 | w.AddFilterHook(watcher.RegexFilterHook(r, false)) 77 | 78 | go func() { 79 | for { 80 | select { 81 | case event := <-w.Event: 82 | fmt.Println(event) // Print the event's info. 83 | case err := <-w.Error: 84 | log.Fatalln(err) 85 | case <-w.Closed: 86 | return 87 | } 88 | } 89 | }() 90 | 91 | // Watch this folder for changes. 92 | if err := w.Add("."); err != nil { 93 | log.Fatalln(err) 94 | } 95 | 96 | // Watch test_folder recursively for changes. 97 | if err := w.AddRecursive("../test_folder"); err != nil { 98 | log.Fatalln(err) 99 | } 100 | 101 | // Print a list of all of the files and folders currently 102 | // being watched and their paths. 103 | for path, f := range w.WatchedFiles() { 104 | fmt.Printf("%s: %s\n", path, f.Name()) 105 | } 106 | 107 | fmt.Println() 108 | 109 | // Trigger 2 events after watcher started. 110 | go func() { 111 | w.Wait() 112 | w.TriggerEvent(watcher.Create, nil) 113 | w.TriggerEvent(watcher.Remove, nil) 114 | }() 115 | 116 | // Start the watching process - it'll check for changes every 100ms. 117 | if err := w.Start(time.Millisecond * 100); err != nil { 118 | log.Fatalln(err) 119 | } 120 | } 121 | ``` 122 | 123 | # Contributing 124 | If you would ike to contribute, simply submit a pull request. 125 | 126 | # Command 127 | 128 | `watcher` comes with a simple command which is installed when using the `go get` command from above. 129 | 130 | # Usage 131 | 132 | ``` 133 | Usage of watcher: 134 | -cmd string 135 | command to run when an event occurs 136 | -dotfiles 137 | watch dot files (default true) 138 | -ignore string 139 | comma separated list of paths to ignore 140 | -interval string 141 | watcher poll interval (default "100ms") 142 | -keepalive 143 | keep alive when a cmd returns code != 0 144 | -list 145 | list watched files on start 146 | -pipe 147 | pipe event's info to command's stdin 148 | -recursive 149 | watch folders recursively (default true) 150 | -startcmd 151 | run the command when watcher starts 152 | ``` 153 | 154 | All of the flags are optional and watcher can also be called by itself: 155 | ```shell 156 | watcher 157 | ``` 158 | (watches the current directory recursively for changes and notifies any events that occur.) 159 | 160 | A more elaborate example using the `watcher` command: 161 | ```shell 162 | watcher -dotfiles=false -recursive=false -cmd="./myscript" main.go ../ 163 | ``` 164 | In this example, `watcher` will ignore dot files and folders and won't watch any of the specified folders recursively. It will also run the script `./myscript` anytime an event occurs while watching `main.go` or any files or folders in the previous directory (`../`). 165 | 166 | Using the `pipe` and `cmd` flags together will send the event's info to the command's stdin when changes are detected. 167 | 168 | First create a file called `script.py` with the following contents: 169 | ```python 170 | import sys 171 | 172 | for line in sys.stdin: 173 | print (line + " - python") 174 | ``` 175 | 176 | Next, start watcher with the `pipe` and `cmd` flags enabled: 177 | ```shell 178 | watcher -cmd="python script.py" -pipe=true 179 | ``` 180 | 181 | Now when changes are detected, the event's info will be output from the running python script. 182 | -------------------------------------------------------------------------------- /cmd/watcher/README.md: -------------------------------------------------------------------------------- 1 | # watcher command 2 | 3 | # Installation 4 | 5 | ```shell 6 | go get -u github.com/radovskyb/watcher/... 7 | ``` 8 | 9 | # Usage 10 | 11 | ``` 12 | Usage of watcher: 13 | -cmd string 14 | command to run when an event occurs 15 | -dotfiles 16 | watch dot files (default true) 17 | -ignore string 18 | comma separated list of paths to ignore 19 | -interval string 20 | watcher poll interval (default "100ms") 21 | -keepalive 22 | keep alive when a cmd returns code != 0 23 | -list 24 | list watched files on start 25 | -pipe 26 | pipe event's info to command's stdin 27 | -recursive 28 | watch folders recursively (default true) 29 | -startcmd 30 | run the command when watcher starts 31 | ``` 32 | 33 | All of the flags are optional and watcher can be simply called by itself: 34 | ```shell 35 | watcher 36 | ``` 37 | (watches the current directory recursively for changes and notifies for any events that occur.) 38 | 39 | A more elaborate example using the `watcher` command: 40 | ```shell 41 | watcher -dotfiles=false -recursive=false -cmd="./myscript" main.go ../ 42 | ``` 43 | In this example, `watcher` will ignore dot files and folders and won't watch any of the specified folders recursively. It will also run the script `./myscript` anytime an event occurs while watching `main.go` or any files or folders in the previous directory (`../`). 44 | 45 | Using the `pipe` and `cmd` flags together will send the event's info to the command's stdin when changes are detected. 46 | 47 | First create a file called `script.py` with the following contents: 48 | ```python 49 | import sys 50 | 51 | for line in sys.stdin: 52 | print (line + " - python") 53 | ``` 54 | 55 | Next, start watcher with the `pipe` and `cmd` flags enabled: 56 | ```shell 57 | watcher -cmd="python script.py" -pipe=true 58 | ``` 59 | 60 | Now when changes are detected, the event's info will be output from the running python script. 61 | -------------------------------------------------------------------------------- /cmd/watcher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "os/signal" 10 | "strings" 11 | "time" 12 | "unicode" 13 | 14 | "github.com/radovskyb/watcher" 15 | ) 16 | 17 | func main() { 18 | interval := flag.String("interval", "100ms", "watcher poll interval") 19 | recursive := flag.Bool("recursive", true, "watch folders recursively") 20 | dotfiles := flag.Bool("dotfiles", true, "watch dot files") 21 | cmd := flag.String("cmd", "", "command to run when an event occurs") 22 | startcmd := flag.Bool("startcmd", false, "run the command when watcher starts") 23 | listFiles := flag.Bool("list", false, "list watched files on start") 24 | stdinPipe := flag.Bool("pipe", false, "pipe event's info to command's stdin") 25 | keepalive := flag.Bool("keepalive", false, "keep alive when a cmd returns code != 0") 26 | ignore := flag.String("ignore", "", "comma separated list of paths to ignore") 27 | 28 | flag.Parse() 29 | 30 | // Retrieve the list of files and folders. 31 | files := flag.Args() 32 | 33 | // If no files/folders were specified, watch the current directory. 34 | if len(files) == 0 { 35 | curDir, err := os.Getwd() 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | files = append(files, curDir) 40 | } 41 | 42 | var cmdName string 43 | var cmdArgs []string 44 | if *cmd != "" { 45 | split := strings.FieldsFunc(*cmd, unicode.IsSpace) 46 | cmdName = split[0] 47 | if len(split) > 1 { 48 | cmdArgs = split[1:] 49 | } 50 | } 51 | 52 | // Create a new Watcher with the specified options. 53 | w := watcher.New() 54 | w.IgnoreHiddenFiles(!*dotfiles) 55 | 56 | // Get any of the paths to ignore. 57 | ignoredPaths := strings.Split(*ignore, ",") 58 | 59 | for _, path := range ignoredPaths { 60 | trimmed := strings.TrimSpace(path) 61 | if trimmed == "" { 62 | continue 63 | } 64 | 65 | err := w.Ignore(trimmed) 66 | if err != nil { 67 | log.Fatalln(err) 68 | } 69 | } 70 | 71 | done := make(chan struct{}) 72 | go func() { 73 | defer close(done) 74 | 75 | for { 76 | select { 77 | case event := <-w.Event: 78 | // Print the event's info. 79 | fmt.Println(event) 80 | 81 | // Run the command if one was specified. 82 | if *cmd != "" { 83 | c := exec.Command(cmdName, cmdArgs...) 84 | if *stdinPipe { 85 | c.Stdin = strings.NewReader(event.String()) 86 | } else { 87 | c.Stdin = os.Stdin 88 | } 89 | c.Stdout = os.Stdout 90 | c.Stderr = os.Stderr 91 | if err := c.Run(); err != nil { 92 | if (c.ProcessState == nil || !c.ProcessState.Success()) && *keepalive { 93 | log.Println(err) 94 | continue 95 | } 96 | log.Fatalln(err) 97 | } 98 | } 99 | case err := <-w.Error: 100 | if err == watcher.ErrWatchedFileDeleted { 101 | fmt.Println(err) 102 | continue 103 | } 104 | log.Fatalln(err) 105 | case <-w.Closed: 106 | return 107 | } 108 | } 109 | }() 110 | 111 | // Add the files and folders specified. 112 | for _, file := range files { 113 | if *recursive { 114 | if err := w.AddRecursive(file); err != nil { 115 | log.Fatalln(err) 116 | } 117 | } else { 118 | if err := w.Add(file); err != nil { 119 | log.Fatalln(err) 120 | } 121 | } 122 | } 123 | 124 | // Print a list of all of the files and folders being watched. 125 | if *listFiles { 126 | for path, f := range w.WatchedFiles() { 127 | fmt.Printf("%s: %s\n", path, f.Name()) 128 | } 129 | fmt.Println() 130 | } 131 | 132 | fmt.Printf("Watching %d files\n", len(w.WatchedFiles())) 133 | 134 | // Parse the interval string into a time.Duration. 135 | parsedInterval, err := time.ParseDuration(*interval) 136 | if err != nil { 137 | log.Fatalln(err) 138 | } 139 | 140 | closed := make(chan struct{}) 141 | 142 | c := make(chan os.Signal) 143 | signal.Notify(c, os.Kill, os.Interrupt) 144 | go func() { 145 | <-c 146 | w.Close() 147 | <-done 148 | fmt.Println("watcher closed") 149 | close(closed) 150 | }() 151 | 152 | // Run the command before watcher starts if one was specified. 153 | go func() { 154 | if *cmd != "" && *startcmd { 155 | c := exec.Command(cmdName, cmdArgs...) 156 | c.Stdin = os.Stdin 157 | c.Stdout = os.Stdout 158 | c.Stderr = os.Stderr 159 | if err := c.Run(); err != nil { 160 | if (c.ProcessState == nil || !c.ProcessState.Success()) && *keepalive { 161 | log.Println(err) 162 | return 163 | } 164 | log.Fatalln(err) 165 | } 166 | } 167 | }() 168 | 169 | // Start the watching process. 170 | if err := w.Start(parsedInterval); err != nil { 171 | log.Fatalln(err) 172 | } 173 | 174 | <-closed 175 | } 176 | -------------------------------------------------------------------------------- /example/basics/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/radovskyb/watcher" 9 | ) 10 | 11 | func main() { 12 | w := watcher.New() 13 | 14 | // Uncomment to use SetMaxEvents set to 1 to allow at most 1 event to be received 15 | // on the Event channel per watching cycle. 16 | // 17 | // If SetMaxEvents is not set, the default is to send all events. 18 | // w.SetMaxEvents(1) 19 | 20 | // Uncomment to only notify rename and move events. 21 | // w.FilterOps(watcher.Rename, watcher.Move) 22 | 23 | // Uncomment to filter files based on a regular expression. 24 | // 25 | // Only files that match the regular expression during file listing 26 | // will be watched. 27 | // r := regexp.MustCompile("^abc$") 28 | // w.AddFilterHook(watcher.RegexFilterHook(r, false)) 29 | 30 | go func() { 31 | for { 32 | select { 33 | case event := <-w.Event: 34 | fmt.Println(event) // Print the event's info. 35 | case err := <-w.Error: 36 | log.Fatalln(err) 37 | case <-w.Closed: 38 | return 39 | } 40 | } 41 | }() 42 | 43 | // Watch this folder for changes. 44 | if err := w.Add("."); err != nil { 45 | log.Fatalln(err) 46 | } 47 | 48 | // Watch test_folder recursively for changes. 49 | if err := w.AddRecursive("../test_folder"); err != nil { 50 | log.Fatalln(err) 51 | } 52 | 53 | // Print a list of all of the files and folders currently 54 | // being watched and their paths. 55 | for path, f := range w.WatchedFiles() { 56 | fmt.Printf("%s: %s\n", path, f.Name()) 57 | } 58 | 59 | fmt.Println() 60 | 61 | // Trigger 2 events after watcher started. 62 | go func() { 63 | w.Wait() 64 | w.TriggerEvent(watcher.Create, nil) 65 | w.TriggerEvent(watcher.Remove, nil) 66 | }() 67 | 68 | // Start the watching process - it'll check for changes every 100ms. 69 | if err := w.Start(time.Millisecond * 100); err != nil { 70 | log.Fatalln(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/close_watcher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/radovskyb/watcher" 9 | ) 10 | 11 | func main() { 12 | w := watcher.New() 13 | 14 | go func() { 15 | for { 16 | select { 17 | case event := <-w.Event: 18 | fmt.Println(event) 19 | case err := <-w.Error: 20 | log.Fatalln(err) 21 | case <-w.Closed: 22 | return 23 | } 24 | } 25 | }() 26 | 27 | // Watch test_folder for changes. 28 | if err := w.Add("../test_folder"); err != nil { 29 | log.Fatalln(err) 30 | } 31 | 32 | // Print a list of all of the files and folders currently 33 | // being watched and their paths. 34 | for path, f := range w.WatchedFiles() { 35 | fmt.Printf("%s: %s\n", path, f.Name()) 36 | } 37 | 38 | fmt.Println() 39 | 40 | // Close the watcher after watcher started. 41 | go func() { 42 | w.Wait() 43 | w.Close() 44 | }() 45 | 46 | // Start the watching process - it'll check for changes every 100ms. 47 | if err := w.Start(time.Millisecond * 100); err != nil { 48 | log.Fatalln(err) 49 | } 50 | 51 | fmt.Println("watcher closed") 52 | } 53 | -------------------------------------------------------------------------------- /example/filter_events/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/radovskyb/watcher" 9 | ) 10 | 11 | func main() { 12 | w := watcher.New() 13 | 14 | w.SetMaxEvents(1) 15 | 16 | // Only show rename and move events. 17 | w.FilterOps(watcher.Rename, watcher.Move) 18 | 19 | go func() { 20 | for { 21 | select { 22 | case event := <-w.Event: 23 | fmt.Println(event) 24 | case err := <-w.Error: 25 | log.Fatalln(err) 26 | case <-w.Closed: 27 | return 28 | } 29 | } 30 | }() 31 | 32 | // Watch test_folder recursively for changes. 33 | if err := w.AddRecursive("../test_folder"); err != nil { 34 | log.Fatalln(err) 35 | } 36 | 37 | // Print a list of all of the files and folders currently 38 | // being watched and their paths. 39 | for path, f := range w.WatchedFiles() { 40 | fmt.Printf("%s: %s\n", path, f.Name()) 41 | } 42 | 43 | fmt.Println() 44 | 45 | // Start the watching process - it'll check for changes every 100ms. 46 | if err := w.Start(time.Millisecond * 100); err != nil { 47 | log.Fatalln(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/ignore_files/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/radovskyb/watcher" 9 | ) 10 | 11 | func main() { 12 | w := watcher.New() 13 | 14 | go func() { 15 | for { 16 | select { 17 | case event := <-w.Event: 18 | // Print the event. 19 | fmt.Println(event) 20 | case err := <-w.Error: 21 | log.Fatalln(err) 22 | case <-w.Closed: 23 | return 24 | } 25 | } 26 | }() 27 | 28 | // Watch test_folder recursively for changes. 29 | if err := w.AddRecursive("../test_folder"); err != nil { 30 | log.Fatalln(err) 31 | } 32 | 33 | // Print a list of all of the files and folders currently 34 | // being watched and their paths. 35 | for path, f := range w.WatchedFiles() { 36 | fmt.Printf("%s: %s\n", path, f.Name()) 37 | } 38 | fmt.Println() 39 | 40 | go func() { 41 | w.Wait() 42 | // Ignore ../test_folder/test_folder_recursive and ../test_folder/.dotfile 43 | if err := w.Ignore("../test_folder/test_folder_recursive", "../test_folder/.dotfile"); err != nil { 44 | log.Fatalln(err) 45 | } 46 | // Print a list of all of the files and folders currently being watched 47 | // and their paths after adding files and folders to the ignore list. 48 | for path, f := range w.WatchedFiles() { 49 | fmt.Printf("%s: %s\n", path, f.Name()) 50 | } 51 | fmt.Println() 52 | }() 53 | 54 | // Start the watching process - it'll check for changes every 100ms. 55 | if err := w.Start(time.Millisecond * 100); err != nil { 56 | log.Fatalln(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/ignore_hidden_files/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/radovskyb/watcher" 9 | ) 10 | 11 | func main() { 12 | w := watcher.New() 13 | 14 | // Ignore hidden files. 15 | w.IgnoreHiddenFiles(true) 16 | 17 | go func() { 18 | for { 19 | select { 20 | case event := <-w.Event: 21 | // Print the event's info. 22 | fmt.Println(event) 23 | case err := <-w.Error: 24 | log.Fatalln(err) 25 | case <-w.Closed: 26 | return 27 | } 28 | } 29 | }() 30 | 31 | // Watch test_folder recursively for changes. 32 | // 33 | // Watcher won't add .dotfile to the watchlist. 34 | if err := w.AddRecursive("../test_folder"); err != nil { 35 | log.Fatalln(err) 36 | } 37 | 38 | // Print a list of all of the files and folders currently 39 | // being watched and their paths. 40 | for path, f := range w.WatchedFiles() { 41 | fmt.Printf("%s: %s\n", path, f.Name()) 42 | } 43 | 44 | // Start the watching process - it'll check for changes every 100ms. 45 | if err := w.Start(time.Millisecond * 100); err != nil { 46 | log.Fatalln(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/test_folder/.dotfile: -------------------------------------------------------------------------------- 1 | I'm a dotfile :O 2 | -------------------------------------------------------------------------------- /example/test_folder/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radovskyb/watcher/f5989f8deca223d590d5a130c77ea375fe9fde30/example/test_folder/file.txt -------------------------------------------------------------------------------- /example/test_folder/test_folder_recursive/file_recursive.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radovskyb/watcher/f5989f8deca223d590d5a130c77ea375fe9fde30/example/test_folder/test_folder_recursive/file_recursive.txt -------------------------------------------------------------------------------- /ishidden.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package watcher 4 | 5 | import ( 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func isHiddenFile(path string) (bool, error) { 11 | return strings.HasPrefix(filepath.Base(path), "."), nil 12 | } 13 | -------------------------------------------------------------------------------- /ishidden_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package watcher 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func isHiddenFile(path string) (bool, error) { 10 | pointer, err := syscall.UTF16PtrFromString(path) 11 | if err != nil { 12 | return false, err 13 | } 14 | 15 | attributes, err := syscall.GetFileAttributes(pointer) 16 | if err != nil { 17 | return false, err 18 | } 19 | 20 | return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil 21 | } 22 | -------------------------------------------------------------------------------- /samefile.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package watcher 4 | 5 | import "os" 6 | 7 | func sameFile(fi1, fi2 os.FileInfo) bool { 8 | return os.SameFile(fi1, fi2) 9 | } 10 | -------------------------------------------------------------------------------- /samefile_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package watcher 4 | 5 | import "os" 6 | 7 | func sameFile(fi1, fi2 os.FileInfo) bool { 8 | return fi1.ModTime() == fi2.ModTime() && 9 | fi1.Size() == fi2.Size() && 10 | fi1.Mode() == fi2.Mode() && 11 | fi1.IsDir() == fi2.IsDir() 12 | } 13 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var ( 16 | // ErrDurationTooShort occurs when calling the watcher's Start 17 | // method with a duration that's less than 1 nanosecond. 18 | ErrDurationTooShort = errors.New("error: duration is less than 1ns") 19 | 20 | // ErrWatcherRunning occurs when trying to call the watcher's 21 | // Start method and the polling cycle is still already running 22 | // from previously calling Start and not yet calling Close. 23 | ErrWatcherRunning = errors.New("error: watcher is already running") 24 | 25 | // ErrWatchedFileDeleted is an error that occurs when a file or folder that was 26 | // being watched has been deleted. 27 | ErrWatchedFileDeleted = errors.New("error: watched file or folder deleted") 28 | 29 | // ErrSkip is less of an error, but more of a way for path hooks to skip a file or 30 | // directory. 31 | ErrSkip = errors.New("error: skipping file") 32 | ) 33 | 34 | // An Op is a type that is used to describe what type 35 | // of event has occurred during the watching process. 36 | type Op uint32 37 | 38 | // Ops 39 | const ( 40 | Create Op = iota 41 | Write 42 | Remove 43 | Rename 44 | Chmod 45 | Move 46 | ) 47 | 48 | var ops = map[Op]string{ 49 | Create: "CREATE", 50 | Write: "WRITE", 51 | Remove: "REMOVE", 52 | Rename: "RENAME", 53 | Chmod: "CHMOD", 54 | Move: "MOVE", 55 | } 56 | 57 | // String prints the string version of the Op consts 58 | func (e Op) String() string { 59 | if op, found := ops[e]; found { 60 | return op 61 | } 62 | return "???" 63 | } 64 | 65 | // An Event describes an event that is received when files or directory 66 | // changes occur. It includes the os.FileInfo of the changed file or 67 | // directory and the type of event that's occurred and the full path of the file. 68 | type Event struct { 69 | Op 70 | Path string 71 | OldPath string 72 | os.FileInfo 73 | } 74 | 75 | // String returns a string depending on what type of event occurred and the 76 | // file name associated with the event. 77 | func (e Event) String() string { 78 | if e.FileInfo == nil { 79 | return "???" 80 | } 81 | 82 | pathType := "FILE" 83 | if e.IsDir() { 84 | pathType = "DIRECTORY" 85 | } 86 | return fmt.Sprintf("%s %q %s [%s]", pathType, e.Name(), e.Op, e.Path) 87 | } 88 | 89 | // FilterFileHookFunc is a function that is called to filter files during listings. 90 | // If a file is ok to be listed, nil is returned otherwise ErrSkip is returned. 91 | type FilterFileHookFunc func(info os.FileInfo, fullPath string) error 92 | 93 | // RegexFilterHook is a function that accepts or rejects a file 94 | // for listing based on whether it's filename or full path matches 95 | // a regular expression. 96 | func RegexFilterHook(r *regexp.Regexp, useFullPath bool) FilterFileHookFunc { 97 | return func(info os.FileInfo, fullPath string) error { 98 | str := info.Name() 99 | 100 | if useFullPath { 101 | str = fullPath 102 | } 103 | 104 | // Match 105 | if r.MatchString(str) { 106 | return nil 107 | } 108 | 109 | // No match. 110 | return ErrSkip 111 | } 112 | } 113 | 114 | // Watcher describes a process that watches files for changes. 115 | type Watcher struct { 116 | Event chan Event 117 | Error chan error 118 | Closed chan struct{} 119 | close chan struct{} 120 | wg *sync.WaitGroup 121 | 122 | // mu protects the following. 123 | mu *sync.Mutex 124 | ffh []FilterFileHookFunc 125 | running bool 126 | names map[string]bool // bool for recursive or not. 127 | files map[string]os.FileInfo // map of files. 128 | ignored map[string]struct{} // ignored files or directories. 129 | ops map[Op]struct{} // Op filtering. 130 | ignoreHidden bool // ignore hidden files or not. 131 | maxEvents int // max sent events per cycle 132 | } 133 | 134 | // New creates a new Watcher. 135 | func New() *Watcher { 136 | // Set up the WaitGroup for w.Wait(). 137 | var wg sync.WaitGroup 138 | wg.Add(1) 139 | 140 | return &Watcher{ 141 | Event: make(chan Event), 142 | Error: make(chan error), 143 | Closed: make(chan struct{}), 144 | close: make(chan struct{}), 145 | mu: new(sync.Mutex), 146 | wg: &wg, 147 | files: make(map[string]os.FileInfo), 148 | ignored: make(map[string]struct{}), 149 | names: make(map[string]bool), 150 | } 151 | } 152 | 153 | // SetMaxEvents controls the maximum amount of events that are sent on 154 | // the Event channel per watching cycle. If max events is less than 1, there is 155 | // no limit, which is the default. 156 | func (w *Watcher) SetMaxEvents(delta int) { 157 | w.mu.Lock() 158 | w.maxEvents = delta 159 | w.mu.Unlock() 160 | } 161 | 162 | // AddFilterHook 163 | func (w *Watcher) AddFilterHook(f FilterFileHookFunc) { 164 | w.mu.Lock() 165 | w.ffh = append(w.ffh, f) 166 | w.mu.Unlock() 167 | } 168 | 169 | // IgnoreHiddenFiles sets the watcher to ignore any file or directory 170 | // that starts with a dot. 171 | func (w *Watcher) IgnoreHiddenFiles(ignore bool) { 172 | w.mu.Lock() 173 | w.ignoreHidden = ignore 174 | w.mu.Unlock() 175 | } 176 | 177 | // FilterOps filters which event op types should be returned 178 | // when an event occurs. 179 | func (w *Watcher) FilterOps(ops ...Op) { 180 | w.mu.Lock() 181 | w.ops = make(map[Op]struct{}) 182 | for _, op := range ops { 183 | w.ops[op] = struct{}{} 184 | } 185 | w.mu.Unlock() 186 | } 187 | 188 | // Add adds either a single file or directory to the file list. 189 | func (w *Watcher) Add(name string) (err error) { 190 | w.mu.Lock() 191 | defer w.mu.Unlock() 192 | 193 | name, err = filepath.Abs(name) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | // If name is on the ignored list or if hidden files are 199 | // ignored and name is a hidden file or directory, simply return. 200 | _, ignored := w.ignored[name] 201 | 202 | isHidden, err := isHiddenFile(name) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | if ignored || (w.ignoreHidden && isHidden) { 208 | return nil 209 | } 210 | 211 | // Add the directory's contents to the files list. 212 | fileList, err := w.list(name) 213 | if err != nil { 214 | return err 215 | } 216 | for k, v := range fileList { 217 | w.files[k] = v 218 | } 219 | 220 | // Add the name to the names list. 221 | w.names[name] = false 222 | 223 | return nil 224 | } 225 | 226 | func (w *Watcher) list(name string) (map[string]os.FileInfo, error) { 227 | fileList := make(map[string]os.FileInfo) 228 | 229 | // Make sure name exists. 230 | stat, err := os.Stat(name) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | fileList[name] = stat 236 | 237 | // If it's not a directory, just return. 238 | if !stat.IsDir() { 239 | return fileList, nil 240 | } 241 | 242 | // It's a directory. 243 | fInfoList, err := ioutil.ReadDir(name) 244 | if err != nil { 245 | return nil, err 246 | } 247 | // Add all of the files in the directory to the file list as long 248 | // as they aren't on the ignored list or are hidden files if ignoreHidden 249 | // is set to true. 250 | outer: 251 | for _, fInfo := range fInfoList { 252 | path := filepath.Join(name, fInfo.Name()) 253 | _, ignored := w.ignored[path] 254 | 255 | isHidden, err := isHiddenFile(path) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | if ignored || (w.ignoreHidden && isHidden) { 261 | continue 262 | } 263 | 264 | for _, f := range w.ffh { 265 | err := f(fInfo, path) 266 | if err == ErrSkip { 267 | continue outer 268 | } 269 | if err != nil { 270 | return nil, err 271 | } 272 | } 273 | 274 | fileList[path] = fInfo 275 | } 276 | return fileList, nil 277 | } 278 | 279 | // AddRecursive adds either a single file or directory recursively to the file list. 280 | func (w *Watcher) AddRecursive(name string) (err error) { 281 | w.mu.Lock() 282 | defer w.mu.Unlock() 283 | 284 | name, err = filepath.Abs(name) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | fileList, err := w.listRecursive(name) 290 | if err != nil { 291 | return err 292 | } 293 | for k, v := range fileList { 294 | w.files[k] = v 295 | } 296 | 297 | // Add the name to the names list. 298 | w.names[name] = true 299 | 300 | return nil 301 | } 302 | 303 | func (w *Watcher) listRecursive(name string) (map[string]os.FileInfo, error) { 304 | fileList := make(map[string]os.FileInfo) 305 | 306 | return fileList, filepath.Walk(name, func(path string, info os.FileInfo, err error) error { 307 | if err != nil { 308 | return err 309 | } 310 | 311 | for _, f := range w.ffh { 312 | err := f(info, path) 313 | if err == ErrSkip { 314 | return nil 315 | } 316 | if err != nil { 317 | return err 318 | } 319 | } 320 | 321 | // If path is ignored and it's a directory, skip the directory. If it's 322 | // ignored and it's a single file, skip the file. 323 | _, ignored := w.ignored[path] 324 | 325 | isHidden, err := isHiddenFile(path) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | if ignored || (w.ignoreHidden && isHidden) { 331 | if info.IsDir() { 332 | return filepath.SkipDir 333 | } 334 | return nil 335 | } 336 | // Add the path and it's info to the file list. 337 | fileList[path] = info 338 | return nil 339 | }) 340 | } 341 | 342 | // Remove removes either a single file or directory from the file's list. 343 | func (w *Watcher) Remove(name string) (err error) { 344 | w.mu.Lock() 345 | defer w.mu.Unlock() 346 | 347 | name, err = filepath.Abs(name) 348 | if err != nil { 349 | return err 350 | } 351 | 352 | // Remove the name from w's names list. 353 | delete(w.names, name) 354 | 355 | // If name is a single file, remove it and return. 356 | info, found := w.files[name] 357 | if !found { 358 | return nil // Doesn't exist, just return. 359 | } 360 | if !info.IsDir() { 361 | delete(w.files, name) 362 | return nil 363 | } 364 | 365 | // Delete the actual directory from w.files 366 | delete(w.files, name) 367 | 368 | // If it's a directory, delete all of it's contents from w.files. 369 | for path := range w.files { 370 | if filepath.Dir(path) == name { 371 | delete(w.files, path) 372 | } 373 | } 374 | return nil 375 | } 376 | 377 | // RemoveRecursive removes either a single file or a directory recursively from 378 | // the file's list. 379 | func (w *Watcher) RemoveRecursive(name string) (err error) { 380 | w.mu.Lock() 381 | defer w.mu.Unlock() 382 | 383 | name, err = filepath.Abs(name) 384 | if err != nil { 385 | return err 386 | } 387 | 388 | // Remove the name from w's names list. 389 | delete(w.names, name) 390 | 391 | // If name is a single file, remove it and return. 392 | info, found := w.files[name] 393 | if !found { 394 | return nil // Doesn't exist, just return. 395 | } 396 | if !info.IsDir() { 397 | delete(w.files, name) 398 | return nil 399 | } 400 | 401 | // If it's a directory, delete all of it's contents recursively 402 | // from w.files. 403 | for path := range w.files { 404 | if strings.HasPrefix(path, name) { 405 | delete(w.files, path) 406 | } 407 | } 408 | return nil 409 | } 410 | 411 | // Ignore adds paths that should be ignored. 412 | // 413 | // For files that are already added, Ignore removes them. 414 | func (w *Watcher) Ignore(paths ...string) (err error) { 415 | for _, path := range paths { 416 | path, err = filepath.Abs(path) 417 | if err != nil { 418 | return err 419 | } 420 | // Remove any of the paths that were already added. 421 | if err := w.RemoveRecursive(path); err != nil { 422 | return err 423 | } 424 | w.mu.Lock() 425 | w.ignored[path] = struct{}{} 426 | w.mu.Unlock() 427 | } 428 | return nil 429 | } 430 | 431 | // WatchedFiles returns a map of files added to a Watcher. 432 | func (w *Watcher) WatchedFiles() map[string]os.FileInfo { 433 | w.mu.Lock() 434 | defer w.mu.Unlock() 435 | 436 | files := make(map[string]os.FileInfo) 437 | for k, v := range w.files { 438 | files[k] = v 439 | } 440 | 441 | return files 442 | } 443 | 444 | // fileInfo is an implementation of os.FileInfo that can be used 445 | // as a mocked os.FileInfo when triggering an event when the specified 446 | // os.FileInfo is nil. 447 | type fileInfo struct { 448 | name string 449 | size int64 450 | mode os.FileMode 451 | modTime time.Time 452 | sys interface{} 453 | dir bool 454 | } 455 | 456 | func (fs *fileInfo) IsDir() bool { 457 | return fs.dir 458 | } 459 | func (fs *fileInfo) ModTime() time.Time { 460 | return fs.modTime 461 | } 462 | func (fs *fileInfo) Mode() os.FileMode { 463 | return fs.mode 464 | } 465 | func (fs *fileInfo) Name() string { 466 | return fs.name 467 | } 468 | func (fs *fileInfo) Size() int64 { 469 | return fs.size 470 | } 471 | func (fs *fileInfo) Sys() interface{} { 472 | return fs.sys 473 | } 474 | 475 | // TriggerEvent is a method that can be used to trigger an event, separate to 476 | // the file watching process. 477 | func (w *Watcher) TriggerEvent(eventType Op, file os.FileInfo) { 478 | w.Wait() 479 | if file == nil { 480 | file = &fileInfo{name: "triggered event", modTime: time.Now()} 481 | } 482 | w.Event <- Event{Op: eventType, Path: "-", FileInfo: file} 483 | } 484 | 485 | func (w *Watcher) retrieveFileList() map[string]os.FileInfo { 486 | w.mu.Lock() 487 | defer w.mu.Unlock() 488 | 489 | fileList := make(map[string]os.FileInfo) 490 | 491 | var list map[string]os.FileInfo 492 | var err error 493 | 494 | for name, recursive := range w.names { 495 | if recursive { 496 | list, err = w.listRecursive(name) 497 | if err != nil { 498 | if os.IsNotExist(err) { 499 | w.mu.Unlock() 500 | if name == err.(*os.PathError).Path { 501 | w.Error <- ErrWatchedFileDeleted 502 | w.RemoveRecursive(name) 503 | } 504 | w.mu.Lock() 505 | } else { 506 | w.Error <- err 507 | } 508 | } 509 | } else { 510 | list, err = w.list(name) 511 | if err != nil { 512 | if os.IsNotExist(err) { 513 | w.mu.Unlock() 514 | if name == err.(*os.PathError).Path { 515 | w.Error <- ErrWatchedFileDeleted 516 | w.Remove(name) 517 | } 518 | w.mu.Lock() 519 | } else { 520 | w.Error <- err 521 | } 522 | } 523 | } 524 | // Add the file's to the file list. 525 | for k, v := range list { 526 | fileList[k] = v 527 | } 528 | } 529 | 530 | return fileList 531 | } 532 | 533 | // Start begins the polling cycle which repeats every specified 534 | // duration until Close is called. 535 | func (w *Watcher) Start(d time.Duration) error { 536 | // Return an error if d is less than 1 nanosecond. 537 | if d < time.Nanosecond { 538 | return ErrDurationTooShort 539 | } 540 | 541 | // Make sure the Watcher is not already running. 542 | w.mu.Lock() 543 | if w.running { 544 | w.mu.Unlock() 545 | return ErrWatcherRunning 546 | } 547 | w.running = true 548 | w.mu.Unlock() 549 | 550 | // Unblock w.Wait(). 551 | w.wg.Done() 552 | 553 | for { 554 | // done lets the inner polling cycle loop know when the 555 | // current cycle's method has finished executing. 556 | done := make(chan struct{}) 557 | 558 | // Any events that are found are first piped to evt before 559 | // being sent to the main Event channel. 560 | evt := make(chan Event) 561 | 562 | // Retrieve the file list for all watched file's and dirs. 563 | fileList := w.retrieveFileList() 564 | 565 | // cancel can be used to cancel the current event polling function. 566 | cancel := make(chan struct{}) 567 | 568 | // Look for events. 569 | go func() { 570 | w.pollEvents(fileList, evt, cancel) 571 | done <- struct{}{} 572 | }() 573 | 574 | // numEvents holds the number of events for the current cycle. 575 | numEvents := 0 576 | 577 | inner: 578 | for { 579 | select { 580 | case <-w.close: 581 | close(cancel) 582 | close(w.Closed) 583 | return nil 584 | case event := <-evt: 585 | if len(w.ops) > 0 { // Filter Ops. 586 | _, found := w.ops[event.Op] 587 | if !found { 588 | continue 589 | } 590 | } 591 | numEvents++ 592 | if w.maxEvents > 0 && numEvents > w.maxEvents { 593 | close(cancel) 594 | break inner 595 | } 596 | w.Event <- event 597 | case <-done: // Current cycle is finished. 598 | break inner 599 | } 600 | } 601 | 602 | // Update the file's list. 603 | w.mu.Lock() 604 | w.files = fileList 605 | w.mu.Unlock() 606 | 607 | // Sleep and then continue to the next loop iteration. 608 | time.Sleep(d) 609 | } 610 | } 611 | 612 | func (w *Watcher) pollEvents(files map[string]os.FileInfo, evt chan Event, 613 | cancel chan struct{}) { 614 | w.mu.Lock() 615 | defer w.mu.Unlock() 616 | 617 | // Store create and remove events for use to check for rename events. 618 | creates := make(map[string]os.FileInfo) 619 | removes := make(map[string]os.FileInfo) 620 | 621 | // Check for removed files. 622 | for path, info := range w.files { 623 | if _, found := files[path]; !found { 624 | removes[path] = info 625 | } 626 | } 627 | 628 | // Check for created files, writes and chmods. 629 | for path, info := range files { 630 | oldInfo, found := w.files[path] 631 | if !found { 632 | // A file was created. 633 | creates[path] = info 634 | continue 635 | } 636 | if oldInfo.ModTime() != info.ModTime() { 637 | select { 638 | case <-cancel: 639 | return 640 | case evt <- Event{Write, path, path, info}: 641 | } 642 | } 643 | if oldInfo.Mode() != info.Mode() { 644 | select { 645 | case <-cancel: 646 | return 647 | case evt <- Event{Chmod, path, path, info}: 648 | } 649 | } 650 | } 651 | 652 | // Check for renames and moves. 653 | for path1, info1 := range removes { 654 | for path2, info2 := range creates { 655 | if sameFile(info1, info2) { 656 | e := Event{ 657 | Op: Move, 658 | Path: path2, 659 | OldPath: path1, 660 | FileInfo: info1, 661 | } 662 | // If they are from the same directory, it's a rename 663 | // instead of a move event. 664 | if filepath.Dir(path1) == filepath.Dir(path2) { 665 | e.Op = Rename 666 | } 667 | 668 | delete(removes, path1) 669 | delete(creates, path2) 670 | 671 | select { 672 | case <-cancel: 673 | return 674 | case evt <- e: 675 | } 676 | } 677 | } 678 | } 679 | 680 | // Send all the remaining create and remove events. 681 | for path, info := range creates { 682 | select { 683 | case <-cancel: 684 | return 685 | case evt <- Event{Create, path, "", info}: 686 | } 687 | } 688 | for path, info := range removes { 689 | select { 690 | case <-cancel: 691 | return 692 | case evt <- Event{Remove, path, path, info}: 693 | } 694 | } 695 | } 696 | 697 | // Wait blocks until the watcher is started. 698 | func (w *Watcher) Wait() { 699 | w.wg.Wait() 700 | } 701 | 702 | // Close stops a Watcher and unlocks its mutex, then sends a close signal. 703 | func (w *Watcher) Close() { 704 | w.mu.Lock() 705 | if !w.running { 706 | w.mu.Unlock() 707 | return 708 | } 709 | w.running = false 710 | w.files = make(map[string]os.FileInfo) 711 | w.names = make(map[string]bool) 712 | w.mu.Unlock() 713 | // Send a close signal to the Start method. 714 | w.close <- struct{}{} 715 | } 716 | -------------------------------------------------------------------------------- /watcher_test.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | // setup creates all required files and folders for 14 | // the tests and returns a function that is used as 15 | // a teardown function when the tests are done. 16 | func setup(t testing.TB) (string, func()) { 17 | testDir, err := ioutil.TempDir(".", "") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | err = ioutil.WriteFile(filepath.Join(testDir, "file.txt"), 23 | []byte{}, 0755) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | files := []string{"file_1.txt", "file_2.txt", "file_3.txt"} 29 | 30 | for _, f := range files { 31 | filePath := filepath.Join(testDir, f) 32 | if err := ioutil.WriteFile(filePath, []byte{}, 0755); err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | 37 | err = ioutil.WriteFile(filepath.Join(testDir, ".dotfile"), 38 | []byte{}, 0755) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | testDirTwo := filepath.Join(testDir, "testDirTwo") 44 | err = os.Mkdir(testDirTwo, 0755) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | err = ioutil.WriteFile(filepath.Join(testDirTwo, "file_recursive.txt"), 50 | []byte{}, 0755) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | abs, err := filepath.Abs(testDir) 56 | if err != nil { 57 | os.RemoveAll(testDir) 58 | t.Fatal(err) 59 | } 60 | return abs, func() { 61 | if os.RemoveAll(testDir); err != nil { 62 | t.Fatal(err) 63 | } 64 | } 65 | } 66 | 67 | func TestEventString(t *testing.T) { 68 | e := &Event{Op: Create, Path: "/fake/path"} 69 | 70 | testCases := []struct { 71 | info os.FileInfo 72 | expected string 73 | }{ 74 | {nil, "???"}, 75 | { 76 | &fileInfo{name: "f1", dir: true}, 77 | "DIRECTORY \"f1\" CREATE [/fake/path]", 78 | }, 79 | { 80 | &fileInfo{name: "f2", dir: false}, 81 | "FILE \"f2\" CREATE [/fake/path]", 82 | }, 83 | } 84 | 85 | for _, tc := range testCases { 86 | e.FileInfo = tc.info 87 | if e.String() != tc.expected { 88 | t.Errorf("expected e.String() to be %s, got %s", tc.expected, e.String()) 89 | } 90 | } 91 | } 92 | 93 | func TestFileInfo(t *testing.T) { 94 | modTime := time.Now() 95 | 96 | fInfo := &fileInfo{ 97 | name: "finfo", 98 | size: 1, 99 | mode: os.ModeDir, 100 | modTime: modTime, 101 | sys: nil, 102 | dir: true, 103 | } 104 | 105 | // Test file info methods. 106 | if fInfo.Name() != "finfo" { 107 | t.Fatalf("expected fInfo.Name() to be 'finfo', got %s", fInfo.Name()) 108 | } 109 | if fInfo.IsDir() != true { 110 | t.Fatalf("expected fInfo.IsDir() to be true, got %t", fInfo.IsDir()) 111 | } 112 | if fInfo.Size() != 1 { 113 | t.Fatalf("expected fInfo.Size() to be 1, got %d", fInfo.Size()) 114 | } 115 | if fInfo.Sys() != nil { 116 | t.Fatalf("expected fInfo.Sys() to be nil, got %v", fInfo.Sys()) 117 | } 118 | if fInfo.ModTime() != modTime { 119 | t.Fatalf("expected fInfo.ModTime() to be %v, got %v", modTime, fInfo.ModTime()) 120 | } 121 | if fInfo.Mode() != os.ModeDir { 122 | t.Fatalf("expected fInfo.Mode() to be os.ModeDir, got %#v", fInfo.Mode()) 123 | } 124 | 125 | w := New() 126 | 127 | w.wg.Done() // Set the waitgroup to done. 128 | 129 | go func() { 130 | // Trigger an event with the file info. 131 | w.TriggerEvent(Create, fInfo) 132 | }() 133 | 134 | e := <-w.Event 135 | 136 | if e.FileInfo != fInfo { 137 | t.Fatal("expected e.FileInfo to be equal to fInfo") 138 | } 139 | } 140 | 141 | func TestWatcherAdd(t *testing.T) { 142 | testDir, teardown := setup(t) 143 | defer teardown() 144 | 145 | w := New() 146 | 147 | // Try to add a non-existing path. 148 | err := w.Add("-") 149 | if err == nil { 150 | t.Error("expected error to not be nil") 151 | } 152 | 153 | if err := w.Add(testDir); err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | if len(w.files) != 7 { 158 | t.Errorf("expected len(w.files) to be 7, got %d", len(w.files)) 159 | } 160 | 161 | // Make sure w.names contains testDir 162 | if _, found := w.names[testDir]; !found { 163 | t.Errorf("expected w.names to contain testDir") 164 | } 165 | 166 | if _, found := w.files[testDir]; !found { 167 | t.Errorf("expected to find %s", testDir) 168 | } 169 | 170 | if w.files[testDir].Name() != filepath.Base(testDir) { 171 | t.Errorf("expected w.files[%q].Name() to be %s, got %s", 172 | testDir, testDir, w.files[testDir].Name()) 173 | } 174 | 175 | dotFile := filepath.Join(testDir, ".dotfile") 176 | if _, found := w.files[dotFile]; !found { 177 | t.Errorf("expected to find %s", dotFile) 178 | } 179 | 180 | if w.files[dotFile].Name() != ".dotfile" { 181 | t.Errorf("expected w.files[%q].Name() to be .dotfile, got %s", 182 | dotFile, w.files[dotFile].Name()) 183 | } 184 | 185 | fileRecursive := filepath.Join(testDir, "testDirTwo", "file_recursive.txt") 186 | if _, found := w.files[fileRecursive]; found { 187 | t.Errorf("expected to not find %s", fileRecursive) 188 | } 189 | 190 | fileTxt := filepath.Join(testDir, "file.txt") 191 | if _, found := w.files[fileTxt]; !found { 192 | t.Errorf("expected to find %s", fileTxt) 193 | } 194 | 195 | if w.files[fileTxt].Name() != "file.txt" { 196 | t.Errorf("expected w.files[%q].Name() to be file.txt, got %s", 197 | fileTxt, w.files[fileTxt].Name()) 198 | } 199 | 200 | dirTwo := filepath.Join(testDir, "testDirTwo") 201 | if _, found := w.files[dirTwo]; !found { 202 | t.Errorf("expected to find %s directory", dirTwo) 203 | } 204 | 205 | if w.files[dirTwo].Name() != "testDirTwo" { 206 | t.Errorf("expected w.files[%q].Name() to be testDirTwo, got %s", 207 | dirTwo, w.files[dirTwo].Name()) 208 | } 209 | } 210 | 211 | func TestIgnore(t *testing.T) { 212 | testDir, teardown := setup(t) 213 | defer teardown() 214 | 215 | w := New() 216 | 217 | err := w.Add(testDir) 218 | if err != nil { 219 | t.Errorf("expected error to be nil, got %s", err) 220 | } 221 | if len(w.files) != 7 { 222 | t.Errorf("expected len(w.files) to be 7, got %d", len(w.files)) 223 | } 224 | 225 | err = w.Ignore(testDir) 226 | if err != nil { 227 | t.Errorf("expected error to be nil, got %s", err) 228 | } 229 | if len(w.files) != 0 { 230 | t.Errorf("expected len(w.files) to be 0, got %d", len(w.files)) 231 | } 232 | 233 | // Now try to add the ignored directory. 234 | err = w.Add(testDir) 235 | if err != nil { 236 | t.Errorf("expected error to be nil, got %s", err) 237 | } 238 | if len(w.files) != 0 { 239 | t.Errorf("expected len(w.files) to be 0, got %d", len(w.files)) 240 | } 241 | } 242 | 243 | func TestRemove(t *testing.T) { 244 | testDir, teardown := setup(t) 245 | defer teardown() 246 | 247 | w := New() 248 | 249 | err := w.Add(testDir) 250 | if err != nil { 251 | t.Errorf("expected error to be nil, got %s", err) 252 | } 253 | if len(w.files) != 7 { 254 | t.Errorf("expected len(w.files) to be 7, got %d", len(w.files)) 255 | } 256 | 257 | err = w.Remove(testDir) 258 | if err != nil { 259 | t.Errorf("expected error to be nil, got %s", err) 260 | } 261 | if len(w.files) != 0 { 262 | t.Errorf("expected len(w.files) to be 0, got %d", len(w.files)) 263 | } 264 | 265 | // TODO: Test remove single file. 266 | } 267 | 268 | // TODO: Test remove recursive function. 269 | 270 | func TestIgnoreHiddenFilesRecursive(t *testing.T) { 271 | // TODO: Write tests for ignore hidden on windows. 272 | if runtime.GOOS == "windows" { 273 | return 274 | } 275 | 276 | testDir, teardown := setup(t) 277 | defer teardown() 278 | 279 | w := New() 280 | w.IgnoreHiddenFiles(true) 281 | 282 | if err := w.AddRecursive(testDir); err != nil { 283 | t.Fatal(err) 284 | } 285 | 286 | if len(w.files) != 7 { 287 | t.Errorf("expected len(w.files) to be 7, got %d", len(w.files)) 288 | } 289 | 290 | // Make sure w.names contains testDir 291 | if _, found := w.names[testDir]; !found { 292 | t.Errorf("expected w.names to contain testDir") 293 | } 294 | 295 | if _, found := w.files[testDir]; !found { 296 | t.Errorf("expected to find %s", testDir) 297 | } 298 | 299 | if w.files[testDir].Name() != filepath.Base(testDir) { 300 | t.Errorf("expected w.files[%q].Name() to be %s, got %s", 301 | testDir, filepath.Base(testDir), w.files[testDir].Name()) 302 | } 303 | 304 | fileRecursive := filepath.Join(testDir, "testDirTwo", "file_recursive.txt") 305 | if _, found := w.files[fileRecursive]; !found { 306 | t.Errorf("expected to find %s", fileRecursive) 307 | } 308 | 309 | if _, found := w.files[filepath.Join(testDir, ".dotfile")]; found { 310 | t.Error("expected to not find .dotfile") 311 | } 312 | 313 | fileTxt := filepath.Join(testDir, "file.txt") 314 | if _, found := w.files[fileTxt]; !found { 315 | t.Errorf("expected to find %s", fileTxt) 316 | } 317 | 318 | if w.files[fileTxt].Name() != "file.txt" { 319 | t.Errorf("expected w.files[%q].Name() to be file.txt, got %s", 320 | fileTxt, w.files[fileTxt].Name()) 321 | } 322 | 323 | dirTwo := filepath.Join(testDir, "testDirTwo") 324 | if _, found := w.files[dirTwo]; !found { 325 | t.Errorf("expected to find %s directory", dirTwo) 326 | } 327 | 328 | if w.files[dirTwo].Name() != "testDirTwo" { 329 | t.Errorf("expected w.files[%q].Name() to be testDirTwo, got %s", 330 | dirTwo, w.files[dirTwo].Name()) 331 | } 332 | } 333 | 334 | func TestIgnoreHiddenFiles(t *testing.T) { 335 | // TODO: Write tests for ignore hidden on windows. 336 | if runtime.GOOS == "windows" { 337 | return 338 | } 339 | 340 | testDir, teardown := setup(t) 341 | defer teardown() 342 | 343 | w := New() 344 | w.IgnoreHiddenFiles(true) 345 | 346 | if err := w.Add(testDir); err != nil { 347 | t.Fatal(err) 348 | } 349 | 350 | if len(w.files) != 6 { 351 | t.Errorf("expected len(w.files) to be 6, got %d", len(w.files)) 352 | } 353 | 354 | // Make sure w.names contains testDir 355 | if _, found := w.names[testDir]; !found { 356 | t.Errorf("expected w.names to contain testDir") 357 | } 358 | 359 | if _, found := w.files[testDir]; !found { 360 | t.Errorf("expected to find %s", testDir) 361 | } 362 | 363 | if w.files[testDir].Name() != filepath.Base(testDir) { 364 | t.Errorf("expected w.files[%q].Name() to be %s, got %s", 365 | testDir, filepath.Base(testDir), w.files[testDir].Name()) 366 | } 367 | 368 | if _, found := w.files[filepath.Join(testDir, ".dotfile")]; found { 369 | t.Error("expected to not find .dotfile") 370 | } 371 | 372 | fileRecursive := filepath.Join(testDir, "testDirTwo", "file_recursive.txt") 373 | if _, found := w.files[fileRecursive]; found { 374 | t.Errorf("expected to not find %s", fileRecursive) 375 | } 376 | 377 | fileTxt := filepath.Join(testDir, "file.txt") 378 | if _, found := w.files[fileTxt]; !found { 379 | t.Errorf("expected to find %s", fileTxt) 380 | } 381 | 382 | if w.files[fileTxt].Name() != "file.txt" { 383 | t.Errorf("expected w.files[%q].Name() to be file.txt, got %s", 384 | fileTxt, w.files[fileTxt].Name()) 385 | } 386 | 387 | dirTwo := filepath.Join(testDir, "testDirTwo") 388 | if _, found := w.files[dirTwo]; !found { 389 | t.Errorf("expected to find %s directory", dirTwo) 390 | } 391 | 392 | if w.files[dirTwo].Name() != "testDirTwo" { 393 | t.Errorf("expected w.files[%q].Name() to be testDirTwo, got %s", 394 | dirTwo, w.files[dirTwo].Name()) 395 | } 396 | } 397 | 398 | func TestWatcherAddRecursive(t *testing.T) { 399 | testDir, teardown := setup(t) 400 | defer teardown() 401 | 402 | w := New() 403 | 404 | if err := w.AddRecursive(testDir); err != nil { 405 | t.Fatal(err) 406 | } 407 | 408 | // Make sure len(w.files) is 8. 409 | if len(w.files) != 8 { 410 | t.Errorf("expected 8 files, found %d", len(w.files)) 411 | } 412 | 413 | // Make sure w.names contains testDir 414 | if _, found := w.names[testDir]; !found { 415 | t.Errorf("expected w.names to contain testDir") 416 | } 417 | 418 | dirTwo := filepath.Join(testDir, "testDirTwo") 419 | if _, found := w.files[dirTwo]; !found { 420 | t.Errorf("expected to find %s directory", dirTwo) 421 | } 422 | 423 | if w.files[dirTwo].Name() != "testDirTwo" { 424 | t.Errorf("expected w.files[%q].Name() to be testDirTwo, got %s", 425 | "testDirTwo", w.files[dirTwo].Name()) 426 | } 427 | 428 | fileRecursive := filepath.Join(dirTwo, "file_recursive.txt") 429 | if _, found := w.files[fileRecursive]; !found { 430 | t.Errorf("expected to find %s directory", fileRecursive) 431 | } 432 | 433 | if w.files[fileRecursive].Name() != "file_recursive.txt" { 434 | t.Errorf("expected w.files[%q].Name() to be file_recursive.txt, got %s", 435 | fileRecursive, w.files[fileRecursive].Name()) 436 | } 437 | } 438 | 439 | func TestWatcherAddNotFound(t *testing.T) { 440 | w := New() 441 | 442 | // Make sure there is an error when adding a 443 | // non-existent file/folder. 444 | if err := w.AddRecursive("random_filename.txt"); err == nil { 445 | t.Error("expected a file not found error") 446 | } 447 | } 448 | 449 | func TestWatcherRemoveRecursive(t *testing.T) { 450 | testDir, teardown := setup(t) 451 | defer teardown() 452 | 453 | w := New() 454 | 455 | // Add the testDir to the watchlist. 456 | if err := w.AddRecursive(testDir); err != nil { 457 | t.Fatal(err) 458 | } 459 | 460 | // Make sure len(w.files) is 8. 461 | if len(w.files) != 8 { 462 | t.Errorf("expected 8 files, found %d", len(w.files)) 463 | } 464 | 465 | // Now remove the folder from the watchlist. 466 | if err := w.RemoveRecursive(testDir); err != nil { 467 | t.Error(err) 468 | } 469 | 470 | // Now check that there is nothing being watched. 471 | if len(w.files) != 0 { 472 | t.Errorf("expected len(w.files) to be 0, got %d", len(w.files)) 473 | } 474 | 475 | // Make sure len(w.names) is now 0. 476 | if len(w.names) != 0 { 477 | t.Errorf("expected len(w.names) to be empty, len(w.names): %d", len(w.names)) 478 | } 479 | } 480 | 481 | func TestListFiles(t *testing.T) { 482 | testDir, teardown := setup(t) 483 | defer teardown() 484 | 485 | w := New() 486 | w.AddRecursive(testDir) 487 | 488 | fileList := w.retrieveFileList() 489 | if fileList == nil { 490 | t.Error("expected file list to not be empty") 491 | } 492 | 493 | // Make sure fInfoTest contains the correct os.FileInfo names. 494 | fname := filepath.Join(testDir, "file.txt") 495 | if fileList[fname].Name() != "file.txt" { 496 | t.Errorf("expected fileList[%s].Name() to be file.txt, got %s", 497 | fname, fileList[fname].Name()) 498 | } 499 | 500 | // Try to call list on a file that's not a directory. 501 | fileList, err := w.list(fname) 502 | if err != nil { 503 | t.Error("expected err to be nil") 504 | } 505 | if len(fileList) != 1 { 506 | t.Errorf("expected len of file list to be 1, got %d", len(fileList)) 507 | } 508 | } 509 | 510 | func TestTriggerEvent(t *testing.T) { 511 | w := New() 512 | 513 | var wg sync.WaitGroup 514 | wg.Add(1) 515 | 516 | go func() { 517 | defer wg.Done() 518 | 519 | select { 520 | case event := <-w.Event: 521 | if event.Name() != "triggered event" { 522 | t.Errorf("expected event file name to be triggered event, got %s", 523 | event.Name()) 524 | } 525 | case <-time.After(time.Millisecond * 250): 526 | t.Fatal("received no event from Event channel") 527 | } 528 | }() 529 | 530 | go func() { 531 | // Start the watching process. 532 | if err := w.Start(time.Millisecond * 100); err != nil { 533 | t.Fatal(err) 534 | } 535 | }() 536 | 537 | w.TriggerEvent(Create, nil) 538 | 539 | wg.Wait() 540 | } 541 | 542 | func TestEventAddFile(t *testing.T) { 543 | testDir, teardown := setup(t) 544 | defer teardown() 545 | 546 | w := New() 547 | w.FilterOps(Create) 548 | 549 | // Add the testDir to the watchlist. 550 | if err := w.AddRecursive(testDir); err != nil { 551 | t.Fatal(err) 552 | } 553 | 554 | files := map[string]bool{ 555 | "newfile_1.txt": false, 556 | "newfile_2.txt": false, 557 | "newfile_3.txt": false, 558 | } 559 | 560 | for f := range files { 561 | filePath := filepath.Join(testDir, f) 562 | if err := ioutil.WriteFile(filePath, []byte{}, 0755); err != nil { 563 | t.Error(err) 564 | } 565 | } 566 | 567 | var wg sync.WaitGroup 568 | wg.Add(1) 569 | 570 | go func() { 571 | defer wg.Done() 572 | 573 | events := 0 574 | for { 575 | select { 576 | case event := <-w.Event: 577 | if event.Op != Create { 578 | t.Errorf("expected event to be Create, got %s", event.Op) 579 | } 580 | 581 | files[event.Name()] = true 582 | events++ 583 | 584 | // Check Path and OldPath content 585 | newFile := filepath.Join(testDir, event.Name()) 586 | if event.Path != newFile { 587 | t.Errorf("Event.Path should be %s but got %s", newFile, event.Path) 588 | } 589 | if event.OldPath != "" { 590 | t.Errorf("Event.OldPath should be empty on create, but got %s", event.OldPath) 591 | } 592 | 593 | if events == len(files) { 594 | return 595 | } 596 | case <-time.After(time.Millisecond * 250): 597 | for f, e := range files { 598 | if !e { 599 | t.Errorf("received no event for file %s", f) 600 | } 601 | } 602 | return 603 | } 604 | } 605 | }() 606 | 607 | go func() { 608 | // Start the watching process. 609 | if err := w.Start(time.Millisecond * 100); err != nil { 610 | t.Fatal(err) 611 | } 612 | }() 613 | 614 | wg.Wait() 615 | } 616 | 617 | // TODO: TestIgnoreFiles 618 | func TestIgnoreFiles(t *testing.T) {} 619 | 620 | func TestEventDeleteFile(t *testing.T) { 621 | testDir, teardown := setup(t) 622 | defer teardown() 623 | 624 | w := New() 625 | w.FilterOps(Remove) 626 | 627 | // Add the testDir to the watchlist. 628 | if err := w.AddRecursive(testDir); err != nil { 629 | t.Fatal(err) 630 | } 631 | 632 | files := map[string]bool{ 633 | "file_1.txt": false, 634 | "file_2.txt": false, 635 | "file_3.txt": false, 636 | } 637 | 638 | for f := range files { 639 | filePath := filepath.Join(testDir, f) 640 | if err := os.Remove(filePath); err != nil { 641 | t.Error(err) 642 | } 643 | } 644 | 645 | var wg sync.WaitGroup 646 | wg.Add(1) 647 | 648 | go func() { 649 | defer wg.Done() 650 | 651 | events := 0 652 | for { 653 | select { 654 | case event := <-w.Event: 655 | if event.Op != Remove { 656 | t.Errorf("expected event to be Remove, got %s", event.Op) 657 | } 658 | 659 | files[event.Name()] = true 660 | events++ 661 | 662 | if events == len(files) { 663 | return 664 | } 665 | case <-time.After(time.Millisecond * 250): 666 | for f, e := range files { 667 | if !e { 668 | t.Errorf("received no event for file %s", f) 669 | } 670 | } 671 | return 672 | } 673 | } 674 | }() 675 | 676 | go func() { 677 | // Start the watching process. 678 | if err := w.Start(time.Millisecond * 100); err != nil { 679 | t.Fatal(err) 680 | } 681 | }() 682 | 683 | wg.Wait() 684 | } 685 | 686 | func TestEventRenameFile(t *testing.T) { 687 | testDir, teardown := setup(t) 688 | defer teardown() 689 | 690 | srcFilename := "file.txt" 691 | dstFilename := "file1.txt" 692 | 693 | w := New() 694 | w.FilterOps(Rename) 695 | 696 | // Add the testDir to the watchlist. 697 | if err := w.AddRecursive(testDir); err != nil { 698 | t.Fatal(err) 699 | } 700 | 701 | // Rename a file. 702 | if err := os.Rename( 703 | filepath.Join(testDir, srcFilename), 704 | filepath.Join(testDir, dstFilename), 705 | ); err != nil { 706 | t.Error(err) 707 | } 708 | 709 | var wg sync.WaitGroup 710 | wg.Add(1) 711 | 712 | go func() { 713 | defer wg.Done() 714 | 715 | select { 716 | case event := <-w.Event: 717 | if event.Op != Rename { 718 | t.Errorf("expected event to be Rename, got %s", event.Op) 719 | } 720 | 721 | // Check Path and OldPath content 722 | oldFile := filepath.Join(testDir, srcFilename) 723 | newFile := filepath.Join(testDir, dstFilename) 724 | if event.Path != newFile { 725 | t.Errorf("Event.Path should be %s but got %s", newFile, event.Path) 726 | } 727 | if event.OldPath != oldFile { 728 | t.Errorf("Event.OldPath should %s but got %s", oldFile, event.OldPath) 729 | } 730 | 731 | case <-time.After(time.Millisecond * 250): 732 | t.Fatal("received no rename event") 733 | } 734 | }() 735 | 736 | go func() { 737 | // Start the watching process. 738 | if err := w.Start(time.Millisecond * 100); err != nil { 739 | t.Fatal(err) 740 | } 741 | }() 742 | 743 | wg.Wait() 744 | } 745 | 746 | func TestEventChmodFile(t *testing.T) { 747 | // Chmod is not supported under windows. 748 | if runtime.GOOS == "windows" { 749 | return 750 | } 751 | 752 | testDir, teardown := setup(t) 753 | defer teardown() 754 | 755 | w := New() 756 | w.FilterOps(Chmod) 757 | 758 | // Add the testDir to the watchlist. 759 | if err := w.Add(testDir); err != nil { 760 | t.Fatal(err) 761 | } 762 | 763 | files := map[string]bool{ 764 | "file_1.txt": false, 765 | "file_2.txt": false, 766 | "file_3.txt": false, 767 | } 768 | 769 | for f := range files { 770 | filePath := filepath.Join(testDir, f) 771 | if err := os.Chmod(filePath, os.ModePerm); err != nil { 772 | t.Error(err) 773 | } 774 | } 775 | 776 | var wg sync.WaitGroup 777 | wg.Add(1) 778 | 779 | go func() { 780 | defer wg.Done() 781 | 782 | events := 0 783 | for { 784 | select { 785 | case event := <-w.Event: 786 | if event.Op != Chmod { 787 | t.Errorf("expected event to be Remove, got %s", event.Op) 788 | } 789 | 790 | files[event.Name()] = true 791 | events++ 792 | 793 | if events == len(files) { 794 | return 795 | } 796 | case <-time.After(time.Millisecond * 250): 797 | for f, e := range files { 798 | if !e { 799 | t.Errorf("received no event for file %s", f) 800 | } 801 | } 802 | return 803 | } 804 | } 805 | }() 806 | 807 | go func() { 808 | // Start the watching process. 809 | if err := w.Start(time.Millisecond * 100); err != nil { 810 | t.Fatal(err) 811 | } 812 | }() 813 | 814 | wg.Wait() 815 | } 816 | 817 | func TestWatcherStartWithInvalidDuration(t *testing.T) { 818 | w := New() 819 | 820 | err := w.Start(0) 821 | if err != ErrDurationTooShort { 822 | t.Fatalf("expected ErrDurationTooShort error, got %s", err.Error()) 823 | } 824 | } 825 | 826 | func TestWatcherStartWhenAlreadyRunning(t *testing.T) { 827 | w := New() 828 | 829 | go func() { 830 | err := w.Start(time.Millisecond * 100) 831 | if err != nil { 832 | t.Fatal(err) 833 | } 834 | }() 835 | w.Wait() 836 | 837 | err := w.Start(time.Millisecond * 100) 838 | if err != ErrWatcherRunning { 839 | t.Fatalf("expected ErrWatcherRunning error, got %s", err.Error()) 840 | } 841 | } 842 | 843 | func BenchmarkEventRenameFile(b *testing.B) { 844 | testDir, teardown := setup(b) 845 | defer teardown() 846 | 847 | w := New() 848 | w.FilterOps(Rename) 849 | 850 | // Add the testDir to the watchlist. 851 | if err := w.AddRecursive(testDir); err != nil { 852 | b.Fatal(err) 853 | } 854 | 855 | go func() { 856 | // Start the watching process. 857 | if err := w.Start(time.Millisecond); err != nil { 858 | b.Fatal(err) 859 | } 860 | }() 861 | 862 | var filenameFrom = filepath.Join(testDir, "file.txt") 863 | var filenameTo = filepath.Join(testDir, "file1.txt") 864 | 865 | for i := 0; i < b.N; i++ { 866 | // Rename a file. 867 | if err := os.Rename( 868 | filenameFrom, 869 | filenameTo, 870 | ); err != nil { 871 | b.Error(err) 872 | } 873 | 874 | select { 875 | case event := <-w.Event: 876 | if event.Op != Rename { 877 | b.Errorf("expected event to be Rename, got %s", event.Op) 878 | } 879 | case <-time.After(time.Millisecond * 250): 880 | b.Fatal("received no rename event") 881 | } 882 | 883 | filenameFrom, filenameTo = filenameTo, filenameFrom 884 | } 885 | } 886 | 887 | func BenchmarkListFiles(b *testing.B) { 888 | testDir, teardown := setup(b) 889 | defer teardown() 890 | 891 | w := New() 892 | err := w.AddRecursive(testDir) 893 | if err != nil { 894 | b.Fatal(err) 895 | } 896 | 897 | for i := 0; i < b.N; i++ { 898 | fileList := w.retrieveFileList() 899 | if fileList == nil { 900 | b.Fatal("expected file list to not be empty") 901 | } 902 | } 903 | } 904 | 905 | func TestClose(t *testing.T) { 906 | testDir, teardown := setup(t) 907 | defer teardown() 908 | 909 | w := New() 910 | 911 | err := w.Add(testDir) 912 | if err != nil { 913 | t.Fatal(err) 914 | } 915 | 916 | wf := w.WatchedFiles() 917 | fileList := w.retrieveFileList() 918 | 919 | if len(wf) != len(fileList) { 920 | t.Fatalf("expected len of wf to be %d, got %d", len(fileList), len(wf)) 921 | } 922 | 923 | // Call close on the watcher even though it's not running. 924 | w.Close() 925 | 926 | wf = w.WatchedFiles() 927 | fileList = w.retrieveFileList() 928 | 929 | // Close will be a no-op so there will still be len(fileList) files. 930 | if len(wf) != len(fileList) { 931 | t.Fatalf("expected len of wf to be %d, got %d", len(fileList), len(wf)) 932 | } 933 | 934 | // Set running to true. 935 | w.running = true 936 | 937 | // Now close the watcher. 938 | go func() { 939 | // Receive from the w.close channel to avoid a deadlock. 940 | <-w.close 941 | }() 942 | 943 | w.Close() 944 | 945 | wf = w.WatchedFiles() 946 | 947 | // Close will be a no-op so there will still be len(fileList) files. 948 | if len(wf) != 0 { 949 | t.Fatalf("expected len of wf to be 0, got %d", len(wf)) 950 | } 951 | 952 | } 953 | 954 | func TestWatchedFiles(t *testing.T) { 955 | testDir, teardown := setup(t) 956 | defer teardown() 957 | 958 | w := New() 959 | 960 | err := w.Add(testDir) 961 | if err != nil { 962 | t.Fatal(err) 963 | } 964 | 965 | wf := w.WatchedFiles() 966 | fileList := w.retrieveFileList() 967 | 968 | if len(wf) != len(fileList) { 969 | t.Fatalf("expected len of wf to be %d, got %d", len(fileList), len(wf)) 970 | } 971 | 972 | for path := range fileList { 973 | if _, found := wf[path]; !found { 974 | t.Fatalf("%s not found in watched file's list", path) 975 | } 976 | } 977 | } 978 | 979 | func TestSetMaxEvents(t *testing.T) { 980 | w := New() 981 | 982 | if w.maxEvents != 0 { 983 | t.Fatalf("expected max events to be 0, got %d", w.maxEvents) 984 | } 985 | 986 | w.SetMaxEvents(3) 987 | 988 | if w.maxEvents != 3 { 989 | t.Fatalf("expected max events to be 3, got %d", w.maxEvents) 990 | } 991 | } 992 | 993 | func TestOpsString(t *testing.T) { 994 | testCases := []struct { 995 | want Op 996 | expected string 997 | }{ 998 | {Create, "CREATE"}, 999 | {Write, "WRITE"}, 1000 | {Remove, "REMOVE"}, 1001 | {Rename, "RENAME"}, 1002 | {Chmod, "CHMOD"}, 1003 | {Move, "MOVE"}, 1004 | {Op(10), "???"}, 1005 | } 1006 | 1007 | for _, tc := range testCases { 1008 | if tc.want.String() != tc.expected { 1009 | t.Errorf("expected %s, got %s", tc.expected, tc.want.String()) 1010 | } 1011 | } 1012 | } 1013 | --------------------------------------------------------------------------------