diff --git a/api.go b/api.go new file mode 100644 index 0000000..87daf10 --- /dev/null +++ b/api.go @@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "kumoly.io/tools/configui/configui" +) + +func ListFiles(w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(files) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(data) +} + +func GetFile(w http.ResponseWriter, r *http.Request) { + name := r.URL.Query().Get("name") + file, ok := files[name] + if name == "" || !ok { + w.WriteHeader(404) + w.Write([]byte("file not found")) + return + } + data, err := file.Read() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + response, err := json.Marshal(map[string]string{ + "path": file.Path, + "name": file.Alias, + "action": file.Action, + "data": string(data), + }) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(response) +} + +func PostFile(w http.ResponseWriter, r *http.Request) { + data, err := ioutil.ReadAll(r.Body) + r.Body.Close() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + f := configui.File{} + if err := json.Unmarshal(data, &f); err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + file, ok := files[f.Alias] + if !ok { + w.WriteHeader(404) + w.Write([]byte("file not found")) + return + } + if err := file.Write([]byte(f.Data)); err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write([]byte("ok")) +} + +func Apply(w http.ResponseWriter, r *http.Request) { + name := r.URL.Query().Get("name") + file, ok := files[name] + if name == "" || !ok { + w.WriteHeader(404) + w.Write([]byte("file not found")) + return + } + result, err := file.Do() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write([]byte(result)) +} + +func Download(w http.ResponseWriter, r *http.Request) { + fs := []string{} + for _, v := range files { + fs = append(fs, v.Path) + } + w.Header().Set("Content-Disposition", `attachment; filename="export.tar.gz"`) + bundle(w, fs, false) +} diff --git a/configui/file.go b/configui/file.go index 8b88994..78c840c 100644 --- a/configui/file.go +++ b/configui/file.go @@ -2,6 +2,7 @@ package configui import ( "encoding/json" + "errors" "log" "os" "os/exec" @@ -16,6 +17,7 @@ type File struct { Path string `json:"path"` Alias string `json:"name"` Action string `json:"action"` + RO bool `json:"ro"` // used for parsing post data Data string `json:"data"` @@ -35,6 +37,9 @@ func (f *File) Read() ([]byte, error) { } func (f *File) Write(data []byte) error { + if f.RO { + return errors.New("this file has readonly set") + } f.lock.Lock() defer f.lock.Unlock() info, err := os.Stat(f.Path) diff --git a/main.go b/main.go index 4ac5a8f..349ae80 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,11 @@ package main import ( "bytes" "embed" - "encoding/json" "errors" "flag" "fmt" "html/template" - "io/ioutil" + "io" "log" "net/http" "os" @@ -22,24 +21,26 @@ var tmplFS embed.FS var tmpl *template.Template var ( - cmdPath string - cmdAction string - cmdAlias string - cmdConfigPath string + flagPath string + flagAction string + flagAlias string + flagConfigPath string - cmdBind string + flagBind string + flagLogFile string ) var files = map[string]*configui.File{} func init() { - log.SetFlags(0) + // log.SetFlags(0) - flag.StringVar(&cmdConfigPath, "f", "", "path to config file") - flag.StringVar(&cmdPath, "p", "", "path to file, precedence over config") - flag.StringVar(&cmdAlias, "n", "", "alias of file") - flag.StringVar(&cmdAction, "c", "", "cmd to apply") - flag.StringVar(&cmdBind, "b", "localhost:8000", "address to bind") + 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(&flagAction, "c", "", "cmd to apply") + flag.StringVar(&flagLogFile, "log", "", "log to file") + flag.StringVar(&flagBind, "bind", "localhost:8000", "address to bind") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: configui [options]\n") flag.PrintDefaults() @@ -49,20 +50,31 @@ func init() { func main() { flag.Parse() + // setup logging + if flagLogFile != "" { + f, err := os.OpenFile(flagLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatalf("Error opening file: %v", err) + } + defer f.Close() + mwriter := io.MultiWriter(f, os.Stderr) + log.SetOutput(mwriter) + } + // setup values - if cmdPath != "" { - if cmdAlias == "" { - cmdAlias = cmdPath + if flagPath != "" { + if flagAlias == "" { + flagAlias = flagPath } - files[cmdAlias] = &configui.File{ - Path: cmdPath, - Alias: cmdAlias, - Action: cmdAction, + files[flagAlias] = &configui.File{ + Path: flagPath, + Alias: flagAlias, + Action: flagAction, } - } else if cmdConfigPath == "" { + } else if flagConfigPath == "" { log.Fatalln(errors.New("no config found")) } else { - conf, err := os.ReadFile(cmdConfigPath) + conf, err := os.ReadFile(flagConfigPath) if err != nil { log.Fatalln(err) } @@ -75,134 +87,62 @@ func main() { // setup routes mux := http.NewServeMux() - mux.HandleFunc("/api/files", func(rw http.ResponseWriter, r *http.Request) { + // mux. + mux.HandleFunc("/api/files", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - ListFiles(rw, r) + ListFiles(w, r) } else { - rw.WriteHeader(404) + w.WriteHeader(404) } }) - mux.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/file", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - GetFile(rw, r) + GetFile(w, r) } else if r.Method == "POST" { - PostFile(rw, r) + PostFile(w, r) } else { - rw.WriteHeader(404) + w.WriteHeader(404) } }) - mux.HandleFunc("/api/apply", func(rw http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/apply", func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { - Apply(rw, r) + Apply(w, r) } else { - rw.WriteHeader(404) + w.WriteHeader(404) } }) - mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/export", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + Download(w, r) + } else { + w.WriteHeader(404) + } + }) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } buf := &bytes.Buffer{} err := tmpl.ExecuteTemplate(buf, "home", nil) if err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) + w.WriteHeader(500) + w.Write([]byte(err.Error())) } else { - rw.Write(buf.Bytes()) + w.Write(buf.Bytes()) } }) tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl")) server := &http.Server{ - Addr: cmdBind, + Addr: flagBind, WriteTimeout: time.Second * 3, ReadTimeout: time.Second * 30, - Handler: mux, + Handler: LogMiddleware(mux), } // start server - log.Println("Listening on ", cmdBind) + log.Println("Listening on", flagBind) log.Fatal(server.ListenAndServe()) } - -func ListFiles(rw http.ResponseWriter, r *http.Request) { - data, err := json.Marshal(files) - if err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - rw.Write(data) -} - -func GetFile(rw http.ResponseWriter, r *http.Request) { - name := r.URL.Query().Get("name") - file, ok := files[name] - if name == "" || !ok { - rw.WriteHeader(404) - rw.Write([]byte("file not found")) - return - } - data, err := file.Read() - if err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - response, err := json.Marshal(map[string]string{ - "path": file.Path, - "name": file.Alias, - "action": file.Action, - "data": string(data), - }) - if err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - rw.Header().Set("Content-Type", "application/json") - rw.Write(response) -} - -func PostFile(rw http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadAll(r.Body) - r.Body.Close() - if err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - f := configui.File{} - if err := json.Unmarshal(data, &f); err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - file, ok := files[f.Alias] - if !ok { - rw.WriteHeader(404) - rw.Write([]byte("file not found")) - return - } - if err := file.Write([]byte(f.Data)); err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - rw.Write([]byte("ok")) -} - -func Apply(rw http.ResponseWriter, r *http.Request) { - name := r.URL.Query().Get("name") - file, ok := files[name] - if name == "" || !ok { - rw.WriteHeader(404) - rw.Write([]byte("file not found")) - return - } - result, err := file.Do() - if err != nil { - rw.WriteHeader(500) - rw.Write([]byte(err.Error())) - return - } - rw.Write([]byte(result)) -} diff --git a/util.go b/util.go new file mode 100644 index 0000000..f77bbaa --- /dev/null +++ b/util.go @@ -0,0 +1,72 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "io" + "log" + "net/http" + "os" +) + +func LogMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + log.Printf("%s %s %s %s\n", r.RemoteAddr, r.Method, r.URL, r.Header.Get("User-Agent")) + }, + ) +} + +func bundle(buf io.Writer, paths []string, flat bool) error { + gw := gzip.NewWriter(buf) + defer gw.Close() + tw := tar.NewWriter(gw) + defer tw.Close() + + for _, file := range paths { + if err := func(file string) error { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + + // Use full path as name (FileInfoHeader only takes the basename) + // If we don't do this the directory strucuture would + // not be preserved + // https://golang.org/src/archive/tar/common.go?#L626 + if !flat { + header.Name = file + } + + // Write file header to the tar archive + err = tw.WriteHeader(header) + if err != nil { + return err + } + + // Copy file content to tar archive + _, err = io.Copy(tw, f) + if err != nil { + return err + } + return nil + + }(file); err != nil { + return err + } + } + + return nil +}