Files
grafana/apps/provisioning/pkg/repository/local/watch.go
T

150 lines
3.2 KiB
Go

package local
import (
"context"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/grafana/grafana-app-sdk/logging"
)
type FileWatcher interface {
Watch(ctx context.Context, events chan<- string)
}
type fileWatcher struct {
prefix string
accept func(string) bool
waitFor time.Duration
timersMu sync.Mutex
timers map[string]*time.Timer
watcher *fsnotify.Watcher
logger logging.Logger
closed bool
}
// File watcher that buffers events for 100ms before actually firing them
// this is helpful because editing a file may often update the same file many many times
// for what seems like a single operation.
// See: https://github.com/fsnotify/fsnotify/blob/main/cmd/fsnotify/dedup.go
func NewFileWatcher(path string, accept func(string) bool) (FileWatcher, error) {
info, _ := os.Stat(path)
if info == nil || !info.IsDir() {
return nil, fmt.Errorf("expecting to watch a folder")
}
w, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
if err := w.Add(path); err != nil {
_ = w.Close()
return nil, err
}
if err = filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if err = w.Add(path); err != nil {
return err
}
}
return nil
}); err != nil {
_ = w.Close()
return nil, err
}
return &fileWatcher{
prefix: path + "/",
accept: accept,
waitFor: 100 * time.Millisecond,
timers: make(map[string]*time.Timer),
watcher: w,
logger: logging.DefaultLogger.With("watch", path),
}, nil
}
// Keep watching for changes until the context is done
func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
defer f.cleanup(events)
for {
select {
case <-ctx.Done():
return
case _, ok := <-f.watcher.Errors:
if !ok { // Channel was closed (i.e. Watcher.Close() was called).
return
}
// Read from Events.
case e, ok := <-f.watcher.Events:
if !ok { // Channel was closed (i.e. Watcher.Close() was called).
return
}
name := filepath.Base(e.Name)
if strings.HasPrefix(name, ".") {
continue // ignore hidden files+folders
}
if !f.accept(name) {
info, _ := os.Stat(e.Name)
if info != nil && info.IsDir() {
if err := f.watcher.Add(e.Name); err != nil {
f.logger.Warn("error adding folder", "folder", e.Name, "error", err)
}
}
continue
}
f.timersMu.Lock()
t, ok := f.timers[e.Name]
if !ok {
nameCopy := e.Name
t = time.AfterFunc(math.MaxInt64, func() {
// before sending the event, check if the watcher has been closed
if f.closed {
return
}
path, _ := strings.CutPrefix(nameCopy, f.prefix)
events <- path
f.timersMu.Lock()
delete(f.timers, nameCopy)
f.timersMu.Unlock()
})
f.timers[e.Name] = t
}
f.timersMu.Unlock()
t.Reset(f.waitFor)
}
}
}
// stop all pending timers and close the event channel
func (f *fileWatcher) cleanup(events chan<- string) {
f.timersMu.Lock()
defer f.timersMu.Unlock()
for _, timer := range f.timers {
timer.Stop()
}
f.timers = make(map[string]*time.Timer)
close(events)
f.closed = true
}