package content import ( "log" "os" "path/filepath" "sync" "github.com/fsnotify/fsnotify" ) type Watcher struct { root string mu sync.RWMutex tree *Node onChange []func() } func NewWatcher(root string) (*Watcher, error) { tree, err := BuildTree(root) if err != nil { return nil, err } return &Watcher{root: root, tree: tree}, nil } func (w *Watcher) Tree() *Node { w.mu.RLock() defer w.mu.RUnlock() return w.tree } func (w *Watcher) Rebuild() error { tree, err := BuildTree(w.root) if err != nil { return err } w.mu.Lock() w.tree = tree w.mu.Unlock() for _, fn := range w.onChange { fn() } return nil } func (w *Watcher) OnChange(fn func()) { w.onChange = append(w.onChange, fn) } func (w *Watcher) Start() { fw, err := fsnotify.NewWatcher() if err != nil { log.Printf("fsnotify: %v", err) return } walkAndWatch(fw, w.root) go func() { for { select { case ev, ok := <-fw.Events: if !ok { return } if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Write) || ev.Has(fsnotify.Rename) { if err := w.Rebuild(); err != nil { log.Printf("rebuild: %v", err) } } case err, ok := <-fw.Errors: if !ok { return } log.Printf("fsnotify error: %v", err) } } }() } func walkAndWatch(fw *fsnotify.Watcher, root string) { filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil || !d.IsDir() { return nil } if d.Name()[0] == '.' && path != root { return filepath.SkipDir } fw.Add(path) return nil }) }