package scanner import ( "context" "log" "os" "path/filepath" "strings" "time" ) type ChangeType int const ( Unchanged ChangeType = iota Created Modified Deleted ) type ChangeHandler interface { HandleCreate(path string) error HandleModify(path string) error HandleDelete(path string) error } type Change struct { Type ChangeType Path string ModTime time.Time } type ScannerService struct { RootDir string interval time.Duration lastStates map[string]time.Time handler ChangeHandler } func NewScanner(path string) *ScannerService { return &ScannerService{RootDir: path} } func (s *ScannerService) FindAll() ([]string, error) { var notePath []string err := filepath.Walk(s.RootDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // skip the root dir itself if s.RootDir == path { return nil } if !isValidNoteFile(path, info) { return nil } notePath = append(notePath, path) return nil }) return notePath, err } func (s *ScannerService) Scan() ([]Change, error) { var changes []Change currentStates := make(map[string]time.Time) // Walk filesystem filepath.Walk(s.RootDir, func(path string, info os.FileInfo, err error) error { // skip the root dir itself if s.RootDir == path { return nil } if !isValidNoteFile(path, info) { return nil } currentStates[path] = info.ModTime() lastMod, existed := s.lastStates[path] if !existed { changes = append(changes, Change{Type: Created, Path: path, ModTime: lastMod}) } else if info.ModTime().After(lastMod) { changes = append(changes, Change{Type: Modified, Path: path, ModTime: info.ModTime()}) } return nil }) // Check for deletions for path := range s.lastStates { if _, exists := currentStates[path]; !exists { changes = append(changes, Change{Type: Deleted, Path: path}) } } s.lastStates = currentStates return changes, nil } func (s *ScannerService) Monitor(ctx context.Context) error { ticker := time.NewTicker(s.interval) defer ticker.Stop() for { select { case <-ticker.C: changes, err := s.Scan() if err != nil { log.Printf("scan error: %v", err) continue } for _, change := range changes { var err error switch change.Type { case Created: err = s.handler.HandleCreate(change.Path) case Modified: err = s.handler.HandleModify(change.Path) case Deleted: err = s.handler.HandleDelete(change.Path) } if err != nil { log.Printf("handler error for %s: %v", change.Path, err) } } case <-ctx.Done(): return ctx.Err() } } } func isValidNoteFile(path string, info os.FileInfo) bool { // ignore temp and backup files for _, nono := range []string{".#", "~"} { if strings.Contains(path, nono) { return false } } // ignore files that are not markdown if info.IsDir() || filepath.Ext(path) != ".md" { return false } // ignore empty folder if info.IsDir() { files, err := os.ReadDir(path) if err != nil { log.Println(err.Error()) } if len(files) == 0 { return false } } return true }