Files
2025-04-09 01:00:12 +01:00

219 lines
3.9 KiB
Go

package gonotify
import (
"context"
"os"
"path/filepath"
"sync"
)
// DirWatcher recursively watches the given root folder, waiting for file events.
// Events can be masked by providing fileMask. DirWatcher does not generate events for
// folders or subfolders.
type DirWatcher struct {
C chan FileEvent
done chan struct{}
}
// NewDirWatcher creates DirWatcher recursively waiting for events in the given root folder and
// emitting FileEvents in channel C, that correspond to fileMask. Folder events are ignored (having IN_ISDIR set to 1)
func NewDirWatcher(ctx context.Context, fileMask uint32, root string) (*DirWatcher, error) {
dw := &DirWatcher{
C: make(chan FileEvent),
done: make(chan struct{}),
}
i, err := NewInotify(ctx)
if err != nil {
return nil, err
}
queue := make([]FileEvent, 0, 100)
err = filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
if err != nil {
return nil
}
if !f.IsDir() {
//fake event for existing files
queue = append(queue, FileEvent{
InotifyEvent: InotifyEvent{
Name: path,
Mask: IN_CREATE,
},
})
return nil
}
_, err = i.AddWatch(path, IN_ALL_EVENTS)
return err
})
if err != nil {
return nil, err
}
events := make(chan FileEvent)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for _, event := range queue {
select {
case <-ctx.Done():
close(events)
return
case events <- event:
}
}
queue = nil
for {
select {
case <-ctx.Done():
close(events)
return
default:
}
raw, err := i.Read()
if err != nil {
close(events)
return
}
select {
case <-ctx.Done():
close(events)
return
default:
}
for _, event := range raw {
// Skip ignored events queued from removed watchers
if event.Mask&IN_IGNORED == IN_IGNORED {
continue
}
// Add watch for folders created in watched folders (recursion)
if event.Mask&(IN_CREATE|IN_ISDIR) == IN_CREATE|IN_ISDIR {
// After the watch for subfolder is added, it may be already late to detect files
// created there right after subfolder creation, so we should generate such events
// ourselves:
filepath.Walk(event.Name, func(path string, f os.FileInfo, err error) error {
if err != nil {
return nil
}
if !f.IsDir() {
// fake event, but there can be duplicates of this event provided by real watcher
select {
case <-ctx.Done():
return nil
case events <- FileEvent{
InotifyEvent: InotifyEvent{
Name: path,
Mask: IN_CREATE,
},
}: //noop
}
}
return nil
})
// Wait for further files to be added
i.AddWatch(event.Name, IN_ALL_EVENTS)
continue
}
// Remove watch for deleted folders
if event.Mask&IN_DELETE_SELF == IN_DELETE_SELF {
i.RmWd(event.Wd)
continue
}
// Skip sub-folder events
if event.Mask&IN_ISDIR == IN_ISDIR {
continue
}
select {
case <-ctx.Done():
return
case events <- FileEvent{
InotifyEvent: event,
}: //noop
}
}
}
}()
wg.Add(1)
go func() {
defer wg.Done()
defer close(dw.C)
for {
select {
case <-ctx.Done():
// drain events
for {
select {
case _, ok := <-events:
if !ok {
return
}
default:
return
}
}
case event, ok := <-events:
if !ok {
select {
case <-ctx.Done():
case dw.C <- FileEvent{
Eof: true,
}:
}
return
}
// Skip events not conforming with provided mask
if event.Mask&fileMask == 0 {
continue
}
select {
case dw.C <- event:
case <-ctx.Done():
return
}
}
}
}()
go func() {
wg.Wait()
<-i.Done()
close(dw.done)
}()
return dw, nil
}
// Done returns a channel that is closed when DirWatcher is done
func (dw *DirWatcher) Done() <-chan struct{} {
return dw.done
}