Skip to content

Commit 099821e

Browse files
committed
enhancement:add watchfs support for darwin systems
1 parent 9ea6327 commit 099821e

File tree

6 files changed

+261
-27
lines changed

6 files changed

+261
-27
lines changed

pkg/storage/fs/posix/tree/inotifywatcher_default.go

Lines changed: 0 additions & 23 deletions
This file was deleted.

pkg/storage/fs/posix/tree/tree.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func New(lu node.PathLookup, bs node.Blobstore, um usermapper.Mapper, trashbin *
159159
return nil, err
160160
}
161161
default:
162-
t.watcher, err = NewInotifyWatcher(t, o, log)
162+
t.watcher, err = NewWatcher(t, o, log)
163163
if err != nil {
164164
return nil, err
165165
}

pkg/storage/fs/posix/tree/watch.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package tree
2+
3+
import (
4+
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
5+
)
6+
7+
var ErrUnsupportedWatcher = errtypes.NotSupported("watching the filesystem is not supported on this platform")
8+
9+
// NoopWatcher is a watcher that does nothing
10+
type NoopWatcher struct{}
11+
12+
// Watch does nothing
13+
func (*NoopWatcher) Watch(_ string) {}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//go:build darwin && experimental_watchfs_fsnotify_darwin
2+
3+
package tree
4+
5+
import (
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"sync"
11+
12+
"github.com/fsnotify/fsnotify"
13+
"github.com/rs/zerolog"
14+
15+
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
16+
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
17+
)
18+
19+
// FSnotifyWatcher fills the gap with fsnotify on Darwin, be careful with its limitations.
20+
// The main reason for its existence is to provide a working watcher on Darwin for development and testing purposes.
21+
type FSnotifyWatcher struct {
22+
tree *Tree
23+
options *options.Options
24+
log *zerolog.Logger
25+
26+
mu sync.Mutex
27+
watched map[string]struct{}
28+
}
29+
30+
// NewWatcher creates a new FSnotifyWatcher which implements the Watcher interface for Darwin using fsnotify.
31+
func NewWatcher(tree *Tree, o *options.Options, log *zerolog.Logger) (*FSnotifyWatcher, error) {
32+
log.Warn().Msg("fsnotify watcher on darwin has limitations and may not work as expected in all scenarios, not recommended for production use")
33+
34+
return &FSnotifyWatcher{
35+
tree: tree,
36+
options: o,
37+
log: log,
38+
watched: make(map[string]struct{}),
39+
}, nil
40+
}
41+
42+
// add takes care of adding watches for root and its subpaths.
43+
func (w *FSnotifyWatcher) add(fsWatcher *fsnotify.Watcher, root string) error {
44+
// Check if the root is ignored before walking the tree
45+
if isPathIgnored(w.tree, root) {
46+
return nil
47+
}
48+
49+
return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
50+
if err != nil {
51+
return err
52+
}
53+
54+
// skip ignored paths or files
55+
if isPathIgnored(w.tree, p) || !d.IsDir() {
56+
return nil
57+
}
58+
59+
w.mu.Lock()
60+
defer w.mu.Unlock()
61+
62+
// path is known, skip
63+
if _, ok := w.watched[p]; ok {
64+
return nil
65+
}
66+
67+
if err := fsWatcher.Add(p); err != nil {
68+
return err
69+
}
70+
71+
w.watched[p] = struct{}{}
72+
73+
return nil
74+
})
75+
}
76+
77+
// remove takes care of removing watches for root and its subpaths.
78+
func (w *FSnotifyWatcher) remove(fsWatcher *fsnotify.Watcher, root string) {
79+
w.mu.Lock()
80+
defer w.mu.Unlock()
81+
82+
for p := range w.watched {
83+
if p == root || isSubpath(root, p) {
84+
if err := fsWatcher.Remove(p); err != nil {
85+
w.log.Debug().Err(err).Str("path", p).Msg("failed to remove watch")
86+
}
87+
88+
delete(w.watched, p)
89+
}
90+
}
91+
}
92+
93+
// handleEvent supervises the handling of fsnotify events.
94+
func (w *FSnotifyWatcher) handleEvent(fsWatcher *fsnotify.Watcher, event fsnotify.Event) error {
95+
isCreate := event.Op&fsnotify.Create != 0
96+
isRemove := event.Op&fsnotify.Remove != 0
97+
isRename := event.Op&fsnotify.Rename != 0
98+
isWrite := event.Op&fsnotify.Write != 0
99+
100+
isKnownEvent := isCreate || isRemove || isRename || isWrite
101+
isIgnored := isPathIgnored(w.tree, event.Name)
102+
103+
// filter out unwanted events
104+
if isIgnored || !isKnownEvent {
105+
return nil
106+
}
107+
108+
st, statErr := os.Stat(event.Name)
109+
exists := statErr == nil
110+
isDir := exists && st.IsDir()
111+
112+
switch {
113+
case isRename:
114+
if exists {
115+
if isDir {
116+
_ = w.add(fsWatcher, event.Name)
117+
}
118+
119+
return w.tree.Scan(event.Name, watcher.ActionMove, isDir)
120+
}
121+
122+
w.remove(fsWatcher, event.Name)
123+
return w.tree.Scan(event.Name, watcher.ActionMoveFrom, false)
124+
case isRemove:
125+
w.remove(fsWatcher, event.Name)
126+
return w.tree.Scan(event.Name, watcher.ActionDelete, false)
127+
128+
case isCreate:
129+
if exists {
130+
if isDir {
131+
_ = w.add(fsWatcher, event.Name)
132+
}
133+
134+
return w.tree.Scan(event.Name, watcher.ActionCreate, isDir)
135+
}
136+
137+
w.remove(fsWatcher, event.Name)
138+
return w.tree.Scan(event.Name, watcher.ActionMoveFrom, false)
139+
case isWrite:
140+
if exists {
141+
return w.tree.Scan(event.Name, watcher.ActionUpdate, isDir)
142+
}
143+
default:
144+
w.log.Warn().Interface("event", event).Msg("unhandled event")
145+
}
146+
147+
return nil
148+
}
149+
150+
// Watch starts watching the given path for changes.
151+
func (w *FSnotifyWatcher) Watch(path string) {
152+
fsWatcher, err := fsnotify.NewWatcher()
153+
if err != nil {
154+
w.log.Error().Err(err).Msg("failed to create watcher")
155+
return
156+
}
157+
defer func() { _ = fsWatcher.Close() }()
158+
159+
if w.options.InotifyStatsFrequency > 0 {
160+
w.log.Debug().Str("watcher", "not implemented on darwin").Msg("fsnotify stats")
161+
}
162+
163+
go func() {
164+
for {
165+
select {
166+
case event, ok := <-fsWatcher.Events:
167+
if !ok {
168+
return
169+
}
170+
171+
if err := w.handleEvent(fsWatcher, event); err != nil {
172+
w.log.Error().Err(err).Str("path", event.Name).Msg("error scanning file")
173+
}
174+
case err, ok := <-fsWatcher.Errors:
175+
if !ok {
176+
return
177+
}
178+
179+
w.log.Error().Err(err).Msg("fsnotify error")
180+
}
181+
}
182+
}()
183+
184+
base := filepath.Join(path, "users")
185+
if err := w.add(fsWatcher, base); err != nil {
186+
w.log.Error().Err(err).Str("path", base).Msg("failed to add initial watches")
187+
}
188+
189+
<-make(chan struct{})
190+
}
191+
192+
// isSubpath checks if p is a subpath of root
193+
func isSubpath(root, p string) bool {
194+
r, err := filepath.Abs(root)
195+
if err != nil {
196+
r = filepath.Clean(root)
197+
}
198+
199+
pp, err := filepath.Abs(p)
200+
if err != nil {
201+
pp = filepath.Clean(p)
202+
}
203+
204+
rel, err := filepath.Rel(r, pp)
205+
if err != nil {
206+
return false
207+
}
208+
209+
return rel != "." && !strings.HasPrefix(rel, "..")
210+
}
211+
212+
// isIgnored checks if the path is ignored by its tree.
213+
func isPathIgnored(tree *Tree, path string) bool {
214+
215+
isLockFile := isLockFile(path)
216+
isTrash := isTrash(path)
217+
isUpload := tree.isUpload(path)
218+
isInternal := tree.isInternal(path)
219+
220+
// ask the tree if the path is internal or ignored
221+
return path == "" ||
222+
isLockFile ||
223+
isTrash ||
224+
isUpload ||
225+
isInternal
226+
}

