Procházet zdrojové kódy

feature: Exec on Calendar file support (#243)

* feature: Exec on calendar file

* feature: support for datetime args

* feature: Schedule commands based on seconds
James Read před 2 roky
rodič
revize
2fb40ff443

+ 2 - 0
cmd/OliveTin/main.go

@@ -9,6 +9,7 @@ import (
 	"github.com/OliveTin/OliveTin/internal/executor"
 	grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
 	"github.com/OliveTin/OliveTin/internal/installationinfo"
+	"github.com/OliveTin/OliveTin/internal/oncalendarfile"
 	"github.com/OliveTin/OliveTin/internal/oncron"
 	"github.com/OliveTin/OliveTin/internal/onfileindir"
 	"github.com/OliveTin/OliveTin/internal/onstartup"
@@ -153,6 +154,7 @@ func main() {
 	go onstartup.Execute(cfg, executor)
 	go oncron.Schedule(cfg, executor)
 	go onfileindir.WatchFilesInDirectory(cfg, executor)
+	go oncalendarfile.Schedule(cfg, executor)
 
 	go entityfiles.SetupEntityFileWatchers(cfg)
 

+ 1 - 0
internal/config/config.go

@@ -17,6 +17,7 @@ type Action struct {
 	ExecOnCron             []string
 	ExecOnFileCreatedInDir []string
 	ExecOnFileChangedInDir []string
+	ExecOnCalendarFile     string
 	Trigger                string
 	MaxConcurrent          int
 	Arguments              []ActionArgument

+ 11 - 0
internal/executor/arguments.go

@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"regexp"
 	"strings"
+	"time"
 )
 
 var (
@@ -94,6 +95,16 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
 		return typeSafetyCheckUrl(name, value)
 	}
 
+	if argumentType == "datetime" {
+		_, err := time.Parse("2006-01-02T15:04:05", value)
+
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+
 	return typeSafetyCheckRegex(name, value, argumentType)
 }
 

+ 75 - 0
internal/filehelper/file_change_notify.go

@@ -0,0 +1,75 @@
+package filehelper
+
+import (
+	"github.com/fsnotify/fsnotify"
+	log "github.com/sirupsen/logrus"
+	"path/filepath"
+)
+
+func WatchFile(fullpath string, callback func()) {
+	watcher, err := fsnotify.NewWatcher()
+
+	if err != nil {
+		log.Errorf("Could not watch for files being created: %v", err)
+		return
+	}
+
+	defer watcher.Close()
+
+	done := make(chan bool)
+
+	filename := filepath.Base(fullpath)
+	filedir := filepath.Dir(fullpath)
+
+	go func() {
+		for {
+			processEvent(filename, watcher, fsnotify.Write, callback)
+		}
+	}()
+
+	err = watcher.Add(filedir)
+
+	if err != nil {
+		log.Errorf("Could not create watcher: %v", err)
+	}
+
+	<-done
+}
+
+func processEvent(filename string, watcher *fsnotify.Watcher, eventType fsnotify.Op, callback func()) {
+	select {
+	case event, ok := <-watcher.Events:
+		if !consumeEvent(ok, filename, &event, callback) {
+			return
+		}
+
+		break
+	case err := <-watcher.Errors:
+		log.Errorf("Error in fsnotify: %v", err)
+		return
+	}
+}
+
+func consumeEvent(ok bool, filename string, event *fsnotify.Event, callback func()) bool {
+	if !ok {
+		return false
+	}
+
+	if filepath.Base(event.Name) != filename {
+		log.Tracef("fsnotify irreleventa event different file %+v", event)
+		return true
+	}
+
+	consumeWriteEvents(event, callback)
+
+	return true
+}
+
+func consumeWriteEvents(event *fsnotify.Event, callback func()) {
+	if event.Has(fsnotify.Write) {
+		log.Debugf("fsnotify write event: %v", event)
+		callback()
+	} else {
+		log.Debugf("fsnotify irrelevant event on file %v", event)
+	}
+}

+ 20 - 0
internal/filehelper/file_touch.go

@@ -0,0 +1,20 @@
+package filehelper
+
+import (
+	log "github.com/sirupsen/logrus"
+	"os"
+)
+
+func Touch(filename string, description string) {
+	_, err := os.Stat(filename)
+
+	if os.IsNotExist(err) {
+		_, err := os.Create(filename)
+
+		if err != nil {
+			log.Warnf("Could not create %v: %v", description, filename)
+		} else {
+			log.Infof("Created %v: %v", description, filename)
+		}
+	}
+}

+ 118 - 0
internal/oncalendarfile/calendar.go

@@ -0,0 +1,118 @@
+package oncalendarfile
+
+import (
+	"context"
+	"github.com/OliveTin/OliveTin/internal/acl"
+	"github.com/OliveTin/OliveTin/internal/config"
+	"github.com/OliveTin/OliveTin/internal/executor"
+	"github.com/OliveTin/OliveTin/internal/filehelper"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v3"
+	"io/ioutil"
+	"time"
+)
+
+var calendar map[*config.Action][]*time.Timer
+
+func Schedule(cfg *config.Config, ex *executor.Executor) {
+	calendar = make(map[*config.Action][]*time.Timer)
+
+	for _, action := range cfg.Actions {
+		if action.ExecOnCalendarFile != "" {
+			x := func() {
+				parseCalendarFile(action, cfg, ex)
+			}
+
+			go filehelper.WatchFile(action.ExecOnCalendarFile, x)
+
+			x()
+		}
+	}
+}
+
+func parseCalendarFile(action *config.Action, cfg *config.Config, ex *executor.Executor) {
+	filehelper.Touch(action.ExecOnCalendarFile, "calendar file")
+
+	log.WithFields(log.Fields{
+		"actionTitle": action.Title,
+	}).Infof("Parsing calendar file")
+
+	yfile, err := ioutil.ReadFile(action.ExecOnCalendarFile)
+
+	if err != nil {
+		log.Errorf("ReadIn: %v", err)
+		return
+	}
+
+	data := make([]string, 1)
+
+	err = yaml.Unmarshal(yfile, &data)
+
+	if err != nil {
+		log.Errorf("Unmarshal: %v", err)
+	}
+
+	scheduleCalendarActions(data, action, cfg, ex)
+}
+
+func scheduleCalendarActions(entries []string, action *config.Action, cfg *config.Config, ex *executor.Executor) {
+	ctx := context.Background()
+
+	for _, instant := range entries {
+		if instant == "" {
+			continue
+		}
+
+		until, _ := time.Parse(time.RFC3339, instant)
+
+		go sleepUntil(ctx, until, action, cfg, ex)
+	}
+}
+
+func sleepUntil(ctx context.Context, instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) {
+	if time.Now().After(instant) {
+		log.WithFields(log.Fields{
+			"instant":     instant,
+			"actionTitle": action.Title,
+		}).Warnf("Not scheduling stale calendar action")
+
+		return
+	}
+
+	log.WithFields(log.Fields{
+		"instant":     instant,
+		"actionTitle": action.Title,
+	}).Infof("Scheduling action on calendar")
+
+	timer := time.NewTimer(time.Until(instant))
+
+	defer timer.Stop()
+
+	select {
+	case <-timer.C:
+		exec(instant, action, cfg, ex)
+		return
+	case <-ctx.Done():
+		log.Infof("Cancelled scheduled action")
+		return
+	}
+}
+
+func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) {
+	// calendar[action] = append(calendar[action], timer)
+	log.WithFields(log.Fields{
+		"instant":     instant,
+		"actionTitle": action.Title,
+	}).Infof("Executing action from calendar")
+
+	req := &executor.ExecutionRequest{
+		Action: action,
+		Cfg:    cfg,
+		Tags:   []string{"calendar"},
+		AuthenticatedUser: &acl.AuthenticatedUser{
+			Username: "calendar",
+		},
+	}
+
+	ex.ExecRequest(req)
+}

+ 4 - 0
webui.dev/js/ArgumentForm.js

@@ -102,6 +102,10 @@ class ArgumentForm extends window.HTMLElement {
         this.domBtnStart.disabled = false
         domEl.disabled = true
       }
+    } else if (arg.type === 'datetime') {
+      domEl = document.createElement('input')
+      domEl.setAttribute('type', 'datetime-local')
+      domEl.setAttribute('step', '1')
     } else {
       domEl = document.createElement('input')
       domEl.onchange = () => {