refacting

pull/36/head
Evan Chen 2021-10-21 21:49:15 +08:00
parent a2c16da520
commit 863b489412
20 changed files with 549 additions and 430 deletions

View File

@ -11,7 +11,7 @@ COPY . .
RUN VERSION=$(git describe --tags --abbrev=0) BUILD=$(git rev-parse --short HEAD) && \ RUN VERSION=$(git describe --tags --abbrev=0) BUILD=$(git rev-parse --short HEAD) && \
GOOS=linux GOARCH=amd64 \ GOOS=linux GOARCH=amd64 \
go build -ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" \ go build -ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" \
-o /go/bin/configui -o /go/bin/configui cmd/configui/main.go
FROM alpine:3.14 FROM alpine:3.14

View File

@ -20,29 +20,33 @@ clean:
run: build run: build
$(shell cd dist; ./${PROJ} -log configui.log) $(shell cd dist; ./${PROJ} -log configui.log)
.PHONY: build .PHONY: web
build: web:
npm run build npm run build
go build ${LDFLAGS} -o dist/${PROJ} # npm run js-dev
.PHONY: build
build: web
go build ${LDFLAGS} -o dist/${PROJ} cmd/$(PROJ)/main.go
build-unix: build-unix:
$(foreach GOOS, $(PLATFORMS), $(foreach GOARCH, $(ARCHITECTURES), $(foreach APP, $(APPS),\ $(foreach GOOS, $(PLATFORMS), $(foreach GOARCH, $(ARCHITECTURES), $(foreach APP, $(APPS),\
$(shell export GOOS=$(GOOS); export GOARCH=$(GOARCH); go build ${LDFLAGS} -o dist/$(APP)) \ $(shell export GOOS=$(GOOS); export GOARCH=$(GOARCH); go build ${LDFLAGS} -o dist/$(APP) cmd/$(APP)/main.go) \
$(shell cd dist; tar -czf ${APP}_$(VERSION)_$(GOOS)_$(GOARCH).tar.gz ${APP}) \ $(shell cd dist; tar -czf ${APP}_$(VERSION)_$(GOOS)_$(GOARCH).tar.gz ${APP}) \
$(shell rm dist/${APP}) \ $(shell rm dist/${APP}) \
))) )))
build-win: build-win:
$(foreach APP, $(APPS), \ $(foreach APP, $(APPS), \
$(shell export GOOS=windows; export GOARCH=amd64; go build ${LDFLAGS} -o dist/${APP}.exe) \ $(shell export GOOS=windows; export GOARCH=amd64; go build ${LDFLAGS} -o dist/${APP}.exe cmd/$(APP)/main.go) \
$(shell cd dist; tar -czf ${APP}_$(VERSION)_windows_amd64.tar.gz ${APP}.exe) \ $(shell cd dist; tar -czf ${APP}_$(VERSION)_windows_amd64.tar.gz ${APP}.exe) \
$(shell rm dist/${APP}.exe) \ $(shell rm dist/${APP}.exe) \
) )
build-mac-m1: build-mac-m1:
$(foreach APP, $(APPS),\ $(foreach APP, $(APPS),\
$(shell export GOOS=darwin; export GOARCH=arm64; go build ${LDFLAGS} -o dist/$(APP)) \ $(shell export GOOS=darwin; export GOARCH=arm64; go build ${LDFLAGS} -o dist/$(APP) cmd/$(APP)/main.go) \
$(shell cd dist; tar -czf ${APP}_$(VERSION)_darwin_arm64.tar.gz ${APP}) \ $(shell cd dist; tar -czf ${APP}_$(VERSION)_darwin_arm64.tar.gz ${APP}) \
$(shell rm dist/${APP}) \ $(shell rm dist/${APP}) \
) )
@ -67,4 +71,4 @@ docker-save:
.PHONY: release .PHONY: release
release: clean binary docker docker-save release: clean web binary docker docker-save

View File

@ -15,7 +15,7 @@ Usage: configui [options]
-log string -log string
log to file log to file
-n string -n string
alias of file Name of file
-p string -p string
path to file, precedence over config path to file, precedence over config
-static -static
@ -84,7 +84,7 @@ res:
### File ### File
`GET /api/file?name=ALIAS` `GET /api/file?name=Name`
res: res:
```json ```json
@ -110,4 +110,4 @@ req:
### Apply ### Apply
`POST /api/apply?name=ALIAS` `POST /api/apply?name=Name`

155
api.go
View File

@ -1,155 +0,0 @@
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"kumoly.io/tools/configui/configui"
)
func ListFiles(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(files)
if err != nil {
panic(err)
}
w.Write(data)
}
func GetFile(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, ok := files[name]
if name == "" || !ok {
MakeResponse(w, 404, []byte("file not found"))
return
}
data, err := file.Read()
if err != nil {
panic(err)
}
response, err := json.Marshal(map[string]string{
"path": file.Path,
"name": file.Alias,
"action": file.Action,
"data": string(data),
})
if err != nil {
panic(err)
}
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 {
panic(err)
}
f := configui.File{}
if err := json.Unmarshal(data, &f); err != nil {
panic(err)
}
file, ok := files[f.Alias]
if !ok {
MakeResponse(w, 404, []byte("file not found"))
return
}
if err := file.Write([]byte(f.Data)); err != nil {
panic(err)
}
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 {
MakeResponse(w, 404, []byte("file not found"))
return
}
result, err := file.Do()
log.Println(err)
if err != nil {
panic(err)
}
w.Write([]byte(result))
}
func Download(w http.ResponseWriter, r *http.Request) {
if name := r.URL.Query().Get("name"); name != "" {
if name == "ConfigUI" {
data, err := GetConfig()
if err != nil {
panic(err)
}
w.Header().Set("Content-Disposition", `attachment; filename="ConfigUI.json"`)
w.Write(data)
return
}
file, ok := files[name]
if !ok {
MakeResponse(w, 404, []byte("file not found"))
return
}
data, err := file.Read()
if err != nil {
panic(err)
}
w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(file.Path)+`"`)
w.Write(data)
return
}
fs := []string{}
for _, v := range files {
fs = append(fs, v.Path)
}
if flagConfigPath != "" {
fs = append(fs, flagConfigPath)
}
w.Header().Set("Content-Disposition", `attachment; filename="export.tar.gz"`)
bundle(w, fs, false)
}
func LoadConfig(w http.ResponseWriter, r *http.Request) {
if flagNoReconfig {
panic("system reconfig is disabled")
}
data, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
panic(err)
}
ftmp, err := configui.ReadConfig(string(data))
if err != nil {
panic(err)
}
if flagConfigPath != "" {
info, err := os.Stat(flagConfigPath)
if err != nil {
panic(err)
}
os.WriteFile(flagConfigPath, data, info.Mode())
}
files = configui.GetFileMap(ftmp)
w.Write([]byte("ok"))
}
func getConfigHandler(w http.ResponseWriter, r *http.Request) {
data, err := GetConfig()
if err != nil {
panic(err)
}
w.Write(data)
}
func GetConfig() ([]byte, error) {
config := []configui.File{}
for _, f := range files {
config = append(config, *f)
}
return json.MarshalIndent(config, "", " ")
}

100
app.go Normal file
View File

@ -0,0 +1,100 @@
package configui
import (
"net/http"
"path/filepath"
"runtime"
"sort"
"strings"
)
type ActiveFile struct {
RO bool
Path string
Name string
Action string
Content string
}
type Editor struct {
Lang string
Platform string
}
type Page struct {
AppName string
Files []ActiveFile
Error string
File ActiveFile
Editor Editor
Static bool
}
func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
Files := []ActiveFile{}
for _, i := range cui.fileIndex {
Files = append(Files, ActiveFile{
Name: cui.Files[i].Name,
Path: cui.Files[i].Path,
})
}
sort.Slice(Files, func(i, j int) bool { return Files[i].Name < Files[j].Name })
plat := "unix"
if runtime.GOOS == "windows" {
plat = "windows"
}
data := Page{
AppName: cui.AppName,
File: ActiveFile{},
Files: Files,
Editor: Editor{
Platform: plat,
},
Static: cui.NoReconfig,
}
content := ""
var tmp []byte
var err error
name := r.URL.Query().Get("name")
file, err := cui.File(name)
if name == "" || err != nil {
tmp, err = cui.Config()
data.File.Name = "ConfigUI"
data.File.Path = ":mem:"
data.Editor.Lang = "json"
if name != "" {
data.Error = name + " not found\n"
}
} else {
tmp, err = file.Read()
data.File.Action = file.Action
data.File.Name = file.Name
if file.Lang != "" {
data.Editor.Lang = file.Lang
} else {
ext := strings.TrimPrefix(filepath.Ext(file.Path), ".")
if Ext2Mode[ext] != "" {
ext = Ext2Mode[ext]
}
data.Editor.Lang = ext
}
data.File.RO = file.RO
data.File.Path = file.Path
}
if err != nil {
data.Error = err.Error()
data.Editor.Lang = ""
} else {
content = string(tmp)
}
data.File.Content = content
cui.Parse(w, "home", data)
}

