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 138 139 140 141 142 143 144 145 146 147 148 149 150 | 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.Watcher.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.Watcher.Errors: fw.log.Warn("error in watcher", "error", err) } } }() return events, nil } |