Created
January 25, 2024 08:42
-
-
Save nd2408/5292e4a7392ebb7e75c2a9dda9505728 to your computer and use it in GitHub Desktop.
fsnotify wrapper
This file contains 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 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
be warned about the simple logic in
runFunc()
. if you need to watch/home
and/home/user
with differentWatcherFunc
's, the path's map range is random order, so they will get mixed up half the time. it's a non-issue for my use case so i kept it simple but could throw someone for a loop.