mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-12 15:28:00 -05:00
323 lines
6.4 KiB
Go
323 lines
6.4 KiB
Go
package filenotify
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
)
|
|
|
|
var (
|
|
// errPollerClosed is returned when the poller is closed
|
|
errPollerClosed = errors.New("poller is closed")
|
|
// errNoSuchWatch is returned when trying to remove a watch that doesn't exist
|
|
errNoSuchWatch = errors.New("watch does not exist")
|
|
)
|
|
|
|
// filePoller is used to poll files for changes, especially in cases where fsnotify
|
|
// can't be run (e.g. when inotify handles are exhausted)
|
|
// filePoller satisfies the FileWatcher interface
|
|
type filePoller struct {
|
|
// the duration between polls.
|
|
interval time.Duration
|
|
// watches is the list of files currently being polled, close the associated channel to stop the watch
|
|
watches map[string]struct{}
|
|
// Will be closed when done.
|
|
done chan struct{}
|
|
// events is the channel to listen to for watch events
|
|
events chan fsnotify.Event
|
|
// errors is the channel to listen to for watch errors
|
|
errors chan error
|
|
// mu locks the poller for modification
|
|
mu sync.Mutex
|
|
// closed is used to specify when the poller has already closed
|
|
closed bool
|
|
}
|
|
|
|
// Add adds a filename to the list of watches
|
|
// once added the file is polled for changes in a separate goroutine
|
|
func (w *filePoller) Add(name string) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if w.closed {
|
|
return errPollerClosed
|
|
}
|
|
|
|
item, err := newItemToWatch(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if item.left.FileInfo == nil {
|
|
return os.ErrNotExist
|
|
}
|
|
|
|
if w.watches == nil {
|
|
w.watches = make(map[string]struct{})
|
|
}
|
|
if _, exists := w.watches[name]; exists {
|
|
return fmt.Errorf("watch exists")
|
|
}
|
|
w.watches[name] = struct{}{}
|
|
|
|
go w.watch(item)
|
|
return nil
|
|
}
|
|
|
|
// Remove stops and removes watch with the specified name
|
|
func (w *filePoller) Remove(name string) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.remove(name)
|
|
}
|
|
|
|
func (w *filePoller) remove(name string) error {
|
|
if w.closed {
|
|
return errPollerClosed
|
|
}
|
|
|
|
_, exists := w.watches[name]
|
|
if !exists {
|
|
return errNoSuchWatch
|
|
}
|
|
delete(w.watches, name)
|
|
return nil
|
|
}
|
|
|
|
// Events returns the event channel
|
|
// This is used for notifications on events about watched files
|
|
func (w *filePoller) Events() <-chan fsnotify.Event {
|
|
return w.events
|
|
}
|
|
|
|
// Errors returns the errors channel
|
|
// This is used for notifications about errors on watched files
|
|
func (w *filePoller) Errors() <-chan error {
|
|
return w.errors
|
|
}
|
|
|
|
// Close closes the poller
|
|
// All watches are stopped, removed, and the poller cannot be added to
|
|
func (w *filePoller) Close() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if w.closed {
|
|
return nil
|
|
}
|
|
w.closed = true
|
|
close(w.done)
|
|
for name := range w.watches {
|
|
w.remove(name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendEvent publishes the specified event to the events channel
|
|
func (w *filePoller) sendEvent(e fsnotify.Event) error {
|
|
select {
|
|
case w.events <- e:
|
|
case <-w.done:
|
|
return fmt.Errorf("closed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sendErr publishes the specified error to the errors channel
|
|
func (w *filePoller) sendErr(e error) error {
|
|
select {
|
|
case w.errors <- e:
|
|
case <-w.done:
|
|
return fmt.Errorf("closed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// watch watches item for changes until done is closed.
|
|
func (w *filePoller) watch(item *itemToWatch) {
|
|
ticker := time.NewTicker(w.interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
case <-w.done:
|
|
return
|
|
}
|
|
|
|
evs, err := item.checkForChanges()
|
|
if err != nil {
|
|
if err := w.sendErr(err); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
item.left, item.right = item.right, item.left
|
|
|
|
for _, ev := range evs {
|
|
if err := w.sendEvent(ev); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// recording records the state of a file or a dir.
|
|
type recording struct {
|
|
os.FileInfo
|
|
|
|
// Set if FileInfo is a dir.
|
|
entries map[string]os.FileInfo
|
|
}
|
|
|
|
func (r *recording) clear() {
|
|
r.FileInfo = nil
|
|
if r.entries != nil {
|
|
for k := range r.entries {
|
|
delete(r.entries, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *recording) record(filename string) error {
|
|
r.clear()
|
|
|
|
fi, err := os.Stat(filename)
|
|
if err != nil && !herrors.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
if fi == nil {
|
|
return nil
|
|
}
|
|
|
|
r.FileInfo = fi
|
|
|
|
// If fi is a dir, we watch the files inside that directory (not recursively).
|
|
// This matches the behavior of fsnotity.
|
|
if fi.IsDir() {
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
if herrors.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
fis, err := f.Readdir(-1)
|
|
if err != nil {
|
|
if herrors.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
for _, fi := range fis {
|
|
r.entries[fi.Name()] = fi
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// itemToWatch may be a file or a dir.
|
|
type itemToWatch struct {
|
|
// Full path to the filename.
|
|
filename string
|
|
|
|
// Snapshots of the stat state of this file or dir.
|
|
left *recording
|
|
right *recording
|
|
}
|
|
|
|
func newItemToWatch(filename string) (*itemToWatch, error) {
|
|
r := &recording{
|
|
entries: make(map[string]os.FileInfo),
|
|
}
|
|
err := r.record(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &itemToWatch{filename: filename, left: r}, nil
|
|
}
|
|
|
|
func (item *itemToWatch) checkForChanges() ([]fsnotify.Event, error) {
|
|
if item.right == nil {
|
|
item.right = &recording{
|
|
entries: make(map[string]os.FileInfo),
|
|
}
|
|
}
|
|
|
|
err := item.right.record(item.filename)
|
|
if err != nil && !herrors.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
dirOp := checkChange(item.left.FileInfo, item.right.FileInfo)
|
|
|
|
if dirOp != 0 {
|
|
evs := []fsnotify.Event{{Op: dirOp, Name: item.filename}}
|
|
return evs, nil
|
|
}
|
|
|
|
if item.left.FileInfo == nil || !item.left.IsDir() {
|
|
// Done.
|
|
return nil, nil
|
|
}
|
|
|
|
leftIsIn := false
|
|
left, right := item.left.entries, item.right.entries
|
|
if len(right) > len(left) {
|
|
left, right = right, left
|
|
leftIsIn = true
|
|
}
|
|
|
|
var evs []fsnotify.Event
|
|
|
|
for name, fi1 := range left {
|
|
fi2 := right[name]
|
|
fil, fir := fi1, fi2
|
|
if leftIsIn {
|
|
fil, fir = fir, fil
|
|
}
|
|
op := checkChange(fil, fir)
|
|
if op != 0 {
|
|
evs = append(evs, fsnotify.Event{Op: op, Name: filepath.Join(item.filename, name)})
|
|
}
|
|
|
|
}
|
|
|
|
return evs, nil
|
|
}
|
|
|
|
func checkChange(fi1, fi2 os.FileInfo) fsnotify.Op {
|
|
if fi1 == nil && fi2 != nil {
|
|
return fsnotify.Create
|
|
}
|
|
if fi1 != nil && fi2 == nil {
|
|
return fsnotify.Remove
|
|
}
|
|
if fi1 == nil && fi2 == nil {
|
|
return 0
|
|
}
|
|
if fi1.IsDir() || fi2.IsDir() {
|
|
return 0
|
|
}
|
|
if fi1.Mode() != fi2.Mode() {
|
|
return fsnotify.Chmod
|
|
}
|
|
if fi1.ModTime() != fi2.ModTime() || fi1.Size() != fi2.Size() {
|
|
return fsnotify.Write
|
|
}
|
|
|
|
return 0
|
|
}
|