Compare commits
3 Commits
1d7b8f5868
...
4ebb686b1c
Author | SHA1 | Date |
---|---|---|
Evan Chen | 4ebb686b1c | |
Evan Chen | 4bcce8ddfd | |
Evan Chen | f654cd347c |
|
@ -1 +1,2 @@
|
|||
dist
|
||||
dist
|
||||
node_modules
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
dist
|
||||
dist
|
||||
node_modules
|
||||
public/css/main.css*
|
||||
public/js/main.js*
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
42
api.go
42
api.go
|
@ -11,7 +11,7 @@ import (
|
|||
func ListFiles(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := json.Marshal(files)
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
|
@ -21,12 +21,12 @@ func GetFile(w http.ResponseWriter, r *http.Request) {
|
|||
name := r.URL.Query().Get("name")
|
||||
file, ok := files[name]
|
||||
if name == "" || !ok {
|
||||
HttpWriter(w, 404, "file not found")
|
||||
MakeResponse(w, 404, []byte("file not found"))
|
||||
return
|
||||
}
|
||||
data, err := file.Read()
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
response, err := json.Marshal(map[string]string{
|
||||
|
@ -36,7 +36,7 @@ func GetFile(w http.ResponseWriter, r *http.Request) {
|
|||
"data": string(data),
|
||||
})
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
@ -47,21 +47,21 @@ func PostFile(w http.ResponseWriter, r *http.Request) {
|
|||
data, err := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
f := configui.File{}
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
file, ok := files[f.Alias]
|
||||
if !ok {
|
||||
HttpWriter(w, 404, "file not found")
|
||||
MakeResponse(w, 404, []byte("file not found"))
|
||||
return
|
||||
}
|
||||
if err := file.Write([]byte(f.Data)); err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("ok"))
|
||||
|
@ -71,12 +71,12 @@ func Apply(w http.ResponseWriter, r *http.Request) {
|
|||
name := r.URL.Query().Get("name")
|
||||
file, ok := files[name]
|
||||
if name == "" || !ok {
|
||||
HttpWriter(w, 404, "file not found")
|
||||
MakeResponse(w, 404, []byte("file not found"))
|
||||
return
|
||||
}
|
||||
result, err := file.Do()
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(result))
|
||||
|
@ -95,27 +95,31 @@ func LoadConfig(w http.ResponseWriter, r *http.Request) {
|
|||
data, err := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
ftmp, err := configui.ReadConfig(string(data))
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
files = configui.GetFileMap(ftmp)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config := []configui.File{}
|
||||
for _, f := range files {
|
||||
config = append(config, *f)
|
||||
}
|
||||
data, err := json.Marshal(config)
|
||||
func getConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := GetConfig()
|
||||
if err != nil {
|
||||
HttpWriter(w, 500, err.Error())
|
||||
AbortError(w, err)
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func GetConfig() ([]byte, error) {
|
||||
config := []configui.File{}
|
||||
for _, f := range files {
|
||||
config = append(config, *f)
|
||||
}
|
||||
return json.Marshal(config)
|
||||
}
|
||||
|
|
25
main.go
25
main.go
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
@ -13,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"kumoly.io/tools/configui/configui"
|
||||
"kumoly.io/tools/configui/public"
|
||||
)
|
||||
|
||||
//go:embed templates
|
||||
|
@ -27,6 +27,7 @@ var (
|
|||
|
||||
flagBind string
|
||||
flagLogFile string
|
||||
flagAllow string
|
||||
flagVer bool
|
||||
)
|
||||
|
||||
|
@ -42,6 +43,7 @@ func init() {
|
|||
flag.StringVar(&flagAlias, "n", "", "alias 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")
|
||||
flag.StringVar(&flagBind, "bind", "localhost:8000", "address to bind")
|
||||
flag.BoolVar(&flagVer, "v", false, "show version")
|
||||
flag.Usage = func() {
|
||||
|
@ -96,7 +98,7 @@ func main() {
|
|||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/conf", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
GetConfig(w, r)
|
||||
getConfigHandler(w, r)
|
||||
} else if r.Method == "POST" {
|
||||
LoadConfig(w, r)
|
||||
} else {
|
||||
|
@ -133,28 +135,15 @@ func main() {
|
|||
w.WriteHeader(404)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
err := tmpl.ExecuteTemplate(buf, "home", nil)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
} else {
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
})
|
||||
|
||||
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{
|
||||
Addr: flagBind,
|
||||
WriteTimeout: time.Second * 3,
|
||||
ReadTimeout: time.Second * 30,
|
||||
Handler: LogMiddleware(mux),
|
||||
Handler: Middleware(mux),
|
||||
}
|
||||
|
||||
// start server
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
StatueCode int
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) WriteHeader(statusCode int) {
|
||||
if w.StatueCode != 0 {
|
||||
return
|
||||
}
|
||||
w.StatueCode = statusCode
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) 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) {
|
||||
w.WriteHeader(status)
|
||||
return w.Write(body)
|
||||
}
|
||||
|
||||
func AbortError(w http.ResponseWriter, err interface{}) (int, error) {
|
||||
switch v := err.(type) {
|
||||
case int:
|
||||
w.WriteHeader(v)
|
||||
return w.Write([]byte(strconv.Itoa(v)))
|
||||
case string:
|
||||
w.WriteHeader(500)
|
||||
return w.Write([]byte(v))
|
||||
case error:
|
||||
w.WriteHeader(500)
|
||||
return w.Write([]byte(v.Error()))
|
||||
default:
|
||||
w.WriteHeader(500)
|
||||
return w.Write([]byte(strconv.Itoa(500)))
|
||||
}
|
||||
}
|
||||
|
||||
func Catch(rw *ResponseWriter, r *http.Request) {
|
||||
ex := recover()
|
||||
if ex != nil {
|
||||
AbortError(rw, r)
|
||||
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)
|
||||
}
|
||||
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{}
|
||||
err := tmpl.ExecuteTemplate(buf, "home", data)
|
||||
if err != nil {
|
||||
AbortError(w, err)
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "configui",
|
||||
"version": "1.0.0",
|
||||
"description": "a web app to edit and action on update",
|
||||
"scripts": {
|
||||
"build-sass": "sass --style compressed src/main.scss public/css/main.css",
|
||||
"build-js": "parcel build src/main.js --dist-dir public/js",
|
||||
"start":"npm run build-sass && npm run build-js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@kumoly.io:tools/configui.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.3",
|
||||
"prismjs": "^1.25.0",
|
||||
"sass": "^1.43.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-sass": "^2.0.0",
|
||||
"parcel": "^2.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package public
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed js css
|
||||
var FS embed.FS
|
|
@ -0,0 +1,42 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func setRoutes(mux *http.ServeMux) {
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
Files := []string{}
|
||||
for file := range files {
|
||||
Files = append(Files, files[file].Alias)
|
||||
}
|
||||
|
||||
contentB, err := GetConfig()
|
||||
content := ""
|
||||
if err != nil {
|
||||
content = err.Error()
|
||||
} else {
|
||||
content = string(contentB)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Active string
|
||||
Files []string
|
||||
Content string
|
||||
Lang string
|
||||
}{
|
||||
Files: Files,
|
||||
Active: "ConfigUI",
|
||||
Content: content,
|
||||
Lang: "json",
|
||||
}
|
||||
|
||||
Parse(w, "home", data)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Prism from "prismjs";
|
||||
import "prismjs/plugins/custom-class/prism-custom-class";
|
||||
|
||||
|
||||
Prism.plugins.customClass.map({ number: "prism-number", tag: "prism-tag" });
|
|
@ -0,0 +1,2 @@
|
|||
@charset "utf-8";
|
||||
@import "../node_modules/bulma/bulma.sass";
|
|
@ -0,0 +1,233 @@
|
|||
/* Code-Input Compability */
|
||||
/* By WebCoder49 */
|
||||
/* First Published on CSS-Tricks.com */
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
|
||||
resize: none; /*remove the resize handle on the bottom right*/
|
||||
}
|
||||
.content .tag, .content .number {
|
||||
display: inline;
|
||||
padding: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
text-align: inherit;
|
||||
vertical-align: inherit;
|
||||
border-radius: inherit;
|
||||
font-weight: inherit;
|
||||
white-space: inherit;
|
||||
background: inherit;
|
||||
margin: inherit;
|
||||
}
|
||||
|
||||
/* Please see the article */
|
||||
|
||||
#editing, #highlighting {
|
||||
/* Both elements need the same text and space styling so they are directly on top of each other */
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
width: calc(100% - 32px);
|
||||
height: 150px;
|
||||
}
|
||||
#editing, #highlighting, #highlighting * {
|
||||
/* Also add text styles to highlighing tokens */
|
||||
font-size: 15pt;
|
||||
font-family: monospace;
|
||||
line-height: 20pt;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
|
||||
#editing, #highlighting {
|
||||
/* In the same place */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Move the textarea in front of the result */
|
||||
|
||||
#editing {
|
||||
z-index: 1;
|
||||
}
|
||||
#highlighting {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Make textarea almost completely transparent */
|
||||
|
||||
#editing {
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
caret-color: white; /* Or choose your favourite color */
|
||||
}
|
||||
|
||||
/* Can be scrolled */
|
||||
#editing, #highlighting {
|
||||
overflow: auto;
|
||||
white-space: nowrap; /* Allows textarea to scroll horizontally */
|
||||
}
|
||||
|
||||
/* No resize on textarea */
|
||||
#editing {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* Paragraphs; First Image */
|
||||
* {
|
||||
font-family: "Fira Code", monospace;
|
||||
}
|
||||
p code {
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
|
||||
/* Syntax Highlighting from prism.js starts below, partly modified: */
|
||||
|
||||
/* PrismJS 1.23.0
|
||||
https://prismjs.com/download.html#themes=prism-funky&languages=markup */
|
||||
/**
|
||||
* prism.js Funky theme
|
||||
* Based on “Polyfilling the gaps” talk slides http://lea.verou.me/polyfilling-the-gaps/
|
||||
* @author Lea Verou
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: .4em .8em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
/* background: url('data:image/svg+xml;charset=utf-8,<svg%20version%3D"1.1"%20xmlns%3D"http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg"%20width%3D"100"%20height%3D"100"%20fill%3D"rgba(0%2C0%2C0%2C.2)">%0D%0A<polygon%20points%3D"0%2C50%2050%2C0%200%2C0"%20%2F>%0D%0A<polygon%20points%3D"0%2C100%2050%2C100%20100%2C50%20100%2C0"%20%2F>%0D%0A<%2Fsvg>');
|
||||
background-size: 1em 1em; - WebCoder49*/
|
||||
background: black; /* - WebCoder49 */
|
||||
}
|
||||
|
||||
code[class*="language-"] {
|
||||
background: black;
|
||||
color: white;
|
||||
box-shadow: -.3em 0 0 .3em black, .3em 0 0 .3em black;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .2em;
|
||||
border-radius: .3em;
|
||||
box-shadow: none;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: #0cf;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.token.variable,
|
||||
.token.inserted {
|
||||
color: yellowgreen;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: deeppink;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.deleted {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Plugin styles: Diff Highlight */
|
||||
pre.diff-highlight.diff-highlight > code .token.deleted:not(.prefix),
|
||||
pre > code.diff-highlight.diff-highlight .token.deleted:not(.prefix) {
|
||||
background-color: rgba(255, 0, 0, .3);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
pre.diff-highlight.diff-highlight > code .token.inserted:not(.prefix),
|
||||
pre > code.diff-highlight.diff-highlight .token.inserted:not(.prefix) {
|
||||
background-color: rgba(0, 255, 128, .3);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* End of prism.js syntax highlighting*/
|
|
@ -0,0 +1,39 @@
|
|||
// codeInput
|
||||
// by WebCoder49
|
||||
// Based on a CSS-Tricks Post
|
||||
// Needs Prism.js
|
||||
function update(text) {
|
||||
let result_element = document.querySelector("#highlighting-content");
|
||||
// Handle final newlines (see article)
|
||||
if(text[text.length-1] == "\n") {
|
||||
text += " ";
|
||||
}
|
||||
// Update code
|
||||
result_element.innerHTML = text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
|
||||
// Syntax Highlight
|
||||
Prism.highlightElement(result_element);
|
||||
}
|
||||
|
||||
function sync_scroll(element) {
|
||||
/* Scroll result to scroll coords of event - sync with textarea */
|
||||
let result_element = document.querySelector("#highlighting");
|
||||
// Get and set x and y
|
||||
result_element.scrollTop = element.scrollTop;
|
||||
result_element.scrollLeft = element.scrollLeft;
|
||||
}
|
||||
|
||||
function check_tab(element, event) {
|
||||
let code = element.value;
|
||||
if(event.key == "Tab") {
|
||||
/* Tab key pressed */
|
||||
event.preventDefault(); // stop normal
|
||||
let before_tab = code.slice(0, element.selectionStart); // text before tab
|
||||
let after_tab = code.slice(element.selectionEnd, element.value.length); // text after tab
|
||||
let cursor_pos = element.selectionEnd + 1; // where cursor moves after tab - moving forward by 1 char to after tab
|
||||
element.value = before_tab + "\t" + after_tab; // add tab char
|
||||
// move cursor
|
||||
element.selectionStart = cursor_pos;
|
||||
element.selectionEnd = cursor_pos;
|
||||
update(element.value); // Update text to include indent
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
{{define "base/footer"}}
|
||||
<script src="/public/js/main.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> -->
|
||||
|
||||
<script src="public/js/prism1.23.0.min.js"></script>
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
|
@ -4,8 +4,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Serviced</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
||||
<title>ConfigUI</title>
|
||||
<link rel="stylesheet" href="/public/css/bulma0.9.3.min.css">
|
||||
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"> -->
|
||||
<link rel="stylesheet" href="/public/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
{{end}}
|
|
@ -1,14 +1,54 @@
|
|||
{{define "home"}}
|
||||
{{template "base/header" .}}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Hello World
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
My first website with <strong>Bulma</strong>!
|
||||
</p>
|
||||
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half">
|
||||
<section class="section">
|
||||
<container class="container is-max-desktop">
|
||||
<h1 class="title">ConfigUI</h1>
|
||||
</container>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="tile is-ancestor mx-2">
|
||||
<div class="tile is-3 is-vertical is-parent">
|
||||
<div class="tile is-child box">
|
||||
<section class="is-large">
|
||||
<aside class="menu">
|
||||
<p class="menu-label">
|
||||
Files
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
{{ range .Files }}
|
||||
<li><a>{{ . }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<p class="menu-label">
|
||||
ConfigUI
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
<li><a{{if eq .Active "ConfigUI"}} class="is-active"{{end}}>config.json</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
<div class="tile is-child box">
|
||||
<p class="title">Two</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<div class="tile is-child box">
|
||||
<p class="title">Three</p>
|
||||
<textarea class="content" placeholder="Enter HTML Source Code" id="editing" spellcheck="false" oninput="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);" onkeydown="check_tab(this, event);"></textarea>
|
||||
<pre id="highlighting" aria-hidden="true">
|
||||
<code class="language-html" id="highlighting-content">{{.Content}}</code>
|
||||
</pre>
|
||||
|
||||
<!-- <code-input class="content" id="edit" lang="{{.Lang}}" name="config">{{.Content}}</code-input> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "base/footer" .}}
|
||||
{{end}}
|
62
util.go
62
util.go
|
@ -4,26 +4,51 @@ import (
|
|||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func LogMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
log.Println("panic", r)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("%s %s %s %s\n", r.RemoteAddr, r.Method, r.URL, r.Header.Get("User-Agent"))
|
||||
},
|
||||
)
|
||||
func matchIPGlob(ip, pattern string) bool {
|
||||
parts := strings.Split(pattern, ".")
|
||||
seg := strings.Split(ip, ".")
|
||||
for i, part := range parts {
|
||||
|
||||
// normalize pattern to 3 digits
|
||||
switch len(part) {
|
||||
case 1:
|
||||
if part == "*" {
|
||||
part = "***"
|
||||
} else {
|
||||
part = "00" + part
|
||||
}
|
||||
case 2:
|
||||
if strings.HasPrefix(part, "*") {
|
||||
part = "*" + part
|
||||
} else if strings.HasSuffix(part, "*") {
|
||||
part = part + "*"
|
||||
} else {
|
||||
part = "0" + part
|
||||
}
|
||||
}
|
||||
|
||||
// normalize ip to 3 digits
|
||||
switch len(seg[i]) {
|
||||
case 1:
|
||||
seg[i] = "00" + seg[i]
|
||||
case 2:
|
||||
seg[i] = "0" + seg[i]
|
||||
}
|
||||
|
||||
for j := range part {
|
||||
if string(part[j]) == "*" {
|
||||
continue
|
||||
}
|
||||
if part[j] != seg[i][j] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func bundle(buf io.Writer, paths []string, flat bool) error {
|
||||
|
@ -83,8 +108,3 @@ func bundle(buf io.Writer, paths []string, flat bool) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HttpWriter(w http.ResponseWriter, status int, errStr string) {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(errStr))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue