package events import ( "io/fs" "maps" "os" "path" "path/filepath" "slices" "time" "go.alanpearce.eu/x/log" "github.com/fsnotify/fsnotify" "gitlab.com/tozd/go/errors" ) var ( ignores = []string{ "*.go", "*-journal", } checkSettleInterval = 200 * time.Millisecond ) type FileWatcher struct { log *log.Logger *fsnotify.Watcher } func NewFileWatcher(logger *log.Logger, dirs ...string) (*FileWatcher, errors.E) { fsn, err := fsnotify.NewWatcher() if err != nil { return nil, errors.WithMessage(err, "could not create file watcher") } fw := &FileWatcher{ Watcher: fsn, log: logger, } for _, dir := range dirs { err = fw.AddRecursive(dir) if err != nil { return nil, errors.WithMessagef(err, "could not add directory %s to file watcher", dir) } } return fw, nil } func (fw *FileWatcher) matches(name string) func(string) bool { return func(pattern string) bool { matched, err := path.Match(pattern, name) if err != nil { fw.log.Warn("error checking watcher ignores", "error", err) } return matched } } func (fw *FileWatcher) ignored(pathname string) bool { return slices.ContainsFunc(ignores, fw.matches(path.Base(pathname))) } func (fw *FileWatcher) AddRecursive(from string) errors.E { fw.log.Debug("walking directory tree", "root", from) err := filepath.WalkDir(from, func(path string, entry fs.DirEntry, err error) error { if err != nil { return errors.WithMessagef(err, "could not walk directory %s", path) } if entry.IsDir() { if entry.Name() == ".git" { fw.log.Debug("skipping directory", "entry", entry.Name()) return fs.SkipDir } fw.log.Debug("adding directory to watcher", "path", path) if err = fw.Add(path); err != nil { return errors.WithMessagef(err, "could not add directory %s to watcher", path) } } return nil }) return errors.WithMessage(err, "error walking directory tree") } func (fw *FileWatcher) GetLatestRunID() (uint64, errors.E) { return 0, nil } func (fw *FileWatcher) Subscribe() (<-chan Event, errors.E) { var timer *time.Timer events := make(chan Event, 1) go func() { fileEvents := make(map[string]fsnotify.Op) for { select { case baseEvent := <-fw.Events: if !fw.ignored(baseEvent.Name) { if baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Rename) { f, err := os.Stat(baseEvent.Name) if err != nil { fw.log.Warn( "error handling event", "op", baseEvent.Op, "name", baseEvent.Name, ) } else if f.IsDir() { err = fw.Add(baseEvent.Name) if err != nil { fw.log.Warn("error adding new folder to watcher", "error", err) } } } if baseEvent.Has(fsnotify.Rename) || baseEvent.Has(fsnotify.Write) || baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Chmod) { fileEvents[baseEvent.Name] |= baseEvent.Op if timer == nil { timer = time.AfterFunc(checkSettleInterval, func() { fw.log.Debug( "file update event", "changed", slices.Collect(maps.Keys(fileEvents)), ) events <- Event{ FSEvent: FSEvent{ Events: fileEvents, }, } clear(fileEvents) }) } timer.Reset(checkSettleInterval) } } case err := <-fw.Errors: fw.log.Warn("error in watcher", "error", err) } } }() return events, nil }