package app import ( "context" "fmt" "log/slog" "os" "path/filepath" "strings" "github.com/fsnotify/fsnotify" ) type FileWatcher struct { watcher *fsnotify.Watcher rootDir string onChange func(path string) logger *slog.Logger } func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, fmt.Errorf("create fsnotify watcher: %w", err) } fw := &FileWatcher{ watcher: watcher, rootDir: rootDir, logger: logger, } if err := fw.addWatches(); err != nil { watcher.Close() return nil, err } return fw, nil } func (fw *FileWatcher) addWatches() error { return filepath.Walk(fw.rootDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { if err := fw.watcher.Add(path); err != nil { fw.logger.Warn("watch directory", "path", path, "error", err) } } return nil }) } func (fw *FileWatcher) SetOnChange(fn func(path string)) { fw.onChange = fn } func (fw *FileWatcher) Run(ctx context.Context) { defer fw.watcher.Close() for { select { case <-ctx.Done(): return case event, ok := <-fw.watcher.Events: if !ok { return } if fw.shouldProcess(event) { if event.Op&fsnotify.Create == fsnotify.Create { if info, err := os.Stat(event.Name); err == nil && info.IsDir() { fw.watcher.Add(event.Name) } } if fw.onChange != nil { fw.onChange(event.Name) } } case err, ok := <-fw.watcher.Errors: if !ok { return } fw.logger.Warn("fsnotify error", "error", err) } } } func (fw *FileWatcher) shouldProcess(event fsnotify.Event) bool { if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 { return false } name := filepath.Base(event.Name) if strings.HasPrefix(name, ".") || strings.HasSuffix(name, "~") { return false } if filepath.Ext(event.Name) == ".md" || event.Op&fsnotify.Remove != 0 { return true } return false }