View File

@ -1,28 +1,21 @@
package main package main
import ( import (
"embed"
"flag" "flag"
"fmt" "fmt"
"html/template"
"io" "io"
"log" "log"
"net/http" "net/http"
"os" "os"
"time" "time"
"kumoly.io/tools/configui/configui" "kumoly.io/tools/configui"
"kumoly.io/tools/configui/public"
) )
//go:embed templates
var tmplFS embed.FS
var tmpl *template.Template
var ( var (
flagPath string flagPath string
flagAction string flagAction string
flagAlias string flagName string
flagConfigPath string flagConfigPath string
flagBind string flagBind string
@ -34,14 +27,13 @@ var (
var Version = "0.0.0" var Version = "0.0.0"
var Build = "alpha" var Build = "alpha"
var files = map[string]*configui.File{}
func init() { func init() {
// log.SetFlags(0) // log.SetFlags(0)
flag.StringVar(&flagConfigPath, "f", "", "path to config file") flag.StringVar(&flagConfigPath, "f", "", "path to config file")
flag.StringVar(&flagPath, "p", "", "path to file, precedence over config") flag.StringVar(&flagPath, "p", "", "path to file, precedence over config")
flag.StringVar(&flagAlias, "n", "", "alias of file") flag.StringVar(&flagName, "n", "", "Name of file")
flag.StringVar(&flagAction, "c", "", "cmd to apply") flag.StringVar(&flagAction, "c", "", "cmd to apply")
flag.StringVar(&flagLogFile, "log", "", "log to file") flag.StringVar(&flagLogFile, "log", "", "log to file")
flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all") flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all")
@ -62,6 +54,8 @@ func main() {
return return
} }
cui := configui.New()
// setup logging // setup logging
if flagLogFile != "" { if flagLogFile != "" {
f, err := os.OpenFile(flagLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) f, err := os.OpenFile(flagLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
@ -75,78 +69,39 @@ func main() {
// setup values // setup values
if flagPath != "" { if flagPath != "" {
if flagAlias == "" { if flagName == "" {
flagAlias = flagPath flagName = flagPath
} }
files[flagAlias] = &configui.File{ file := &configui.File{
Path: flagPath, Path: flagPath,
Alias: flagAlias, Name: flagName,
Action: flagAction, Action: flagAction,
} }
if err := cui.AppendFile(file); err != nil {
log.Fatalln(err)
}
} else if flagConfigPath == "" { } else if flagConfigPath == "" {
log.Println("no config found") log.Println("no config specified")
} else { } else {
conf, err := os.ReadFile(flagConfigPath) conf, err := os.ReadFile(flagConfigPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
ftmp, err := configui.ReadConfig(string(conf)) cui.LoadConfig(string(conf))
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
files = configui.GetFileMap(ftmp)
} }
cui.LogPath = flagLogFile
cui.ConfigPath = flagConfigPath
// setup routes // setup routes
mux := http.NewServeMux()
mux.HandleFunc("/api/conf", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
getConfigHandler(w, r)
} else if r.Method == "POST" {
LoadConfig(w, r)
} else {
w.WriteHeader(404)
}
})
mux.HandleFunc("/api/files", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
ListFiles(w, r)
} else {
w.WriteHeader(404)
}
})
mux.HandleFunc("/api/file", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
GetFile(w, r)
} else if r.Method == "POST" {
PostFile(w, r)
} else {
w.WriteHeader(404)
}
})
mux.HandleFunc("/api/apply", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
Apply(w, r)
} else {
w.WriteHeader(404)
}
})
mux.HandleFunc("/api/export", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
Download(w, r)
} else {
w.WriteHeader(404)
}
})
mux.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(public.FS))))
tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
setRoutes(mux)
server := &http.Server{ server := &http.Server{
Addr: flagBind, Addr: flagBind,
WriteTimeout: time.Second * 3, WriteTimeout: time.Second * 3,
ReadTimeout: time.Second * 30, ReadTimeout: time.Second * 30,
Handler: Middleware(mux), Handler: cui,
} }
// start server // start server

121
configui.go Normal file
View File

