internal/events/file.go (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | package events 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 } |