pkg/storage/fs/posix/tree/inotifywatcher.go renamed to pkg/storage/fs/posix/tree/watcher_linux.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ import (
2929
"strings"
3030
"time"
3131

32-
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
33-
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
3432
"github.com/pablodz/inotifywaitgo/inotifywaitgo"
3533
"github.com/rs/zerolog"
3634
slogzerolog "github.com/samber/slog-zerolog/v2"
35+
36+
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
37+
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
3738
)
3839

3940
type InotifyWatcher struct {
@@ -42,7 +43,7 @@ type InotifyWatcher struct {
4243
log *zerolog.Logger
4344
}
4445

45-
func NewInotifyWatcher(tree *Tree, o *options.Options, log *zerolog.Logger) (*InotifyWatcher, error) {
46+
func NewWatcher(tree *Tree, o *options.Options, log *zerolog.Logger) (*InotifyWatcher, error) {
4647
return &InotifyWatcher{
4748
tree: tree,
4849
options: o,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build !linux && (!darwin || !experimental_watchfs_fsnotify_darwin)
2+
3+
// Copyright 2025 OpenCloud GmbH <[email protected]>
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package tree
7+
8+
import (
9+
"github.com/rs/zerolog"
10+
11+
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
12+
)
13+
14+
// NewWatcher returns a NoopWatcher on unsupported platforms
15+
func NewWatcher(_ *Tree, _ *options.Options, _ *zerolog.Logger) (*NoopWatcher, error) {
16+
return nil, ErrUnsupportedWatcher
17+
}

0 commit comments

Comments
 (0)