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.
341 lines
6.3 KiB
341 lines
6.3 KiB
6 years ago
|
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)
|
||
|
}
|