commit 70255bc2a257ccce982b6f4a5014ea54c4cb3d38
Author: Sebastian Frank <sebastian@webmakers.de>
Date:   Tue Aug 10 16:56:44 2021 +0200

    init

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c036379
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+tmp/
\ No newline at end of file
diff --git a/config.yml b/config.yml
new file mode 100644
index 0000000..a2107d3
--- /dev/null
+++ b/config.yml
@@ -0,0 +1,36 @@
+directories:
+  - path: /tmp/foo
+    recursive: false
+    events: [ "CLOSE_WRITE", "MOVED_TO" ]
+    regex: "/[^\\.][^/]+.xml$"
+    actions:
+        - type: http
+          method: post
+          url: http://localhost:8080/api/v1/_/renz_einfo/xml
+          header:
+            Token: Ksi02vcaHHasd1sjYxiq4J
+            ContentType: application/xml
+          # content: PLAIN IS FILECONTENT
+          onError:
+            type: move
+            to: /tmp/error/{{ filename }}
+            stop: true
+
+        - type: http
+          method: post
+          url: http://localhost:8080/api/v1/_/renz_einfo/operation-file
+          header:
+            Token: abc
+            ContentType: application/json
+          content: |
+            {
+              "filename": "{{ .Name }}"
+            }
+          onError:
+            type: move
+            to: /tmp/error/{{ filename }}
+            stop: true
+
+        - type: delete
+        - type: log
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..6d0675f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,9 @@
+module gitbase.de/apairon/fstrigger
+
+go 1.16
+
+require (
+	github.com/flosch/pongo2/v4 v4.0.2
+	github.com/rjeczalik/notify v0.9.2
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..64783ac
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,16 @@
+github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
+github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
+github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
+golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7 h1:bit1t3mgdR35yN0cX0G8orgLtOuyL9Wqxa1mccLB0ig=
+golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
+gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..fc519ac
--- /dev/null
+++ b/main.go
@@ -0,0 +1,276 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/flosch/pongo2/v4"
+	"github.com/rjeczalik/notify"
+	"gopkg.in/yaml.v3"
+)
+
+type ActionConfig struct {
+	Type string
+
+	// http
+	Method  *string
+	URL     *string
+	Content *string
+	Header  map[string]string
+
+	// move
+	To *string
+
+	Stop      bool
+	OnSuccess *ActionConfig `yaml:"onSuccess"`
+	OnError   *ActionConfig `yaml:"onError"`
+}
+
+type DirectoryConfig struct {
+	Path      string
+	Recursive bool
+	Events    []string
+	Regex     *string
+	_regex    *regexp.Regexp
+	Actions   []ActionConfig
+}
+
+type Config struct {
+	Directories []DirectoryConfig
+}
+
+var wg sync.WaitGroup
+
+func startWatcher(directory *DirectoryConfig) {
+	c := make(chan notify.EventInfo, 10)
+	monitoredEvents := []notify.Event{}
+	for _, e := range directory.Events {
+		switch e {
+		case "CREATE":
+			monitoredEvents = append(monitoredEvents, notify.Create)
+		case "REMOVE":
+			monitoredEvents = append(monitoredEvents, notify.Remove)
+		case "WRITE":
+			monitoredEvents = append(monitoredEvents, notify.Write)
+		case "RENAME":
+			monitoredEvents = append(monitoredEvents, notify.Rename)
+		case "ACCESS":
+			monitoredEvents = append(monitoredEvents, notify.InAccess)
+		case "MODIFY":
+			monitoredEvents = append(monitoredEvents, notify.InModify)
+		case "ATTRIB":
+			monitoredEvents = append(monitoredEvents, notify.InAttrib)
+		case "CLOSE_WRITE":
+			monitoredEvents = append(monitoredEvents, notify.InCloseWrite)
+		case "CLOSE_NOWRITE":
+			monitoredEvents = append(monitoredEvents, notify.InCloseNowrite)
+		case "OPEN":
+			monitoredEvents = append(monitoredEvents, notify.InOpen)
+		case "MOVED_FROM":
+			monitoredEvents = append(monitoredEvents, notify.InMovedFrom)
+		case "MOVED_TO":
+			monitoredEvents = append(monitoredEvents, notify.InMovedTo)
+		case "DELETE":
+			monitoredEvents = append(monitoredEvents, notify.InDelete)
+		case "DELETE_SELF":
+			monitoredEvents = append(monitoredEvents, notify.InDeleteSelf)
+		case "MOVE_SELF":
+			monitoredEvents = append(monitoredEvents, notify.InMoveSelf)
+		default:
+			log.Fatalf("event %s unknown", e)
+		}
+	}
+
+	if directory.Regex != nil {
+		directory._regex = regexp.MustCompile(*directory.Regex)
+	}
+
+	wg.Add(1)
+	go func(directory *DirectoryConfig, monitoredEvents []notify.Event) {
+		defer wg.Done()
+
+		log.Printf("watching %s for %v\n", directory.Path, directory.Events)
+
+		if err := notify.Watch(directory.Path, c, monitoredEvents...); err != nil {
+			log.Fatal(err)
+		}
+		defer notify.Stop(c)
+
+		for {
+			eventInfo := <-c
+			p := eventInfo.Path()
+			log.Printf("path: %s; event: %s\n", p, eventInfo.Event().String())
+			if directory._regex == nil || directory._regex.MatchString(p) {
+				for _, action := range directory.Actions {
+					contin, _ := runAction(&action, eventInfo)
+					if !contin {
+						break
+					}
+				}
+			} else {
+				log.Printf("path: %s; no regex match\n", p)
+			}
+		}
+	}(directory, monitoredEvents)
+
+}
+
+func runAction(action *ActionConfig, eventInfo notify.EventInfo) (contin bool, err error) {
+	contin = !action.Stop
+	log.Printf("path: %s; action: %s; stop: %v\n", eventInfo.Path(), action.Type, !contin)
+
+	switch action.Type {
+	case "http":
+		err = actionHttp(action, eventInfo)
+	case "move":
+		err = actionMove(action, eventInfo)
+	case "delete":
+		err = actionDelete(action, eventInfo)
+	case "log":
+		err = actionLog(action, eventInfo)
+	default:
+		err = fmt.Errorf("action type %s unknown", action.Type)
+	}
+
+	if err != nil {
+		log.Printf("path: %s; action: %s; error %s", eventInfo.Path(), action.Type, err)
+		if action.OnError != nil {
+			_c, errE := runAction(action.OnError, eventInfo)
+			contin = contin && _c
+			if errE != nil {
+				log.Printf("path: %s; action: %s; onError %s, error %s", eventInfo.Path(), action.Type, action.OnError.Type, errE)
+			}
+		}
+	} else if err == nil && action.OnSuccess != nil {
+		_c, errS := runAction(action.OnSuccess, eventInfo)
+		contin = contin && _c
+		if errS != nil {
+			log.Printf("path: %s; action: %s; onSuccess %s, error %s", eventInfo.Path(), action.Type, action.OnError.Type, errS)
+		}
+		err = errS
+	}
+
+	return contin, err
+}
+
+func actionHttp(action *ActionConfig, eventInfo notify.EventInfo) error {
+	method := "POST"
+	if action.Method != nil {
+		method = strings.ToUpper(*action.Method)
+	}
+	if action.URL == nil {
+		return fmt.Errorf("missing url in http action")
+	}
+
+	postData := bytes.NewBuffer([]byte{})
+	if method != "GET" {
+		if action.Content == nil {
+			// read file
+			postBytes, err := ioutil.ReadFile(eventInfo.Path())
+			if err != nil {
+				return nil
+			}
+			postData = bytes.NewBuffer(postBytes)
+		}
+	}
+
+	log.Printf("%s %s", method, *action.URL)
+	req, err := http.NewRequest(method, *action.URL, postData)
+	if err != nil {
+		return err
+	}
+	for key, val := range action.Header {
+		req.Header.Set(key, val)
+	}
+	client := &http.Client{}
+	client.Timeout = time.Second * 20
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+
+	body, _ := ioutil.ReadAll(resp.Body)
+	fmt.Println(string(body))
+	defer resp.Body.Close()
+
+	if resp.StatusCode >= 400 {
+		return fmt.Errorf("response status %s", resp.Status)
+	}
+
+	return nil
+}
+
+func actionMove(action *ActionConfig, eventInfo notify.EventInfo) error {
+	if action.To == nil {
+		return fmt.Errorf("missing to: as destination for move action")
+	}
+
+	to, err := tpl2String(*action.To, action, eventInfo)
+	if err != nil {
+		return err
+	}
+
+	err = os.Rename(eventInfo.Path(), to)
+	return err
+}
+
+func actionDelete(action *ActionConfig, eventInfo notify.EventInfo) error {
+	log.Println("DELETE")
+	return nil
+}
+
+func actionLog(action *ActionConfig, eventInfo notify.EventInfo) error {
+	var logstr string
+	if action.Content != nil {
+		logstr = *action.Content
+	} else {
+		logstr = eventInfo.Path()
+	}
+	log.Printf("LOG: %s", logstr)
+	return nil
+}
+
+func tpl2String(tpl string, action *ActionConfig, eventInfo notify.EventInfo) (string, error) {
+	_tpl, err := pongo2.FromString(tpl)
+	if err != nil {
+		return "", err
+	}
+
+	p := eventInfo.Path()
+	return _tpl.Execute(pongo2.Context{
+		"path":     p,
+		"dirname":  filepath.Dir(p),
+		"filename": filepath.Base(p),
+	})
+}
+
+func main() {
+
+	conf := Config{}
+
+	confDat, err := ioutil.ReadFile("./config.yml")
+	if err != nil {
+		log.Fatalf("error %v", err)
+	}
+
+	err = yaml.Unmarshal(confDat, &conf)
+	if err != nil {
+		log.Fatalf("error %v", err)
+	}
+
+	for _, directory := range conf.Directories {
+		startWatcher(&directory)
+	}
+
+	wg.Wait()
+	log.Println("fstrigger exit")
+}