Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: ["master"]
pull_request:
branches: ["master"]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: install deps
run: sudo apt install -y libmagic-dev

- uses: glimps-re/go-actions@v0.1.0
with:
golangci-lint-version: v2.4.0
go-private-repo: github.com/glimps-re/*
go-version: '1.25'
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# rfsnotify v0.1.0
recursive directory notifications built as a wrapper around fsnotify (golang)
# rfsnotify

recursive directory notifications built as a wrapper around fsnotify (golang), forked from [this repo](https://github.com/farmergreg/rfsnotify)

[![GoDoc](https://godoc.org/github.com/farmergreg/rfsnotify?status.svg)](https://godoc.org/github.com/farmergreg/rfsnotify)

Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/glimps-re/rfsnotify

go 1.25

require github.com/fsnotify/fsnotify v1.9.0

require golang.org/x/sys v0.37.0 // indirect
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
29 changes: 20 additions & 9 deletions rfsnotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
package rfsnotify

import (
"gopkg.in/fsnotify.v1"

"errors"
"log/slog"
"os"
"path/filepath"
"strings"

"github.com/fsnotify/fsnotify"
)

// RWatcher wraps fsnotify.Watcher. When fsnotify adds recursive watches, you should be able to switch your code to use fsnotify.Watcher
Expand Down Expand Up @@ -84,24 +86,33 @@ func (m *RWatcher) start() {
select {

case e := <-m.fsnotify.Events:

s, err := os.Stat(e.Name)
if err == nil && s != nil && s.IsDir() {
if e.Op&fsnotify.Create != 0 {
m.watchRecursive(e.Name, false)
if e.Has(fsnotify.Create) {
if ewr := m.watchRecursive(e.Name, false); ewr != nil {
slog.Error("an error occurred with m.watchRecursive", slog.String("path", e.Name), slog.String("error", ewr.Error()))
}
}
}
//Can't stat a deleted directory, so just pretend that it's always a directory and
//try to remove from the watch list... we really have no clue if it's a directory or not...
if e.Op&fsnotify.Remove != 0 {
m.fsnotify.Remove(e.Name)
// Can't stat a deleted directory, so just pretend that it's always a directory and
// try to remove from the watch list... we really have no clue if it's a directory or not...
if e.Has(fsnotify.Remove) {
if er := m.fsnotify.Remove(e.Name); er != nil {
if !strings.Contains(er.Error(), "non-existent watch") {
slog.Warn("an error occurred with fsnotify.Remove", slog.String("path", e.Name), slog.String("error", er.Error()))
}
}
}
m.Events <- e

case e := <-m.fsnotify.Errors:
m.Errors <- e

case <-m.done:
m.fsnotify.Close()
if ec := m.fsnotify.Close(); ec != nil {
slog.Error("an error occurred with fsnotify.Close", slog.String("error", ec.Error()))
}
close(m.Events)
close(m.Errors)
return
Expand Down
178 changes: 178 additions & 0 deletions rfsnotify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package rfsnotify

import (
"fmt"
"os"
"path/filepath"
"sync"
)

func ExampleRWatcher_AddRecursive() {
// create test folder
rd := filepath.Join(os.TempDir(), "testRWatcher_recursive")
err := os.Mkdir(rd, 0o750)
if err != nil {
fmt.Printf("could not create root test folder, error: %s", err)
return
}
defer func() {
if e := os.RemoveAll(rd); e != nil {
fmt.Printf("could not remove test folder %s, error: %s", rd, e)
}
}()
if err = os.MkdirAll(filepath.Join(rd, "a/b/c"), 0o750); err != nil {
fmt.Printf("could not create test folders, error: %s", err)
return
}

// create new watcher
watcher, err := NewWatcher()
if err != nil {
fmt.Printf("could not create recursive watcher, error: %s", err)
return
}

defer func() {
if e := watcher.Close(); e != nil {
fmt.Printf("could not close recursive watcher, error: %s", e)
}
}()

// add test folder to be watched
err = watcher.AddRecursive(rd)
if err != nil {
fmt.Printf("could not add recursive folder %s, error: %s", rd, err)
return
}

// create new monitored folder
if err = os.MkdirAll(filepath.Join(rd, "a/b/c/d"), 0o750); err != nil {
fmt.Printf("could not create test folders, error: %s", err)
return
}

// dump folder creation event

<-watcher.Events

// add dummy event listener, real one must use a controlled loop
wg := sync.WaitGroup{}
wg.Go(func() {
event, ok := <-watcher.Events
if !ok {
fmt.Printf("ko event")
return
}
fmt.Printf("received event %s", event)
})

// create a test file
f, err := os.Create(filepath.Clean(filepath.Join(rd, "a/b/c/d", "test.txt")))
if err != nil {
fmt.Printf("could not create test file %s, error: %s", rd, err)
return
}
defer func() {
if e := f.Close(); e != nil {
fmt.Printf("could not close file %s, error: %s", f.Name(), e)
}
}()

// wait until dummy listener is informed of the file creation
wg.Wait()

// cleanup
if err = watcher.RemoveRecursive(rd); err != nil {
fmt.Printf("could not remove test folder %s from notify, error: %s", rd, err)
return
}

// output:
// received event CREATE "/tmp/testRWatcher_recursive/a/b/c/d/test.txt"
}

func ExampleRWatcher_Add() {
// create test folder
rd := filepath.Join(os.TempDir(), "testRWatcher_add")
err := os.Mkdir(rd, 0o750)
if err != nil {
fmt.Printf("could not create root test folder, error: %s", err)
return
}
defer func() {
if e := os.RemoveAll(rd); e != nil {
fmt.Printf("could not remove test folder %s, error: %s", rd, e)
}
}()
if err = os.MkdirAll(filepath.Join(rd, "a/b/c"), 0o750); err != nil {
fmt.Printf("could not create test folders, error: %s", err)
return
}

// create new watcher
watcher, err := NewWatcher()
if err != nil {
fmt.Printf("could not create recursive watcher, error: %s", err)
return
}

defer func() {
if e := watcher.Close(); e != nil {
fmt.Printf("could not close recursive watcher, error: %s", e)
}
}()

// add dummy event listener, real one must use a controlled loop
wg := sync.WaitGroup{}
wg.Go(func() {
event, ok := <-watcher.Events
if !ok {
fmt.Printf("ko event")
return
}
fmt.Printf("received event %s", event)
})

// add test folder to be watched
err = watcher.Add(rd)
if err != nil {
fmt.Printf("could not add recursive folder %s, error: %s", rd, err)
return
}

// create a test file that will not trigger notify
f, err := os.Create(filepath.Clean(filepath.Join(rd, "a/b/c", "test.txt")))
if err != nil {
fmt.Printf("could not create test file %s, error: %s", rd, err)
return
}
defer func() {
if e := f.Close(); e != nil {
fmt.Printf("could not close file %s, error: %s", f.Name(), e)
}
}()

// create a test file that will trigger notify
f2, err := os.Create(filepath.Clean(filepath.Join(rd, "test2.txt")))
if err != nil {
fmt.Printf("could not create test file2 %s, error: %s", rd, err)
return
}
defer func() {
if e := f2.Close(); e != nil {
fmt.Printf("could not close file %s, error: %s", f2.Name(), e)
}
}()

// wait until dummy listener is informed of the file creation
wg.Wait()

// cleanup
if err = watcher.Remove(rd); err != nil {
fmt.Printf("could not remove test folder %s from notify, error: %s", rd, err)
return
}

// output:
// received event CREATE "/tmp/testRWatcher_add/test2.txt"
}