diff --git a/README.md b/README.md index b269420..ae8e2c4 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,11 @@ Parity features * [x] crontab integration * [ ] source batching * [x] add item from web -* [ ] web edit channels +* [x] web edit channels * web edit sources - * [ ] edit action argv - * [ ] edit source envs - * [ ] edit source crontab + * [x] edit action argv + * [x] edit source envs + * [x] edit source crontab * [x] Nix build * [ ] NixOS module * [ ] NixOS vm demo diff --git a/core/env.go b/core/env.go index 7c5b94c..cb8337b 100644 --- a/core/env.go +++ b/core/env.go @@ -5,7 +5,7 @@ import ( "strings" ) -func GetEnvs(db DB, source string) ([]string, error) { +func GetEnvs(db DB, source string) (envs []string, err error) { rows, err := db.Query(` select name, value from envs @@ -15,19 +15,43 @@ func GetEnvs(db DB, source string) ([]string, error) { return nil, err } defer rows.Close() - var envs []string for rows.Next() { var name string var value string - if err := rows.Scan(&name, &value); err != nil { + if err = rows.Scan(&name, &value); err != nil { return nil, err } envs = append(envs, fmt.Sprintf("%s=%s", name, value)) } - if err := rows.Err(); err != nil { + if err = rows.Err(); err != nil { return nil, err } - return envs, nil + return +} + +func GetEnvsMap(db DB, source string) (envs map[string]string, err error) { + rows, err := db.Query(` + select name, value + from envs + where source = ? + `, source) + if err != nil { + return nil, err + } + defer rows.Close() + envs = make(map[string]string) + for rows.Next() { + var name string + var value string + if err = rows.Scan(&name, &value); err != nil { + return nil, err + } + envs[name] = value + } + if err = rows.Err(); err != nil { + return nil, err + } + return } func SetEnvs(db DB, source string, envs []string) error { diff --git a/web/html/editChannels.html b/web/html/editChannels.html index b852c92..ea61926 100644 --- a/web/html/editChannels.html +++ b/web/html/editChannels.html @@ -19,7 +19,7 @@

diff --git a/web/html/editSource.html b/web/html/editSource.html new file mode 100644 index 0000000..40531b3 --- /dev/null +++ b/web/html/editSource.html @@ -0,0 +1,54 @@ +{{ define "title" }}{{ .Name }} - Intake{{ end }} + +{{ define "content" -}} + + + +{{- end }} diff --git a/web/html/home.html b/web/html/home.html index 06f2c9c..0610f57 100644 --- a/web/html/home.html +++ b/web/html/home.html @@ -34,6 +34,9 @@ + +(edit) + {{ .Name }} diff --git a/web/html/html.go b/web/html/html.go index 6c0c1ff..f3c6705 100644 --- a/web/html/html.go +++ b/web/html/html.go @@ -182,3 +182,17 @@ func EditChannels(writer io.Writer, data EditChannelsData) { log.Printf("error: failed to render edit channels: %v", err) } } + +var editSource = load("editSource.html") + +type EditSourceData struct { + Name string + Envs map[string]string + Actions map[string]string +} + +func EditSource(writer io.Writer, data EditSourceData) { + if err := editSource.Execute(writer, data); err != nil { + log.Printf("error: failed to render edit source: %v", err) + } +} diff --git a/web/html/intake.css b/web/html/intake.css index e8f951b..8c260ce 100644 --- a/web/html/intake.css +++ b/web/html/intake.css @@ -98,3 +98,6 @@ article textarea { span.error-message { color: red; } +#envvars input { + font-family: monospace; +} diff --git a/web/main.go b/web/main.go index 82dd731..1c087f2 100644 --- a/web/main.go +++ b/web/main.go @@ -40,6 +40,8 @@ func RunServer(db core.DB, addr string, port string) { handleFunc("GET /htmx.org@2.0.4.js", env.getScript, logged) handleFunc("POST /login", env.login, logged) handleFunc("GET /source/{source}", env.getSource, env.authed, logged) + handleFunc("GET /source/{source}/edit", env.getEditSource, env.authed, logged) + handleFunc("POST /source/{source}/edit", env.editSource, env.authed, logged) handleFunc("POST /source/{source}/fetch", env.fetchSource, env.authed, logged) handleFunc("GET /channel/", env.getChannels, env.authed, logged) handleFunc("POST /channel/", env.editChannel, env.authed, logged) diff --git a/web/shlex.go b/web/shlex.go new file mode 100644 index 0000000..2d3fdb8 --- /dev/null +++ b/web/shlex.go @@ -0,0 +1,44 @@ +package web + +import ( + "io" + "os/exec" + "strings" +) + +func Shlex(cmd string) (argv []string, err error) { + xargs := exec.Command("xargs", "-n1") + stdin, err := xargs.StdinPipe() + if err != nil { + return + } + _, err = io.WriteString(stdin, cmd) + if err != nil { + return + } + err = stdin.Close() + if err != nil { + return + } + output, err := xargs.Output() + if err != nil { + return + } + argv = strings.Split(string(output), "\n") + if argv[len(argv)-1] == "" { + argv = argv[:len(argv)-1] + } + return +} + +func Quote(argv []string) (cmd string, err error) { + argv = append([]string{"%q "}, argv...) + printf := exec.Command("printf", argv...) + output, err := printf.Output() + if err != nil { + return + } + cmd = string(output) + cmd = cmd[:len(cmd)-1] // strip off the last space from '%q ' + return +} diff --git a/web/shlex_test.go b/web/shlex_test.go new file mode 100644 index 0000000..93ca364 --- /dev/null +++ b/web/shlex_test.go @@ -0,0 +1,60 @@ +package web + +import "testing" + +func TestShlex(t *testing.T) { + shlex := func(t *testing.T, cmd string, expected ...string) { + t.Helper() + actual, err := Shlex(cmd) + if err != nil { + t.Fatalf("shlex failed: %v", err) + } + if len(actual) != len(expected) { + t.Fatalf("expected %d args, got %d\nexpect: %v\nactual: %v", len(expected), len(actual), expected, actual) + } + for i := range len(expected) { + if actual[i] != expected[i] { + t.Fatalf("arg %d incorrect: expected %s got %s", i, expected[i], actual[i]) + } + } + } + + quote := func(t *testing.T, argvAndExpected ...string) { + t.Helper() + argv := argvAndExpected[:len(argvAndExpected)-1] + expected := argvAndExpected[len(argvAndExpected)-1] + actual, err := Quote(argv) + if err != nil { + t.Fatalf("quote failed: %v", err) + } + if actual != expected { + t.Fatalf("expected %q, got %q", expected, actual) + } + } + + shlex(t, + `Lord "have mercy"`, + "Lord", "have mercy", + ) + shlex(t, + `jq -cR '{id: .}'`, + "jq", "-cR", `{id: .}`, + ) + shlex(t, + `jq -cR '{id: "hello"}'`, + "jq", "-cR", `{id: "hello"}`, + ) + + quote(t, + "Lord", "have mercy", + `Lord 'have mercy'`, + ) + quote(t, + "jq", "-cR", `{id: .}`, + `jq -cR '{id: .}'`, + ) + quote(t, + "jq", "-cR", `{id: "hello"}`, + `jq -cR '{id: "hello"}'`, + ) +} diff --git a/web/source.go b/web/source.go index ec16f8e..c930594 100644 --- a/web/source.go +++ b/web/source.go @@ -78,3 +78,80 @@ func (env *Env) fetchSource(writer http.ResponseWriter, req *http.Request) { } html.Fetch(writer, data) } + +func (env *Env) getEditSource(writer http.ResponseWriter, req *http.Request) { + source := req.PathValue("source") + if exists, err := core.SourceExists(env.db, source); !exists || err != nil { + http.NotFound(writer, req) + return + } + + envs, err := core.GetEnvsMap(env.db, source) + if err != nil { + http.Error(writer, err.Error(), 500) + return + } + + actions, _ := core.GetActionsForSource(env.db, source) + argvs := make(map[string]string) + for _, action := range actions { + argv, _ := core.GetArgvForAction(env.db, source, action) + args, _ := Quote(argv) + argvs[action] = args + } + + data := html.EditSourceData{ + Name: source, + Envs: envs, + Actions: argvs, + } + html.EditSource(writer, data) +} + +func (env *Env) editSource(writer http.ResponseWriter, req *http.Request) { + source := req.PathValue("source") + if exists, err := core.SourceExists(env.db, source); !exists || err != nil { + http.NotFound(writer, req) + return + } + + if err := req.ParseForm(); err != nil { + http.Error(writer, err.Error(), 400) + return + } + + envName := req.PostForm.Get("envName") + envValue := req.PostForm.Get("envValue") + if envName != "" { + log.Printf("setting %s=%s", envName, envValue) + if err := core.SetEnvs(env.db, source, []string{fmt.Sprintf("%s=%s", envName, envValue)}); err != nil { + http.Error(writer, err.Error(), 500) + return + } + } + + actionName := req.PostForm.Get("actionName") + actionArgv := req.PostForm.Get("actionArgv") + if actionName != "" { + log.Printf("setting %s -- %s", actionName, actionArgv) + if actionArgv == "" { + if err := core.DeleteAction(env.db, source, actionName); err != nil { + http.Error(writer, err.Error(), 500) + return + } + } else { + argv, err := Shlex(actionArgv) + if err != nil { + http.Error(writer, err.Error(), 400) + return + } + if err = core.SetAction(env.db, source, actionName, argv); err != nil { + http.Error(writer, err.Error(), 500) + return + } + } + } + + writer.Header()["HX-Refresh"] = []string{"true"} + writer.WriteHeader(http.StatusNoContent) +}