@ -0,0 +1,121 @@
package configui
import (
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"kumoly.io/tools/configui/public"
)
var UNIX_SHELL = "sh"
var WIN_SHELL = "cmd"
//go:embed templates
var tmplFS embed.FS
var Ext2Mode map[string]string = map[string]string{
"go": "golang",
"log": "sh",
"txt": "text",
"yml": "yaml",
"conf": "ini",
}
type ConfigUI struct {
AppName string `json:"app_name"`
ConfigPath string `json:"config_path"`
NoReconfig bool `json:"no_reconfig"`
AllowIP string `json:"allow_ip"`
Files []*File `json:"files"`
fileIndex map[string]int
// TODO
ResultBellow bool `json:"result_bellow"`
HideConfig bool `json:"hide_config"`
// Should be in main app
LogPath string `json:"log_path"`
SilentSysOut bool `json:"silent_sys_out"`
TmplFS embed.FS `json:"-"`
tmpl *template.Template
PublicFS embed.FS `json:"-"`
}
func New() *ConfigUI {
tmpl := template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
return &ConfigUI{
fileIndex: map[string]int{},
AppName: "ConfigUI",
PublicFS: public.FS,
TmplFS: tmplFS,
tmpl: tmpl,
}
}
func (cui *ConfigUI) File(file_name string) (*File, error) {
if file_name == cui.AppName {
return &File{
Path: cui.ConfigPath,
Name: cui.AppName,
Lang: "json",
}, nil
}
index, ok := cui.fileIndex[file_name]
if !ok {
return nil, errors.New("no file found")
}
return cui.Files[index], nil
}
func (cui *ConfigUI) LoadConfig(confstr string) error {
tmpConf := &ConfigUI{}
err := json.Unmarshal([]byte(confstr), tmpConf)
if err != nil {
return nil
}
// construct fileIndex
tmpIndex := map[string]int{}
for i, f := range tmpConf.Files {
if f.Name == "" {
f.Name = f.Path
}
tmpIndex[f.Name] = i
}
// copy
cui.fileIndex = tmpIndex
cui.Files = tmpConf.Files
cui.AllowIP = tmpConf.AllowIP
cui.ConfigPath = tmpConf.ConfigPath
cui.HideConfig = tmpConf.HideConfig
cui.LogPath = tmpConf.LogPath
cui.NoReconfig = tmpConf.NoReconfig
cui.ResultBellow = tmpConf.ResultBellow
cui.SilentSysOut = tmpConf.SilentSysOut
// fmt.Printf("%+v", cui)
return nil
}
func (cui *ConfigUI) Config() ([]byte, error) {
return json.MarshalIndent(cui, "", " ")
}
func (cui *ConfigUI) AppendFile(file *File) error {
if file.Name == "" {
file.Name = file.Path
}
i, ok := cui.fileIndex[file.Name]
if ok {
return fmt.Errorf("%v already exists at %d", file.Name, i)
}
cui.fileIndex[file.Name] = len(cui.Files)
cui.Files = append(cui.Files, file)
return nil
}

View File

