gaper/watcher.go

248 lines
5.1 KiB
Go
Raw Normal View History

package gaper
2018-06-16 19:22:21 -05:00
import (
"errors"
"fmt"
2018-06-16 19:22:21 -05:00
"os"
"path/filepath"
2018-07-24 19:31:19 -05:00
"regexp"
2018-06-22 19:44:26 -05:00
"strings"
2018-06-16 19:22:21 -05:00
"time"
zglob "github.com/mattn/go-zglob"
2018-06-16 19:22:21 -05:00
)
2018-06-20 21:04:16 -05:00
// Watcher is a interface for the watch process
2018-07-12 08:27:07 -05:00
type Watcher interface {
Watch()
Errors() chan error
Events() chan string
}
// watcher is a interface for the watch process
type watcher struct {
defaultIgnore bool
2018-07-12 08:27:07 -05:00
pollInterval int
watchItems map[string]bool
ignoreItems map[string]bool
allowedExtensions map[string]bool
events chan string
errors chan error
2018-06-16 19:22:21 -05:00
}
// WatcherConfig defines the settings available for the watcher
type WatcherConfig struct {
DefaultIgnore bool
PollInterval int
WatchItems []string
IgnoreItems []string
Extensions []string
}
2018-06-20 21:04:16 -05:00
// NewWatcher creates a new watcher
func NewWatcher(cfg WatcherConfig) (Watcher, error) {
if cfg.PollInterval == 0 {
cfg.PollInterval = DefaultPoolInterval
2018-06-20 20:40:09 -05:00
}
if len(cfg.Extensions) == 0 {
cfg.Extensions = DefaultExtensions
2018-06-20 20:40:09 -05:00
}
2018-06-16 19:22:21 -05:00
allowedExts := make(map[string]bool)
for _, ext := range cfg.Extensions {
2018-06-16 19:22:21 -05:00
allowedExts["."+ext] = true
}
watchPaths, err := resolvePaths(cfg.WatchItems, allowedExts)
if err != nil {
return nil, err
}
ignorePaths, err := resolvePaths(cfg.IgnoreItems, allowedExts)
if err != nil {
return nil, err
}
2018-06-22 19:44:26 -05:00
logger.Debugf("Resolved watch paths: %v", watchPaths)
logger.Debugf("Resolved ignore paths: %v", ignorePaths)
2018-07-12 08:27:07 -05:00
return &watcher{
events: make(chan string),
errors: make(chan error),
defaultIgnore: cfg.DefaultIgnore,
pollInterval: cfg.PollInterval,
2018-07-12 08:27:07 -05:00
watchItems: watchPaths,
ignoreItems: ignorePaths,
allowedExtensions: allowedExts,
}, nil
2018-06-16 19:22:21 -05:00
}
var startTime = time.Now()
var errDetectedChange = errors.New("done")
2018-06-20 21:04:16 -05:00
// Watch starts watching for file changes
2018-07-12 08:27:07 -05:00
func (w *watcher) Watch() {
2018-06-16 19:22:21 -05:00
for {
2018-07-12 08:27:07 -05:00
for watchPath := range w.watchItems {
2018-06-22 19:44:26 -05:00
fileChanged, err := w.scanChange(watchPath)
2018-06-18 21:22:18 -05:00
if err != nil {
2018-07-12 08:27:07 -05:00
w.errors <- err
2018-06-18 21:22:18 -05:00
return
2018-06-16 19:22:21 -05:00
}
2018-06-18 21:22:18 -05:00
if fileChanged != "" {
2018-07-12 08:27:07 -05:00
w.events <- fileChanged
2018-06-18 21:22:18 -05:00
startTime = time.Now()
2018-06-16 19:22:21 -05:00
}
2018-06-18 21:22:18 -05:00
}
2018-06-16 19:22:21 -05:00
2018-07-12 08:27:07 -05:00
time.Sleep(time.Duration(w.pollInterval) * time.Millisecond)
2018-06-18 21:22:18 -05:00
}
}
2018-06-16 19:22:21 -05:00
2018-07-12 08:27:07 -05:00
// Events get events occurred during the watching
// these events are emitted only a file changing is detected
2018-07-12 08:27:07 -05:00
func (w *watcher) Events() chan string {
return w.events
}
// Errors get errors occurred during the watching
func (w *watcher) Errors() chan error {
return w.errors
}
func (w *watcher) scanChange(watchPath string) (string, error) {
2018-06-18 21:22:18 -05:00
logger.Debug("Watching ", watchPath)
var fileChanged string
err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
if w.ignoreFile(path, info) {
return skipFile(info)
2018-06-18 21:22:18 -05:00
}
2018-06-16 19:22:21 -05:00
2018-06-18 21:22:18 -05:00
ext := filepath.Ext(path)
2018-07-12 08:27:07 -05:00
if _, ok := w.allowedExtensions[ext]; ok && info.ModTime().After(startTime) {
2018-06-18 21:22:18 -05:00
fileChanged = path
return errDetectedChange
2018-06-16 19:22:21 -05:00
}
2018-06-18 21:22:18 -05:00
return nil
})
if err != nil && err != errDetectedChange {
return "", err
2018-06-16 19:22:21 -05:00
}
2018-06-18 21:22:18 -05:00
return fileChanged, nil
2018-06-16 19:22:21 -05:00
}
func (w *watcher) ignoreFile(path string, info os.FileInfo) bool {
// if a file has been deleted after gaper was watching it
// info will be nil in the other iterations
if info == nil {
return true
}
// check if preset ignore is enabled
if w.defaultIgnore {
// check for hidden files and directories
if name := info.Name(); name[0] == '.' && name != "." {
return true
}
// check if it is a Go testing file
if strings.HasSuffix(path, "_test.go") {
return true
}
// check if it is the vendor folder
if info.IsDir() && info.Name() == "vendor" {
return true
}
}
if _, ignored := w.ignoreItems[path]; ignored {
return true
}
2018-07-24 19:31:19 -05:00
return false
}
2018-06-22 19:44:26 -05:00
func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) {
result := map[string]bool{}
for _, path := range paths {
2018-06-22 19:44:26 -05:00
matches := []string{path}
isGlob := strings.Contains(path, "*")
if isGlob {
var err error
matches, err = zglob.Glob(path)
if err != nil {
return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err)
}
}
2018-06-22 19:44:26 -05:00
for _, match := range matches {
// don't care for extension filter right now for non glob paths
// since they could be a directory
if isGlob {
2018-07-24 19:31:19 -05:00
if _, ok := extensions[filepath.Ext(match)]; !ok {
2018-06-22 19:44:26 -05:00
continue
}
}
if _, ok := result[match]; !ok {
result[match] = true
}
}
}
2018-06-22 19:44:26 -05:00
removeOverlappedPaths(result)
return result, nil
}
2018-06-22 19:44:26 -05:00
// remove overlapped paths so it makes the scan for changes later faster and simpler
func removeOverlappedPaths(mapPaths map[string]bool) {
2018-07-24 19:31:19 -05:00
startDot := regexp.MustCompile(`^\./`)
2018-06-22 19:44:26 -05:00
for p1 := range mapPaths {
2018-07-24 19:31:19 -05:00
p1 = startDot.ReplaceAllString(p1, "")
// skip to next item if this path has already been checked
if v, ok := mapPaths[p1]; ok && !v {
continue
}
2018-06-22 19:44:26 -05:00
for p2 := range mapPaths {
2018-07-24 19:31:19 -05:00
p2 = startDot.ReplaceAllString(p2, "")
2018-06-22 19:44:26 -05:00
if p1 == p2 {
continue
}
if strings.HasPrefix(p2, p1) {
mapPaths[p2] = false
} else if strings.HasPrefix(p1, p2) {
mapPaths[p1] = false
}
}
}
// cleanup path list
for p := range mapPaths {
if !mapPaths[p] {
delete(mapPaths, p)
}
}
}
func skipFile(info os.FileInfo) error {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}