package library import ( "context" "errors" "log" "os" "path/filepath" "strings" "time" "github.com/fsnotify/fsnotify" ) // Watch calls onChange after filesystem activity under root has been quiet for debounce. func Watch(ctx context.Context, root string, debounce time.Duration, onChange func()) error { w, err := fsnotify.NewWatcher() if err != nil { return err } if err := addTree(w, root); err != nil { w.Close() return err } go func() { defer w.Close() watchLoop(ctx, w.Events, w.Errors, w.Add, debounce, onChange) }() return nil } // watchLoop debounces events into onChange calls and watches newly created directories. // An event overflow means changes were dropped, so it schedules a rescan too. func watchLoop(ctx context.Context, events <-chan fsnotify.Event, errs <-chan error, add func(string) error, debounce time.Duration, onChange func()) { timer := time.NewTimer(debounce) timer.Stop() for { select { case <-ctx.Done(): return case ev, ok := <-events: if !ok { return } if ev.Has(fsnotify.Create) { if fi, err := os.Stat(ev.Name); err == nil && fi.IsDir() { if err := add(ev.Name); err != nil { log.Printf("watch %s: %v", ev.Name, err) } } } timer.Reset(debounce) case err, ok := <-errs: if !ok { return } log.Printf("watch: %v", err) if errors.Is(err, fsnotify.ErrEventOverflow) { timer.Reset(debounce) } case <-timer.C: onChange() } } } func addTree(w *fsnotify.Watcher, root string) error { if err := w.Add(root); err != nil { return err } entries, err := os.ReadDir(root) if err != nil { return err } for _, e := range entries { if e.IsDir() && !strings.HasPrefix(e.Name(), ".") { if err := w.Add(filepath.Join(root, e.Name())); err != nil { return err } } } return nil }