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 }