diff --git a/README.md b/README.md index 01cc106..759c396 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ # inotifywaitgo + Binding for inotifywait in golang, Fetch any directory event in your linux server easily. Fsnotify alternative + +- Works with mounted volumes in Docker linux containers + +Author: pablodz + + +## Example + +```go +package main + +import ( + "log" + + "github.com/pablodz/inotifywaitgo/inotifywaitgo" +) + +func main() { + + dir := "./" + files := make(chan []byte) + errors := make(chan []byte) + + go inotifywaitgo.WatchPath(&inotifywaitgo.Settings{ + Dir: dir, + OutFiles: files, + ErrorChan: errors, + Options: &inotifywaitgo.OptionsInotify{ + Recursive: true, + Events: []string{inotifywaitgo.EventCloseWrite}, + Monitor: true, + }, + Verbose: true, + }) + + log.Println("Watching for changes in", dir) + log.Println("Press Ctrl+C to stop") + +loopFiles: + for { + select { + case file := <-files: + println(string(file)) + case err := <-errors: + println(string(err)) + break loopFiles + } + } +} +``` \ No newline at end of file diff --git a/example/watcher.go b/example/watcher.go new file mode 100644 index 0000000..5a2980c --- /dev/null +++ b/example/watcher.go @@ -0,0 +1,33 @@ +package example + +import "github.com/pablodz/inotifywaitgo/inotifywaitgo" + +func Example() { + + dir := "./safasfsas" + files := make(chan []byte) + errors := make(chan []byte) + + go inotifywaitgo.WatchPath(&inotifywaitgo.Settings{ + Dir: dir, + OutFiles: files, + ErrorChan: errors, + Options: &inotifywaitgo.OptionsInotify{ + Recursive: true, + Events: []string{inotifywaitgo.EventCloseWrite}, + Monitor: true, + }, + Verbose: true, + }) + +loopFiles: + for { + select { + case file := <-files: + println(string(file)) + case err := <-errors: + println(string(err)) + break loopFiles + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ab13d78 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/pablodz/inotifywaitgo + +go 1.19 diff --git a/inotifywaitgo/check.go b/inotifywaitgo/check.go new file mode 100644 index 0000000..0580ef3 --- /dev/null +++ b/inotifywaitgo/check.go @@ -0,0 +1,28 @@ +package inotifywaitgo + +import ( + "bufio" + "os/exec" +) + +// Function to checkDependencies if inotifywait is installed +func checkDependencies() (bool, error) { + cmd := exec.Command("bash", "-c", "which inotifywait") + stdout, err := cmd.StdoutPipe() + if err != nil { + return false, err + } + if err := cmd.Start(); err != nil { + return false, err + } + + // Read the output of inotifywait and split it into lines + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + if line != "" { + return true, nil + } + } + return false, nil +} diff --git a/inotifywaitgo/command.go b/inotifywaitgo/command.go new file mode 100644 index 0000000..bc6d36f --- /dev/null +++ b/inotifywaitgo/command.go @@ -0,0 +1,65 @@ +package inotifywaitgo + +import ( + "errors" + "fmt" + "strings" +) + +func GenerateBashCommands(s *Settings) ([]string, error) { + + if s.Options == nil { + return nil, errors.New(OPT_NIL) + } + + if s.Dir == "" { + return nil, errors.New(DIR_EMPTY) + } + + baseCmd := []string{ + "bash", "-c", "inotifywait", + } + + if s.Options.Monitor { + baseCmd = append(baseCmd, "-m") + } + + if s.Options.Recursive { + baseCmd = append(baseCmd, "-r") + } + + baseCmd = append(baseCmd, s.Dir) + + if len(s.Options.Events) > 0 { + baseCmd = append(baseCmd, "-e") + for _, event := range s.Options.Events { + // if event not in VALID_EVENTS + if !contains(VALID_EVENTS, event) { + return nil, errors.New(INVALID_EVENT) + } + baseCmd = append(baseCmd, event) + } + } + if s.Verbose { + fmt.Println("baseCmd:", baseCmd) + } + + // join baseCmd from third to last element + var outCmd []string + outCmd = append(outCmd, baseCmd[0]) + outCmd = append(outCmd, baseCmd[1]) + outCmd = append(outCmd, strings.Join(baseCmd[2:], " ")) + + return outCmd, nil + +} + +// function that checks if a string is in a slice of strings +func contains(slice []string, s string) bool { + for _, v := range slice { + if v == s { + return true + } + } + return false +} diff --git a/inotifywaitgo/killer.go b/inotifywaitgo/killer.go new file mode 100644 index 0000000..d81ff0e --- /dev/null +++ b/inotifywaitgo/killer.go @@ -0,0 +1,8 @@ +package inotifywaitgo + +import "os/exec" + +func killOthers() error { + cmd := exec.Command("bash", "-c", "pkill inotifywait").Run() + return cmd +} diff --git a/inotifywaitgo/models.go b/inotifywaitgo/models.go new file mode 100644 index 0000000..de84abe --- /dev/null +++ b/inotifywaitgo/models.go @@ -0,0 +1,88 @@ +package inotifywaitgo + +type Settings struct { + // Directory to watch + Dir string + // Channel to send the file name to + OutFiles chan []byte + // Channel to send errors to + ErrorChan chan []byte + // Options for inotifywait + Options *OptionsInotify + // Kill other inotifywait processes + KillOthers bool + // verbose + Verbose bool +} + +type OptionsInotify struct { + // Watch the specified file or directory. If this option is not specified, inotifywait will watch the current working directory. + Events []string + // Print the name of the file that triggered the event. + Format string + // Watch all subdirectories of any directories passed as arguments. Watches will be set up recursively to an unlimited depth. Symbolic links are not traversed. Newly created subdirectories will also be watched. + Recursive bool + // Set a time format string as accepted by strftime(3) for use with the `%T' conversion in the --format option. + TimeFmt string + // Instead of exiting after receiving a single event, execute indefinitely. The default behaviour is to exit after the first event occurs. + Monitor bool +} + +const ( + // A watched file or a file within a watched directory was read from. + EventAccess = "access" + //A watched file or a file within a watched directory was written to. + EventModify = "modify" + // The metadata of a watched file or a file within a watched directory was modified. This includes timestamps, file permissions, extended attributes etc. + EventAttrib = "attrib" + // A watched file or a file within a watched directory was closed, after being opened in writable mode. This does not necessarily imply the file was written to. + EventCloseWrite = "close_write" + // A watched file or a file within a watched directory was closed, after being opened in read-only mode. + EventCloseNowrite = "close_nowrite" + // A watched file or a file within a watched directory was closed, regardless of how it was opened. Note that this is actually implemented simply by listening for both close_write and close_nowrite, hence all close events received will be output as one of these, not CLOSE. + EventClose = "close" + // A watched file or a file within a watched directory was opened. + EventOpen = "open" + // A watched file or a file within a watched directory was moved to the watched directory. + EventMovedTo = "moved_to" + // A watched file or a file within a watched directory was moved from the watched directory. + EventMovedFrom = "moved_from" + // A watched file or a file within a watched directory was moved to or from the watched directory. This is equivalent to listening for both moved_from and moved_to. + EventMove = "move" + // A watched file or directory was moved. After this event, the file or directory is no longer being watched. + EventMoveSelf = "move_self" + // A file or directory was created within a watched directory. + EventCreate = "create" + // A watched file or a file within a watched directory was deleted. + EventDelete = "delete" + // A watched file or directory was deleted. After this event the file or directory is no longer being watched. Note that this event can occur even if it is not explicitly being listened for. + EventDeleteSelf = "delete_self" + // The filesystem on which a watched file or directory resides was unmounted. After this event the file or directory is no longer being watched. Note that this event can occur even if it is not explicitly being listened to. + EventUnmount = "unmount" +) + +var VALID_EVENTS = []string{ + EventAccess, + EventModify, + EventAttrib, + EventCloseWrite, + EventCloseNowrite, + EventClose, + EventOpen, + EventMovedTo, + EventMovedFrom, + EventMove, + EventMoveSelf, + EventCreate, + EventDelete, + EventDeleteSelf, + EventUnmount, +} + +/* ERRORS */ +const NOT_INSTALLED = "inotifywait is not installed" +const OPT_NIL = "optionsInotify is nil" +const DIR_EMPTY = "directory is empty" +const INVALID_EVENT = "invalid event" +const INVALID_OUTPUT = "invalid output" +const DIR_NOT_EXISTS = "directory does not exists" diff --git a/inotifywaitgo/watcher.go b/inotifywaitgo/watcher.go new file mode 100644 index 0000000..9b58c70 --- /dev/null +++ b/inotifywaitgo/watcher.go @@ -0,0 +1,67 @@ +package inotifywaitgo + +import ( + "bufio" + "os" + "os/exec" + "strings" +) + +// Function that starts watching a path for new files and returns the file name (abspath) when a new file is finished writing +func WatchPath(s *Settings) { + + // Check if inotifywait is installed + ok, err := checkDependencies() + if !ok || err != nil { + s.ErrorChan <- []byte(NOT_INSTALLED) + return + } + + // check if dir exists + _, err = os.Stat(s.Dir) + if os.IsNotExist(err) { + s.ErrorChan <- []byte(DIR_NOT_EXISTS) + return + } + + // Stop any existing inotifywait processes + if s.KillOthers { + killOthers() + } + + // Generate bash command + cmdString, err := GenerateBashCommands(s) + if err != nil { + s.ErrorChan <- []byte(err.Error()) + return + } + + // Start inotifywait in the input directory and watch for close_write events + cmd := exec.Command(cmdString[0], cmdString[1:]...) + stdout, err := cmd.StdoutPipe() + if err != nil { + s.ErrorChan <- []byte(err.Error()) + return + } + if err := cmd.Start(); err != nil { + s.ErrorChan <- []byte(err.Error()) + return + } + + // Read the output of inotifywait and split it into lines + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, " ") + if len(parts) < 2 { + s.ErrorChan <- []byte(INVALID_OUTPUT) + continue + } + + // Extract the input file name from the inotifywait output + prefix := parts[0] + file := parts[len(parts)-1] + // Send the file name to the channel + s.OutFiles <- []byte(prefix + file) + } +} diff --git a/inotifywaitgo/watcher_test.go b/inotifywaitgo/watcher_test.go new file mode 100644 index 0000000..ce4bb6e --- /dev/null +++ b/inotifywaitgo/watcher_test.go @@ -0,0 +1,3 @@ +package inotifywaitgo + +// TODO: Do test diff --git a/main.go b/main.go new file mode 100644 index 0000000..c35d184 --- /dev/null +++ b/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "log" + + "github.com/pablodz/inotifywaitgo/inotifywaitgo" +) + +func main() { + + dir := "./" + files := make(chan []byte) + errors := make(chan []byte) + + go inotifywaitgo.WatchPath(&inotifywaitgo.Settings{ + Dir: dir, + OutFiles: files, + ErrorChan: errors, + Options: &inotifywaitgo.OptionsInotify{ + Recursive: true, + Events: []string{inotifywaitgo.EventCloseWrite}, + Monitor: true, + }, + Verbose: true, + }) + + log.Println("Watching for changes in", dir) + log.Println("Press Ctrl+C to stop") + +loopFiles: + for { + select { + case file := <-files: + println(string(file)) + case err := <-errors: + println(string(err)) + break loopFiles + } + } +}