esync

Directory watching and remote syncing
Log | Files | Refs | README | LICENSE

commit 2578e56de39a4744a75def559258c8c9e275d929
parent 3290272c523f71239e030be9e15a7732264381bd
Author: Erik Loualiche <eloualic@umn.edu>
Date:   Sun, 22 Mar 2026 10:15:51 -0500

feat: watcher skips directories with broken symlinks instead of crashing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
Minternal/watcher/watcher.go | 23+++++++++++++++--------
Minternal/watcher/watcher_test.go | 30++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 8 deletions(-)

diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go @@ -124,13 +124,14 @@ func (d *Debouncer) Stop() { // Events are debounced so that a burst of rapid changes results in a single // call to the configured handler. type Watcher struct { - fsw *fsnotify.Watcher - debouncer *Debouncer - path string - rootPath string - ignores []string - includes []string - done chan struct{} + fsw *fsnotify.Watcher + debouncer *Debouncer + path string + rootPath string + ignores []string + includes []string + done chan struct{} + BrokenSymlinks []BrokenSymlink } // New creates a Watcher for the given directory path. debounceMs sets the @@ -318,7 +319,13 @@ func (w *Watcher) addRecursive(path string) error { } if info.IsDir() { - return w.fsw.Add(p) + if err := w.fsw.Add(p); err != nil { + if broken := findBrokenSymlinks(p); len(broken) > 0 { + w.BrokenSymlinks = append(w.BrokenSymlinks, broken...) + return nil // skip dir, continue walking + } + return err // non-symlink error, propagate + } } return nil diff --git a/internal/watcher/watcher_test.go b/internal/watcher/watcher_test.go @@ -195,3 +195,33 @@ func TestFindBrokenSymlinks(t *testing.T) { t.Errorf("path base = %q, want %q", filepath.Base(broken[0].Path), "bad.txt") } } + +// --------------------------------------------------------------------------- +// 8. TestAddRecursiveSkipsBrokenSymlinks — watcher starts despite broken symlinks +// --------------------------------------------------------------------------- +func TestAddRecursiveSkipsBrokenSymlinks(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "subdir") + os.Mkdir(sub, 0755) + + // Create a broken symlink inside subdir + os.Symlink("/nonexistent/target", filepath.Join(sub, "broken.csv")) + + w, err := New(dir, 100, nil, nil, func() {}) + if err != nil { + t.Fatalf("New: %v", err) + } + defer w.Stop() + + // Start should succeed despite broken symlinks + if err := w.Start(); err != nil { + t.Fatalf("Start failed: %v", err) + } + + if len(w.BrokenSymlinks) != 1 { + t.Fatalf("BrokenSymlinks = %d, want 1", len(w.BrokenSymlinks)) + } + if w.BrokenSymlinks[0].Target != "/nonexistent/target" { + t.Errorf("target = %q, want %q", w.BrokenSymlinks[0].Target, "/nonexistent/target") + } +}