github.com/gofunct/common@v0.0.0-20190131174352-fd058c7fbf22/pkg/fs/watcher/fswatch/watcher.go (about)

     1  package fswatch
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/mgutz/str"
    10  )
    11  
    12  // Watcher represents a file system watcher. It should be initialised
    13  // with NewWatcher or NewAutoWatcher, and started with Watcher.Start().
    14  type Watcher struct {
    15  	paths     map[string]*watchItem
    16  	cnotify   chan *Notification
    17  	cadd      chan *watchItem
    18  	autoWatch bool
    19  
    20  	// ignoreFn is used to ignore paths
    21  	IgnorePathFn func(path string) bool
    22  }
    23  
    24  // newWatcher is the internal function for properly setting up a new watcher.
    25  func newWatcher(dirNotify bool, initpaths ...string) (w *Watcher) {
    26  	w = new(Watcher)
    27  	w.autoWatch = dirNotify
    28  	w.paths = make(map[string]*watchItem, 0)
    29  	w.IgnorePathFn = ignorePathDefault
    30  
    31  	var paths []string
    32  	for _, path := range initpaths {
    33  		matches, err := filepath.Glob(path)
    34  		if err != nil {
    35  			continue
    36  		}
    37  		paths = append(paths, matches...)
    38  	}
    39  	if dirNotify {
    40  		w.syncAddPaths(paths...)
    41  	} else {
    42  		for _, path := range paths {
    43  			w.paths[path] = watchPath(path)
    44  		}
    45  	}
    46  	return
    47  }
    48  
    49  // NewWatcher initialises a new Watcher with an initial set of paths. It
    50  // does not start listening, and this Watcher will not automatically add
    51  // files created under any directories it is watching.
    52  func NewWatcher(paths ...string) *Watcher {
    53  	return newWatcher(false, paths...)
    54  }
    55  
    56  // NewAutoWatcher initialises a new Watcher with an initial set of paths.
    57  // It behaves the same as NewWatcher, except it will automatically add
    58  // files created in directories it is watching, including adding any
    59  // subdirectories.
    60  func NewAutoWatcher(paths ...string) *Watcher {
    61  	return newWatcher(true, paths...)
    62  }
    63  
    64  // Start begins watching the files, sending notifications when files change.
    65  // It returns a channel that notifications are sent on.
    66  func (w *Watcher) Start() <-chan *Notification {
    67  	if w.cnotify != nil {
    68  		return w.cnotify
    69  	}
    70  	if w.autoWatch {
    71  		w.cadd = make(chan *watchItem, NotificationBufLen)
    72  		go w.watchItemListener()
    73  	}
    74  	w.cnotify = make(chan *Notification, NotificationBufLen)
    75  	go w.watch(w.cnotify)
    76  	return w.cnotify
    77  }
    78  
    79  // Stop listening for changes to the files.
    80  func (w *Watcher) Stop() {
    81  	if w.cnotify != nil {
    82  		close(w.cnotify)
    83  	}
    84  
    85  	if w.cadd != nil {
    86  		close(w.cadd)
    87  	}
    88  }
    89  
    90  // Active returns true if the Watcher is actively looking for changes.
    91  func (w *Watcher) Active() bool {
    92  	return w.paths != nil && len(w.paths) > 0
    93  }
    94  
    95  // Add method takes a variable number of string arguments and adds those
    96  // files to the watch list, returning the number of files added.
    97  func (w *Watcher) Add(inpaths ...string) {
    98  	var paths []string
    99  	for _, path := range inpaths {
   100  		matches, err := filepath.Glob(path)
   101  		if err != nil {
   102  			continue
   103  		}
   104  		paths = append(paths, matches...)
   105  	}
   106  	if w.autoWatch && w.cnotify != nil {
   107  		for _, path := range paths {
   108  			wi := watchPath(path)
   109  			w.addPaths(wi)
   110  		}
   111  	} else if w.autoWatch {
   112  		w.syncAddPaths(paths...)
   113  	} else {
   114  		for _, path := range paths {
   115  			w.paths[path] = watchPath(path)
   116  		}
   117  	}
   118  }
   119  
   120  // goroutine that cycles through the list of paths and checks for updates.
   121  func (w *Watcher) watch(sndch chan<- *Notification) {
   122  	defer func() {
   123  		recover()
   124  	}()
   125  
   126  	for {
   127  		//fmt.Printf("updating watch info %s\n", time.Now())
   128  		<-time.After(WatchDelay)
   129  
   130  		for _, wi := range w.paths {
   131  			//fmt.Printf("cheecking %#v\n", wi.Path)
   132  
   133  			if wi.Update() && w.shouldNotify(wi) {
   134  				sndch <- wi.Notification()
   135  			}
   136  
   137  			if wi.LastEvent == NOEXIST && w.autoWatch {
   138  				delete(w.paths, wi.Path)
   139  			}
   140  
   141  			if len(w.paths) == 0 {
   142  				w.Stop()
   143  			}
   144  			// if filepath.Base(wi.Path) == "sub1.txt" {
   145  			// 	fmt.Printf("%s\n", wi.Path)
   146  			// }
   147  		}
   148  	}
   149  }
   150  
   151  func (w *Watcher) shouldNotify(wi *watchItem) bool {
   152  	if w.autoWatch && wi.StatInfo.IsDir() &&
   153  		!(wi.LastEvent == DELETED || wi.LastEvent == NOEXIST) {
   154  		go w.addPaths(wi)
   155  		return false
   156  	}
   157  	return true
   158  }
   159  
   160  func (w *Watcher) addPaths(wi *watchItem) {
   161  	walker := getWalker(w, wi.Path, w.cadd)
   162  	go filepath.Walk(wi.Path, walker)
   163  }
   164  
   165  func (w *Watcher) watchItemListener() {
   166  	defer func() {
   167  		recover()
   168  	}()
   169  	for {
   170  		wi := <-w.cadd
   171  		if wi == nil {
   172  			continue
   173  		} else if _, watching := w.paths[wi.Path]; watching {
   174  			continue
   175  		}
   176  		w.paths[wi.Path] = wi
   177  	}
   178  }
   179  
   180  func getWalker(w *Watcher, root string, addch chan<- *watchItem) func(string, os.FileInfo, error) error {
   181  	walker := func(path string, info os.FileInfo, err error) error {
   182  		if w.IgnorePathFn(path) {
   183  			if info.IsDir() {
   184  				//fmt.Println("SKIPPING dir", path)
   185  				return filepath.SkipDir
   186  			}
   187  			return nil
   188  		}
   189  		if err != nil {
   190  			return err
   191  		}
   192  		if path == root {
   193  			return nil
   194  		}
   195  		wi := watchPath(path)
   196  		if wi == nil {
   197  			return nil
   198  		} else if _, watching := w.paths[wi.Path]; !watching {
   199  			wi.LastEvent = CREATED
   200  			w.cnotify <- wi.Notification()
   201  			addch <- wi
   202  			if !wi.StatInfo.IsDir() {
   203  				return nil
   204  			}
   205  			w.addPaths(wi)
   206  		}
   207  		return nil
   208  	}
   209  	return walker
   210  }
   211  
   212  // DefaultIsIgnorePath checks whether a path is ignored. Currently defaults
   213  // to hidden files on *nix systems, ie they start with a ".".
   214  func ignorePathDefault(path string) bool {
   215  	if strings.HasPrefix(path, ".") || strings.Contains(path, "/.") {
   216  		return true
   217  	}
   218  
   219  	// ignore node
   220  	if strings.HasPrefix(path, "node_modules") || strings.Contains(path, "/node_modules") {
   221  		return true
   222  	}
   223  
   224  	// vim creates random numeric files
   225  	base := filepath.Base(path)
   226  	if str.IsNumeric(base) {
   227  		return true
   228  	}
   229  	return false
   230  }
   231  
   232  func (w *Watcher) syncAddPaths(paths ...string) {
   233  	for _, path := range paths {
   234  		if w.IgnorePathFn(path) {
   235  			//fmt.Println("SKIPPING path", path)
   236  			continue
   237  		}
   238  		wi := watchPath(path)
   239  		if wi == nil {
   240  			continue
   241  		} else if wi.LastEvent == NOEXIST {
   242  			continue
   243  		} else if _, watching := w.paths[wi.Path]; watching {
   244  			continue
   245  		}
   246  		w.paths[wi.Path] = wi
   247  		if wi.StatInfo.IsDir() {
   248  			w.syncAddDir(wi)
   249  		}
   250  	}
   251  }
   252  
   253  func (w *Watcher) syncAddDir(wi *watchItem) {
   254  	walker := func(path string, info os.FileInfo, err error) error {
   255  		if w.IgnorePathFn(path) {
   256  			if info.IsDir() {
   257  				//fmt.Println("SKIPPING dir", path)
   258  				return filepath.SkipDir
   259  			}
   260  			return nil
   261  		}
   262  
   263  		if err != nil {
   264  			return err
   265  		}
   266  		if path == wi.Path {
   267  			return nil
   268  		}
   269  		newWI := watchPath(path)
   270  		if newWI != nil {
   271  			w.paths[path] = newWI
   272  			if !newWI.StatInfo.IsDir() {
   273  				return nil
   274  			}
   275  			if _, watching := w.paths[newWI.Path]; !watching {
   276  				w.syncAddDir(newWI)
   277  			}
   278  		}
   279  		return nil
   280  	}
   281  	filepath.Walk(wi.Path, walker)
   282  }
   283  
   284  // Watching returns a list of the files being watched.
   285  func (w *Watcher) Watching() (paths []string) {
   286  	paths = make([]string, 0)
   287  	for path := range w.paths {
   288  		paths = append(paths, path)
   289  	}
   290  	return
   291  }
   292  
   293  // State returns a slice of Notifications representing the files being watched
   294  // and their last event.
   295  func (w *Watcher) State() (state []Notification) {
   296  	state = make([]Notification, 0)
   297  	if w.paths == nil {
   298  		return
   299  	}
   300  	for _, wi := range w.paths {
   301  		state = append(state, *wi.Notification())
   302  	}
   303  	return
   304  }