internal/library/watch.go
83 lines · 1838 bytes
1package library
2
3import (
4 "context"
5 "errors"
6 "log"
7 "os"
8 "path/filepath"
9 "strings"
10 "time"
11
12 "github.com/fsnotify/fsnotify"
13)
14
15// Watch calls onChange after filesystem activity under root has been quiet for debounce.
16func Watch(ctx context.Context, root string, debounce time.Duration, onChange func()) error {
17 w, err := fsnotify.NewWatcher()
18 if err != nil {
19 return err
20 }
21 if err := addTree(w, root); err != nil {
22 w.Close()
23 return err
24 }
25 go func() {
26 defer w.Close()
27 watchLoop(ctx, w.Events, w.Errors, w.Add, debounce, onChange)
28 }()
29 return nil
30}
31
32// watchLoop debounces events into onChange calls and watches newly created directories.
33// An event overflow means changes were dropped, so it schedules a rescan too.
34func watchLoop(ctx context.Context, events <-chan fsnotify.Event, errs <-chan error, add func(string) error, debounce time.Duration, onChange func()) {
35 timer := time.NewTimer(debounce)
36 timer.Stop()
37 for {
38 select {
39 case <-ctx.Done():
40 return
41 case ev, ok := <-events:
42 if !ok {
43 return
44 }
45 if ev.Has(fsnotify.Create) {
46 if fi, err := os.Stat(ev.Name); err == nil && fi.IsDir() {
47 if err := add(ev.Name); err != nil {
48 log.Printf("watch %s: %v", ev.Name, err)
49 }
50 }
51 }
52 timer.Reset(debounce)
53 case err, ok := <-errs:
54 if !ok {
55 return
56 }
57 log.Printf("watch: %v", err)
58 if errors.Is(err, fsnotify.ErrEventOverflow) {
59 timer.Reset(debounce)
60 }
61 case <-timer.C:
62 onChange()
63 }
64 }
65}
66
67func addTree(w *fsnotify.Watcher, root string) error {
68 if err := w.Add(root); err != nil {
69 return err
70 }
71 entries, err := os.ReadDir(root)
72 if err != nil {
73 return err
74 }
75 for _, e := range entries {
76 if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
77 if err := w.Add(filepath.Join(root, e.Name())); err != nil {
78 return err
79 }
80 }
81 }
82 return nil
83}