From 00784804edecce69459ff2891df39065e352a3f7 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Tue, 30 Dec 2025 13:45:18 +0100 Subject: [PATCH] enhancement:add watchfs support for darwin systems --- .../fs/posix/tree/inotifywatcher_default.go | 23 -- pkg/storage/fs/posix/tree/tree.go | 2 +- pkg/storage/fs/posix/tree/watch.go | 13 + pkg/storage/fs/posix/tree/watcher_darwin.go | 226 ++++++++++++++++++ .../{inotifywatcher.go => watcher_linux.go} | 7 +- pkg/storage/fs/posix/tree/watcher_others.go | 17 ++ 6 files changed, 261 insertions(+), 27 deletions(-) delete mode 100644 pkg/storage/fs/posix/tree/inotifywatcher_default.go create mode 100644 pkg/storage/fs/posix/tree/watch.go create mode 100644 pkg/storage/fs/posix/tree/watcher_darwin.go rename pkg/storage/fs/posix/tree/{inotifywatcher.go => watcher_linux.go} (98%) create mode 100644 pkg/storage/fs/posix/tree/watcher_others.go diff --git a/pkg/storage/fs/posix/tree/inotifywatcher_default.go b/pkg/storage/fs/posix/tree/inotifywatcher_default.go deleted file mode 100644 index ebe5019a222..00000000000 --- a/pkg/storage/fs/posix/tree/inotifywatcher_default.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build !linux - -// Copyright 2025 OpenCloud GmbH -// SPDX-License-Identifier: Apache-2.0 - -package tree - -import ( - "github.com/opencloud-eu/reva/v2/pkg/errtypes" - "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options" - "github.com/rs/zerolog" -) - -// NullWatcher is a dummy watcher that does nothing -type NullWatcher struct{} - -// Watch does nothing -func (*NullWatcher) Watch(path string) {} - -// NewInotifyWatcher returns a new inotify watcher -func NewInotifyWatcher(_ *Tree, _ *options.Options, _ *zerolog.Logger) (*NullWatcher, error) { - return nil, errtypes.NotSupported("inotify watcher is not supported on this platform") -} diff --git a/pkg/storage/fs/posix/tree/tree.go b/pkg/storage/fs/posix/tree/tree.go index fd81b52580b..2db9643d64c 100644 --- a/pkg/storage/fs/posix/tree/tree.go +++ b/pkg/storage/fs/posix/tree/tree.go @@ -159,7 +159,7 @@ func New(lu node.PathLookup, bs node.Blobstore, um usermapper.Mapper, trashbin * return nil, err } default: - t.watcher, err = NewInotifyWatcher(t, o, log) + t.watcher, err = NewWatcher(t, o, log) if err != nil { return nil, err } diff --git a/pkg/storage/fs/posix/tree/watch.go b/pkg/storage/fs/posix/tree/watch.go new file mode 100644 index 00000000000..a609f08f278 --- /dev/null +++ b/pkg/storage/fs/posix/tree/watch.go @@ -0,0 +1,13 @@ +package tree + +import ( + "github.com/opencloud-eu/reva/v2/pkg/errtypes" +) + +var ErrUnsupportedWatcher = errtypes.NotSupported("watching the filesystem is not supported on this platform") + +// NoopWatcher is a watcher that does nothing +type NoopWatcher struct{} + +// Watch does nothing +func (*NoopWatcher) Watch(_ string) {} diff --git a/pkg/storage/fs/posix/tree/watcher_darwin.go b/pkg/storage/fs/posix/tree/watcher_darwin.go new file mode 100644 index 00000000000..ec16106e12a --- /dev/null +++ b/pkg/storage/fs/posix/tree/watcher_darwin.go @@ -0,0 +1,226 @@ +//go:build darwin && experimental_watchfs_darwin + +package tree + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog" + + "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options" + "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher" +) + +// FSnotifyWatcher fills the gap with fsnotify on Darwin, be careful with its limitations. +// The main reason for its existence is to provide a working watcher on Darwin for development and testing purposes. +type FSnotifyWatcher struct { + tree *Tree + options *options.Options + log *zerolog.Logger + + mu sync.Mutex + watched map[string]struct{} +} + +// NewWatcher creates a new FSnotifyWatcher which implements the Watcher interface for Darwin using fsnotify. +func NewWatcher(tree *Tree, o *options.Options, log *zerolog.Logger) (*FSnotifyWatcher, error) { + log.Warn().Msg("fsnotify watcher on darwin has limitations and may not work as expected in all scenarios, not recommended for production use") + + return &FSnotifyWatcher{ + tree: tree, + options: o, + log: log, + watched: make(map[string]struct{}), + }, nil +} + +// add takes care of adding watches for root and its subpaths. +func (w *FSnotifyWatcher) add(fsWatcher *fsnotify.Watcher, root string) error { + // Check if the root is ignored before walking the tree + if isPathIgnored(w.tree, root) { + return nil + } + + return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // skip ignored paths or files + if isPathIgnored(w.tree, p) || !d.IsDir() { + return nil + } + + w.mu.Lock() + defer w.mu.Unlock() + + // path is known, skip + if _, ok := w.watched[p]; ok { + return nil + } + + if err := fsWatcher.Add(p); err != nil { + return err + } + + w.watched[p] = struct{}{} + + return nil + }) +} + +// remove takes care of removing watches for root and its subpaths. +func (w *FSnotifyWatcher) remove(fsWatcher *fsnotify.Watcher, root string) { + w.mu.Lock() + defer w.mu.Unlock() + + for p := range w.watched { + if p == root || isSubpath(root, p) { + if err := fsWatcher.Remove(p); err != nil { + w.log.Debug().Err(err).Str("path", p).Msg("failed to remove watch") + } + + delete(w.watched, p) + } + } +} + +// handleEvent supervises the handling of fsnotify events. +func (w *FSnotifyWatcher) handleEvent(fsWatcher *fsnotify.Watcher, event fsnotify.Event) error { + isCreate := event.Op&fsnotify.Create != 0 + isRemove := event.Op&fsnotify.Remove != 0 + isRename := event.Op&fsnotify.Rename != 0 + isWrite := event.Op&fsnotify.Write != 0 + + isKnownEvent := isCreate || isRemove || isRename || isWrite + isIgnored := isPathIgnored(w.tree, event.Name) + + // filter out unwanted events + if isIgnored || !isKnownEvent { + return nil + } + + st, statErr := os.Stat(event.Name) + exists := statErr == nil + isDir := exists && st.IsDir() + + switch { + case isRename: + if exists { + if isDir { + _ = w.add(fsWatcher, event.Name) + } + + return w.tree.Scan(event.Name, watcher.ActionMove, isDir) + } + + w.remove(fsWatcher, event.Name) + return w.tree.Scan(event.Name, watcher.ActionMoveFrom, false) + case isRemove: + w.remove(fsWatcher, event.Name) + return w.tree.Scan(event.Name, watcher.ActionDelete, false) + + case isCreate: + if exists { + if isDir { + _ = w.add(fsWatcher, event.Name) + } + + return w.tree.Scan(event.Name, watcher.ActionCreate, isDir) + } + + w.remove(fsWatcher, event.Name) + return w.tree.Scan(event.Name, watcher.ActionMoveFrom, false) + case isWrite: + if exists { + return w.tree.Scan(event.Name, watcher.ActionUpdate, isDir) + } + default: + w.log.Warn().Interface("event", event).Msg("unhandled event") + } + + return nil +} + +// Watch starts watching the given path for changes. +func (w *FSnotifyWatcher) Watch(path string) { + fsWatcher, err := fsnotify.NewWatcher() + if err != nil { + w.log.Error().Err(err).Msg("failed to create watcher") + return + } + defer func() { _ = fsWatcher.Close() }() + + if w.options.InotifyStatsFrequency > 0 { + w.log.Debug().Str("watcher", "not implemented on darwin").Msg("fsnotify stats") + } + + go func() { + for { + select { + case event, ok := <-fsWatcher.Events: + if !ok { + return + } + + if err := w.handleEvent(fsWatcher, event); err != nil { + w.log.Error().Err(err).Str("path", event.Name).Msg("error scanning file") + } + case err, ok := <-fsWatcher.Errors: + if !ok { + return + } + + w.log.Error().Err(err).Msg("fsnotify error") + } + } + }() + + base := filepath.Join(path, "users") + if err := w.add(fsWatcher, base); err != nil { + w.log.Error().Err(err).Str("path", base).Msg("failed to add initial watches") + } + + <-make(chan struct{}) +} + +// isSubpath checks if p is a subpath of root +func isSubpath(root, p string) bool { + r, err := filepath.Abs(root) + if err != nil { + r = filepath.Clean(root) + } + + pp, err := filepath.Abs(p) + if err != nil { + pp = filepath.Clean(p) + } + + rel, err := filepath.Rel(r, pp) + if err != nil { + return false + } + + return rel != "." && !strings.HasPrefix(rel, "..") +} + +// isIgnored checks if the path is ignored by its tree. +func isPathIgnored(tree *Tree, path string) bool { + + isLockFile := isLockFile(path) + isTrash := isTrash(path) + isUpload := tree.isUpload(path) + isInternal := tree.isInternal(path) + + // ask the tree if the path is internal or ignored + return path == "" || + isLockFile || + isTrash || + isUpload || + isInternal +} diff --git a/pkg/storage/fs/posix/tree/inotifywatcher.go b/pkg/storage/fs/posix/tree/watcher_linux.go similarity index 98% rename from pkg/storage/fs/posix/tree/inotifywatcher.go rename to pkg/storage/fs/posix/tree/watcher_linux.go index d5a1b475a36..6ba6227127f 100644 --- a/pkg/storage/fs/posix/tree/inotifywatcher.go +++ b/pkg/storage/fs/posix/tree/watcher_linux.go @@ -29,11 +29,12 @@ import ( "strings" "time" - "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options" - "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher" "github.com/pablodz/inotifywaitgo/inotifywaitgo" "github.com/rs/zerolog" slogzerolog "github.com/samber/slog-zerolog/v2" + + "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options" + "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher" ) type InotifyWatcher struct { @@ -42,7 +43,7 @@ type InotifyWatcher struct { log *zerolog.Logger } -func NewInotifyWatcher(tree *Tree, o *options.Options, log *zerolog.Logger) (*InotifyWatcher, error) { +func NewWatcher(tree *Tree, o *options.Options, log *zerolog.Logger) (*InotifyWatcher, error) { return &InotifyWatcher{ tree: tree, options: o, diff --git a/pkg/storage/fs/posix/tree/watcher_others.go b/pkg/storage/fs/posix/tree/watcher_others.go new file mode 100644 index 00000000000..028d71fae0f --- /dev/null +++ b/pkg/storage/fs/posix/tree/watcher_others.go @@ -0,0 +1,17 @@ +//go:build !linux && (!darwin || !experimental_watchfs_darwin) + +// Copyright 2025 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package tree + +import ( + "github.com/rs/zerolog" + + "github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options" +) + +// NewWatcher returns a NoopWatcher on unsupported platforms +func NewWatcher(_ *Tree, _ *options.Options, _ *zerolog.Logger) (*NoopWatcher, error) { + return nil, ErrUnsupportedWatcher +}