server auth
This commit is contained in:
@@ -7,15 +7,19 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
const fileWatcherDebounce = 500 * time.Millisecond
|
||||
|
||||
type FileWatcher struct {
|
||||
watcher *fsnotify.Watcher
|
||||
rootDir string
|
||||
onChange func(path string)
|
||||
logger *slog.Logger
|
||||
debounce time.Duration
|
||||
}
|
||||
|
||||
func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) {
|
||||
@@ -25,9 +29,10 @@ func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) {
|
||||
}
|
||||
|
||||
fw := &FileWatcher{
|
||||
watcher: watcher,
|
||||
rootDir: rootDir,
|
||||
logger: logger,
|
||||
watcher: watcher,
|
||||
rootDir: rootDir,
|
||||
logger: logger,
|
||||
debounce: fileWatcherDebounce,
|
||||
}
|
||||
|
||||
if err := fw.addWatches(); err != nil {
|
||||
@@ -39,19 +44,150 @@ func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) {
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) addWatches() error {
|
||||
return filepath.Walk(fw.rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
return filepath.WalkDir(fw.rootDir, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
if err := fw.watcher.Add(path); err != nil {
|
||||
fw.logger.Warn("watch directory", "path", path, "error", err)
|
||||
}
|
||||
if !entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if path != fw.rootDir && fw.shouldIgnorePath(path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if err := fw.watcher.Add(path); err != nil {
|
||||
fw.logger.Warn("watch directory", "path", path, "error", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) addWatchIfDirectory(path string) {
|
||||
if fw.shouldIgnorePath(path) {
|
||||
return
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
if err := fw.watcher.Add(path); err != nil {
|
||||
fw.logger.Warn("watch new directory", "path", path, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) flushPending(pending map[string]struct{}) {
|
||||
if len(pending) == 0 || fw.onChange == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var changedPath string
|
||||
for path := range pending {
|
||||
changedPath = path
|
||||
break
|
||||
}
|
||||
clear(pending)
|
||||
fw.onChange(changedPath)
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) scheduleSync(path string, pending map[string]struct{}, timer **time.Timer, timerC *<-chan time.Time) {
|
||||
pending[path] = struct{}{}
|
||||
|
||||
if *timer == nil {
|
||||
*timer = time.NewTimer(fw.debounce)
|
||||
*timerC = (*timer).C
|
||||
return
|
||||
}
|
||||
|
||||
if !(*timer).Stop() {
|
||||
select {
|
||||
case <-(*timer).C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
(*timer).Reset(fw.debounce)
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) stopTimer(timer *time.Timer) {
|
||||
if timer == nil {
|
||||
return
|
||||
}
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) isMarkdownPath(path string) bool {
|
||||
return strings.EqualFold(filepath.Ext(path), ".md")
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) shouldIgnorePath(path string) bool {
|
||||
rel, err := filepath.Rel(fw.rootDir, path)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if rel == "." {
|
||||
return false
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, part := range strings.Split(rel, string(filepath.Separator)) {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(part, ".") {
|
||||
return true
|
||||
}
|
||||
if isTempFileName(part) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTempFileName(name string) bool {
|
||||
if strings.HasSuffix(name, "~") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") {
|
||||
return true
|
||||
}
|
||||
|
||||
switch strings.ToLower(filepath.Ext(name)) {
|
||||
case ".swp", ".swo", ".tmp", ".temp", ".bak", ".orig", ".part", ".crdownload":
|
||||
return true
|
||||
}
|
||||
|
||||
switch name {
|
||||
case ".DS_Store", "Thumbs.db":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) isProcessableEvent(event fsnotify.Event) bool {
|
||||
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
|
||||
return false
|
||||
}
|
||||
if fw.shouldIgnorePath(event.Name) {
|
||||
return false
|
||||
}
|
||||
return fw.isMarkdownPath(event.Name)
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) handleEvent(event fsnotify.Event, pending map[string]struct{}, timer **time.Timer, timerC *<-chan time.Time) {
|
||||
if event.Op&(fsnotify.Create|fsnotify.Rename) != 0 {
|
||||
fw.addWatchIfDirectory(event.Name)
|
||||
}
|
||||
if fw.isProcessableEvent(event) {
|
||||
fw.scheduleSync(event.Name, pending, timer, timerC)
|
||||
}
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) SetOnChange(fn func(path string)) {
|
||||
fw.onChange = fn
|
||||
}
|
||||
@@ -59,24 +195,24 @@ func (fw *FileWatcher) SetOnChange(fn func(path string)) {
|
||||
func (fw *FileWatcher) Run(ctx context.Context) {
|
||||
defer fw.watcher.Close()
|
||||
|
||||
pending := make(map[string]struct{})
|
||||
var timer *time.Timer
|
||||
var timerC <-chan time.Time
|
||||
defer fw.stopTimer(timer)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timerC:
|
||||
fw.flushPending(pending)
|
||||
timer = nil
|
||||
timerC = nil
|
||||
case event, ok := <-fw.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if fw.shouldProcess(event) {
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
|
||||
fw.watcher.Add(event.Name)
|
||||
}
|
||||
}
|
||||
if fw.onChange != nil {
|
||||
fw.onChange(event.Name)
|
||||
}
|
||||
}
|
||||
fw.handleEvent(event, pending, &timer, &timerC)
|
||||
case err, ok := <-fw.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
@@ -85,17 +221,3 @@ func (fw *FileWatcher) Run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fw *FileWatcher) shouldProcess(event fsnotify.Event) bool {
|
||||
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
|
||||
return false
|
||||
}
|
||||
name := filepath.Base(event.Name)
|
||||
if strings.HasPrefix(name, ".") || strings.HasSuffix(name, "~") {
|
||||
return false
|
||||
}
|
||||
if filepath.Ext(event.Name) == ".md" || event.Op&fsnotify.Remove != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user