feat/prism
Evan Chen 2021-10-18 18:59:09 +08:00
parent ef4a8021a5
commit 95e9383138
4 changed files with 242 additions and 123 deletions

102
api.go Normal file
View File

@ -0,0 +1,102 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"kumoly.io/tools/configui/configui"
)
func ListFiles(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(files)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
w.Write(data)
}
func GetFile(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, ok := files[name]
if name == "" || !ok {
w.WriteHeader(404)
w.Write([]byte("file not found"))
return
}
data, err := file.Read()
if err != nil {
w.WriteHeader(500)
w.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 {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
func PostFile(w http.ResponseWriter, r *http.Request) {
data, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
f := configui.File{}
if err := json.Unmarshal(data, &f); err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
file, ok := files[f.Alias]
if !ok {
w.WriteHeader(404)
w.Write([]byte("file not found"))
return
}
if err := file.Write([]byte(f.Data)); err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
w.Write([]byte("ok"))
}
func Apply(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, ok := files[name]
if name == "" || !ok {
w.WriteHeader(404)
w.Write([]byte("file not found"))
return
}
result, err := file.Do()
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
w.Write([]byte(result))
}
func Download(w http.ResponseWriter, r *http.Request) {
fs := []string{}
for _, v := range files {
fs = append(fs, v.Path)
}
w.Header().Set("Content-Disposition", `attachment; filename="export.tar.gz"`)
bundle(w, fs, false)
}

View File

@ -2,6 +2,7 @@ package configui
import ( import (
"encoding/json" "encoding/json"
"errors"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@ -16,6 +17,7 @@ type File struct {
Path string `json:"path"` Path string `json:"path"`
Alias string `json:"name"` Alias string `json:"name"`
Action string `json:"action"` Action string `json:"action"`
RO bool `json:"ro"`
// used for parsing post data // used for parsing post data
Data string `json:"data"` Data string `json:"data"`
@ -35,6 +37,9 @@ func (f *File) Read() ([]byte, error) {
} }
func (f *File) Write(data []byte) error { func (f *File) Write(data []byte) error {
if f.RO {
return errors.New("this file has readonly set")
}
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
info, err := os.Stat(f.Path) info, err := os.Stat(f.Path)

186
main.go
View File

@ -3,12 +3,11 @@ package main
import ( import (
"bytes" "bytes"
"embed" "embed"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -22,24 +21,26 @@ var tmplFS embed.FS
var tmpl *template.Template var tmpl *template.Template
var ( var (
cmdPath string flagPath string
cmdAction string flagAction string
cmdAlias string flagAlias string
cmdConfigPath string flagConfigPath string
cmdBind string flagBind string
flagLogFile string
) )
var files = map[string]*configui.File{} var files = map[string]*configui.File{}
func init() { func init() {
log.SetFlags(0) // log.SetFlags(0)
flag.StringVar(&cmdConfigPath, "f", "", "path to config file") flag.StringVar(&flagConfigPath, "f", "", "path to config file")
flag.StringVar(&cmdPath, "p", "", "path to file, precedence over config") flag.StringVar(&flagPath, "p", "", "path to file, precedence over config")
flag.StringVar(&cmdAlias, "n", "", "alias of file") flag.StringVar(&flagAlias, "n", "", "alias of file")
flag.StringVar(&cmdAction, "c", "", "cmd to apply") flag.StringVar(&flagAction, "c", "", "cmd to apply")
flag.StringVar(&cmdBind, "b", "localhost:8000", "address to bind") flag.StringVar(&flagLogFile, "log", "", "log to file")
flag.StringVar(&flagBind, "bind", "localhost:8000", "address to bind")
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: configui [options]\n") fmt.Fprintf(os.Stderr, "Usage: configui [options]\n")
flag.PrintDefaults() flag.PrintDefaults()
@ -49,20 +50,31 @@ func init() {
func main() { func main() {
flag.Parse() flag.Parse()
// setup logging
if flagLogFile != "" {
f, err := os.OpenFile(flagLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Error opening file: %v", err)
}
defer f.Close()
mwriter := io.MultiWriter(f, os.Stderr)
log.SetOutput(mwriter)
}
// setup values // setup values
if cmdPath != "" { if flagPath != "" {
if cmdAlias == "" { if flagAlias == "" {
cmdAlias = cmdPath flagAlias = flagPath
} }
files[cmdAlias] = &configui.File{ files[flagAlias] = &configui.File{
Path: cmdPath, Path: flagPath,
Alias: cmdAlias, Alias: flagAlias,
Action: cmdAction, Action: flagAction,
} }
} else if cmdConfigPath == "" { } else if flagConfigPath == "" {
log.Fatalln(errors.New("no config found")) log.Fatalln(errors.New("no config found"))
} else { } else {
conf, err := os.ReadFile(cmdConfigPath) conf, err := os.ReadFile(flagConfigPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
@ -75,134 +87,62 @@ func main() {
// setup routes // setup routes
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/api/files", func(rw http.ResponseWriter, r *http.Request) { // mux.
mux.HandleFunc("/api/files", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" { if r.Method == "GET" {
ListFiles(rw, r) ListFiles(w, r)
} else { } else {
rw.WriteHeader(404) w.WriteHeader(404)
} }
}) })
mux.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/file", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" { if r.Method == "GET" {
GetFile(rw, r) GetFile(w, r)
} else if r.Method == "POST" { } else if r.Method == "POST" {
PostFile(rw, r) PostFile(w, r)
} else { } else {
rw.WriteHeader(404) w.WriteHeader(404)
} }
}) })
mux.HandleFunc("/api/apply", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/apply", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" { if r.Method == "POST" {
Apply(rw, r) Apply(w, r)
} else { } else {
rw.WriteHeader(404) w.WriteHeader(404)
} }
}) })
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/export", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
Download(w, r)
} else {
w.WriteHeader(404)
}
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := tmpl.ExecuteTemplate(buf, "home", nil) err := tmpl.ExecuteTemplate(buf, "home", nil)
if err != nil { if err != nil {
rw.WriteHeader(500) w.WriteHeader(500)
rw.Write([]byte(err.Error())) w.Write([]byte(err.Error()))
} else { } else {
rw.Write(buf.Bytes()) w.Write(buf.Bytes())
} }
}) })
tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl")) tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
server := &http.Server{ server := &http.Server{
Addr: cmdBind, Addr: flagBind,
WriteTimeout: time.Second * 3, WriteTimeout: time.Second * 3,
ReadTimeout: time.Second * 30, ReadTimeout: time.Second * 30,
Handler: mux, Handler: LogMiddleware(mux),
} }
// start server // start server
log.Println("Listening on ", cmdBind) log.Println("Listening on", flagBind)
log.Fatal(server.ListenAndServe()) 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))
}

72
util.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"archive/tar"
"compress/gzip"
"io"
"log"
"net/http"
"os"
)
func LogMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
log.Printf("%s %s %s %s\n", r.RemoteAddr, r.Method, r.URL, r.Header.Get("User-Agent"))
},
)
}
func bundle(buf io.Writer, paths []string, flat bool) error {
gw := gzip.NewWriter(buf)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for _, file := range paths {
if err := func(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
// Use full path as name (FileInfoHeader only takes the basename)
// If we don't do this the directory strucuture would
// not be preserved
// https://golang.org/src/archive/tar/common.go?#L626
if !flat {
header.Name = file
}
// Write file header to the tar archive
err = tw.WriteHeader(header)
if err != nil {
return err
}
// Copy file content to tar archive
_, err = io.Copy(tw, f)
if err != nil {
return err
}
return nil
}(file); err != nil {
return err
}
}
return nil
}