-
-
Save avary/4ad18001c5f17a21bab1759cd4991e88 to your computer and use it in GitHub Desktop.
fsnotify wrapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ | |
AddSource: false, | |
Level: slog.LevelDebug, | |
})) | |
slog.SetDefault(logger) | |
watcher, err := watcher.New(logger) | |
defer watcher.Shutdown() | |
err = watcher.Watch("/home/user", func(e watcher.Event) error { | |
// only files in /home/user | |
return nil | |
}) | |
err = watcher.Watch("/home/user2/*", func(e watcher.Event) error { | |
// files and subdirectories of /home/user2 | |
return nil | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package watcher | |
import ( | |
"io/fs" | |
"log/slog" | |
"os" | |
"path/filepath" | |
"strings" | |
"sync" | |
"time" | |
"github.com/fsnotify/fsnotify" | |
) | |
func New(log *slog.Logger) (*Watcher, error) { | |
var err error | |
s := &Watcher{ | |
log: log.With("service", "watcher"), | |
paths: make(map[string]WatcherFunc), | |
cooldown: time.Millisecond * 250, | |
} | |
s.fsw, err = fsnotify.NewWatcher() | |
if err != nil { | |
return nil, err | |
} | |
go s.runLoop() | |
return s, nil | |
} | |
type Event struct { | |
Path string | |
Op string | |
} | |
func (s Event) String() string { | |
return s.Op + " - " + s.Path | |
} | |
type WatcherFunc func(Event) error | |
type Watcher struct { | |
log *slog.Logger | |
fsw *fsnotify.Watcher | |
paths map[string]WatcherFunc | |
mu sync.Mutex | |
cooldown time.Duration | |
} | |
func (s *Watcher) addSubDirs(root string) error { | |
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { | |
if err != nil { | |
return err | |
} | |
if !d.IsDir() { | |
return nil | |
} | |
return s.fsw.Add(path) | |
}) | |
} | |
func (s *Watcher) Watch(path string, fn WatcherFunc) (err error) { | |
s.mu.Lock() | |
defer s.mu.Unlock() | |
dir, file := filepath.Split(path) | |
if file == "*" { | |
err = s.addSubDirs(dir) | |
} else { | |
err = s.fsw.Add(path) | |
} | |
if err == nil { | |
s.paths[strings.TrimSuffix(path, "*")] = fn | |
} | |
return err | |
} | |
func (s *Watcher) Shutdown() { | |
_ = s.fsw.Close() | |
} | |
func (s *Watcher) runLoop() { | |
ts := time.Now() | |
for { | |
select { | |
case err, ok := <-s.fsw.Errors: | |
if !ok { | |
return | |
} | |
s.log.Error(err.Error()) | |
case event, ok := <-s.fsw.Events: | |
if !ok { | |
return | |
} | |
if isCreateDir(event) { | |
if err := s.fsw.Add(event.Name); err != nil { | |
s.log.Error("failed to watch created dir", slog.String("error", err.Error())) | |
} | |
continue | |
} | |
if time.Since(ts) < s.cooldown { | |
continue | |
} | |
ts = time.Now() | |
s.runFunc(event) | |
} | |
} | |
} | |
func (s *Watcher) runFunc(event fsnotify.Event) { | |
s.mu.Lock() | |
defer s.mu.Unlock() | |
for path, fn := range s.paths { | |
if !strings.HasPrefix(event.Name, path) { | |
continue | |
} | |
s.log.Info( | |
"file change detected", | |
slog.String("path", event.Name), | |
slog.String("op", event.Op.String()), | |
) | |
err := fn(Event{ | |
Path: event.Name, | |
Op: event.Op.String(), | |
}) | |
if err != nil { | |
s.log.Error( | |
"error running watcher func", | |
slog.String("path", event.Name), | |
slog.String("op", event.Op.String()), | |
slog.String("error", err.Error()), | |
) | |
} | |
return | |
} | |
} | |
func isCreateDir(event fsnotify.Event) bool { | |
if !event.Op.Has(fsnotify.Create) { | |
return false | |
} | |
fi, err := os.Stat(event.Name) | |
if err != nil { | |
return false | |
} | |
return fi.IsDir() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment