212 lines
4.9 KiB
Go
212 lines
4.9 KiB
Go
package filehelper
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
|
"github.com/fsnotify/fsnotify"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
debounceWriteLog map[string]*FsNotifyLogEntry
|
|
|
|
debounceWriteLogMutex = sync.Mutex{}
|
|
)
|
|
|
|
func init() {
|
|
debounceWriteLog = make(map[string]*FsNotifyLogEntry)
|
|
}
|
|
|
|
type FsNotifyLogEntry struct {
|
|
callbackWrapper *time.Timer
|
|
callbackComplete bool
|
|
}
|
|
|
|
const (
|
|
debounceDelay = 300 * time.Millisecond
|
|
)
|
|
|
|
// WatchMeta attaches action context to watcher setup failures so Diagnostics
|
|
// can apply the same action view ACL as other configuration issues.
|
|
type WatchMeta struct {
|
|
ActionID string
|
|
ActionTitle string
|
|
ConfigFile string
|
|
}
|
|
|
|
type watchContext struct {
|
|
callback func(filename string)
|
|
event *fsnotify.Event
|
|
meta WatchMeta
|
|
filename string
|
|
filedir string
|
|
interestedEvent fsnotify.Op
|
|
}
|
|
|
|
func WatchDirectoryCreate(fullpath string, callback func(filename string), meta WatchMeta) {
|
|
watchPath(&watchContext{
|
|
filedir: fullpath,
|
|
filename: "",
|
|
callback: callback,
|
|
interestedEvent: fsnotify.Create,
|
|
meta: meta,
|
|
})
|
|
}
|
|
|
|
func WatchDirectoryWrite(fullpath string, callback func(filename string), meta WatchMeta) {
|
|
watchPath(&watchContext{
|
|
filedir: fullpath,
|
|
filename: "",
|
|
callback: callback,
|
|
interestedEvent: fsnotify.Write,
|
|
meta: meta,
|
|
})
|
|
}
|
|
|
|
func WatchFileWrite(fullpath string, callback func(filename string), meta WatchMeta) {
|
|
filename := filepath.Base(fullpath)
|
|
filedir := filepath.Dir(fullpath)
|
|
|
|
watchPath(&watchContext{
|
|
filedir: filedir,
|
|
filename: filename,
|
|
callback: callback,
|
|
interestedEvent: fsnotify.Write,
|
|
meta: meta,
|
|
})
|
|
}
|
|
|
|
func watchPath(ctx *watchContext) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
reportWatcherFailure(ctx, err)
|
|
return
|
|
}
|
|
|
|
defer closeWatcher(watcher)
|
|
|
|
if err := watcher.Add(ctx.filedir); err != nil {
|
|
reportWatcherFailure(ctx, err)
|
|
return
|
|
}
|
|
|
|
for processEvent(ctx, watcher) {
|
|
}
|
|
}
|
|
|
|
func closeWatcher(watcher *fsnotify.Watcher) {
|
|
if err := watcher.Close(); err != nil {
|
|
log.Errorf("Failed to close file watcher: %v", err)
|
|
}
|
|
}
|
|
|
|
func watchTarget(ctx *watchContext) string {
|
|
if ctx.filename == "" {
|
|
return ctx.filedir
|
|
}
|
|
return filepath.Join(ctx.filedir, ctx.filename)
|
|
}
|
|
|
|
func reportWatcherFailure(ctx *watchContext, err error) {
|
|
path := watchTarget(ctx)
|
|
message := fmt.Sprintf("Could not create watcher for %q: %v", path, err)
|
|
configissues.Report(configissues.Issue{
|
|
Severity: configissues.SeverityError,
|
|
Code: configissues.CodeWatcherPath,
|
|
Message: message,
|
|
ActionID: ctx.meta.ActionID,
|
|
ActionTitle: ctx.meta.ActionTitle,
|
|
Source: path,
|
|
ConfigFile: ctx.meta.ConfigFile,
|
|
})
|
|
}
|
|
|
|
// processEvent waits for one watcher event. It returns false when the watcher
|
|
// channels are closed so the caller can stop looping.
|
|
func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) bool {
|
|
select {
|
|
case event, ok := <-watcher.Events:
|
|
return handleWatcherEvent(ctx, event, ok)
|
|
case err, ok := <-watcher.Errors:
|
|
return handleWatcherError(ctx, err, ok)
|
|
}
|
|
}
|
|
|
|
func handleWatcherEvent(ctx *watchContext, event fsnotify.Event, ok bool) bool {
|
|
if !ok {
|
|
return false
|
|
}
|
|
ctx.event = &event
|
|
consumeEvent(ctx)
|
|
return true
|
|
}
|
|
|
|
func handleWatcherError(ctx *watchContext, err error, ok bool) bool {
|
|
if !ok {
|
|
return false
|
|
}
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"path": watchTarget(ctx),
|
|
}).Errorf("Error in fsnotify: %v", err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func consumeEvent(ctx *watchContext) {
|
|
if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename {
|
|
log.Tracef("fsnotify irreleventa event different file %+v", ctx.event)
|
|
return
|
|
}
|
|
|
|
consumeRelevantEvents(ctx)
|
|
}
|
|
|
|
func consumeRelevantEvents(ctx *watchContext) {
|
|
if ctx.event.Has(ctx.interestedEvent) {
|
|
log.Debugf("fsnotify event relevant: %v", ctx.event)
|
|
|
|
processDebounce(ctx)
|
|
} else {
|
|
log.Debugf("fsnotify event irrelevant: %v", ctx.event)
|
|
}
|
|
}
|
|
|
|
func processDebounce(ctx *watchContext) {
|
|
debounceWriteLogMutex.Lock()
|
|
|
|
logEntry, found := debounceWriteLog[ctx.filename]
|
|
|
|
if !found {
|
|
logEntry = &FsNotifyLogEntry{
|
|
callbackComplete: false,
|
|
callbackWrapper: nil,
|
|
}
|
|
|
|
debounceWriteLog[ctx.filename] = logEntry
|
|
}
|
|
|
|
log.Debugf("fsnotify event %+v", logEntry)
|
|
|
|
if logEntry.callbackComplete || logEntry.callbackWrapper == nil {
|
|
log.Debugf("fsnotify event callback queued within debounce delay: %v", ctx.filename)
|
|
|
|
logEntry.callbackComplete = false
|
|
logEntry.callbackWrapper = time.AfterFunc(debounceDelay, func() {
|
|
log.Debugf("fsnotify event callback being fired: %v", ctx.filename)
|
|
|
|
ctx.callback(ctx.event.Name)
|
|
|
|
logEntry.callbackComplete = true
|
|
})
|
|
} else {
|
|
log.Debugf("fsnotify event suppressed because it's within the debounce delay: %v", ctx.filename)
|
|
}
|
|
|
|
debounceWriteLogMutex.Unlock()
|
|
}
|