@ -11,12 +11,9 @@ import (
"time" "time"
) )
var UNIX_SHELL = "sh"
var WIN_SHELL = "cmd"
type File struct { type File struct {
Path string `json:"path"` Path string `json:"path"`
Alias string `json:"name"` Name string `json:"name"`
Action string `json:"action"` Action string `json:"action"`
RO bool `json:"ro"` RO bool `json:"ro"`
Lang string `json:"lang"` Lang string `json:"lang"`
@ -42,6 +39,7 @@ func (f *File) Write(data []byte) error {
if f.RO { if f.RO {
return errors.New("this file has readonly set") return errors.New("this file has readonly set")
} }
log.Println("dfsdf")
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)
@ -52,6 +50,7 @@ func (f *File) Write(data []byte) error {
} }
func (f *File) Do() (string, error) { func (f *File) Do() (string, error) {
log.Println("do ", f.Action)
if f.Action == "" { if f.Action == "" {
return "", nil return "", nil
} }
@ -62,22 +61,24 @@ func (f *File) Do() (string, error) {
} else { } else {
exec.Command(UNIX_SHELL, "-c", f.Action) exec.Command(UNIX_SHELL, "-c", f.Action)
} }
var out []byte done := make(chan string, 1)
done := make(chan struct{}, 1) log.Println("start ", f.Action)
go func() { go func() {
out, _ = cmd.CombinedOutput() out, _ := cmd.CombinedOutput()
// real cmd err is only pass down // real cmd err is only pass down
// if err != nil { // if err != nil {
// return string(out), err // return string(out), err
// } // }
done <- struct{}{} done <- string(out)
}() }()
select { select {
case <-timeout: case <-timeout:
cmd.Process.Kill() cmd.Process.Kill()
log.Println("timeout")
return "", errors.New("command timeout") return "", errors.New("command timeout")
case <-done: case out := <-done:
return string(out), nil log.Println(out)
return out, nil
} }
} }
@ -88,8 +89,8 @@ func ReadConfig(confstr string) ([]File, error) {
return nil, err return nil, err
} }
for i := range conf { for i := range conf {
if conf[i].Alias == "" { if conf[i].Name == "" {
conf[i].Alias = conf[i].Path conf[i].Name = conf[i].Path
} }
} }
return conf, nil return conf, nil
@ -98,7 +99,7 @@ func ReadConfig(confstr string) ([]File, error) {
func GetFileMap(files []File) map[string]*File { func GetFileMap(files []File) map[string]*File {
fileMap := map[string]*File{} fileMap := map[string]*File{}
for i := range files { for i := range files {
fileMap[files[i].Alias] = &files[i] fileMap[files[i].Name] = &files[i]
} }
return fileMap return fileMap
} }

1
handler.go Normal file
View File

@ -0,0 +1 @@
package configui

View File

@ -1,20 +1,18 @@
package main package configui
import ( import (
"bytes" "bytes"
"log" "log"
"net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
type ResponseWriter struct { type CuiResponseWriter struct {
http.ResponseWriter http.ResponseWriter
StatueCode int StatueCode int
} }
func (w *ResponseWriter) WriteHeader(statusCode int) { func (w *CuiResponseWriter) WriteHeader(statusCode int) {
if w.StatueCode != 0 { if w.StatueCode != 0 {
return return
} }
@ -22,19 +20,19 @@ func (w *ResponseWriter) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode) w.ResponseWriter.WriteHeader(statusCode)
} }
func (w *ResponseWriter) Write(body []byte) (int, error) { func (w *CuiResponseWriter) Write(body []byte) (int, error) {
if w.StatueCode == 0 { if w.StatueCode == 0 {
w.WriteHeader(200) w.WriteHeader(200)
} }
return w.ResponseWriter.Write(body) return w.ResponseWriter.Write(body)
} }
func MakeResponse(w http.ResponseWriter, status int, body []byte) (int, error) { func response(w http.ResponseWriter, status int, body []byte) (int, error) {
w.WriteHeader(status) w.WriteHeader(status)
return w.Write(body) return w.Write(body)
} }
func AbortError(w http.ResponseWriter, err interface{}) (int, error) { func abort(w http.ResponseWriter, err interface{}) (int, error) {
switch v := err.(type) { switch v := err.(type) {
case int: case int:
w.WriteHeader(v) w.WriteHeader(v)
@ -51,56 +49,17 @@ func AbortError(w http.ResponseWriter, err interface{}) (int, error) {
} }
} }
func Catch(rw *ResponseWriter, r *http.Request) { func catch(rw *CuiResponseWriter, r *http.Request) {
ex := recover() ex := recover()
if ex != nil { if ex != nil {
AbortError(rw, ex) abort(rw, ex)
log.Printf("%s %s %d %s %s\n", GetIP(r), r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent")) log.Printf("%s %s %d %s %s\n", GetIP(r), r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
} }
} }
func Middleware(next http.Handler) http.Handler { func (cui *ConfigUI) Parse(w http.ResponseWriter, name string, data interface{}) error {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
rw := &ResponseWriter{w, 0}
defer Catch(rw, r)
abort := false
if flagAllow != "" {
if !matchIPGlob(GetIP(r), flagAllow) {
MakeResponse(rw, 403, []byte("permission denyed"))
abort = true
}
}
if !abort {
next.ServeHTTP(rw, r)
}
if !strings.HasPrefix(r.URL.Path, "/public") && r.URL.Query().Get("f") != "true" {
log.Printf("%s %s %d %s %s\n", GetIP(r), r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
}
},
)
}
func GetIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {
ips := r.Header.Get("X-Forwarded-For")
ipArr := strings.Split(ips, ",")
ip = strings.Trim(ipArr[len(ipArr)-1], " ")
}
if ip == "" {
var err error
ip, _, err = net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
}
}
return ip
}
func Parse(w http.ResponseWriter, name string, data interface{}) error {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := tmpl.ExecuteTemplate(buf, "home", data) err := cui.tmpl.ExecuteTemplate(buf, "home", data)
if err != nil { if err != nil {
panic(err) panic(err)
} }

File diff suppressed because one or more lines are too long

101
route.go
View File

@ -1,101 +0,0 @@
package main
import (
"net/http"
"path/filepath"
"runtime"
"sort"
"strings"
)
type OnPageFile struct {
RO bool
Path string
Alias string
Action string
Content string
}
type Editor struct {
Lang string
Platform string
}
type Page struct {
Files []OnPageFile
Error string
File OnPageFile
Editor Editor
Static bool
}
func setRoutes(mux *http.ServeMux) {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
Files := []OnPageFile{}
for file := range files {
Files = append(Files, OnPageFile{
Alias: files[file].Alias,
Path: files[file].Path,
})
}
sort.Slice(Files, func(i, j int) bool { return Files[i].Alias < Files[j].Alias })
plat := "unix"
if runtime.GOOS == "windows" {
plat = "windows"
}
data := Page{
File: OnPageFile{},
Files: Files,
Editor: Editor{
Platform: plat,
},
Static: flagNoReconfig,
}
content := ""
var tmp []byte
var err error
name := r.URL.Query().Get("name")
file, ok := files[name]
if name == "" || !ok {
tmp, err = GetConfig()
data.File.Alias = "ConfigUI"
data.File.Path = ":mem:"
data.Editor.Lang = "json"
if name != "" {
data.Error = name + " not found\n"
}
} else {
tmp, err = file.Read()
data.File.Action = file.Action
data.File.Alias = file.Alias
if file.Lang != "" {
data.Editor.Lang = file.Lang
} else {
ext := strings.TrimPrefix(filepath.Ext(file.Path), ".")
if ext2mode[ext] != "" {
ext = ext2mode[ext]
}
data.Editor.Lang = ext
}
data.File.RO = file.RO
data.File.Path = file.Path
}
if err != nil {
data.Error = err.Error()
data.Editor.Lang = ""
} else {
content = string(tmp)
}
data.File.Content = content
Parse(w, "home", data)
})
}

221
server.go Normal file
View File

@ -0,0 +1,221 @@
package configui
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
func (cui *ConfigUI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cui.middleware(cui.mux()).ServeHTTP(w, r)
}
func (cui *ConfigUI) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
rw := &CuiResponseWriter{w, 0}
defer catch(rw, r)
ip := GetIP(r)
if cui.AllowIP != "" {
if !matchIPGlob(ip, cui.AllowIP) {
rw.WriteHeader(403)
panic("permission denied")
}
}
next.ServeHTTP(rw, r)
//logging
if !strings.HasPrefix(r.URL.Path, "/public") && r.URL.Query().Get("f") != "true" {
log.Printf("%s %s %d %s %s\n", ip, r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
}
},
)
}
func (cui *ConfigUI) mux() *http.ServeMux {
cuiR := http.NewServeMux()
cuiR.HandleFunc("/api/conf", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
cui.GetConfig(w, r)
} else if r.Method == "POST" {
cui.PostConfig(w, r)
} else {
w.WriteHeader(404)
}
})
cuiR.HandleFunc("/api/files", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
cui.ListFiles(w, r)
} else {
w.WriteHeader(404)
}
})
cuiR.HandleFunc("/api/file", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
cui.GetFile(w, r)
} else if r.Method == "POST" {
cui.PostFile(w, r)
} else {
w.WriteHeader(404)
}
})
cuiR.HandleFunc("/api/apply", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
cui.Apply(w, r)
} else {
w.WriteHeader(404)
}
})
cuiR.HandleFunc("/api/export", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
cui.Download(w, r)
} else {
w.WriteHeader(404)
}
})
cuiR.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(cui.PublicFS))))
cuiR.HandleFunc("/", cui.App)
return cuiR
}
func (cui *ConfigUI) ListFiles(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(cui.Files)
if err != nil {
panic(err)
}
w.Write(data)
}
func (cui *ConfigUI) GetFile(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
return
}
data, err := file.Read()
if err != nil {
panic(err)
}
response, err := json.Marshal(map[string]string{
"path": file.Path,
"name": file.Name,
"action": file.Action,
"data": string(data),
})
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
func (cui *ConfigUI) PostFile(w http.ResponseWriter, r *http.Request) {
data, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
panic(err)
}
f := File{}
if err := json.Unmarshal(data, &f); err != nil {
panic(err)
}
file, err := cui.File(f.Name)
if err != nil {
response(w, 404, []byte("file not found"))
return
}
if err := file.Write([]byte(f.Data)); err != nil {
panic(err)
}
w.Write([]byte("ok"))
}
func (cui *ConfigUI) Apply(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
return
}
result, err := file.Do()
if err != nil {
panic(err)
}
w.Write([]byte(result))
}
func (cui *ConfigUI) Download(w http.ResponseWriter, r *http.Request) {
if name := r.URL.Query().Get("name"); name != "" {
if name == cui.AppName {
data, err := cui.Config()
if err != nil {
panic(err)
}
w.Header().Set(
"Content-Disposition", `
attachment; filename="`+cui.AppName+`.json"`,
)
w.Write(data)
return
}
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
return
}
data, err := file.Read()
if err != nil {
panic(err)
}
w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(file.Path)+`"`)
w.Write(data)
return
}
fs := []string{}
for _, i := range cui.fileIndex {
fs = append(fs, cui.Files[i].Path)
}
if cui.ConfigPath != "" {
fs = append(fs, cui.ConfigPath)
}
w.Header().Set("Content-Disposition", `attachment; filename="export.tar.gz"`)
bundle(w, fs, cui.AppName, false)
}
func (cui *ConfigUI) PostConfig(w http.ResponseWriter, r *http.Request) {
if cui.NoReconfig {
panic("system reconfig is disabled")
}
data, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
panic(err)
}
err = cui.LoadConfig(string(data))
if err != nil {
panic(err)
}
if cui.ConfigPath != "" {
info, err := os.Stat(cui.ConfigPath)
if err != nil {
panic(err)
}
err = os.WriteFile(cui.ConfigPath, data, info.Mode())
if err != nil {
panic(err)
}
}
w.Write([]byte("ok"))
}
func (cui *ConfigUI) GetConfig(w http.ResponseWriter, r *http.Request) {
data, err := cui.Config()
if err != nil {
panic(err)
}
w.Write(data)
}

