1
0
Fork 0
mirror of https://github.com/Luzifer/go_helpers.git synced 2024-12-25 13:31:21 +00:00
go_helpers/file/watcher.go
Knut Ahlers d67686d26f
Allow to set watcher to follow symlinks
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-03-19 01:59:31 +01:00

180 lines
5.3 KiB
Go

package file
import (
"crypto/sha256"
"os"
"sync"
"time"
"github.com/pkg/errors"
)
type (
// Watcher creates a background routine and emits events when the
// watched file changes on its C channel. If an error occurs the
// loop is stopped and the error is exposed on the Err property.
Watcher struct {
C <-chan WatcherEvent
CheckInterval time.Duration
Err error
FilePath string
c chan WatcherEvent
checks []WatcherCheck
followSymlinks bool
lock sync.RWMutex
stateCache map[string]any
}
// WatcherCheck is an interface to implement own checks
WatcherCheck func(*Watcher) (WatcherEvent, error)
// WatcherEvent is the detected change to be signeld through the
// channel within the Watcher
WatcherEvent uint
// WatcherOpts holds configuration for newly created watcher
WatcherOpts struct {
// FollowSymlinks switches watchers based on file metadata into
// a mode where they follow a symlink and check the real target
// file instead of the symlink
FollowSymlinks bool
}
)
const (
WatcherEventInvalid WatcherEvent = iota
WatcherEventNoChange
WatcherEventFileAppeared
WatcherEventFileModified
WatcherEventFileVanished
)
// DefaultWatcherOpts is used when creating the Watcher without
// giving options
var DefaultWatcherOpts = WatcherOpts{
FollowSymlinks: false,
}
// NewCryptographicWatcher is a wrapper around NewWatcher to configure
// the Watcher with presence and sha256 hash checks.
func NewCryptographicWatcher(filePath string, interval time.Duration) (*Watcher, error) {
return NewWatcher(filePath, interval, WatcherCheckPresence, WatcherCheckHash(sha256.New))
}
// NewSimpleWatcher is a wrapper around NewWatcher to configure the
// Watcher with presence, size and mtime checks.
func NewSimpleWatcher(filePath string, interval time.Duration) (*Watcher, error) {
return NewWatcher(filePath, interval, WatcherCheckPresence, WatcherCheckSize, WatcherCheckMtime)
}
// NewWatcher creates a new Watcher configured with the given filePath,
// default options, interval and checks given. The checks are executed
// once during initialization and will not cause an event to be sent. The
// created Watcher will automatically start its periodic check and the C
// channel should immediately be watched for changes. If the channel is
// not listened on the check loop will be paused until events are
// retrieved. If during the initial checks an error is detected the loop
// is NOT started and the watcher needs to be initialized again.
func NewWatcher(filePath string, interval time.Duration, checks ...WatcherCheck) (*Watcher, error) {
return NewWatcherWithOpts(filePath, DefaultWatcherOpts, interval, checks...)
}
// NewWatcherWithOpts creates a new Watcher configured with the given
// filePath, options, interval and checks given. The checks are executed
// once during initialization and will not cause an event to be sent. The
// created Watcher will automatically start its periodic check and the C
// channel should immediately be watched for changes. If the channel is
// not listened on the check loop will be paused until events are
// retrieved. If during the initial checks an error is detected the loop
// is NOT started and the watcher needs to be initialized again.
func NewWatcherWithOpts(filePath string, opts WatcherOpts, interval time.Duration, checks ...WatcherCheck) (*Watcher, error) {
w, err := newWatcher(filePath, opts, interval, checks...)
if err == nil {
go w.loop()
}
return w, err
}
func newWatcher(filePath string, opts WatcherOpts, interval time.Duration, checks ...WatcherCheck) (*Watcher, error) {
notify := make(chan WatcherEvent, 1)
w := &Watcher{
C: notify,
CheckInterval: interval,
FilePath: filePath,
c: notify,
checks: checks,
followSymlinks: opts.FollowSymlinks,
stateCache: make(map[string]any),
}
// Initially run checks once
_, err := w.runStateChecks()
return w, errors.Wrap(err, "executing initial checks")
}
// GetState is a helper to retrieve state from the internal store for
// usage in checks to have their state retained.
func (w *Watcher) GetState(key string) any {
w.lock.RLock()
defer w.lock.RUnlock()
return w.stateCache[key]
}
// SetState is a helper to set state into the internal store for
// usage in checks to have their state retained.
func (w *Watcher) SetState(key string, value any) {
w.lock.Lock()
defer w.lock.Unlock()
w.stateCache[key] = value
}
func (w *Watcher) loop() {
for {
evt, err := w.runStateChecks()
if err != nil {
w.Err = err
break
}
if evt != WatcherEventNoChange && evt != WatcherEventInvalid {
// On "no change" and "invalid" events sending the new event is skipped
w.c <- evt
}
time.Sleep(w.CheckInterval)
}
}
func (w *Watcher) runStateChecks() (WatcherEvent, error) {
for _, c := range w.checks {
evt, err := c(w)
if err != nil {
return WatcherEventInvalid, errors.Wrap(err, "checking file state")
}
if evt == WatcherEventNoChange {
// Watcher noticed no change, ask the next one. If one notices
// a change we will return that one.
continue
}
return evt, nil
}
return WatcherEventNoChange, nil
}
func (w *Watcher) stat(filePath string) (os.FileInfo, error) {
if w.followSymlinks {
return os.Stat(filePath)
}
return os.Lstat(filePath)
}