wip/refact-lib (#36)
Co-authored-by: Evan Chen <evanchen@kumoly.io> Co-authored-by: Evan <evanchen333@gmail.com> Reviewed-on: #36 Co-authored-by: evanchen <evanchen@kumoly.io> Co-committed-by: evanchen <evanchen@kumoly.io>pull/39/head
							parent
							
								
									a2c16da520
								
							
						
					
					
						commit
						db1f9f3ee8
					
				|  | @ -1,5 +1,5 @@ | |||
| dist | ||||
| node_modules | ||||
| .parcel-cache | ||||
| public/css/main.css* | ||||
| public/css/main.css.map | ||||
| public/js/main.js* | ||||
							
								
								
									
										13
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										13
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -4,13 +4,20 @@ | |||
| 
 | ||||
| * download files #26 | ||||
| * add goto last line toolbar #21 | ||||
| * use parcel to load main.js | ||||
| * use icons | ||||
| * bind ctrl-s (cmd-s) to fileSave | ||||
| * refactor as lib #15 | ||||
| * Set Title to be changeable #24 | ||||
| * show run result below #33 | ||||
| * set font size #31 | ||||
| * Hide Config zone for other usage such as go playground #23 | ||||
| 
 | ||||
| ## Fix | ||||
| 
 | ||||
| * all cmds should be pass straight back to user | ||||
| * timeout on none ending cmds #34 | ||||
| * all cmd output should be pass straight back to user | ||||
| * timeout on none ending cmd #34 | ||||
| * add sys setting to conf #32 | ||||
| * Apply should save before taking action #19 | ||||
| 
 | ||||
| # 0.1.1 | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ COPY . . | |||
| RUN VERSION=$(git describe --tags --abbrev=0) 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 | ||||
|     -o /go/bin/configui cmd/configui/main.go | ||||
| 
 | ||||
| 
 | ||||
| FROM alpine:3.14 | ||||
|  |  | |||
							
								
								
									
										16
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										16
									
								
								Makefile
								
								
								
								
							|  | @ -20,29 +20,33 @@ clean: | |||
| run: build | ||||
| 	$(shell cd dist; ./${PROJ} -log configui.log) | ||||
| 
 | ||||
| .PHONY: web | ||||
| web: | ||||
| 	npm run build | ||||
| 	# npm run js-dev | ||||
| 
 | ||||
| .PHONY: build | ||||
| build: | ||||
| 	npm run build | ||||
| 	go build ${LDFLAGS} -o dist/${PROJ} | ||||
| 	go build ${LDFLAGS} -o dist/${PROJ} cmd/$(PROJ)/main.go | ||||
| 
 | ||||
| 
 | ||||
| build-unix: | ||||
| 	$(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 rm dist/${APP}) \
 | ||||
| 	))) | ||||
| 
 | ||||
| build-win: | ||||
| 	$(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 rm dist/${APP}.exe) \
 | ||||
| 	) | ||||
| 
 | ||||
| build-mac-m1: | ||||
| 	$(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 rm dist/${APP}) \
 | ||||
| 	) | ||||
|  | @ -67,4 +71,4 @@ docker-save: | |||
| 
 | ||||
| 
 | ||||
| .PHONY: release | ||||
| release: clean binary docker docker-save | ||||
| release: clean web binary docker docker-save | ||||
|  | @ -15,7 +15,7 @@ Usage: configui [options] | |||
|   -log string | ||||
|         log to file | ||||
|   -n string | ||||
|         alias of file | ||||
|         Name of file | ||||
|   -p string | ||||
|         path to file, precedence over config | ||||
|   -static | ||||
|  | @ -84,7 +84,7 @@ res: | |||
| 
 | ||||
| ### File | ||||
| 
 | ||||
| `GET /api/file?name=ALIAS` | ||||
| `GET /api/file?name=Name` | ||||
| 
 | ||||
