package events
import (
"fmt"
"io/fs"
"maps"
"os"
"path"
"path/filepath"
"slices"
"time"
"alin.ovh/x/log"
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fmsg"
"github.com/fsnotify/fsnotify"
)
type FileWatcher struct {
log *log.Logger
*fsnotify.Watcher
}
var (
ignores = []string{
"*.go",
"*-journal",
}
checkSettleInterval = 200 * time.Millisecond
)
func NewFileWatcher(logger *log.Logger, dirs ...string) (*FileWatcher, error) {
fsn, err := fsnotify.NewWatcher()
if err != nil {
return nil, fault.Wrap(err, fmsg.With("could not create file watcher"))
}
fw := &FileWatcher{
Watcher: fsn,
log: logger,
}
for _, dir := range dirs {
err = fw.AddRecursive(dir)
if err != nil {
return nil, fault.Wrap(
err,
fmsg.With(fmt.Sprintf("could not add directory %s to file watcher", dir)),
)
}
}
return fw, nil
}
func (fw *FileWatcher) AddRecursive(from string) error {
fw.log.Debug("walking directory tree", "root", from)
err := filepath.WalkDir(from, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not walk directory %s", path)))
}
if entry.IsDir() {
if entry.Name() == ".git" {
fw.log.Debug("skipping directory", "entry", entry.Name())
return fs.SkipDir
}
fw.log.Debug("adding directory to watcher", "path", path)
if err = fw.Add(path); err != nil {
return fault.Wrap(
err,
fmsg.With(fmt.Sprintf("could not add directory %s to watcher", path)),
)
}
}
return nil
})
return fault.Wrap(err, fmsg.With("error walking directory tree"))
}
func (fw *FileWatcher) GetLatestRunID() (uint64, error) {
return 0, nil
}
func (fw *FileWatcher) Subscribe() (<-chan Event, error) {
var timer *time.Timer
events := make(chan Event, 1)
go func() {
fileEvents := make(map[string]fsnotify.Op)
for {
select {
case baseEvent := <-fw.Events:
if !fw.ignored(baseEvent.Name) {
if baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Rename) {
f, err := os.Stat(baseEvent.Name)
if err != nil {
fw.log.Warn(
"error handling event",
"op",
baseEvent.Op,
"name",
baseEvent.Name,
)
} else if f.IsDir() {
err = fw.Add(baseEvent.Name)
if err != nil {
fw.log.Warn("error adding new folder to watcher", "error", err)
}
}
}
if baseEvent.Has(fsnotify.Rename) || baseEvent.Has(fsnotify.Write) ||
baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Chmod) {
fileEvents[baseEvent.Name] |= baseEvent.Op
if timer == nil {
timer = time.AfterFunc(checkSettleInterval, func() {
fw.log.Debug(
"file update event",
"changed",
slices.Collect(maps.Keys(fileEvents)),
)
events <- Event{
FSEvent: FSEvent{
Events: fileEvents,
},
}
clear(fileEvents)
})
}
timer.Reset(checkSettleInterval)
}
}
case err := <-fw.Errors:
fw.log.Warn("error in watcher", "error", err)
}
}
}()
return events, nil
}
func (fw *FileWatcher) matches(name string) func(string) bool {
return func(pattern string) bool {
matched, err := path.Match(pattern, name)
if err != nil {
fw.log.Warn("error checking watcher ignores", "error", err)
}
return matched
}
}
func (fw *FileWatcher) ignored(pathname string) bool {
return slices.ContainsFunc(ignores, fw.matches(path.Base(pathname)))
}
shared/events/file.go (view raw)