From baa44d181224c9596df0d085915e5679167d9a04 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Mon, 3 Jul 2023 13:31:44 -0400 Subject: [PATCH] :sparkles: Add support for outputFile handler --- cmd/parka/cmds/examples.go | 179 +++++++++--------- cmd/parka/cmds/serve.go | 4 +- pkg/glazed/handler.go | 56 ------ pkg/glazed/handlers/datatables/datatables.go | 2 +- pkg/glazed/handlers/glazed/glazed.go | 119 ++++++++++++ pkg/glazed/handlers/handler.go | 10 + pkg/glazed/handlers/helpers.go | 26 ++- pkg/glazed/handlers/json/json.go | 2 +- .../handlers/output-file/output-file.go | 119 ++++++++++++ pkg/handlers/command-dir/command-dir.go | 61 +----- pkg/handlers/config-file.go | 45 +++++ 11 files changed, 421 insertions(+), 202 deletions(-) delete mode 100644 pkg/glazed/handler.go create mode 100644 pkg/glazed/handlers/glazed/glazed.go create mode 100644 pkg/glazed/handlers/handler.go create mode 100644 pkg/glazed/handlers/output-file/output-file.go diff --git a/cmd/parka/cmds/examples.go b/cmd/parka/cmds/examples.go index d531c87..53b161d 100644 --- a/cmd/parka/cmds/examples.go +++ b/cmd/parka/cmds/examples.go @@ -7,7 +7,9 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/parameters" "github.com/go-go-golems/glazed/pkg/middlewares" + "github.com/go-go-golems/glazed/pkg/settings" "github.com/go-go-golems/glazed/pkg/types" + "github.com/spf13/cobra" ) type ExampleCommand struct { @@ -15,94 +17,101 @@ type ExampleCommand struct { } func NewExampleCommand() *ExampleCommand { - return &ExampleCommand{ - description: &cmds.CommandDescription{ - Name: "example", - Short: "Short parka example command", - Long: "", - Flags: []*parameters.ParameterDefinition{ - // required string test argument - { - Name: "test", - ShortFlag: "t", - Type: parameters.ParameterTypeString, - Help: "Test string argument", - Default: "test", - }, - { - Name: "string", - ShortFlag: "s", - Type: parameters.ParameterTypeString, - Help: "Test string flag", - Default: "default", - Required: false, - }, - { - Name: "string_from_file", - Type: parameters.ParameterTypeStringFromFile, - Help: "Test string from file flag", - }, - { - Name: "object_from_file", - Type: parameters.ParameterTypeObjectFromFile, - Help: "Test object from file flag", - }, - { - Name: "integer", - ShortFlag: "i", - Type: parameters.ParameterTypeInteger, - Help: "Test integer flag", - Default: 1, - }, - { - Name: "float", - ShortFlag: "f", - Type: parameters.ParameterTypeFloat, - Help: "Test float flag", - Default: 1.0, - }, - { - Name: "bool", - ShortFlag: "b", - Type: parameters.ParameterTypeBool, - Help: "Test bool flag", - }, - { - Name: "date", - ShortFlag: "d", - Type: parameters.ParameterTypeDate, - Help: "Test date flag", - }, - { - Name: "string_list", - ShortFlag: "l", - Type: parameters.ParameterTypeStringList, - Help: "Test string list flag", - Default: []string{"default", "default2"}, - }, - { - Name: "integer_list", - Type: parameters.ParameterTypeIntegerList, - Help: "Test integer list flag", - Default: []int{1, 2}, - }, - { - Name: "float_list", - Type: parameters.ParameterTypeFloatList, - Help: "Test float list flag", - Default: []float64{1.0, 2.0}, - }, - { - Name: "choice", - ShortFlag: "c", - Type: parameters.ParameterTypeChoice, - Help: "Test choice flag", - Choices: []string{"choice1", "choice2"}, - Default: "choice1", - }, + glazedParameterLayer, err := settings.NewGlazedParameterLayers() + cobra.CheckErr(err) + + description := &cmds.CommandDescription{ + Name: "example", + Short: "Short parka example command", + Long: "", + Flags: []*parameters.ParameterDefinition{ + // required string test argument + { + Name: "test", + ShortFlag: "t", + Type: parameters.ParameterTypeString, + Help: "Test string argument", + Default: "test", + }, + { + Name: "string", + ShortFlag: "s", + Type: parameters.ParameterTypeString, + Help: "Test string flag", + Default: "default", + Required: false, + }, + { + Name: "string_from_file", + Type: parameters.ParameterTypeStringFromFile, + Help: "Test string from file flag", + }, + { + Name: "object_from_file", + Type: parameters.ParameterTypeObjectFromFile, + Help: "Test object from file flag", + }, + { + Name: "integer", + ShortFlag: "i", + Type: parameters.ParameterTypeInteger, + Help: "Test integer flag", + Default: 1, + }, + { + Name: "float", + ShortFlag: "f", + Type: parameters.ParameterTypeFloat, + Help: "Test float flag", + Default: 1.0, + }, + { + Name: "bool", + ShortFlag: "b", + Type: parameters.ParameterTypeBool, + Help: "Test bool flag", }, + { + Name: "date", + ShortFlag: "d", + Type: parameters.ParameterTypeDate, + Help: "Test date flag", + }, + { + Name: "string_list", + ShortFlag: "l", + Type: parameters.ParameterTypeStringList, + Help: "Test string list flag", + Default: []string{"default", "default2"}, + }, + { + Name: "integer_list", + Type: parameters.ParameterTypeIntegerList, + Help: "Test integer list flag", + Default: []int{1, 2}, + }, + { + Name: "float_list", + Type: parameters.ParameterTypeFloatList, + Help: "Test float list flag", + Default: []float64{1.0, 2.0}, + }, + { + Name: "choice", + ShortFlag: "c", + Type: parameters.ParameterTypeChoice, + Help: "Test choice flag", + Choices: []string{"choice1", "choice2"}, + Default: "choice1", + }, + }, + Layers: []layers.ParameterLayer{ + glazedParameterLayer, }, } + return &ExampleCommand{ + description: description, + } } func (e *ExampleCommand) Run( diff --git a/cmd/parka/cmds/serve.go b/cmd/parka/cmds/serve.go index 336067c..834dd3c 100644 --- a/cmd/parka/cmds/serve.go +++ b/cmd/parka/cmds/serve.go @@ -9,6 +9,7 @@ import ( "github.com/go-go-golems/glazed/pkg/types" "github.com/go-go-golems/parka/pkg/glazed/handlers/datatables" json2 "github.com/go-go-golems/parka/pkg/glazed/handlers/json" + output_file "github.com/go-go-golems/parka/pkg/glazed/handlers/output-file" "github.com/go-go-golems/parka/pkg/render" "github.com/go-go-golems/parka/pkg/server" "github.com/go-go-golems/parka/pkg/utils/fs" @@ -72,7 +73,8 @@ var ServeCmd = &cobra.Command{ // NOTE(manuel, 2023-05-26) This could also be done with a simple Command config file struct once // implemented as part of sqleton serve s.Router.GET("/api/example", json2.HandleJSONQueryHandler(NewExampleCommand())) - s.Router.GET("/sqleton/example", datatables.HandleDataTables(NewExampleCommand(), "", "example")) + s.Router.GET("/example", datatables.HandleDataTables(NewExampleCommand(), "", "example")) + s.Router.GET("/download/example.csv", output_file.HandleGlazedOutputFileHandler(NewExampleCommand(), "example.csv")) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/pkg/glazed/handler.go b/pkg/glazed/handler.go deleted file mode 100644 index 97bc2f6..0000000 --- a/pkg/glazed/handler.go +++ /dev/null @@ -1,56 +0,0 @@ -package glazed - -import ( - "bytes" - "github.com/gin-gonic/gin" - "io" - "os" - "path/filepath" -) - -type Handler interface { - Handle(c *gin.Context, w io.Writer) error -} - -type OutputFileHandler struct { - handler Handler - outputFileName string -} - -func NewOutputFileHandler(handler Handler, outputFileName string) *OutputFileHandler { - h := &OutputFileHandler{ - handler: handler, - outputFileName: outputFileName, - } - - return h -} - -func (h *OutputFileHandler) Handle(c *gin.Context, w io.Writer) error { - buf := &bytes.Buffer{} - err := h.handler.Handle(c, buf) - if err != nil { - return err - } - - c.Status(200) - - f, err := os.Open(h.outputFileName) - if err != nil { - return err - } - defer func(f *os.File) { - _ = f.Close() - }(f) - - baseName := filepath.Base(h.outputFileName) - - c.Writer.Header().Set("Content-Disposition", "attachment; filename="+baseName) - - _, err = io.Copy(c.Writer, f) - if err != nil { - return err - } - - return nil -} diff --git a/pkg/glazed/handlers/datatables/datatables.go b/pkg/glazed/handlers/datatables/datatables.go index 62172fe..4f7985c 100644 --- a/pkg/glazed/handlers/datatables/datatables.go +++ b/pkg/glazed/handlers/datatables/datatables.go @@ -198,7 +198,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { dt_.HTMLStream = make(chan template.HTML, 100) } - gp, err := handlers.CreateTableProcessor(pc, "table", "") + gp, err := handlers.CreateTableProcessorWithOutput(pc, "table", "") if err != nil { return err } diff --git a/pkg/glazed/handlers/glazed/glazed.go b/pkg/glazed/handlers/glazed/glazed.go new file mode 100644 index 0000000..396afbf --- /dev/null +++ b/pkg/glazed/handlers/glazed/glazed.go @@ -0,0 +1,119 @@ +package glazed + +import ( + "github.com/gin-gonic/gin" + "github.com/go-go-golems/glazed/pkg/cmds" + "github.com/go-go-golems/glazed/pkg/middlewares/table" + "github.com/go-go-golems/glazed/pkg/settings" + "github.com/go-go-golems/parka/pkg/glazed" + "github.com/go-go-golems/parka/pkg/glazed/parser" + "github.com/rs/zerolog/log" + "io" + "net/http" +) + +type QueryHandler struct { + cmd cmds.GlazeCommand + contextMiddlewares []glazed.ContextMiddleware + parserOptions []parser.ParserOption +} + +type QueryHandlerOption func(*QueryHandler) + +func NewQueryHandler(cmd cmds.GlazeCommand, options ...QueryHandlerOption) *QueryHandler { + h := &QueryHandler{ + cmd: cmd, + } + + for _, option := range options { + option(h) + } + + return h +} + +func WithQueryHandlerContextMiddlewares(middlewares ...glazed.ContextMiddleware) QueryHandlerOption { + return func(h *QueryHandler) { + h.contextMiddlewares = middlewares + } +} + +// WithQueryHandlerParserOptions sets the parser options for the QueryHandler +func WithQueryHandlerParserOptions(options ...parser.ParserOption) QueryHandlerOption { + return func(h *QueryHandler) { + h.parserOptions = options + } +} + +func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { + pc := glazed.NewCommandContext(h.cmd) + + h.contextMiddlewares = append( + h.contextMiddlewares, + glazed.NewContextParserMiddleware( + h.cmd, + glazed.NewCommandQueryParser(h.cmd, h.parserOptions...), + ), + ) + + for _, h := range h.contextMiddlewares { + err := h.Handle(c, pc) + if err != nil { + return err + } + } + + glazedLayer := pc.ParsedLayers["glazed"] + + ps := make(map[string]interface{}) + if glazedLayer != nil { + ps = glazedLayer.Parameters + } + + gp, err := settings.SetupTableProcessor(ps) + if err != nil { + return err + } + + of, err := settings.SetupTableOutputFormatter(ps) + if err != nil { + return err + } + + // TODO(manuel, 2023-07-02) It would be good to use streaming here for the formats that support it + // See: https://github.com/go-go-golems/parka/issues/68 + gp.AddTableMiddleware(table.NewOutputMiddleware(of, writer)) + + c.Header("Content-Type", of.ContentType()) + + ctx := c.Request.Context() + err = h.cmd.Run(ctx, pc.ParsedLayers, pc.ParsedParameters, gp) + if err != nil { + return err + } + + err = gp.Close(ctx) + if err != nil { + return err + } + + return nil +} + +func HandleQueryHandler( + cmd cmds.GlazeCommand, + parserOptions ...parser.ParserOption, +) gin.HandlerFunc { + handler := NewQueryHandler(cmd, + WithQueryHandlerParserOptions(parserOptions...), + ) + return func(c *gin.Context) { + err := handler.Handle(c, c.Writer) + if err != nil { + log.Error().Err(err).Msg("failed to handle query") + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + } + } +} diff --git a/pkg/glazed/handlers/handler.go b/pkg/glazed/handlers/handler.go new file mode 100644 index 0000000..6a61c90 --- /dev/null +++ b/pkg/glazed/handlers/handler.go @@ -0,0 +1,10 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "io" +) + +type Handler interface { + Handle(c *gin.Context, w io.Writer) error +} diff --git a/pkg/glazed/handlers/helpers.go b/pkg/glazed/handlers/helpers.go index 2ccd921..da5f029 100644 --- a/pkg/glazed/handlers/helpers.go +++ b/pkg/glazed/handlers/helpers.go @@ -6,7 +6,7 @@ import ( "github.com/go-go-golems/parka/pkg/glazed" ) -func CreateTableProcessor(pc *glazed.CommandContext, outputType string, tableType string) (*middlewares.TableProcessor, error) { +func CreateTableProcessorWithOutput(pc *glazed.CommandContext, outputType string, tableFormat string) (*middlewares.TableProcessor, error) { var gp *middlewares.TableProcessor var err error @@ -14,17 +14,35 @@ func CreateTableProcessor(pc *glazed.CommandContext, outputType string, tableTyp if glazedLayer != nil { glazedLayer.Parameters["output"] = outputType - glazedLayer.Parameters["table"] = tableType + glazedLayer.Parameters["table"] = tableFormat gp, err = settings.SetupTableProcessor(glazedLayer.Parameters) if err != nil { return nil, err } } else { gp, err = settings.SetupTableProcessor(map[string]interface{}{ - "output": outputType, - "table": tableType, + "output": outputType, + "table-format": tableFormat, }) } return gp, err } + +func CreateTableProcessor(pc *glazed.CommandContext) (*middlewares.TableProcessor, error) { + var gp *middlewares.TableProcessor + var err error + + glazedLayer := pc.ParsedLayers["glazed"] + + if glazedLayer != nil { + gp, err = settings.SetupTableProcessor(glazedLayer.Parameters) + if err != nil { + return nil, err + } + } else { + gp, err = settings.SetupTableProcessor(map[string]interface{}{}) + } + + return gp, err +} diff --git a/pkg/glazed/handlers/json/json.go b/pkg/glazed/handlers/json/json.go index 3ac1b17..a028134 100644 --- a/pkg/glazed/handlers/json/json.go +++ b/pkg/glazed/handlers/json/json.go @@ -64,7 +64,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { } } - gp, err := handlers.CreateTableProcessor(pc, "json", "") + gp, err := handlers.CreateTableProcessorWithOutput(pc, "json", "") if err != nil { return err } diff --git a/pkg/glazed/handlers/output-file/output-file.go b/pkg/glazed/handlers/output-file/output-file.go new file mode 100644 index 0000000..b81541a --- /dev/null +++ b/pkg/glazed/handlers/output-file/output-file.go @@ -0,0 +1,119 @@ +package output_file + +import ( + "bytes" + "fmt" + "github.com/gin-gonic/gin" + "github.com/go-go-golems/glazed/pkg/cmds" + "github.com/go-go-golems/parka/pkg/glazed/handlers" + "github.com/go-go-golems/parka/pkg/glazed/handlers/glazed" + "github.com/go-go-golems/parka/pkg/glazed/parser" + "io" + "os" + "path/filepath" + "strings" +) + +type OutputFileHandler struct { + handler handlers.Handler + outputFileName string +} + +func NewOutputFileHandler(handler handlers.Handler, outputFileName string) *OutputFileHandler { + h := &OutputFileHandler{ + handler: handler, + outputFileName: outputFileName, + } + + return h +} + +func (h *OutputFileHandler) Handle(c *gin.Context, w io.Writer) error { + buf := &bytes.Buffer{} + err := h.handler.Handle(c, buf) + if err != nil { + return err + } + + c.Status(200) + + f, err := os.Open(h.outputFileName) + if err != nil { + return err + } + defer func(f *os.File) { + _ = f.Close() + }(f) + + baseName := filepath.Base(h.outputFileName) + + c.Writer.Header().Set("Content-Disposition", "attachment; filename="+baseName) + + _, err = io.Copy(c.Writer, f) + if err != nil { + return err + } + + return nil +} + +func HandleGlazedOutputFileHandler( + cmd cmds.GlazeCommand, + fileName string, + parserOptions ...parser.ParserOption, +) gin.HandlerFunc { + return func(c *gin.Context) { + // create a temporary file for glazed output + tmpFile, err := os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName)) + if err != nil { + c.JSON(500, gin.H{"error": "could not create temporary file"}) + return + } + defer func(name string) { + _ = os.Remove(name) + }(tmpFile.Name()) + + // now check file suffix for content-type + glazedOverrides := map[string]interface{}{ + "output-file": tmpFile.Name(), + } + if strings.HasSuffix(fileName, ".csv") { + glazedOverrides["output"] = "table" + glazedOverrides["table-format"] = "csv" + } else if strings.HasSuffix(fileName, ".tsv") { + glazedOverrides["output"] = "table" + glazedOverrides["table-format"] = "tsv" + } else if strings.HasSuffix(fileName, ".md") { + glazedOverrides["output"] = "table" + glazedOverrides["table-format"] = "markdown" + } else if strings.HasSuffix(fileName, ".html") { + glazedOverrides["output"] = "table" + glazedOverrides["table-format"] = "html" + } else if strings.HasSuffix(fileName, ".json") { + glazedOverrides["output"] = "json" + } else if strings.HasSuffix(fileName, ".yaml") { + glazedOverrides["output"] = "yaml" + } else if strings.HasSuffix(fileName, ".xlsx") { + glazedOverrides["output"] = "excel" + } else if strings.HasSuffix(fileName, ".txt") { + glazedOverrides["output"] = "table" + glazedOverrides["table-format"] = "ascii" + } else { + c.JSON(500, gin.H{"error": "could not determine output format"}) + return + } + + // override parameter layers at the end + parserOptions = append(parserOptions, parser.WithAppendOverrides("glazed", glazedOverrides)) + + handler := glazed.NewQueryHandler(cmd, glazed.WithQueryHandlerParserOptions(parserOptions...)) + outputFileHandler := NewOutputFileHandler(handler, tmpFile.Name()) + + err = outputFileHandler.Handle(c, c.Writer) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + } +} diff --git a/pkg/handlers/command-dir/command-dir.go b/pkg/handlers/command-dir/command-dir.go index 37932a5..1cabdf9 100644 --- a/pkg/handlers/command-dir/command-dir.go +++ b/pkg/handlers/command-dir/command-dir.go @@ -8,6 +8,7 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/parka/pkg/glazed/handlers/datatables" "github.com/go-go-golems/parka/pkg/glazed/handlers/json" + output_file "github.com/go-go-golems/parka/pkg/glazed/handlers/output-file" "github.com/go-go-golems/parka/pkg/glazed/parser" "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/go-go-golems/parka/pkg/render" @@ -463,61 +464,13 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { // JSON output and error code already handled by getRepositoryCommand return } - - // create a temporary file for glazed output - tmpFile, err := os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName)) - if err != nil { - c.JSON(500, gin.H{"error": "could not create temporary file"}) - return - } - defer func(name string) { - _ = os.Remove(name) - }(tmpFile.Name()) - - // now check file suffix for content-type - glazedOverrides := map[string]interface{}{ - "output-file": tmpFile.Name(), - } - if strings.HasSuffix(fileName, ".csv") { - glazedOverrides["output"] = "table" - glazedOverrides["table-format"] = "csv" - } else if strings.HasSuffix(fileName, ".tsv") { - glazedOverrides["output"] = "table" - glazedOverrides["table-format"] = "tsv" - } else if strings.HasSuffix(fileName, ".md") { - glazedOverrides["output"] = "table" - glazedOverrides["table-format"] = "markdown" - } else if strings.HasSuffix(fileName, ".html") { - glazedOverrides["output"] = "table" - glazedOverrides["table-format"] = "html" - } else if strings.HasSuffix(fileName, ".json") { - glazedOverrides["output"] = "json" - } else if strings.HasSuffix(fileName, ".yaml") { - glazedOverrides["yaml"] = "yaml" - } else if strings.HasSuffix(fileName, ".xlsx") { - glazedOverrides["output"] = "excel" - } else if strings.HasSuffix(fileName, ".txt") { - glazedOverrides["output"] = "table" - glazedOverrides["table-format"] = "ascii" - } else { - c.JSON(500, gin.H{"error": "could not determine output format"}) - return - } - parserOptions := cd.computeParserOptions() - // override parameter layers at the end - parserOptions = append(parserOptions, parser.WithAppendOverrides("glazed", glazedOverrides)) - _ = parserOptions - - _ = sqlCommand - //handle := server.HandleSimpleQueryOutputFileCommand( - // sqlCommand, - // tmpFile.Name(), - // fileName, - // glazed.WithParserOptions(parserOptions...), - //) - // - //handle(c) + + output_file.HandleGlazedOutputFileHandler( + sqlCommand, + fileName, + parserOptions..., + ) }) return nil diff --git a/pkg/handlers/config-file.go b/pkg/handlers/config-file.go index 589b19b..6f8deed 100644 --- a/pkg/handlers/config-file.go +++ b/pkg/handlers/config-file.go @@ -2,7 +2,10 @@ package handlers import ( "context" + "fmt" "github.com/go-go-golems/clay/pkg/repositories" + "github.com/go-go-golems/glazed/pkg/cmds" + "github.com/go-go-golems/glazed/pkg/cmds/loaders" "github.com/go-go-golems/glazed/pkg/helpers/strings" "github.com/go-go-golems/parka/pkg/handlers/command-dir" "github.com/go-go-golems/parka/pkg/handlers/config" @@ -13,6 +16,7 @@ import ( "github.com/go-go-golems/parka/pkg/render" "github.com/go-go-golems/parka/pkg/server" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/spf13/viper" "golang.org/x/sync/errgroup" "os" @@ -26,6 +30,47 @@ import ( // This is used to provision the CommandDir handlers. type RepositoryFactory func(dirs []string) (*repositories.Repository, error) +func NewRepositoryFactoryFromLoaders( + commandLoader loaders.ReaderCommandLoader, + fsLoader loaders.FSCommandLoader, +) RepositoryFactory { + return func(dirs []string) (*repositories.Repository, error) { + r := repositories.NewRepository( + repositories.WithDirectories(dirs), + repositories.WithUpdateCallback(func(cmd cmds.Command) error { + description := cmd.Description() + log.Info().Str("name", description.Name). + Str("source", description.Source). + Msg("Updating cmd") + // TODO(manuel, 2023-04-19) This is where we would recompute the HandlerFunc used below in GET and POST + return nil + }), + repositories.WithRemoveCallback(func(cmd cmds.Command) error { + description := cmd.Description() + log.Info().Str("name", description.Name). + Str("source", description.Source). + Msg("Removing cmd") + // TODO(manuel, 2023-04-19) This is where we would recompute the HandlerFunc used below in GET and POST + // NOTE(manuel, 2023-05-25) Regarding the above TODO, why? + // We don't need to recompute the func, since it fetches the command at runtime. + return nil + }), + repositories.WithCommandLoader(commandLoader), + repositories.WithFSLoader(fsLoader), + ) + + err := r.LoadCommands() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error initializing commands: %s\n", err) + os.Exit(1) + } + + return r, nil + + } + +} + // ConfigFileHandler contains everything needed to serve a config file type ConfigFileHandler struct { Config *config.Config