diff --git a/README.md b/README.md index b965ce4..5987d4b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # inotify-proxy This tools helps to detect changed files in Docker containers. -If a file is changed from hostsystem a file watcher inside the container detects the change -and triggers a inotify event. +If a file is changed from host system a file watcher inside the container detects the change +and triggers an inotify event. ## Purpose @@ -37,12 +37,17 @@ Example config: --- watch: - - dir: /tmp/watch1 - - dir: /tmp/watch2 - profile: magento2 - + - directory: /tmp/watch1 + profile: magento2 + + - dir: /tmp/watch2 + profile: sass + + - dir: /tmp/watch3 + extensions: [.css, .html] + The profile setting is optional. -The config loading can be skiped by adding the option `-no-config`. +The config loading can be skipped by adding the option `-no-config`. ## Supported Profiles diff --git a/inotify-proxy.go b/inotify-proxy.go index fa524a6..560270a 100644 --- a/inotify-proxy.go +++ b/inotify-proxy.go @@ -25,38 +25,64 @@ func main() { c := config.Config{} if !*noConfig { - includedDirectories = loadConfig(c, includedDirectories, profilePtr) - } + if util.FileExists("inotify-proxy.yaml") { - // If no argument is defined, the current directory is used - if len(includedDirectories) == 0 { - includedDirectories = append(includedDirectories, ".") - } + r, err := os.Open("inotify-proxy.yaml") - color.Style{color.FgCyan, color.OpBold}.Println("PROFILE: " + *profilePtr) - color.Style{color.FgCyan, color.OpBold}.Println("DIRECTORIES: " + strings.Join(includedDirectories, ",")) + if err != nil { + color.Errorf("cannot read file: %v\n", err) + os.Exit(1) + } - watcher.Watch(includedDirectories, *sleepPtr, *profilePtr) -} + defer r.Close() -func loadConfig(c config.Config, includedDirectories []string, profilePtr *string) []string { - if util.FileExists("inotify-proxy.yaml") { - color.Info.Println("load config") - c, err := config.ReadFile("inotify-proxy.yaml"); + c, err = config.Read(r) - if err != nil { - color.Errorf("error: Invalid config provided.\n") - os.Exit(1) + if err != nil { + color.Errorf("cannot read config: %v\n", err) + } + + if c.OldGlobalProfile != nil { + color.Errorf("You are using the old configuration format. Please use the new configuration version.\n") + color.Print("\nPlease refer: https://github.com/cmuench/inotify-proxy/blob/master/README.md#config\n") + os.Exit(1) + } } + } - for _, watch := range c.Watch { - includedDirectories = append(includedDirectories, watch.Dir) + if len(includedDirectories) > 0 { + for _, includedDirectory := range includedDirectories { + c.Entries = append(c.Entries, config.WatchEntry{ + Directory: includedDirectory, + Extensions: nil, + Profile: profilePtr, + }) } + } - if c.Profile != "" { - *profilePtr = c.Profile + // If no argument is defined, the current directory is used + if len(c.Entries) == 0 { + c.AddEntry(config.WatchEntry{ + Directory: ".", + Extensions: nil, + Profile: profilePtr, + }) + } + + color.Style{color.FgMagenta, color.OpBold}.Println("Watching ...") + color.Style{color.FgWhite}.Println(strings.Repeat("-", 80)) + + for _, e := range c.Entries { + color.Style{color.FgCyan, color.OpBold}.Printf("Directory: %s\n", e.Directory) + if *e.Profile != "" { + color.Style{color.FgCyan, color.OpBold}.Printf("Profile: %s\n", *e.Profile) + } + if len(e.Extensions) > 0 { + color.Style{color.FgCyan, color.OpBold}.Printf("Extensions: %s\n", e.Extensions) } + + color.Style{color.FgWhite}.Println(strings.Repeat("-", 80)) } - return includedDirectories + watcher.Watch(c, *sleepPtr) } diff --git a/internal/config/config.go b/internal/config/config.go index 2460b2b..fd4a1b9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,25 +2,43 @@ package config import ( "gopkg.in/yaml.v3" + "io" "io/ioutil" ) -type Watch struct { - Dir string `yaml:"dir"` +type Config struct { + Entries []WatchEntry `yaml:"watch"` + + OldGlobalProfile *string `yaml:"profile"` } -type Config struct { - Watch []Watch `yaml:"watch"` - Profile string `yaml:"profile"` +type WatchEntry struct { + Directory string `yaml:"directory"` + Extensions []string `yaml:"extensions"` + Profile *string `yaml:"profile"` +} + +func (c *Config) AddEntry(e WatchEntry) { + c.Entries = append(c.Entries, e) +} + +func (c *Config) GetEntryByDirectory(dir string) WatchEntry { + for _, e := range c.Entries { + if e.Directory == dir { + return e + } + } + + return WatchEntry{} } -func ReadFile(filename string) (Config, error) { +func Read(f io.Reader) (Config, error) { var ( - c Config - err error + c Config + err error yamlData []byte ) - yamlData, err = ioutil.ReadFile(filename) + yamlData, err = ioutil.ReadAll(f) if err != nil { return c, err diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 937bd39..c3351e6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,15 +10,28 @@ func TestParseValidYaml(t *testing.T) { validYamlData := ` --- watch: - - dir: /tmp/watch1 - - dir: /tmp/watch2 -profile: magento2 +- directory: /tmp/watch1 + extensions: + - ".scss" + - ".js" + - ".twig" + +- directory: /tmp/watch2 + profile: magento2 + extensions: + - ".scss" + - ".js" + - ".twig" ` c, err := Parse([]byte(validYamlData)) assert.NoError(t, err, "Config is valid and should not throw an error") assert.IsType(t, Config{}, c) + + assert.Equal(t, "/tmp/watch1", c.Entries[0].Directory) + assert.Equal(t, "/tmp/watch2", c.Entries[1].Directory) + } func TestParseInvalidYaml(t *testing.T) { @@ -31,3 +44,7 @@ watch assert.Error(t, err, "Config is invalid and should throw an error") } + +func TestLoad(t *testing.T) { + +} diff --git a/internal/profile/types.go b/internal/profile/types.go index b5f9af3..03a8b82 100644 --- a/internal/profile/types.go +++ b/internal/profile/types.go @@ -1,74 +1,33 @@ package profile -import ( - "path/filepath" - "strings" -) - type Profile struct { - fileExtensions []string -} - -func (l *Profile) IsAllowedFileExtension(path string) bool { - - // if profile contains only one extension definition with "*" then allow every extension. - if len(l.fileExtensions) == 1 && l.fileExtensions[0] == "*" { - return true - } - - extension := filepath.Ext(path) - - for _, a := range l.fileExtensions { - if a == extension { - return true - } - } - - return false -} - -func (l *Profile) IsAllowedDirectory(path string) bool { - // Exclude some directories by default - excludedDirectories := [...]string{ - "node_modules/", - ".idea/", - ".git/", - ".svn/", - } - - for _, excludedDirectory := range excludedDirectories { - if strings.Contains(path, excludedDirectory) { - return false - } - } - - return true + Extensions []string } var Default = Profile{ - fileExtensions: []string{"*"}, + Extensions: []string{"*"}, } var LESS = Profile{ - fileExtensions: []string{".less"}, + Extensions: []string{".less"}, } var Magento2Theme = Profile{ - fileExtensions: []string{".css", ".js", ".less", ".sass", ".ts"}, + Extensions: []string{".css", ".js", ".less", ".sass", ".ts"}, } var Magento2 = Profile{ - fileExtensions: []string{".css", ".html", ".less", ".sass", ".js", ".php", ".phtml", ".ts", ".xml"}, + Extensions: []string{".css", ".html", ".less", ".sass", ".js", ".php", ".phtml", ".ts", ".xml"}, } var SASS = Profile{ - fileExtensions: []string{".sass", ".scss"}, + Extensions: []string{".sass", ".scss"}, } var VueStorefront = Profile{ - fileExtensions: []string{".css", ".js", ".sass", ".ts"}, + Extensions: []string{".css", ".js", ".sass", ".ts"}, } var Javascript = Profile{ - fileExtensions: []string{".js", ".ts"}, + Extensions: []string{".js", ".ts"}, } diff --git a/internal/profile/validator/path.go b/internal/profile/validator/path.go index 696d074..c9cc2f2 100644 --- a/internal/profile/validator/path.go +++ b/internal/profile/validator/path.go @@ -1,14 +1,29 @@ package validator import ( + "github.com/cmuench/inotify-proxy/internal/config" "github.com/cmuench/inotify-proxy/internal/profile" + "path/filepath" + "strings" ) -func IsPathValid(path string, profileName string) bool { +func IsPathValid(path string, entryConfig config.WatchEntry) bool { + + if !isAllowedDirectory(path) { + return false + } + + if len(entryConfig.Extensions) > 0 && !isAllowedFileExtension(path, entryConfig.Extensions) { + return false + } + + if entryConfig.Profile == nil { + return true + } var selectedProfile profile.Profile - switch profileName { + switch *entryConfig.Profile { case "less": selectedProfile = profile.LESS case "magento2": @@ -25,5 +40,41 @@ func IsPathValid(path string, profileName string) bool { selectedProfile = profile.Default } - return selectedProfile.IsAllowedDirectory(path) && selectedProfile.IsAllowedFileExtension(path) + return isAllowedFileExtension(path, selectedProfile.Extensions) +} + +func isAllowedDirectory(path string) bool { + // Exclude some directories by default + excludedDirectories := [...]string{ + "node_modules/", + ".idea/", + ".git/", + ".svn/", + } + + for _, excludedDirectory := range excludedDirectories { + if strings.Contains(path, excludedDirectory) { + return false + } + } + + return true +} + +func isAllowedFileExtension(path string, fileExtensions []string) bool { + + // if profile contains only one extension definition with "*" then allow every extension. + if len(fileExtensions) == 1 && fileExtensions[0] == "*" { + return true + } + + extension := filepath.Ext(path) + + for _, a := range fileExtensions { + if a == extension { + return true + } + } + + return false } diff --git a/internal/profile/validator/path_test.go b/internal/profile/validator/path_test.go index 81ccd92..3e54791 100644 --- a/internal/profile/validator/path_test.go +++ b/internal/profile/validator/path_test.go @@ -1,49 +1,81 @@ package validator import ( + "github.com/cmuench/inotify-proxy/internal/config" "github.com/stretchr/testify/assert" "testing" ) +func TestExtensionAllowList(t *testing.T) { + we := config.WatchEntry{ + Extensions: []string{".js", ".less"}, + } + + // Node modules are excluded + assert.False(t, IsPathValid("foo/node_modules/test.js", we)) + assert.False(t, IsPathValid("foo/node_modules/test.less", we)) + + // Node modules are excluded + assert.True(t, IsPathValid("example/test.js", we)) + assert.True(t, IsPathValid("example/test.less", we)) + + assert.False(t, IsPathValid("README.md", we)) + assert.False(t, IsPathValid(".git/config", we)) + assert.False(t, IsPathValid("foo/.git/config", we)) + +} + func TestMagentoProfile(t *testing.T) { - selectedProfile := "magento2" - - assert.False(t, IsPathValid("foo/node_modules/test.js", selectedProfile)) - assert.False(t, IsPathValid(".git/config", selectedProfile)) - assert.False(t, IsPathValid("foo/.git/config", selectedProfile)) - - assert.False(t, IsPathValid("README.md", selectedProfile)) - assert.False(t, IsPathValid("test.zip", selectedProfile)) - assert.True(t, IsPathValid("foo.js", selectedProfile)) - assert.True(t, IsPathValid("foo.ts", selectedProfile)) - assert.True(t, IsPathValid("foo.php", selectedProfile)) - assert.True(t, IsPathValid("foo.phtml", selectedProfile)) - assert.True(t, IsPathValid("foo.html", selectedProfile)) + p := "magento2" + we := config.WatchEntry{ + Extensions: nil, + Profile: &p, + } + + assert.False(t, IsPathValid("foo/node_modules/test.js", we)) + assert.False(t, IsPathValid(".git/config", we)) + assert.False(t, IsPathValid("foo/.git/config", we)) + + assert.False(t, IsPathValid("README.md", we)) + assert.False(t, IsPathValid("test.zip", we)) + assert.True(t, IsPathValid("foo.js", we)) + assert.True(t, IsPathValid("foo.ts", we)) + assert.True(t, IsPathValid("foo.php", we)) + assert.True(t, IsPathValid("foo.phtml", we)) + assert.True(t, IsPathValid("foo.html", we)) } func TestDefaultProfile(t *testing.T) { - selectedProfile := "default" - - assert.False(t, IsPathValid("foo/node_modules/test.js", selectedProfile)) - assert.False(t, IsPathValid(".git/config", selectedProfile)) - assert.False(t, IsPathValid("foo/.git/config", selectedProfile)) - - assert.True(t, IsPathValid("README.md", selectedProfile)) - assert.True(t, IsPathValid("foo.js", selectedProfile)) - assert.True(t, IsPathValid("foo.ts", selectedProfile)) - assert.True(t, IsPathValid("foo.php", selectedProfile)) - assert.True(t, IsPathValid("foo.phtml", selectedProfile)) - assert.True(t, IsPathValid("foo.html", selectedProfile)) - assert.True(t, IsPathValid("foo.xyz", selectedProfile)) + p := "default" + we := config.WatchEntry{ + Extensions: nil, + Profile: &p, + } + + assert.False(t, IsPathValid("foo/node_modules/test.js", we)) + assert.False(t, IsPathValid(".git/config", we)) + assert.False(t, IsPathValid("foo/.git/config", we)) + + assert.True(t, IsPathValid("README.md", we)) + assert.True(t, IsPathValid("foo.js", we)) + assert.True(t, IsPathValid("foo.ts", we)) + assert.True(t, IsPathValid("foo.php", we)) + assert.True(t, IsPathValid("foo.phtml", we)) + assert.True(t, IsPathValid("foo.html", we)) + assert.True(t, IsPathValid("foo.xyz", we)) } func TestSASSProfile(t *testing.T) { - selectedProfile := "sass" + p := "sass" + we := config.WatchEntry{ + Extensions: nil, + Profile: &p, + } - assert.False(t, IsPathValid("foo/node_modules/test.js", selectedProfile)) - assert.False(t, IsPathValid(".git/config", selectedProfile)) - assert.False(t, IsPathValid("foo/.git/config", selectedProfile)) + assert.False(t, IsPathValid("foo/node_modules/test.js", we)) + assert.False(t, IsPathValid(".git/config", we)) + assert.False(t, IsPathValid("foo/.git/config", we)) - assert.True(t, IsPathValid("foo.sass", selectedProfile)) - assert.True(t, IsPathValid("foo.scss", selectedProfile)) + assert.True(t, IsPathValid("foo.sass", we)) + assert.True(t, IsPathValid("foo.scss", we)) } diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 85cb500..93363ea 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -1,6 +1,7 @@ package watcher import ( + "github.com/cmuench/inotify-proxy/internal/config" "github.com/cmuench/inotify-proxy/internal/profile/validator" "github.com/cmuench/inotify-proxy/internal/util" "github.com/gookit/color" @@ -13,33 +14,13 @@ import ( var mu sync.Mutex var wg sync.WaitGroup -func visit(osPathname string, de *godirwalk.Dirent) error { - // we only process files - if de.IsDir() { - return nil - } - - if !validator.IsPathValid(osPathname, selectedProfile) { - return godirwalk.SkipThis - } - - fileChanged := isFileChanged(osPathname) - if fileChanged { - color.Style{color.FgGreen, color.OpBold}.Printf("Changed: %s | %s\n", osPathname, time.Now().Format("2006-01-02T15:04:05")) - } - - return nil -} - -func Watch(includedDirectories []string, watchFrequenceSeconds int, profile string) { - selectedProfile = profile - +func Watch(c config.Config, watchFrequenceSeconds int) { i := 0 for { - wg.Add(len(includedDirectories)) - for _, directoryToWalk := range includedDirectories { - go walkSingleDirectory(directoryToWalk) + wg.Add(len(c.Entries)) + for _, e := range c.Entries { + go walkSingleDirectory(c.GetEntryByDirectory(e.Directory)) } wg.Wait() @@ -54,12 +35,30 @@ func Watch(includedDirectories []string, watchFrequenceSeconds int, profile stri } } -func walkSingleDirectory(directoryToWalk string) { +func walkSingleDirectory(we config.WatchEntry) { mu.Lock() defer mu.Unlock() defer wg.Done() - err := godirwalk.Walk(directoryToWalk, &godirwalk.Options{ - Callback: visit, + + err := godirwalk.Walk(we.Directory, &godirwalk.Options{ + Callback: func(osPathname string, directoryEntry *godirwalk.Dirent) error { + + // we only process files + if directoryEntry.IsDir() { + return nil + } + + if !validator.IsPathValid(osPathname, we) { + return godirwalk.SkipThis + } + + fileChanged := isFileChanged(osPathname) + if fileChanged { + color.Style{color.FgGreen, color.OpBold}.Printf("Changed: %s | %s\n", osPathname, time.Now().Format("2006-01-02T15:04:05")) + } + + return nil + }, Unsorted: true, })