From 103906b1cd380d94fecfbeb573992b45bde9f963 Mon Sep 17 00:00:00 2001 From: olongfe Date: Thu, 1 Sep 2022 14:45:42 +0800 Subject: [PATCH 1/6] add: reload watch dir when add new folder --- watch.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/watch.go b/watch.go index 97a9c78..096b2d2 100644 --- a/watch.go +++ b/watch.go @@ -17,7 +17,7 @@ import ( // // Unique values sent to the channel are stored in an internal map and all // are processed once the the interval is up. -func debounce(interval time.Duration, input chan string, firstCallback func(arg string), callback func(arg string)) { +func debounce(interval time.Duration, input chan string, exit chan struct{}, firstCallback func(arg string), callback func(arg string)) { // keep a log of unique paths var items = make(map[string]bool) var item string @@ -37,6 +37,9 @@ func debounce(interval time.Duration, input chan string, firstCallback func(arg callback(path) delete(items, path) } + case <-exit: + log.Info("debounce return") + return } } } @@ -76,9 +79,9 @@ func (w *FSWatcher) Close() { // WatchDir sets up the filesystem watcher for baseDir and all existing subdirectories func (w *FSWatcher) WatchDir(baseDir string) error { c := make(chan string) - + exit := make(chan struct{}) // debounced call to create / update tileset - go debounce(500*time.Millisecond, c, func(path string) { + go debounce(500*time.Millisecond, c, exit, func(path string) { // callback for first time path is debounced id, err := w.generateID(path, baseDir) if err != nil { @@ -128,7 +131,6 @@ func (w *FSWatcher) WatchDir(baseDir string) error { } return }) - go func() { for { select { @@ -146,9 +148,14 @@ func (w *FSWatcher) WatchDir(baseDir string) error { } path := event.Name - if ext := filepath.Ext(path); ext != ".mbtiles" { - continue + exit <- struct{}{} + err := w.WatchDir(baseDir) + if err != nil { + return + } + log.Info("reload watch dir") + return } if _, err := os.Stat(path + "-journal"); err == nil { From 5bf08d4a89268f4c39d3773b40ead856c4729656 Mon Sep 17 00:00:00 2001 From: olongfen <824426699@qq.com> Date: Mon, 12 Sep 2022 15:56:00 +0800 Subject: [PATCH 2/6] fix: reload dir when change watch dir --- watch.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/watch.go b/watch.go index 096b2d2..ec8f540 100644 --- a/watch.go +++ b/watch.go @@ -38,7 +38,7 @@ func debounce(interval time.Duration, input chan string, exit chan struct{}, fir delete(items, path) } case <-exit: - log.Info("debounce return") + //log.Info("debounce return") return } } @@ -149,13 +149,19 @@ func (w *FSWatcher) WatchDir(baseDir string) error { path := event.Name if ext := filepath.Ext(path); ext != ".mbtiles" { - exit <- struct{}{} - err := w.WatchDir(baseDir) - if err != nil { + if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write || + event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename { + exit <- struct{}{} + err := w.WatchDir(baseDir) + if err != nil { + return + } + log.Info("reload watch dir") return + } else { + continue } - log.Info("reload watch dir") - return + } if _, err := os.Stat(path + "-journal"); err == nil { From 6fd67088f41cb976a3979fffc2e17e575bd3e87f Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Fri, 27 Dec 2024 11:47:49 -0800 Subject: [PATCH 3/6] Watch only directories for changes --- watch.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/watch.go b/watch.go index ec8f540..3f1dad0 100644 --- a/watch.go +++ b/watch.go @@ -38,7 +38,6 @@ func debounce(interval time.Duration, input chan string, exit chan struct{}, fir delete(items, path) } case <-exit: - //log.Info("debounce return") return } } @@ -148,15 +147,20 @@ func (w *FSWatcher) WatchDir(baseDir string) error { } path := event.Name - if ext := filepath.Ext(path); ext != ".mbtiles" { + if ext := filepath.Ext(path); ext == "" { if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename { + + // NOTE: we cannot distinguish which incoming event paths + // correspond to directory events or file events, so we + // trigger a reload in all cases + exit <- struct{}{} err := w.WatchDir(baseDir) if err != nil { return } - log.Info("reload watch dir") + log.Info("Reload watch dir on directory change") return } else { continue @@ -193,11 +197,13 @@ func (w *FSWatcher) WatchDir(baseDir string) error { if err != nil { log.Errorf("Could not create ID for tileset %q\n%v", path, err) } - err = w.svcSet.RemoveTileset(id) - if err != nil { - log.Errorf("Could not remove tileset %q with ID %q\n%v", path, id, err) - } else { - log.Infof("Removed tileset %q with ID %q\n", path, id) + if w.svcSet.HasTileset(id) { + err = w.svcSet.RemoveTileset(id) + if err != nil { + log.Errorf("Could not remove tileset %q with ID %q\n%v", path, id, err) + } else { + log.Infof("Removed tileset %q with ID %q\n", path, id) + } } } From 351d4cf2d1dcc5c8d95443b7c08a856a8dbfa019 Mon Sep 17 00:00:00 2001 From: jelly <824426699@qq.com> Date: Fri, 15 Aug 2025 11:48:02 +0800 Subject: [PATCH 4/6] add: fsnotify is invalid when monitoring the NAS directory. Add polling mode to monitor the NAS directory. Due to the NAS file system cache, the files in the directory cannot be deleted elsewhere when being polled. --- watch.go | 351 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 212 insertions(+), 139 deletions(-) diff --git a/watch.go b/watch.go index 3f1dad0..f365cd6 100644 --- a/watch.go +++ b/watch.go @@ -3,6 +3,7 @@ package main import ( "os" "path/filepath" + "sync" "time" log "github.com/sirupsen/logrus" @@ -10,24 +11,20 @@ import ( mbtiles "github.com/brendan-ward/mbtiles-go" "github.com/consbio/mbtileserver/handlers" "github.com/fsnotify/fsnotify" + "golang.org/x/sys/unix" ) -// debounce debounces requests to a callback function to occur no more -// frequently than interval; once this is reached, the callback is called. -// -// Unique values sent to the channel are stored in an internal map and all -// are processed once the the interval is up. +// ===================== debounce ===================== func debounce(interval time.Duration, input chan string, exit chan struct{}, firstCallback func(arg string), callback func(arg string)) { - // keep a log of unique paths var items = make(map[string]bool) var item string timer := time.NewTimer(interval) + defer timer.Stop() + for { select { case item = <-input: if _, ok := items[item]; !ok { - // first time we see a given path, we need to call lockHandler - // to lock it (unlocked by callback) firstCallback(item) } items[item] = true @@ -37,25 +34,48 @@ func debounce(interval time.Duration, input chan string, exit chan struct{}, fir callback(path) delete(items, path) } - case <-exit: - return + case _, ok := <-exit: + if !ok { + return + } } } } -// FSWatcher provides a filesystem watcher to detect when mbtiles files are -// created, updated, or removed on the filesystem. +// ================== File system detection ================== +var fsTypeMap = map[int64]string{ + 0xEF53: "ext2/ext3/ext4", + 0x58465342: "xfs", + 0x9123683E: "btrfs", + 0x6969: "nfs", + 0xFF534D42: "cifs", + 0x65735546: "fuse", +} + +func detectFSType(path string) string { + var stat unix.Statfs_t + if err := unix.Statfs(path, &stat); err != nil { + log.Warnf("Failed to detect filesystem type: %v", err) + return "unknown" + } + if name, ok := fsTypeMap[int64(stat.Type)]; ok { + return name + } + return "unknown" +} + +func isNetworkFS(fs string) bool { + return fs == "nfs" || fs == "cifs" || fs == "fuse" || fs == "unknown" +} + +// FSWatcher ================== FSWatcher ================== type FSWatcher struct { watcher *fsnotify.Watcher svcSet *handlers.ServiceSet generateID handlers.IDGenerator } -// NewFSWatcher creates a new FSWatcher to watch the filesystem for changes to -// mbtiles files and updates the ServiceSet accordingly. -// -// The generateID function needs to be of the same type used when the tilesets -// were originally added to the ServiceSet. +// NewFSWatcher ================== Constructor and Close ================== func NewFSWatcher(svcSet *handlers.ServiceSet, generateID handlers.IDGenerator) (*FSWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -68,148 +88,209 @@ func NewFSWatcher(svcSet *handlers.ServiceSet, generateID handlers.IDGenerator) }, nil } -// Close closes the FSWatcher and stops watching the filesystem. func (w *FSWatcher) Close() { if w.watcher != nil { w.watcher.Close() } } -// WatchDir sets up the filesystem watcher for baseDir and all existing subdirectories -func (w *FSWatcher) WatchDir(baseDir string) error { - c := make(chan string) - exit := make(chan struct{}) - // debounced call to create / update tileset - go debounce(500*time.Millisecond, c, exit, func(path string) { - // callback for first time path is debounced - id, err := w.generateID(path, baseDir) - if err != nil { - log.Errorf("Could not create ID for tileset %q\n%v", path, err) - return - } - // lock tileset for writing, if it exists - w.svcSet.LockTileset(id) - }, func(path string) { - // callback after debouncing incoming requests +// ================== Common file handling ================== +func (w *FSWatcher) handleFileChange(path, baseDir string) { + // Skip files still being written (SQLite -journal or -wal) + if _, err := os.Stat(path + "-journal"); err == nil { + log.Debugf("Tileset %q is currently being written", path) + return + } + if _, err := os.Stat(path + "-wal"); err == nil { + log.Debugf("Tileset %q is currently being written (wal)", path) + return + } - // Verify that file can be opened with mbtiles-go, which runs - // validation on open. - // If file cannot be opened, assume it is still being written / copied. - db, err := mbtiles.Open(path) - if err != nil { - return + db, err := mbtiles.Open(path) + if err != nil { + return + } + db.Close() + + id, err := w.generateID(path, baseDir) + if err != nil { + log.Errorf("Failed to generate ID: %v", err) + return + } + + if w.svcSet.HasTileset(id) { + w.svcSet.LockTileset(id) + defer w.svcSet.UnlockTileset(id) + if err := w.svcSet.UpdateTileset(id); err != nil { + log.Errorf("Failed to update tileset: %v", err) + } else { + log.Infof("Updated tileset: %s", id) } - db.Close() + return + } - // determine file ID for tileset - id, err := w.generateID(path, baseDir) - if err != nil { - log.Errorf("Could not create ID for tileset %q\n%v", path, err) - return + w.svcSet.LockTileset(id) + defer w.svcSet.UnlockTileset(id) + if err := w.svcSet.AddTileset(path, id); err != nil { + log.Errorf("Failed to add tileset: %v", err) + } else { + log.Infof("Added tileset: %s", id) + } +} + +func (w *FSWatcher) handleFileRemove(path, baseDir string) { + id, err := w.generateID(path, baseDir) + if err != nil { + log.Errorf("Failed to generate ID: %v", err) + return + } + if w.svcSet.HasTileset(id) { + if err := w.svcSet.RemoveTileset(id); err != nil { + log.Errorf("Failed to remove tileset: %v", err) + } else { + log.Infof("Removed tileset: %s", id) } + } +} - // update existing tileset - if w.svcSet.HasTileset(id) { - err = w.svcSet.UpdateTileset(id) - if err != nil { - log.Errorf("Could not update tileset %q with ID %q\n%v", path, id, err) - } else { - // only unlock if successfully updated - w.svcSet.UnlockTileset(id) - log.Infof("Updated tileset %q with ID %q\n", path, id) +// ================== High-performance polling ================== +func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { + log.Infof("Using polling mode to watch %s every %v", baseDir, interval) + + type fileState struct { + ModTime time.Time + Size int64 + } + + dirCache := make(map[string]map[string]fileState) + mu := sync.Mutex{} + + scanDir := func(path string) map[string]fileState { + files := make(map[string]fileState) + filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + if filepath.Ext(p) != ".mbtiles" { + return nil + } + files[p] = fileState{ModTime: info.ModTime(), Size: info.Size()} + return nil + }) + return files + } + + // Initial scan + mu.Lock() + dirCache[baseDir] = scanDir(baseDir) + for p := range dirCache[baseDir] { + w.handleFileChange(p, baseDir) + } + mu.Unlock() + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for range ticker.C { + mu.Lock() + current := scanDir(baseDir) + + // New or modified files + for p, st := range current { + oldSt, exists := dirCache[baseDir][p] + if !exists || oldSt.ModTime != st.ModTime || oldSt.Size != st.Size { + w.handleFileChange(p, baseDir) } - return } - // create new tileset - err = w.svcSet.AddTileset(path, id) - if err != nil { - log.Errorf("Could not add tileset for %q with ID %q\n%v", path, id, err) - } else { - log.Infof("Updated tileset %q with ID %q\n", path, id) + // Deleted files + for p := range dirCache[baseDir] { + if _, exists := current[p]; !exists { + w.handleFileRemove(p, baseDir) + } } - return - }) + + dirCache[baseDir] = current + mu.Unlock() + } +} + +// WatchDir ================== WatchDir with auto mode ================== +func (w *FSWatcher) WatchDir(baseDir string) error { + fsType := detectFSType(baseDir) + log.Infof("Detected filesystem type: %s", fsType) + + if isNetworkFS(fsType) { + // Network disk → polling mode + go w.startPolling(baseDir, 5*time.Second) + return nil + } + + // Local disk → fsnotify mode + c := make(chan string, 1024) // buffered to prevent blocking + exit := make(chan struct{}) + + go debounce(500*time.Millisecond, c, exit, + func(path string) { + id, err := w.generateID(path, baseDir) + if err != nil { + log.Errorf("Failed to generate ID: %v", err) + return + } + w.svcSet.LockTileset(id) + }, + func(path string) { + w.handleFileChange(path, baseDir) + }, + ) + go func() { for { select { case event, ok := <-w.watcher.Events: if !ok { - log.Errorf("error in filewatcher for %q, exiting filewatcher", event.Name) return } - if !((event.Op&fsnotify.Create == fsnotify.Create) || - (event.Op&fsnotify.Write == fsnotify.Write) || - (event.Op&fsnotify.Remove == fsnotify.Remove) || - (event.Op&fsnotify.Rename == fsnotify.Rename)) { - continue + info, err := os.Stat(event.Name) + if err == nil && info.IsDir() && event.Op&fsnotify.Create != 0 { + // dynamically watch new directories + w.watcher.Add(event.Name) } - path := event.Name - if ext := filepath.Ext(path); ext == "" { - if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write || - event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename { - - // NOTE: we cannot distinguish which incoming event paths - // correspond to directory events or file events, so we - // trigger a reload in all cases - - exit <- struct{}{} - err := w.WatchDir(baseDir) - if err != nil { - return - } - log.Info("Reload watch dir on directory change") - return - } else { - continue - } - + ext := filepath.Ext(event.Name) + if ext != ".mbtiles" { + continue } - if _, err := os.Stat(path + "-journal"); err == nil { - // Don't try to load .mbtiles files that are being written - log.Debugf("Tileset %q is currently being created or is incomplete\n", path) + // Skip files still being written + if _, err := os.Stat(event.Name + "-journal"); err == nil { continue } - - if (event.Op&fsnotify.Create == fsnotify.Create) || - (event.Op&fsnotify.Write == fsnotify.Write) { - // This event may get called multiple times while a file is being copied into a watched directory, - // so we debounce this instead. - c <- path + if _, err := os.Stat(event.Name + "-wal"); err == nil { continue } - if (event.Op&fsnotify.Remove == fsnotify.Remove) || (event.Op&fsnotify.Rename == fsnotify.Rename) { - // some file move events trigger remove / rename, so if the file still exists, assume it is - // one of these - _, err := os.Stat(path) - if err == nil { - // debounce to give it a little more time to update, if needed - c <- path - continue - } - - // remove tileset immediately so that there are not other errors in request handlers - id, err := w.generateID(path, baseDir) - if err != nil { - log.Errorf("Could not create ID for tileset %q\n%v", path, err) + if event.Op&(fsnotify.Create|fsnotify.Write) != 0 { + select { + case c <- event.Name: + default: + log.Warnf("Event channel full, skipping %s", event.Name) } - if w.svcSet.HasTileset(id) { - err = w.svcSet.RemoveTileset(id) - if err != nil { - log.Errorf("Could not remove tileset %q with ID %q\n%v", path, id, err) - } else { - log.Infof("Removed tileset %q with ID %q\n", path, id) + } else if event.Op&(fsnotify.Remove|fsnotify.Rename) != 0 { + if _, err := os.Stat(event.Name); err == nil { + select { + case c <- event.Name: + default: + log.Warnf("Event channel full, skipping %s", event.Name) } + } else { + w.handleFileRemove(event.Name, baseDir) } } case err, ok := <-w.watcher.Errors: if !ok { - log.Errorf("error in filewatcher, exiting filewatcher") return } log.Error(err) @@ -217,27 +298,19 @@ func (w *FSWatcher) WatchDir(baseDir string) error { } }() - err := filepath.Walk(baseDir, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.Mode().IsDir() { - return w.watcher.Add(path) - } - return nil - }) - if err != nil { - return err - } - - return nil + // Recursively add directories + return filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return w.watcher.Add(path) + } + return nil + }) } func exists(path string) bool { _, err := os.Stat(path) - if err != nil { - return false - } - return true + return err == nil } From 196cd78df0ade925d906b31795b0480a2c726013 Mon Sep 17 00:00:00 2001 From: jelly <824426699@qq.com> Date: Fri, 15 Aug 2025 14:06:29 +0800 Subject: [PATCH 5/6] fix: add annotate --- watch.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/watch.go b/watch.go index f365cd6..da2418f 100644 --- a/watch.go +++ b/watch.go @@ -14,7 +14,11 @@ import ( "golang.org/x/sys/unix" ) -// ===================== debounce ===================== +// debounce debounces requests to a callback function to occur no more +// frequently than interval; once this is reached, the callback is called. +// +// Unique values sent to the channel are stored in an internal map and all +// are processed once the interval is up. func debounce(interval time.Duration, input chan string, exit chan struct{}, firstCallback func(arg string), callback func(arg string)) { var items = make(map[string]bool) var item string @@ -42,7 +46,7 @@ func debounce(interval time.Duration, input chan string, exit chan struct{}, fir } } -// ================== File system detection ================== +// fsTypeMap maps filesystem type identifiers to human-readable names. var fsTypeMap = map[int64]string{ 0xEF53: "ext2/ext3/ext4", 0x58465342: "xfs", @@ -52,6 +56,7 @@ var fsTypeMap = map[int64]string{ 0x65735546: "fuse", } +// detectFSType detects the filesystem type of the given path using func detectFSType(path string) string { var stat unix.Statfs_t if err := unix.Statfs(path, &stat); err != nil { @@ -64,18 +69,24 @@ func detectFSType(path string) string { return "unknown" } +// isNetworkFS checks if the given filesystem type is a network filesystem. func isNetworkFS(fs string) bool { return fs == "nfs" || fs == "cifs" || fs == "fuse" || fs == "unknown" } -// FSWatcher ================== FSWatcher ================== +// FSWatcher provides a filesystem watcher to detect when mbtiles files are +// created, updated, or removed on the filesystem. type FSWatcher struct { watcher *fsnotify.Watcher svcSet *handlers.ServiceSet generateID handlers.IDGenerator } -// NewFSWatcher ================== Constructor and Close ================== +// NewFSWatcher creates a new FSWatcher to watch the filesystem for changes to +// mbtiles files and updates the ServiceSet accordingly. +// +// The generateID function needs to be of the same type used when the tilesets +// were originally added to the ServiceSet. func NewFSWatcher(svcSet *handlers.ServiceSet, generateID handlers.IDGenerator) (*FSWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -88,13 +99,14 @@ func NewFSWatcher(svcSet *handlers.ServiceSet, generateID handlers.IDGenerator) }, nil } +// Close closes the FSWatcher and stops watching the filesystem. func (w *FSWatcher) Close() { if w.watcher != nil { w.watcher.Close() } } -// ================== Common file handling ================== +// handleFileChange processes a file change event for mbtiles file. func (w *FSWatcher) handleFileChange(path, baseDir string) { // Skip files still being written (SQLite -journal or -wal) if _, err := os.Stat(path + "-journal"); err == nil { @@ -153,7 +165,7 @@ func (w *FSWatcher) handleFileRemove(path, baseDir string) { } } -// ================== High-performance polling ================== +// polling directories for changes to mbtiles files func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { log.Infof("Using polling mode to watch %s every %v", baseDir, interval) @@ -215,7 +227,7 @@ func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { } } -// WatchDir ================== WatchDir with auto mode ================== +// WatchDir sets up the filesystem watcher for baseDir and all existing subdirectories func (w *FSWatcher) WatchDir(baseDir string) error { fsType := detectFSType(baseDir) log.Infof("Detected filesystem type: %s", fsType) From f9d5febffe896003bec79960e9a9f71b841a3d09 Mon Sep 17 00:00:00 2001 From: jelly <824426699@qq.com> Date: Fri, 15 Aug 2025 14:14:58 +0800 Subject: [PATCH 6/6] fix: add annotate --- watch.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/watch.go b/watch.go index da2418f..e1470f7 100644 --- a/watch.go +++ b/watch.go @@ -113,6 +113,7 @@ func (w *FSWatcher) handleFileChange(path, baseDir string) { log.Debugf("Tileset %q is currently being written", path) return } + // Skip files still being written (SQLite -wal) if _, err := os.Stat(path + "-wal"); err == nil { log.Debugf("Tileset %q is currently being written (wal)", path) return @@ -123,7 +124,7 @@ func (w *FSWatcher) handleFileChange(path, baseDir string) { return } db.Close() - + // Valid mbtiles file id, err := w.generateID(path, baseDir) if err != nil { log.Errorf("Failed to generate ID: %v", err) @@ -150,6 +151,7 @@ func (w *FSWatcher) handleFileChange(path, baseDir string) { } } +// handleFileRemove processes a file removal event for mbtiles file. func (w *FSWatcher) handleFileRemove(path, baseDir string) { id, err := w.generateID(path, baseDir) if err != nil { @@ -168,7 +170,7 @@ func (w *FSWatcher) handleFileRemove(path, baseDir string) { // polling directories for changes to mbtiles files func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { log.Infof("Using polling mode to watch %s every %v", baseDir, interval) - + // fileState holds the modification time and size of a file type fileState struct { ModTime time.Time Size int64 @@ -176,7 +178,7 @@ func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { dirCache := make(map[string]map[string]fileState) mu := sync.Mutex{} - + // scanDir scans the directory for mbtiles files and returns a map of their states scanDir := func(path string) map[string]fileState { files := make(map[string]fileState) filepath.Walk(path, func(p string, info os.FileInfo, err error) error { @@ -199,7 +201,7 @@ func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { w.handleFileChange(p, baseDir) } mu.Unlock() - + // Start polling ticker ticker := time.NewTicker(interval) defer ticker.Stop() @@ -231,7 +233,7 @@ func (w *FSWatcher) startPolling(baseDir string, interval time.Duration) { func (w *FSWatcher) WatchDir(baseDir string) error { fsType := detectFSType(baseDir) log.Infof("Detected filesystem type: %s", fsType) - + // Use polling mode for network filesystems if isNetworkFS(fsType) { // Network disk → polling mode go w.startPolling(baseDir, 5*time.Second) @@ -241,7 +243,7 @@ func (w *FSWatcher) WatchDir(baseDir string) error { // Local disk → fsnotify mode c := make(chan string, 1024) // buffered to prevent blocking exit := make(chan struct{}) - + // Ensure the watcher is closed when done go debounce(500*time.Millisecond, c, exit, func(path string) { id, err := w.generateID(path, baseDir)