GoDoxy/internal/watcher/directory_watcher.go

146 lines
2.8 KiB
Go

package watcher
import (
"context"
"errors"
"strings"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
E "github.com/yusing/go-proxy/internal/error"
F "github.com/yusing/go-proxy/internal/utils/functional"
"github.com/yusing/go-proxy/internal/watcher/events"
)
type dirWatcher struct {
dir string
w *fsnotify.Watcher
fwMap F.Map[string, *fileWatcher]
mu sync.Mutex
eventCh chan Event
errCh chan E.NestedError
ctx context.Context
}
func NewDirectoryWatcher(ctx context.Context, dirPath string) *dirWatcher {
//! subdirectories are not watched
w, err := fsnotify.NewWatcher()
if err != nil {
logrus.Panicf("unable to create fs watcher: %s", err)
}
if err = w.Add(dirPath); err != nil {
logrus.Panicf("unable to create fs watcher: %s", err)
}
helper := &dirWatcher{
dir: dirPath,
w: w,
fwMap: F.NewMapOf[string, *fileWatcher](),
eventCh: make(chan Event),
errCh: make(chan E.NestedError),
ctx: ctx,
}
go helper.start()
return helper
}
func (h *dirWatcher) Events(_ context.Context) (<-chan Event, <-chan E.NestedError) {
return h.eventCh, h.errCh
}
func (h *dirWatcher) Add(relPath string) *fileWatcher {
h.mu.Lock()
defer h.mu.Unlock()
// check if the watcher already exists
s, ok := h.fwMap.Load(relPath)
if ok {
return s
}
s = &fileWatcher{
relPath: relPath,
eventCh: make(chan Event),
errCh: make(chan E.NestedError),
}
go func() {
defer func() {
close(s.eventCh)
close(s.errCh)
}()
for {
select {
case <-h.ctx.Done():
return
case _, ok := <-h.eventCh:
if !ok { // directory watcher closed
return
}
}
}
}()
h.fwMap.Store(relPath, s)
return s
}
func (h *dirWatcher) start() {
defer close(h.eventCh)
defer h.w.Close()
for {
select {
case <-h.ctx.Done():
return
case fsEvent, ok := <-h.w.Events:
if !ok {
return
}
// retrieve the watcher
relPath := strings.TrimPrefix(fsEvent.Name, h.dir)
relPath = strings.TrimPrefix(relPath, "/")
msg := Event{
Type: events.EventTypeFile,
ActorName: relPath,
}
switch {
case fsEvent.Has(fsnotify.Write):
msg.Action = events.ActionFileWritten
case fsEvent.Has(fsnotify.Create):
msg.Action = events.ActionFileCreated
case fsEvent.Has(fsnotify.Remove):
msg.Action = events.ActionFileDeleted
case fsEvent.Has(fsnotify.Rename):
msg.Action = events.ActionFileRenamed
default: // ignore other events
continue
}
// send event to directory watcher
select {
case h.eventCh <- msg:
default:
}
// send event to file watcher too
w, ok := h.fwMap.Load(relPath)
if ok {
select {
case w.eventCh <- msg:
default:
}
}
case err := <-h.w.Errors:
if errors.Is(err, fsnotify.ErrClosed) {
// closed manually?
return
}
select {
case h.errCh <- E.From(err):
default:
}
}
}
}