commit ef4a8021a5aff1170b907630dd5df83806dcb95b Author: Evan Chen Date: Mon Oct 18 16:49:16 2021 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ef8c2ca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.17.2-alpine3.14 as builder +RUN apk update && apk add --no-cache git tzdata +WORKDIR /src +COPY go.mod go.sum /src/ +RUN go mod download + +COPY . . +RUN VERSION=$(git describe --tags) BUILD=$(git rev-parse --short HEAD) && \ + GOOS=linux GOARCH=amd64 \ + go build -ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" \ + -o /go/bin/configui main.go + + +FROM alpine:3.14 +EXPOSE 5080 +ENV PATH="/go/bin:${PATH}" +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /go/bin/configui /go/bin/configui +CMD ["/go/bin/configui"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0bc9768 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +VERSION=$(shell git describe --tags --abbrev=0) +BUILD=$(shell git rev-parse --short HEAD) +PROJ := $(shell basename "$(PWD)") +HUB=hub.kumoly.io +HUB_PROJECT=tools + +LDFLAGS=-ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" + +default: build + +.PHONY: build +build: + go build ${LDFLAGS} -o dist/${PROJ} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9f5cc2 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Config UI + +a web app to edit and action on update + +## Api + +### Files + +`GET /api/files` + +res: +```json +{ + "test": { + "path": "test", + "name": "test", + "action": "myip local -P", + "data": "" + } +} +``` + +### File + +`GET /api/file?name=ALIAS` + +res: +```json +{ + "action": "myip local -P", + "data": "test", + "name": "test", + "path": "test" +} +``` + +### Update + +`POST /api/file` + +req: +```json +{ + "data": "test", + "name": "test", +} +``` + +### Apply + +`POST /api/apply?name=ALIAS` diff --git a/configui/file.go b/configui/file.go new file mode 100644 index 0000000..8b88994 --- /dev/null +++ b/configui/file.go @@ -0,0 +1,77 @@ +package configui + +import ( + "encoding/json" + "log" + "os" + "os/exec" + "runtime" + "sync" +) + +var UNIX_SHELL = "bash" +var WIN_SHELL = "cmd" + +type File struct { + Path string `json:"path"` + Alias string `json:"name"` + Action string `json:"action"` + + // used for parsing post data + Data string `json:"data"` + + lock sync.RWMutex `json:"-"` +} + +func (f *File) Read() ([]byte, error) { + f.lock.RLock() + defer f.lock.RUnlock() + data, err := os.ReadFile(f.Path) + if err != nil { + log.Println(err) + return nil, err + } + return data, nil +} + +func (f *File) Write(data []byte) error { + f.lock.Lock() + defer f.lock.Unlock() + info, err := os.Stat(f.Path) + if err != nil { + return err + } + return os.WriteFile(f.Path, data, info.Mode()) +} + +func (f *File) Do() (string, error) { + if f.Action == "" { + return "", nil + } + cmd := exec.Command(UNIX_SHELL, "-c", f.Action) + if runtime.GOOS == "windows" { + cmd = exec.Command(WIN_SHELL, "/c", f.Action) + } + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + return string(out), nil +} + +func ReadConfig(confstr string) ([]File, error) { + conf := []File{} + err := json.Unmarshal([]byte(confstr), &conf) + if err != nil { + return nil, err + } + return conf, nil +} + +func GetFileMap(files []File) map[string]*File { + fileMap := map[string]*File{} + for i := range files { + fileMap[files[i].Alias] = &files[i] + } + return fileMap +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1773852 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module kumoly.io/tools/configui + +go 1.17 diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ac5a8f --- /dev/null +++ b/main.go @@ -0,0 +1,208 @@ +package main + +import ( + "bytes" + "embed" + "encoding/json" + "errors" + "flag" + "fmt" + "html/template" + "io/ioutil" + "log" + "net/http" + "os" + "time" + + "kumoly.io/tools/configui/configui" +) + +//go:embed templates +var tmplFS embed.FS +var tmpl *template.Template + +var ( + cmdPath string + cmdAction string + cmdAlias string + cmdConfigPath string + + cmdBind string +) + +var files = map[string]*configui.File{} + +func init() { + log.SetFlags(0) + + flag.StringVar(&cmdConfigPath, "f", "", "path to config file") + flag.StringVar(&cmdPath, "p", "", "path to file, precedence over config") + flag.StringVar(&cmdAlias, "n", "", "alias of file") + flag.StringVar(&cmdAction, "c", "", "cmd to apply") + flag.StringVar(&cmdBind, "b", "localhost:8000", "address to bind") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: configui [options]\n") + flag.PrintDefaults() + } +} + +func main() { + flag.Parse() + + // setup values + if cmdPath != "" { + if cmdAlias == "" { + cmdAlias = cmdPath + } + files[cmdAlias] = &configui.File{ + Path: cmdPath, + Alias: cmdAlias, + Action: cmdAction, + } + } else if cmdConfigPath == "" { + log.Fatalln(errors.New("no config found")) + } else { + conf, err := os.ReadFile(cmdConfigPath) + if err != nil { + log.Fatalln(err) + } + ftmp, err := configui.ReadConfig(string(conf)) + if err != nil { + log.Fatalln(err) + } + files = configui.GetFileMap(ftmp) + } + + // setup routes + mux := http.NewServeMux() + mux.HandleFunc("/api/files", func(rw http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + ListFiles(rw, r) + } else { + rw.WriteHeader(404) + } + }) + mux.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + GetFile(rw, r) + } else if r.Method == "POST" { + PostFile(rw, r) + } else { + rw.WriteHeader(404) + } + }) + mux.HandleFunc("/api/apply", func(rw http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + Apply(rw, r) + } else { + rw.WriteHeader(404) + } + }) + mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} + err := tmpl.ExecuteTemplate(buf, "home", nil) + if err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + } else { + rw.Write(buf.Bytes()) + } + }) + + tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl")) + + server := &http.Server{ + Addr: cmdBind, + WriteTimeout: time.Second * 3, + ReadTimeout: time.Second * 30, + Handler: mux, + } + + // start server + log.Println("Listening on ", cmdBind) + log.Fatal(server.ListenAndServe()) +} + +func ListFiles(rw http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(files) + if err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + rw.Write(data) +} + +func GetFile(rw http.ResponseWriter, r *http.Request) { + name := r.URL.Query().Get("name") + file, ok := files[name] + if name == "" || !ok { + rw.WriteHeader(404) + rw.Write([]byte("file not found")) + return + } + data, err := file.Read() + if err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + response, err := json.Marshal(map[string]string{ + "path": file.Path, + "name": file.Alias, + "action": file.Action, + "data": string(data), + }) + if err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + rw.Header().Set("Content-Type", "application/json") + rw.Write(response) +} + +func PostFile(rw http.ResponseWriter, r *http.Request) { + data, err := ioutil.ReadAll(r.Body) + r.Body.Close() + if err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + f := configui.File{} + if err := json.Unmarshal(data, &f); err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + file, ok := files[f.Alias] + if !ok { + rw.WriteHeader(404) + rw.Write([]byte("file not found")) + return + } + if err := file.Write([]byte(f.Data)); err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + rw.Write([]byte("ok")) +} + +func Apply(rw http.ResponseWriter, r *http.Request) { + name := r.URL.Query().Get("name") + file, ok := files[name] + if name == "" || !ok { + rw.WriteHeader(404) + rw.Write([]byte("file not found")) + return + } + result, err := file.Do() + if err != nil { + rw.WriteHeader(500) + rw.Write([]byte(err.Error())) + return + } + rw.Write([]byte(result)) +} diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..e69de29 diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl new file mode 100644 index 0000000..7f92449 --- /dev/null +++ b/templates/base/footer.tmpl @@ -0,0 +1,6 @@ +{{define "base/footer"}} + + + + +{{end}} \ No newline at end of file diff --git a/templates/base/header.tmpl b/templates/base/header.tmpl new file mode 100644 index 0000000..4286390 --- /dev/null +++ b/templates/base/header.tmpl @@ -0,0 +1,11 @@ +{{define "base/header"}} + + + + + + Serviced + + + +{{end}} \ No newline at end of file diff --git a/templates/home.tmpl b/templates/home.tmpl new file mode 100644 index 0000000..45e9873 --- /dev/null +++ b/templates/home.tmpl @@ -0,0 +1,14 @@ +{{define "home"}} +{{template "base/header" .}} +
+
+

+ Hello World +

+

+ My first website with Bulma! +

+
+
+{{template "base/footer" .}} +{{end}} \ No newline at end of file