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
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) |
|
}
|
|
|