update
							parent
							
								
									8432135f3d
								
							
						
					
					
						commit
						1dbb80d326
					
				
							
								
								
									
										16
									
								
								api.go
								
								
								
								
							
							
						
						
									
										16
									
								
								api.go
								
								
								
								
							|  | @ -107,15 +107,19 @@ func LoadConfig(w http.ResponseWriter, r *http.Request) { | |||
| 	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()) | ||||
| 		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
 | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -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,6 @@ | |||
| package public | ||||
| 
 | ||||
| import "embed" | ||||
| 
 | ||||
| //go:embed js css
 | ||||
| var FS embed.FS | ||||
|  | @ -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
 | ||||
|     } | ||||
|   } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -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", | ||||
| 		} | ||||
| 
 | ||||
| 		serve(w, "home", data) | ||||
| 	}) | ||||
| } | ||||
|  | @ -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}} | ||||
							
								
								
									
										46
									
								
								util.go
								
								
								
								
							
							
						
						
									
										46
									
								
								util.go
								
								
								
								
							|  | @ -2,15 +2,17 @@ package main | |||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"bytes" | ||||
| 	"compress/gzip" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| func LogMiddleware(next http.Handler) http.Handler { | ||||
| func Middleware(next http.Handler) http.Handler { | ||||
| 	return http.HandlerFunc( | ||||
| 		func(w http.ResponseWriter, r *http.Request) { | ||||
| 			defer func() { | ||||
|  | @ -20,8 +22,18 @@ func LogMiddleware(next http.Handler) http.Handler { | |||
| 					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")) | ||||
| 			abort := false | ||||
| 			if flagAllow != "" { | ||||
| 				if GetIP(r) != flagAllow { | ||||
| 					HttpWriter(w, 403, "permission denyed") | ||||
| 					abort = true | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if !abort { | ||||
| 				next.ServeHTTP(w, r) | ||||
| 			} | ||||
| 			log.Printf("%s %s %s %s\n", GetIP(r), r.Method, r.URL, r.Header.Get("User-Agent")) | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
|  | @ -88,3 +100,31 @@ func HttpWriter(w http.ResponseWriter, status int, errStr string) { | |||
| 	w.WriteHeader(status) | ||||
| 	w.Write([]byte(errStr)) | ||||
| } | ||||
| 
 | ||||
| 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 serve(w http.ResponseWriter, name string, data interface{}) error { | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	err := tmpl.ExecuteTemplate(buf, "home", data) | ||||
| 	if err != nil { | ||||
| 		HttpWriter(w, 500, err.Error()) | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = w.Write(buf.Bytes()) | ||||
| 	return err | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue