first commit

feat/prism
Evan Chen 2021-10-18 16:49:16 +08:00
commit ef4a8021a5
12 changed files with 404 additions and 0 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
dist

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

19
Dockerfile Normal file
View File

@ -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"]

13
Makefile Normal file
View File

@ -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}

51
README.md Normal file
View File

@ -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`

77
configui/file.go Normal file
View File

@ -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
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module kumoly.io/tools/configui
go 1.17

208
main.go Normal file
View File

@ -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))
}

0
release.sh Normal file
View File

View File

@ -0,0 +1,6 @@
{{define "base/footer"}}
<script src="/public/js/main.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</body>
</html>
{{end}}

View File

@ -0,0 +1,11 @@
{{define "base/header"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Serviced</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
</head>
<body>
{{end}}

14
templates/home.tmpl Normal file
View File

@ -0,0 +1,14 @@
{{define "home"}}
{{template "base/header" .}}
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <strong>Bulma</strong>!
</p>
</div>
</section>
{{template "base/footer" .}}
{{end}}