| res: | ||||
| ```json | ||||
|  | @ -110,4 +110,4 @@ req: | |||
| 
 | ||||
| ### Apply | ||||
| 
 | ||||
| `POST /api/apply?name=ALIAS` | ||||
| `POST /api/apply?name=Name` | ||||
|  |  | |||
							
								
								
									
										155
									
								
								api.go
								
								
								
								
							
							
						
						
									
										155
									
								
								api.go
								
								
								
								
							|  | @ -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, "", "  ") | ||||
| } | ||||
|  | @ -0,0 +1,112 @@ | |||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type ActiveFile struct { | ||||
| 	RO      bool | ||||
| 	Path    string | ||||
| 	Name    string | ||||
| 	Action  string | ||||
| 	Content string | ||||
| 	Order   int | ||||
| } | ||||
| 
 | ||||
| type Editor struct { | ||||
| 	Lang     string | ||||
| 	Platform string | ||||
| } | ||||
| 
 | ||||
| type Page struct { | ||||
| 	AppName string | ||||
| 	Files   []ActiveFile | ||||
| 	Error   string | ||||
| 	File    ActiveFile | ||||
| 	Editor  Editor | ||||
| 
 | ||||
| 	Static       bool | ||||
| 	HideConfig   bool | ||||
| 	ResultBellow 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, | ||||
| 			Order: cui.Files[i].Order, | ||||
| 		}) | ||||
| 	} | ||||
| 	sort.Slice(Files, func(i, j int) bool { | ||||
| 		if Files[i].Order == Files[j].Order { | ||||
| 			return Files[i].Name < Files[j].Name | ||||
| 		} | ||||
| 		return Files[i].Order < Files[j].Order | ||||
| 	}) | ||||
| 	plat := "unix" | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		plat = "windows" | ||||
| 	} | ||||
| 	data := Page{ | ||||
| 		AppName: cui.AppName, | ||||
| 		File:    ActiveFile{}, | ||||
| 		Files:   Files, | ||||
| 		Editor: Editor{ | ||||
| 			Platform: plat, | ||||
| 		}, | ||||
| 		Static:       cui.NoReconfig, | ||||
| 		HideConfig:   cui.HideConfig, | ||||
| 		ResultBellow: cui.ResultBellow, | ||||
| 	} | ||||
| 
 | ||||
| 	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 = cui.AppName | ||||
| 		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) | ||||
| } | ||||
|  | @ -1,28 +1,21 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"embed" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"kumoly.io/tools/configui/configui" | ||||
| 	"kumoly.io/tools/configui/public" | ||||
| 	"kumoly.io/tools/configui" | ||||
| ) | ||||
| 
 | ||||
| //go:embed templates
 | ||||
| var tmplFS embed.FS | ||||
| var tmpl *template.Template | ||||
| 
 | ||||
| var ( | ||||
| 	flagPath       string | ||||
| 	flagAction     string | ||||
| 	flagAlias      string | ||||
| 	flagName       string | ||||
| 	flagConfigPath string | ||||
| 
 | ||||
| 	flagBind       string | ||||
|  | @ -34,14 +27,13 @@ var ( | |||
| 
 | ||||
| var Version = "0.0.0" | ||||
| var Build = "alpha" | ||||
| var files = map[string]*configui.File{} | ||||
| 
 | ||||
| func init() { | ||||
| 	// log.SetFlags(0)
 | ||||
| 
 | ||||
| 	flag.StringVar(&flagConfigPath, "f", "", "path to config file") | ||||
| 	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(&flagLogFile, "log", "", "log to file") | ||||
| 	flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all") | ||||
|  | @ -62,6 +54,8 @@ func main() { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	cui := configui.New() | ||||
| 
 | ||||
| 	// setup logging
 | ||||
| 	if flagLogFile != "" { | ||||
| 		f, err := os.OpenFile(flagLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) | ||||
|  | @ -75,78 +69,40 @@ func main() { | |||
| 
 | ||||
| 	// setup values
 | ||||
| 	if flagPath != "" { | ||||
| 		if flagAlias == "" { | ||||
| 			flagAlias = flagPath | ||||
| 		if flagName == "" { | ||||
| 			flagName = flagPath | ||||
| 		} | ||||
| 		files[flagAlias] = &configui.File{ | ||||
| 		file := &configui.File{ | ||||
| 			Path:   flagPath, | ||||
| 			Alias:  flagAlias, | ||||
| 			Name:   flagName, | ||||
| 			Action: flagAction, | ||||
| 		} | ||||
| 		if err := cui.AppendFile(file); err != nil { | ||||
| 			log.Fatalln(err) | ||||
| 		} | ||||
| 	} else if flagConfigPath == "" { | ||||
| 		log.Println("no config found") | ||||
| 		log.Println("no config specified") | ||||
| 	} else { | ||||
| 		conf, err := os.ReadFile(flagConfigPath) | ||||
| 		if err != nil { | ||||
| 			log.Fatalln(err) | ||||
| 		} | ||||
| 		ftmp, err := configui.ReadConfig(string(conf)) | ||||
| 		cui.LoadConfig(string(conf)) | ||||
| 		if err != nil { | ||||
| 			log.Fatalln(err) | ||||
| 		} | ||||
| 		files = configui.GetFileMap(ftmp) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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) | ||||
| 		} | ||||
| 	}) | ||||
| 	cui.LogPath = flagLogFile | ||||
| 	cui.ConfigPath = flagConfigPath | ||||
| 	cui.NoReconfig = flagNoReconfig | ||||
| 
 | ||||
| 	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) | ||||
| 	// setup routes
 | ||||
| 	server := &http.Server{ | ||||
| 		Addr:         flagBind, | ||||
| 		WriteTimeout: time.Second * 3, | ||||
| 		WriteTimeout: time.Second * 30, | ||||
| 		ReadTimeout:  time.Second * 30, | ||||
| 		Handler:      Middleware(mux), | ||||
| 		Handler:      cui, | ||||
| 	} | ||||
| 
 | ||||
| 	// start server
 | ||||
|  | @ -0,0 +1,132 @@ | |||
| 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("").Funcs(template.FuncMap{ | ||||
| 		"step": func(from, to, step uint) []uint { | ||||
| 			items := []uint{} | ||||
| 			for i := from; i <= to; i += step { | ||||
| 				items = append(items, i) | ||||
| 			} | ||||
| 			return items | ||||
| 		}, | ||||
| 	}).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.AppName = tmpConf.AppName | ||||
| 
 | ||||
| 	// NOT implemented
 | ||||
| 	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 | ||||
| } | ||||
|  | @ -11,15 +11,16 @@ import ( | |||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| var UNIX_SHELL = "sh" | ||||
| var WIN_SHELL = "cmd" | ||||
| 
 | ||||
| type File struct { | ||||
| 	Path   string `json:"path"` | ||||
| 	Alias  string `json:"name"` | ||||
| 	Name   string `json:"name"` | ||||
| 	Action string `json:"action"` | ||||
| 	RO     bool   `json:"ro"` | ||||
| 	Lang   string `json:"lang"` | ||||
| 	// RO is readonly
 | ||||
| 	RO   bool   `json:"ro"` | ||||
| 	Lang string `json:"lang"` | ||||
| 
 | ||||
| 	// Order order of the display on ui
 | ||||
| 	Order int `json:"order"` | ||||
| 
 | ||||
| 	// used for parsing post data
 | ||||
| 	Data string `json:"data"` | ||||
|  | @ -55,29 +56,32 @@ func (f *File) Do() (string, error) { | |||
| 	if f.Action == "" { | ||||
| 		return "", nil | ||||
| 	} | ||||
| 	timeout := time.After(2 * time.Second) | ||||
| 	f.lock.RLock() | ||||
| 	defer f.lock.RUnlock() | ||||
| 	cmd := &exec.Cmd{} | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		cmd = exec.Command(WIN_SHELL, "/c", f.Action) | ||||
| 	} else { | ||||
| 		exec.Command(UNIX_SHELL, "-c", f.Action) | ||||
| 		cmd = exec.Command(UNIX_SHELL, "-c", f.Action) | ||||
| 	} | ||||
| 	var out []byte | ||||
| 	done := make(chan struct{}, 1) | ||||
| 	log.Println("DO: ", f.Action) | ||||
| 	done := make(chan string, 1) | ||||
| 	go func() { | ||||
| 		out, _ = cmd.CombinedOutput() | ||||
| 		// real cmd err is only pass down
 | ||||
| 		out, _ := cmd.CombinedOutput() | ||||
| 		// real cmd err is unhandled, but passed to client
 | ||||
| 		// if err != nil {
 | ||||
| 		// 	return string(out), err
 | ||||
| 		// }
 | ||||
| 		done <- struct{}{} | ||||
| 		done <- string(out) | ||||
| 	}() | ||||
| 	select { | ||||
| 	case <-timeout: | ||||
| 	case <-time.After(10 * time.Second): | ||||
| 		cmd.Process.Kill() | ||||
| 		log.Println("timeout") | ||||
| 		return "", errors.New("command timeout") | ||||
| 	case <-done: | ||||
| 		return string(out), nil | ||||
| 	case out := <-done: | ||||
| 		log.Printf("\n%v", out) | ||||
| 		return out, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -88,8 +92,8 @@ func ReadConfig(confstr string) ([]File, error) { | |||
| 		return nil, err | ||||
| 	} | ||||
| 	for i := range conf { | ||||
| 		if conf[i].Alias == "" { | ||||
| 			conf[i].Alias = conf[i].Path | ||||
| 		if conf[i].Name == "" { | ||||
| 			conf[i].Name = conf[i].Path | ||||
| 		} | ||||
| 	} | ||||
| 	return conf, nil | ||||
|  | @ -98,7 +102,7 @@ func ReadConfig(confstr string) ([]File, error) { | |||
| func GetFileMap(files []File) map[string]*File { | ||||
| 	fileMap := map[string]*File{} | ||||
| 	for i := range files { | ||||
| 		fileMap[files[i].Alias] = &files[i] | ||||
| 		fileMap[files[i].Name] = &files[i] | ||||
| 	} | ||||
| 	return fileMap | ||||
| } | ||||
|  | @ -0,0 +1,148 @@ | |||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| ) | ||||
| 
 | ||||
| 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) | ||||
| } | ||||
							
								
								
									
										61
									
								
								netutil.go
								
								
								
								
							
							
						
						
									
										61
									
								
								netutil.go
								
								
								
								
							|  | @ -1,20 +1,18 @@ | |||
| package main | ||||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type ResponseWriter struct { | ||||
| type CuiResponseWriter struct { | ||||
| 	http.ResponseWriter | ||||
| 	StatueCode int | ||||
| } | ||||
| 
 | ||||
| func (w *ResponseWriter) WriteHeader(statusCode int) { | ||||
| func (w *CuiResponseWriter) WriteHeader(statusCode int) { | ||||
| 	if w.StatueCode != 0 { | ||||
| 		return | ||||
| 	} | ||||
|  | @ -22,19 +20,19 @@ func (w *ResponseWriter) WriteHeader(statusCode int) { | |||
| 	w.ResponseWriter.WriteHeader(statusCode) | ||||
| } | ||||
| 
 | ||||
| func (w *ResponseWriter) Write(body []byte) (int, error) { | ||||
| func (w *CuiResponseWriter) Write(body []byte) (int, error) { | ||||
| 	if w.StatueCode == 0 { | ||||
| 		w.WriteHeader(200) | ||||
| 	} | ||||
| 	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) | ||||
| 	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) { | ||||
| 	case int: | ||||
| 		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() | ||||
| 	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")) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Middleware(next http.Handler) http.Handler { | ||||
| 	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 { | ||||
| func (cui *ConfigUI) Parse(w http.ResponseWriter, name string, data interface{}) error { | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	err := tmpl.ExecuteTemplate(buf, "home", data) | ||||
| 	err := cui.tmpl.ExecuteTemplate(buf, "home", data) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -2,5 +2,5 @@ package public | |||
| 
 | ||||
| import "embed" | ||||
| 
 | ||||
| //go:embed js css ace assets
 | ||||
| //go:embed css ace assets
 | ||||
| var FS embed.FS | ||||
|  |  | |||
							
								
								
									
										101
									
								
								route.go
								
								
								
								
							
							
						
						
									
										101
									
								
								route.go
								
								
								
								
							|  | @ -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) | ||||
| 	}) | ||||
| } | ||||
|  | @ -0,0 +1,78 @@ | |||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"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 | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/main.js
								
								
								
								
							
							
						
						
									
										40
									
								
								src/main.js
								
								
								
								
							|  | @ -2,31 +2,33 @@ | |||
| 
 | ||||
| window.ToolIsFollow = false; | ||||
| 
 | ||||
| async function FileGet(follower=false){ | ||||
| FileGet= async function(follower=false){ | ||||
|   let f = '' | ||||
|   if (Active == 'ConfigUI') { | ||||
|     if (follower) f = '?f=true' | ||||
|     const res = await fetch('/api/conf'+f); | ||||
|     const res = await fetch('/api/conf'+f) | ||||
|       .catch(err=>{console.log(err);return;}); | ||||
|     const body = await res.text(); | ||||
|     if(!res.ok){ | ||||
|       handleError(res) | ||||
|       Catch(res) | ||||
|       return | ||||
|     } | ||||
|     editor.session.setValue(body); | ||||
|   } | ||||
|   else { | ||||
|     if (follower) f = '&f=true' | ||||
|     const res = await fetch('/api/file?name=' + Active + f); | ||||
|     const res = await fetch('/api/file?name=' + Active + f) | ||||
|       .catch(err=>{console.log(err);return;}); | ||||
|     const body = await res.json(); | ||||
|     if(!res.ok){ | ||||
|       handleError(res) | ||||
|       Catch(res) | ||||
|       return | ||||
|     } | ||||
|     editor.session.setValue(body.data); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function FileSave(){ | ||||
| FileSave = async function(){ | ||||
|   if (Active == 'ConfigUI') { | ||||
|     const res = await fetch('/api/conf', { | ||||
|       method: 'POST', | ||||
|  | @ -34,9 +36,9 @@ async function FileSave(){ | |||
|       headers: new Headers({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }) | ||||
|     }).catch(err=>{console.log(err);return;}); | ||||
|     if (res.ok) window.location.reload(); | ||||
|     else handleError(res) | ||||
|     else Catch(res) | ||||
|   } | ||||
|   else { | ||||
|     const res = await fetch('/api/file', { | ||||
|  | @ -45,21 +47,21 @@ async function FileSave(){ | |||
|       headers: new Headers({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }) | ||||
|     if(!res.ok) handleError(res) | ||||
|     }).catch(err=>{console.log(err);return;}); | ||||
|     if(!res.ok) Catch(res) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function FileApply(){ | ||||
| FileApply = async function(){ | ||||
|   if (Active == 'ConfigUI') { | ||||
|     return; | ||||
|   } | ||||
|   else { | ||||
|     const res = await fetch('/api/apply?name='+ Active, { | ||||
|       method: 'POST', | ||||
|     }) | ||||
|     }).catch(err=>{console.log(err);return;}); | ||||
|     if(!res.ok){ | ||||
|       const result = await handleError(res) | ||||
|       const result = await Catch(res) | ||||
|       result_editor.session.setValue(result) | ||||
|       return | ||||
|     } | ||||
|  | @ -68,7 +70,9 @@ async function FileApply(){ | |||
|   } | ||||
| } | ||||
| 
 | ||||
| async function handleError(res){ | ||||
| Catch=async function(res){ | ||||
|   console.trace() | ||||
|   console.log(res) | ||||
|   const msg = await res.text() | ||||
|   ShowError(msg) | ||||
|   return msg | ||||
|  | @ -89,4 +93,12 @@ async function handleError(res){ | |||
|       editor.gotoLine(editor.session.getLength()); | ||||
|     } | ||||
|   }), 1000) | ||||
| 
 | ||||
|   // block ctl-s
 | ||||
|   window.addEventListener("keypress", function(event) { | ||||
|     if (!(event.which == 115 && event.ctrlKey) && !(event.which == 19)) return true | ||||
|     alert("Ctrl-S pressed") | ||||
|     event.preventDefault() | ||||
|     return false | ||||
|   }) | ||||
| }()) | ||||
|  | @ -22,7 +22,12 @@ $modal-content-width: 90vw; | |||
| } | ||||
| 
 | ||||
| .icn-spinner { | ||||
|   animation: spin-animation 0.5s infinite; | ||||
|   animation: spin-animation 1s infinite; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .icn-loading { | ||||
|   animation: spin-animation 3s infinite; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| {{define "base/footer"}} | ||||
|   <script src="/public/ace/js/ace.js" type="text/javascript" charset="utf-8"></script> | ||||
|   <script src="/public/ace/js/ext-beautify.js" type="text/javascript" charset="utf-8"></script> | ||||
|   <script src="/public/js/main.js"></script> | ||||
|   {{template "base/script" .}} | ||||
|   <!-- <script src="/public/js/main.js"></script> --> | ||||
|   </body> | ||||
| </html> | ||||
| {{end}} | ||||
|  | @ -0,0 +1,116 @@ | |||
| {{define "base/script"}} | ||||
| <script> | ||||
| window.ToolIsFollow = false; | ||||
| window.ContentChanged = false; | ||||
| 
 | ||||
| async function FileGet(follower=false){ | ||||
|   let f = '' | ||||
|   if (Active == 'ConfigUI') { | ||||
|     if (follower) f = '?f=true' | ||||
|     const res = await fetch('/api/conf'+f) | ||||
|       .catch(err=>{console.log(err);return;}); | ||||
|     const body = await res.text(); | ||||
|     if(!res.ok){ | ||||
|       Catch(res) | ||||
|       return | ||||
|     } | ||||
|     editor.session.setValue(body); | ||||
|   } | ||||
|   else { | ||||
|     if (follower) f = '&f=true' | ||||
|     const res = await fetch('/api/file?name=' + Active + f) | ||||
|       .catch(err=>{console.log(err);return;}); | ||||
|     const body = await res.json(); | ||||
|     if(!res.ok){ | ||||
|       Catch(res) | ||||
|       return | ||||
|     } | ||||
|     editor.session.setValue(body.data); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function FileSave(){ | ||||
|   if (Active == '{{.AppName}}') { | ||||
|     const res = await fetch('/api/conf', { | ||||
|       method: 'POST', | ||||
|       body: editor.getValue(), | ||||
|       headers: new Headers({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }).catch(err=>{console.log(err);return;}); | ||||
|     if (res.ok) { | ||||
|       window.location.reload(); | ||||
|       window.ContentChanged = false | ||||
|     } | ||||
|     else Catch(res) | ||||
|   } | ||||
|   else { | ||||
|     const res = await fetch('/api/file', { | ||||
|       method: 'POST', | ||||
|       body: JSON.stringify({"name":Active,"data":editor.getValue()}), | ||||
|       headers: new Headers({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }).catch(err=>{console.log(err);return;}); | ||||
|     if(!res.ok) Catch(res) | ||||
|     else window.ContentChanged = false | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function FileApply(){ | ||||
|   if (Active == '{{.AppName}}') { | ||||
|     return; | ||||
|   } | ||||
|   else { | ||||
|     const res = await fetch('/api/apply?name='+ Active, { | ||||
|       method: 'POST', | ||||
|     }); | ||||
|     if(!res.ok){ | ||||
|       const result = await Catch(res) | ||||
|       result_editor.session.setValue(result) | ||||
|       return | ||||
|     } | ||||
|     const result = await res.text() | ||||
|     result_editor.session.setValue(result) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function Catch(res){ | ||||
|   {{/* console.trace() | ||||
|   console.log(res) */}} | ||||
|   const msg = await res.text() | ||||
|   ShowError(msg) | ||||
|   return msg | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function setResult(){ | ||||
|   var result_editor = ace.edit("result_editor"); | ||||
|   window.result_editor = result_editor | ||||
|   result_editor.setTheme("ace/theme/monokai"); | ||||
|   result_editor.session.setMode("ace/mode/sh"); | ||||
|   result_editor.setReadOnly(true); | ||||
|   result_editor.setOptions({ | ||||
|     maxLines: 1000 | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| // starting point | ||||
| (function(){ | ||||
| 
 | ||||
|   // setup ace editor | ||||
|   setEditor() | ||||
| 
 | ||||
|   // setup result code block | ||||
|   setResult() | ||||
|    | ||||
|   // for follow mode | ||||
|   setInterval((async ()=>{ | ||||
|     if (ToolIsFollow){ | ||||
|       await FileGet(true) | ||||
|       editor.gotoLine(editor.session.getLength()); | ||||
|     } | ||||
|   }), 1000) | ||||
| }()) | ||||
| </script> | ||||
| {{end}} | ||||
|  | @ -15,6 +15,21 @@ function setEditor(){ | |||
|   editor.session.setUseWrapMode(false); | ||||
|   editor.session.setNewLineMode('{{.Editor.Platform}}') | ||||
|   editor.setReadOnly({{.File.RO}}); | ||||
| 
 | ||||
|   editor.session.on('change', function(delta) { | ||||
|     // delta.start, delta.end, delta.lines, delta.action | ||||
|     window.ContentChanged = true | ||||
|   }); | ||||
| 
 | ||||
|   editor.commands.addCommand({ | ||||
|     name: 'SaveContent', | ||||
|     bindKey: {win: 'Ctrl-S',  mac: 'Command-S'}, | ||||
|     exec: function(editor) { | ||||
|         //... | ||||
|         FileSave() | ||||
|     }, | ||||
|     readOnly: false // false if this command should not apply in readOnly mode | ||||
|   }); | ||||
| } | ||||
| </script> | ||||
| {{end}} | ||||
|  | @ -7,23 +7,25 @@ | |||
|     <ul class="menu-list"> | ||||
|       {{ range .Files }} | ||||
|       <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}}" | ||||
|           href="/?name={{.Alias}}" | ||||
|         >{{ .Alias }}</a> | ||||
|           href="/?name={{.Name}}" | ||||
|         >{{ .Name }}</a> | ||||
|       </li> | ||||
|       {{ end }} | ||||
|     </ul> | ||||
|     {{if not .HideConfig}} | ||||
|     <p class="menu-label"> | ||||
|       ConfigUI | ||||
|       System | ||||
|     </p> | ||||
|     <ul class="menu-list"> | ||||
|       <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}}" | ||||
|           href="/" | ||||
|         >config.json</a></li> | ||||
|         >Config</a></li> | ||||
|     </ul> | ||||
|     {{end}} | ||||
|   </aside> | ||||
| </section> | ||||
| {{end}} | ||||
|  | @ -3,7 +3,7 @@ | |||
|   <div class="modal-background"></div> | ||||
|   <div class="modal-card"> | ||||
|     <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> | ||||
|     </header> | ||||
|     <section class="modal-card-body"> | ||||
|  | @ -23,17 +23,6 @@ | |||
| </div> | ||||
| 
 | ||||
| <script> | ||||
| function setResult(){ | ||||
|   var result_editor = ace.edit("result_editor"); | ||||
|   window.result_editor = result_editor | ||||
|   result_editor.setTheme("ace/theme/monokai"); | ||||
|   result_editor.session.setMode("ace/mode/sh"); | ||||
|   result_editor.setReadOnly(true); | ||||
|   result_editor.setOptions({ | ||||
|     maxLines: 1000 | ||||
| }); | ||||
| } | ||||
| 
 | ||||
| function ResultViewTog(){ | ||||
|   let el = document.getElementById('result_view') | ||||
|   el.classList.toggle('is-active') | ||||
|  |  | |||
|  | @ -11,21 +11,11 @@ | |||
|           </span></span> | ||||
|         </button> | ||||
|       </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"> | ||||
|         <button class="button is-small has-tooltip-arrow" id="toolFollow"  | ||||
|           data-tooltip="Follow file change (1s)" onclick="toolFollow()"> | ||||
|           <span class="icon is-small"><span class="material-icons" id="toolFollowIcon"> | ||||
|             pause | ||||
|             sync_disabled | ||||
|           </span></span> | ||||
|         </button> | ||||
|       </p> | ||||
|  | @ -47,32 +37,54 @@ | |||
|           </span></span> | ||||
|         </button> | ||||
|       </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"> | ||||
|         <div class="select is-small"> | ||||
|           <select onchange="toolSetFontSize(this)"> | ||||
|             <option value="1rem">1rem</option> | ||||
|             {{range step 8 36 2}} | ||||
|             <option value="{{.}}px">{{.}}px</option> | ||||
|             {{end}} | ||||
|           </select> | ||||
|         </div> | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
|    | ||||
|   <div class="level-right"> | ||||
|     <div class="field has-addons"> | ||||
|       <p class="control"> | ||||
|         <button class="button is-small has-tooltip-arrow" id="toolRefresh" | ||||
|         <button class="button is-small has-tooltip-arrow" | ||||
|           data-tooltip="Refresh" onclick="toolRefresh()"> | ||||
|           <span class="icon is-small"><span class="material-icons"> | ||||
|           <span class="icon is-small"><span class="material-icons" id="toolRefreshIco"> | ||||
|             refresh | ||||
|           </span></span> | ||||
|         </button> | ||||
|       </p> | ||||
|       {{if not .File.RO}} | ||||
|         <p class="control"> | ||||
|           {{if eq .File.Alias "ConfigUI"}} | ||||
|             <button class="button is-small has-tooltip-arrow" id="toolSave"  | ||||
|           {{if eq .File.Name .AppName}} | ||||
|             {{if not .Static}} | ||||
|             <button class="button is-small has-tooltip-arrow" | ||||
|               data-tooltip="Apply" onclick="toolSave()"> | ||||
|               <span class="icon is-small"><span class="material-icons"> | ||||
|               <span class="icon is-small"><span class="material-icons" id="toolSaveIco"> | ||||
|                 play_arrow | ||||
|               </span></span>   | ||||
|             </button> | ||||
|             {{end}} | ||||
|           {{else}} | ||||
|             <button class="button is-small has-tooltip-arrow" id="toolSave"  | ||||
|             <button class="button is-small has-tooltip-arrow" | ||||
|               data-tooltip="Save" onclick="toolSave()"> | ||||
|               <span class="icon is-small"><span class="material-icons"> | ||||
|               <span class="icon is-small"><span class="material-icons" id="toolSaveIco"> | ||||
|                 save | ||||
|               </span></span> | ||||
|             </button> | ||||
|  | @ -83,18 +95,17 @@ | |||
|       <p class="control"> | ||||
|         <button class="button is-small has-tooltip-arrow" | ||||
|           data-tooltip="{{.File.Action}}" | ||||
|           id="toolApply" onclick="toolApply()"> | ||||
|           <span class="icon is-small"><span class="material-icons"> | ||||
|           onclick="toolApply()"> | ||||
|           <span class="icon is-small"><span class="material-icons"id="toolApplyIco"> | ||||
|             play_arrow | ||||
|           </span></span>  | ||||
|         </button> | ||||
|       </p> | ||||
|       {{end}} | ||||
| 
 | ||||
|       {{/* TEST */}}  | ||||
|       <p class="control"> | ||||
|         <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"> | ||||
|             download | ||||
|           </span></span> | ||||
|  | @ -145,6 +156,12 @@ function toolFormat(){ | |||
|   beautify.beautify(editor.session); | ||||
| } | ||||
| 
 | ||||
| function toolSetFontSize(obj){ | ||||
|   let value = obj.value; | ||||
|   document.getElementById('editor').style.fontSize=value; | ||||
|   document.getElementById('result_editor').style.fontSize=value; | ||||
| } | ||||
| 
 | ||||
| function toolFollow(){ | ||||
|   if (ToolIsFollow){ | ||||
|     ToolIsFollow = false | ||||
|  | @ -152,7 +169,7 @@ function toolFollow(){ | |||
|     let icon = document.getElementById('toolFollowIcon'); | ||||
|     el.classList.remove('is-primary'); | ||||
|     icon.classList.remove('icn-spinner'); | ||||
|     icon.innerText='pause'; | ||||
|     icon.innerText='sync_disabled'; | ||||
|   } else { | ||||
|     ToolIsFollow = true | ||||
|     let el = document.getElementById('toolFollow'); | ||||
|  | @ -165,25 +182,34 @@ function toolFollow(){ | |||
| } | ||||
| 
 | ||||
| async function toolRefresh(){ | ||||
|   let el = document.getElementById('toolRefresh'); | ||||
|   el.classList.add("is-loading") | ||||
|   let el = document.getElementById('toolRefreshIco'); | ||||
|   el.classList.toggle('icn-loading') | ||||
|   await FileGet() | ||||
|   el.classList.remove("is-loading") | ||||
|   el.classList.toggle('icn-loading') | ||||
| } | ||||
| 
 | ||||
| async function toolSave(){ | ||||
|   let el = document.getElementById('toolSave'); | ||||
|   el.classList.add("is-loading") | ||||
|   let el = document.getElementById('toolSaveIco'); | ||||
|   let originText = el.innerText; | ||||
|   el.innerText='refreash'; | ||||
|   el.classList.add("icn-loading") | ||||
|   await FileSave() | ||||
|   el.classList.remove("is-loading") | ||||
|   el.innerText=originText; | ||||
|   el.classList.remove("icn-loading") | ||||
| } | ||||
| 
 | ||||
| async function toolApply(){ | ||||
|   let el = document.getElementById('toolApply'); | ||||
|   el.classList.add("is-loading") | ||||
|   let el = document.getElementById('toolApplyIco'); | ||||
|   let originText = el.innerText; | ||||
|   el.innerText='rotate_right'; | ||||
|   el.classList.add("icn-loading") | ||||
|   if (window.ContentChanged){ | ||||
|     await FileSave() | ||||
|   } | ||||
|   await FileApply() | ||||
|   el.classList.remove("is-loading") | ||||
|   ResultViewTog() | ||||
|   el.innerText=originText; | ||||
|   el.classList.remove("icn-loading") | ||||
|   {{if not .ResultBellow}}ResultViewTog(){{end}} | ||||
| } | ||||
| </script> | ||||
| {{end}} | ||||
|  | @ -2,13 +2,13 @@ | |||
| {{template "base/header" .}} | ||||
| {{template "components/error" .}} | ||||
| <script> | ||||
| var Active = "{{.File.Alias}}"; | ||||
| var Active = "{{.File.Name}}"; | ||||
| </script> | ||||
| 
 | ||||
| <section class="hero is-small is-primary"> | ||||
|   <div class="hero-body" id="title"> | ||||
|     <p class="title"> | ||||
|       ConfigUI | ||||
|       {{.AppName}} | ||||
|     </p> | ||||
|   </div> | ||||
| </section> | ||||
|  | @ -24,11 +24,21 @@ var Active = "{{.File.Alias}}"; | |||
|   </div> | ||||
|   <div class="column"> | ||||
|     <div class="box"> | ||||
|       {{if and .HideConfig (eq .File.Name .AppName)}} | ||||
|       Home | ||||
|       {{else}} | ||||
|       {{template "components/editor" .}} | ||||
|       {{end}} | ||||
|     </div> | ||||
|     {{if .ResultBellow}} | ||||
|     <div class="box"> | ||||
|       <div id="result_editor"></div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|   </div> | ||||
| </div> | ||||
| {{if not .ResultBellow}} | ||||
| {{template "components/result" .}} | ||||
| 
 | ||||
| {{end}} | ||||
| {{template "base/footer" .}} | ||||
| {{end}} | ||||
							
								
								
									
										31
									
								
								util.go
								
								
								
								
							
							
						
						
									
										31
									
								
								util.go
								
								
								
								
							|  | @ -1,9 +1,11 @@ | |||
| package main | ||||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"compress/gzip" | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| ) | ||||
|  | @ -51,7 +53,7 @@ func matchIPGlob(ip, pattern string) bool { | |||
| 	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) | ||||
| 	defer gw.Close() | ||||
| 	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
 | ||||
| 			if !flat { | ||||
| 				if !strings.HasPrefix(file, "/") { | ||||
| 					file = "configui/" + file | ||||
| 					file = rootDir + "/" + file | ||||
| 				} else { | ||||
| 					file = "configui" + file | ||||
| 					file = rootDir + file | ||||
| 				} | ||||
| 				header.Name = file | ||||
| 			} | ||||
|  | @ -109,10 +111,19 @@ func bundle(buf io.Writer, paths []string, flat bool) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var ext2mode map[string]string = map[string]string{ | ||||
| 	"go":   "golang", | ||||
| 	"log":  "sh", | ||||
| 	"txt":  "text", | ||||
| 	"yml":  "yaml", | ||||
| 	"conf": "ini", | ||||
| 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 | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue