From d67686d26fe07a96012483db65a37327318d1aa6 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sun, 19 Mar 2023 01:42:30 +0100 Subject: [PATCH] Allow to set watcher to follow symlinks Signed-off-by: Knut Ahlers --- file/watcher.go | 69 ++++++++++++++++++++++++++++++++---------- file/watcher_checks.go | 8 ++--- file/watcher_test.go | 8 ++--- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/file/watcher.go b/file/watcher.go index 288f79e..fa2fbb7 100644 --- a/file/watcher.go +++ b/file/watcher.go @@ -2,6 +2,7 @@ package file import ( "crypto/sha256" + "os" "sync" "time" @@ -18,10 +19,11 @@ type ( Err error FilePath string - c chan WatcherEvent - checks []WatcherCheck - lock sync.RWMutex - stateCache map[string]any + c chan WatcherEvent + checks []WatcherCheck + followSymlinks bool + lock sync.RWMutex + stateCache map[string]any } // WatcherCheck is an interface to implement own checks @@ -30,6 +32,14 @@ type ( // 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 ( @@ -40,6 +50,12 @@ const ( 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) { @@ -53,15 +69,27 @@ func NewSimpleWatcher(filePath string, interval time.Duration) (*Watcher, error) } // NewWatcher creates a new Watcher configured with the given filePath, -// 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. +// 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) { - w, err := newWatcher(filePath, interval, checks...) + 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() @@ -70,7 +98,7 @@ func NewWatcher(filePath string, interval time.Duration, checks ...WatcherCheck) return w, err } -func newWatcher(filePath string, interval time.Duration, checks ...WatcherCheck) (*Watcher, error) { +func newWatcher(filePath string, opts WatcherOpts, interval time.Duration, checks ...WatcherCheck) (*Watcher, error) { notify := make(chan WatcherEvent, 1) w := &Watcher{ @@ -78,9 +106,10 @@ func newWatcher(filePath string, interval time.Duration, checks ...WatcherCheck) CheckInterval: interval, FilePath: filePath, - c: notify, - checks: checks, - stateCache: make(map[string]any), + c: notify, + checks: checks, + followSymlinks: opts.FollowSymlinks, + stateCache: make(map[string]any), } // Initially run checks once _, err := w.runStateChecks() @@ -141,3 +170,11 @@ func (w *Watcher) runStateChecks() (WatcherEvent, error) { return WatcherEventNoChange, nil } + +func (w *Watcher) stat(filePath string) (os.FileInfo, error) { + if w.followSymlinks { + return os.Stat(filePath) + } + + return os.Lstat(filePath) +} diff --git a/file/watcher_checks.go b/file/watcher_checks.go index 11f2367..0e006fd 100644 --- a/file/watcher_checks.go +++ b/file/watcher_checks.go @@ -29,7 +29,7 @@ func WatcherCheckHash(hcf func() hash.Hash) WatcherCheck { lastHash = v } - if _, err := os.Lstat(w.FilePath); errors.Is(err, fs.ErrNotExist) { + if _, err := w.stat(w.FilePath); errors.Is(err, fs.ErrNotExist) { return WatcherEventInvalid, nil } @@ -65,7 +65,7 @@ func WatcherCheckMtime(w *Watcher) (WatcherEvent, error) { lastChange = v } - s, err := os.Lstat(w.FilePath) + s, err := w.stat(w.FilePath) switch { case err == nil: // handle size change @@ -94,7 +94,7 @@ func WatcherCheckPresence(w *Watcher) (WatcherEvent, error) { wasPresent = v } - _, err := os.Lstat(w.FilePath) + _, err := w.stat(w.FilePath) if err != nil && !errors.Is(err, fs.ErrNotExist) { // Some weird error occurred return WatcherEventInvalid, errors.Wrap(err, "getting file stat") @@ -124,7 +124,7 @@ func WatcherCheckSize(w *Watcher) (WatcherEvent, error) { knownSize = v } - s, err := os.Lstat(w.FilePath) + s, err := w.stat(w.FilePath) switch { case err == nil: // handle size change diff --git a/file/watcher_test.go b/file/watcher_test.go index 91220b0..58e191c 100644 --- a/file/watcher_test.go +++ b/file/watcher_test.go @@ -22,7 +22,7 @@ func TestWatcherCheckHash(t *testing.T) { testFile := path.Join(testDir, "test.txt") - w, err := newWatcher(testFile, time.Second, WatcherCheckHash(sha256.New)) + w, err := newWatcher(testFile, DefaultWatcherOpts, time.Second, WatcherCheckHash(sha256.New)) require.NoError(t, err, "initial check should not error on non existing file") evt, err := w.runStateChecks() @@ -73,7 +73,7 @@ func TestWatcherCheckMtime(t *testing.T) { testFile := path.Join(testDir, "test.txt") - w, err := newWatcher(testFile, time.Second, WatcherCheckMtime) + w, err := newWatcher(testFile, DefaultWatcherOpts, time.Second, WatcherCheckMtime) require.NoError(t, err, "initial check should not error on non existing file") evt, err := w.runStateChecks() @@ -128,7 +128,7 @@ func TestWatcherCheckPresence(t *testing.T) { testFile := path.Join(testDir, "test.txt") - w, err := newWatcher(testFile, time.Second, WatcherCheckPresence) + w, err := newWatcher(testFile, DefaultWatcherOpts, time.Second, WatcherCheckPresence) require.NoError(t, err, "initial check should not error on non existing file") evt, err := w.runStateChecks() @@ -165,7 +165,7 @@ func TestWatcherCheckSize(t *testing.T) { testFile := path.Join(testDir, "test.txt") - w, err := newWatcher(testFile, time.Second, WatcherCheckSize) + w, err := newWatcher(testFile, DefaultWatcherOpts, time.Second, WatcherCheckSize) require.NoError(t, err, "initial check should not error on non existing file") evt, err := w.runStateChecks()