From be26443d372de9c5a4b9134251ec088dc9f0d455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCnch?= Date: Fri, 14 Aug 2020 18:57:26 +0200 Subject: [PATCH] Initial commit --- .gitignore | 19 +++++ LICENSE.md | 21 ++++++ Makefile | 6 ++ README.md | 9 +++ go.mod | 5 ++ go.sum | 9 +++ inotify-proxy.go | 151 ++++++++++++++++++++++++++++++++++++++ internal/profile/types.go | 27 +++++++ 8 files changed, 247 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 inotify-proxy.go create mode 100644 internal/profile/types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b9f596 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.idea + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +inotify-proxy diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5e82f53 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Christian Münch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e7c398 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: dev + +linux: + GOOS=linux GOARCH=amd64 go build -o inotify-proxy inotify-proxy.go + +all: linux diff --git a/README.md b/README.md new file mode 100644 index 0000000..441eba1 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# 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. + +## Purpose + +Enables file watcher in a Docker container with a NFS mounted filesystem. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ab41e1f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/cmuench/inotify-proxy + +go 1.12 + +require github.com/gookit/color v1.2.7 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..661e329 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gookit/color v1.2.7 h1:4qePMNWZhrmbfYJDix+J4V2l0iVW+6jQGjicELlN14E= +github.com/gookit/color v1.2.7/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/inotify-proxy.go b/inotify-proxy.go new file mode 100644 index 0000000..89c202e --- /dev/null +++ b/inotify-proxy.go @@ -0,0 +1,151 @@ +package main + +import ( + "flag" + "github.com/cmuench/inotify-proxy/internal/profile" + "github.com/gookit/color" + "os" + "path/filepath" + "strings" + "time" +) + +type NodeInfo struct { + modificationUnixTime int64 +} + +var fileMap = make(map[string]NodeInfo) +var selectedProfile = "" + +func shouldSkipFile(path string) bool { + + fileExtension := filepath.Ext(path) + + // Exclude some directories by default + + if strings.Contains(path, "node_modules/") { + return true + } + + if strings.Contains(path, ".idea/") { + return true + } + + // Check profiles + + if selectedProfile == "" { + return false + } + + if selectedProfile == "magento2-theme" { + if profile.Magento2ThemeProfile.IsAllowedFileExtension(fileExtension) { + return false + } + } + + if selectedProfile == "magento2" { + if profile.Magento2.IsAllowedFileExtension(fileExtension) { + return false + } + } + + if selectedProfile == "vue-storefront" { + if profile.VueStorefront.IsAllowedFileExtension(fileExtension) { + return false + } + } + + return true +} + +func isFileChanged(path string, fileInfo os.FileInfo) bool { + + if shouldSkipFile(path) { + return false + } + + nodeInfo, found := fileMap[path] + + currentModificationTime := fileInfo.ModTime() + + changed := false + + if !found { + nodeInfo := NodeInfo{ + modificationUnixTime: currentModificationTime.Unix(), + } + fileMap[path] = nodeInfo + + color.Info.Println("Watching: " + path) + } else { + if nodeInfo.modificationUnixTime < currentModificationTime.Unix() { + changed = true + + currentTime := time.Now() + + err := os.Chtimes(path, currentModificationTime, currentTime) + + if err != nil { + panic("Error touching file" + path) + } + + fileMap[path] = NodeInfo{ + modificationUnixTime: currentTime.Unix(), + } + } + } + + return changed +} + +func visit(path string, fileInfo os.FileInfo, err error) error { + + if err != nil { + return err + } + + if fileInfo.IsDir() { + return nil + } + + fileChanged := isFileChanged(path, fileInfo) + + if fileChanged { + color.Style{color.FgGreen, color.OpBold}.Printf("Changed: %s | %s\n", path, time.Now().Format("2006-01-02T15:04:05")) + } + + return nil +} + + +func main() { + sleepPtr := flag.Int("sleep", 2, "Cycle time in seconds. Defines time to sleep after each filesystem walk. Default 2s") + profilePtr := flag.String("profile", "", "Defines a special profile with extensions to look for. This speeds up the process. Available profiles are 'magento2-theme'") + + flag.Parse() + + includedDirectories := flag.Args() + + selectedProfile = *profilePtr + + // If no argument is defined, the current directory is used + if len(includedDirectories) == 0 { + includedDirectories = append(includedDirectories, ".") + } + + color.Style{color.FgCyan, color.OpBold}.Println("PROFILE: " + selectedProfile) + color.Style{color.FgCyan, color.OpBold}.Println("DIRECTORIES: " + strings.Join(includedDirectories, ",")) + + for { + + for _, directoryToWalk := range includedDirectories { + err := filepath.Walk(directoryToWalk, visit) + + if err != nil { + panic(err) + } + } + + time.Sleep(time.Duration(*sleepPtr) * time.Second) + } +} diff --git a/internal/profile/types.go b/internal/profile/types.go new file mode 100644 index 0000000..8e12ace --- /dev/null +++ b/internal/profile/types.go @@ -0,0 +1,27 @@ +package profile + +type Profile struct { + fileExtensions []string +} + +func (l *Profile) IsAllowedFileExtension(extension string) bool { + for _, a := range l.fileExtensions { + if a == extension { + return true + } + } + + return false +} + +var Magento2ThemeProfile = Profile{ + fileExtensions: []string{ ".css", ".js", ".less", ".sass" }, +} + +var Magento2 = Profile{ + fileExtensions: []string{ ".css", ".html", ".less", ".sass", ".js", ".php", ".phtml", ".xml" }, +} + +var VueStorefront = Profile{ + fileExtensions: []string{ ".css", ".js", ".sass", ".ts" }, +}