You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

340 lines
6.3 KiB

package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"regexp"
"sync"
"syscall"
"github.com/fsnotify/fsnotify"
"github.com/ipsusila/opt"
"oslog.id/putu/jyoti/utility"
_ "github.com/mattn/go-sqlite3"
)
type watcherOptions struct {
watchPaths []string
maxLastFile int
pattern string
verbose bool
catchSignal bool
regPattern *regexp.Regexp
}
type node struct {
name string
fullName string
next *node
}
type list struct {
sync.Mutex
numItems int
head *node
tail *node
}
func (l *list) clear() {
l.head = nil
l.tail = nil
l.numItems = 0
}
func (l *list) count() int {
l.Lock()
defer l.Unlock()
return l.numItems
}
func (l *list) newNode(name string) (*node, error) {
fullName, err := filepath.Abs(name)
if err != nil {
return nil, err
}
nd := &node{
name: name,
fullName: fullName,
}
return nd, nil
}
func (l *list) push(name string) error {
l.Lock()
defer l.Unlock()
nd, err := l.newNode(name)
if err != nil {
return err
}
if l.tail == nil {
l.head = nd
l.tail = nd
} else {
l.tail.next = nd
l.tail = nd
}
l.numItems++
return nil
}
func (l *list) peek() *node {
l.Lock()
defer l.Unlock()
if l.head == nil {
return nil
}
return l.head
}
func (l *list) safePop() *node {
if l.head == nil {
return nil
}
nd := l.head
l.head = nd.next
if l.head == nil {
l.tail = nil
}
l.numItems--
return nd
}
func (l *list) pop() *node {
l.Lock()
defer l.Unlock()
return l.safePop()
}
func (l *list) remove(name string) *node {
l.Lock()
defer l.Unlock()
//create item
item, err := l.newNode(name)
if err != nil {
return nil
}
//empty
if l.head == nil || item == nil {
return nil
} else if l.head.fullName == item.fullName {
return l.safePop()
}
//node current and next
nd := l.head
next := l.head.next
for next != nil {
if next.fullName == item.fullName {
nextNext := next.next
nd.next = nextNext
l.numItems--
if next == l.tail {
l.tail = nd
}
return next
}
//next items
nd = next
next = nd.next
}
return nil
}
func (l *list) iterate(fn func(nd *node)) {
l.Lock()
defer l.Unlock()
nd := l.head
for nd != nil {
fn(nd)
nd = nd.next
}
}
func startWatching(options *watcherOptions) {
//file items
items := &list{}
errItems := &list{}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
done := make(chan bool, 1)
mustExit := make(chan bool, 1)
// Process events
go func() {
defer func() {
close(done)
}()
for {
select {
case ev := <-watcher.Events:
isCreate := ev.Op&fsnotify.Create == fsnotify.Create
isDelete := ev.Op&fsnotify.Remove == fsnotify.Remove
if isCreate {
//match pattern
//when new items created, delete old items
//if number of items exceed threshold
if options.regPattern == nil || options.regPattern.MatchString(ev.Name) {
items.push(ev.Name)
if options.verbose {
log.Printf("Item %v ADDED", ev.Name)
}
if items.count() > options.maxLastFile {
node := items.pop()
err := os.Remove(node.fullName)
if err != nil {
errItems.push(node.name)
log.Printf("Error %v while deleting %v", err, node.fullName)
}
}
}
} else if isDelete {
if options.verbose {
log.Printf("Item %v REMOVED", ev.Name)
}
//if manually deleted: remove from list
items.remove(ev.Name)
}
case err := <-watcher.Errors:
log.Println("Watcher error:", err)
case <-mustExit:
return
}
}
}()
//Start watching items
nwatched := 0
for _, sPath := range options.watchPaths {
err = watcher.Add(sPath)
if err != nil {
log.Printf("Error watching `%s` -> %v", sPath, err)
} else {
nwatched++
if options.verbose {
log.Printf("Watching %s...", sPath)
}
}
}
if nwatched == 0 {
close(mustExit)
} else {
//catch signal
if options.catchSignal {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
go func() {
for _ = range signalChan {
fmt.Println("Received an interrupt, stopping watcher...")
close(mustExit)
return
}
}()
} else {
//wait for q/q
buf := make([]byte, 1)
for {
//wait input
n, err := os.Stdin.Read(buf)
if err != nil {
log.Println(err)
} else if n > 0 && buf[0] == 'q' || buf[0] == 'Q' {
close(mustExit)
break
}
}
}
}
// Hang so program doesn't exit
<-done
watcher.Close()
//erritems
if errItems.count() > 0 {
log.Printf("The following item(s) were not removed do to error")
errItems.iterate(func(nd *node) {
log.Printf("%v", nd.fullName)
})
}
log.Printf("Watcher %d DONE", items.count())
}
func main() {
//parse configurationf file
cfgFile := flag.String("conf", "config.hjson", "Configuration file")
flag.Parse()
//load options
config, err := opt.FromFile(*cfgFile, opt.FormatAuto)
if err != nil {
log.Printf("Error while loading configuration file %v -> %v\n", *cfgFile, err)
return
}
//display server ID (displayed in console)
log.Printf("Application ID: %v\n", config.GetString("id", "undefined (check configuration)"))
//configure log writer
lw, err := utility.NewLogWriter(config)
if err != nil {
log.Printf("Error while setup logging: %v\n", err)
return
}
log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime)
log.SetOutput(lw)
defer lw.Close()
//options
/*
watchPaths []string
maxLastFile int
pattern string
verbose bool
catchSignal bool
*/
options := &watcherOptions{}
wcfg := config.Get("watch")
options.watchPaths = wcfg.GetStringArray("paths")
options.maxLastFile = wcfg.GetInt("maxFile", 50)
options.pattern = wcfg.GetString("pattern", "")
options.verbose = wcfg.GetBool("verbose", false)
options.catchSignal = wcfg.GetBool("catchSignal", false)
//verbose?
if options.verbose {
log.Printf("%s", config.AsJSON())
}
//compile regex
if len(options.pattern) > 0 {
options.regPattern = regexp.MustCompile(options.pattern)
}
startWatching(options)
}