all repos — homestead @ e4bdf0dd0740c21e89dd42413b7bc76da381fd37

Code for my website

generalise file watcher to an UpdateListener

Alan Pearce
commit

e4bdf0dd0740c21e89dd42413b7bc76da381fd37

parent

7a987dac4c9c4c0fa2bc8484566f08bdd6d82f56

1 file changed, 137 insertions(+), 0 deletions(-)

changed files
A internal/update/file.go
@@ -0,0 +1,137 @@
+package update + +import ( + "io/fs" + "os" + "path" + "path/filepath" + "slices" + "time" + + "go.alanpearce.eu/x/log" + + "github.com/fsnotify/fsnotify" + "gitlab.com/tozd/go/errors" +) + +var ( + ignores = []string{ + "*.templ", + "*.go", + } + checkSettleInterval = 200 * time.Millisecond +) + +type FileWatcher struct { + log *log.Logger + *fsnotify.Watcher +} + +func NewFileWatcher(logger *log.Logger, dirs ...string) (*FileWatcher, error) { + fsn, err := fsnotify.NewWatcher() + if err != nil { + return nil, errors.WithMessage(err, "could not create file watcher") + } + + for _, dir := range dirs { + err = fsn.Add(dir) + if err != nil { + return nil, errors.WithMessagef(err, "could not add directory %s to file watcher", dir) + } + } + + return &FileWatcher{ + Watcher: fsn, + log: logger, + }, 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) error { + 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) Wait(events chan<- Event, errs chan<- error) error { + var timer *time.Timer + + go func() { + var fileEvents []fsnotify.Event + for { + select { + case baseEvent := <-fw.Watcher.Events: + if !fw.ignored(baseEvent.Name) { + fw.log.Debug( + "watcher event", + "name", + baseEvent.Name, + "op", + baseEvent.Op.String(), + ) + if baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Rename) { + f, err := os.Stat(baseEvent.Name) + if err != nil { + errs <- errors.WithMessagef(err, "error handling event %s", baseEvent.Op.String()) + } else if f.IsDir() { + err = fw.Add(baseEvent.Name) + if err != nil { + errs <- errors.WithMessage(err, "error adding new folder to watcher") + } + } + } + if baseEvent.Has(fsnotify.Rename) || baseEvent.Has(fsnotify.Write) || + baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Chmod) { + fileEvents = append(fileEvents, baseEvent) + if timer == nil { + timer = time.AfterFunc(checkSettleInterval, func() { + events <- Event{ + FileEvents: fileEvents, + Revision: "", + } + fileEvents = []fsnotify.Event{} + }) + } + timer.Reset(checkSettleInterval) + } + } + case err := <-fw.Watcher.Errors: + errs <- errors.WithMessage(err, "error in watcher") + } + } + }() + + return nil +}