View File

@ -9,7 +9,7 @@ async function FileGet(follower=false){
const res = await fetch('/api/conf'+f); const res = await fetch('/api/conf'+f);
const body = await res.text(); const body = await res.text();
if(!res.ok){ if(!res.ok){
handleError(res) Catch(res)
return return
} }
editor.session.setValue(body); editor.session.setValue(body);
@ -19,7 +19,7 @@ async function FileGet(follower=false){
const res = await fetch('/api/file?name=' + Active + f); const res = await fetch('/api/file?name=' + Active + f);
const body = await res.json(); const body = await res.json();
if(!res.ok){ if(!res.ok){
handleError(res) Catch(res)
return return
} }
editor.session.setValue(body.data); editor.session.setValue(body.data);
@ -36,7 +36,7 @@ async function FileSave(){
}) })
}) })
if (res.ok) window.location.reload(); if (res.ok) window.location.reload();
else handleError(res) else Catch(res)
} }
else { else {
const res = await fetch('/api/file', { const res = await fetch('/api/file', {
@ -46,7 +46,7 @@ async function FileSave(){
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}) })
}) })
if(!res.ok) handleError(res) if(!res.ok) Catch(res)
} }
} }
@ -59,7 +59,7 @@ async function FileApply(){
method: 'POST', method: 'POST',
}) })
if(!res.ok){ if(!res.ok){
const result = await handleError(res) const result = await Catch(res)
result_editor.session.setValue(result) result_editor.session.setValue(result)
return return
} }
@ -68,7 +68,9 @@ async function FileApply(){
} }
} }
async function handleError(res){ async function Catch(res){
console.trace()
console.log(res)
const msg = await res.text() const msg = await res.text()
ShowError(msg) ShowError(msg)
return msg return msg

View File

@ -7,10 +7,10 @@
<ul class="menu-list"> <ul class="menu-list">
{{ range .Files }} {{ range .Files }}
<li> <li>
<a class="has-tooltip-arrow {{if eq $.File.Alias .Alias }}is-active{{end}}" <a class="has-tooltip-arrow {{if eq $.File.Name .Name }}is-active{{end}}"
data-tooltip="{{.Path}}" data-tooltip="{{.Path}}"
href="/?name={{.Alias}}" href="/?name={{.Name}}"
>{{ .Alias }}</a> >{{ .Name }}</a>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
@ -19,7 +19,7 @@
</p> </p>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a class="has-tooltip-arrow {{if eq .File.Alias "ConfigUI" }}is-active{{end}}" <a class="has-tooltip-arrow {{if eq .File.Name .AppName }}is-active{{end}}"
data-tooltip="{{$.File.Path}}" data-tooltip="{{$.File.Path}}"
href="/" href="/"
>config.json</a></li> >config.json</a></li>

View File

@ -3,7 +3,7 @@
<div class="modal-background"></div> <div class="modal-background"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">{{.File.Alias}}</p> <p class="modal-card-title">{{.File.Name}}</p>
<button class="delete" aria-label="close" onclick="ResultViewTog()"></button> <button class="delete" aria-label="close" onclick="ResultViewTog()"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">

View File

@ -11,16 +11,6 @@
</span></span> </span></span>
</button> </button>
</p> </p>
<p class="control">
<div class="select is-small">
<select onchange="SetTabstop(this)">
<option value="0">Tabstop</option>
<option value="4">Tabstop 4</option>
<option value="2">Tabstop 2</option>
<option value="-1">Tab</option>
</select>
</div>
</p>
<p class="control"> <p class="control">
<button class="button is-small has-tooltip-arrow" id="toolFollow" <button class="button is-small has-tooltip-arrow" id="toolFollow"
data-tooltip="Follow file change (1s)" onclick="toolFollow()"> data-tooltip="Follow file change (1s)" onclick="toolFollow()">
@ -47,6 +37,16 @@
</span></span> </span></span>
</button> </button>
</p> </p>
<p class="control">
<div class="select is-small">
<select onchange="SetTabstop(this)">
<option value="0">Tabstop</option>
<option value="4">Tabstop 4</option>
<option value="2">Tabstop 2</option>
<option value="-1">Tab</option>
</select>
</div>
</p>
</div> </div>
</div> </div>
@ -62,7 +62,7 @@
</p> </p>
{{if not .File.RO}} {{if not .File.RO}}
<p class="control"> <p class="control">
{{if eq .File.Alias "ConfigUI"}} {{if eq .File.Name .AppName}}
<button class="button is-small has-tooltip-arrow" id="toolSave" <button class="button is-small has-tooltip-arrow" id="toolSave"
data-tooltip="Apply" onclick="toolSave()"> data-tooltip="Apply" onclick="toolSave()">
<span class="icon is-small"><span class="material-icons"> <span class="icon is-small"><span class="material-icons">
@ -94,7 +94,7 @@
{{/* TEST */}} {{/* TEST */}}
<p class="control"> <p class="control">
<a class="button is-small has-tooltip-arrow" data-tooltip="Download" <a class="button is-small has-tooltip-arrow" data-tooltip="Download"
href="/api/export?name={{.File.Alias}}"> href="/api/export?name={{.File.Name}}">
<span class="icon is-small"><span class="material-icons"> <span class="icon is-small"><span class="material-icons">
download download
</span></span> </span></span>

View File

@ -2,13 +2,13 @@
{{template "base/header" .}} {{template "base/header" .}}
{{template "components/error" .}} {{template "components/error" .}}
<script> <script>
var Active = "{{.File.Alias}}"; var Active = "{{.File.Name}}";
</script> </script>
<section class="hero is-small is-primary"> <section class="hero is-small is-primary">
<div class="hero-body" id="title"> <div class="hero-body" id="title">
<p class="title"> <p class="title">
ConfigUI {{.AppName}}
</p> </p>
</div> </div>
</section> </section>

31
util.go
View File

@ -1,9 +1,11 @@
package main package configui
import ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"io" "io"
"net"
"net/http"
"os" "os"
"strings" "strings"
) )
@ -51,7 +53,7 @@ func matchIPGlob(ip, pattern string) bool {
return true return true
} }
func bundle(buf io.Writer, paths []string, flat bool) error { func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error {
gw := gzip.NewWriter(buf) gw := gzip.NewWriter(buf)
defer gw.Close() defer gw.Close()
tw := tar.NewWriter(gw) tw := tar.NewWriter(gw)
@ -81,9 +83,9 @@ func bundle(buf io.Writer, paths []string, flat bool) error {
// https://golang.org/src/archive/tar/common.go?#L626 // https://golang.org/src/archive/tar/common.go?#L626
if !flat { if !flat {
if !strings.HasPrefix(file, "/") { if !strings.HasPrefix(file, "/") {
file = "configui/" + file file = rootDir + "/" + file
} else { } else {
file = "configui" + file file = rootDir + file
} }
header.Name = file header.Name = file
} }
@ -109,10 +111,19 @@ func bundle(buf io.Writer, paths []string, flat bool) error {
return nil return nil
} }
var ext2mode map[string]string = map[string]string{ func GetIP(r *http.Request) string {
"go": "golang", ip := r.Header.Get("X-Real-Ip")
"log": "sh", if ip == "" {
"txt": "text", ips := r.Header.Get("X-Forwarded-For")
"yml": "yaml", ipArr := strings.Split(ips, ",")
"conf": "ini", ip = strings.Trim(ipArr[len(ipArr)-1], " ")
}
if ip == "" {
var err error
ip, _, err = net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
}
}
return ip
} }