Skip to content

Commit

Permalink
Merge pull request #107 from wesen/bug/pass-middlewares-all-handlers
Browse files Browse the repository at this point in the history
Properly pass middlewares through all handlers
  • Loading branch information
wesen authored Feb 7, 2025
2 parents bc16f5c + b6026f5 commit 52c569a
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 155 deletions.
15 changes: 10 additions & 5 deletions pkg/glazed/handlers/glazed/glazed.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package glazed

import (
"net/http"

"github.com/go-go-golems/glazed/pkg/cmds"
"github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
Expand All @@ -10,7 +12,6 @@ import (
middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"net/http"
)

type QueryHandler struct {
Expand Down Expand Up @@ -44,16 +45,20 @@ func (h *QueryHandler) Handle(c echo.Context) error {
description := h.cmd.Description()
parsedLayers := layers.NewParsedLayers()

middlewares_ := append(h.middlewares,
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
middlewares.SetFromDefaults(),
middlewares_ := append(
[]middlewares.Middleware{
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
},
h.middlewares...,
)
middlewares_ = append(middlewares_, middlewares.SetFromDefaults())

err := middlewares.ExecuteMiddlewares(description.Layers, parsedLayers, middlewares_...)
if err != nil {
return err
}

glazedLayer, ok := parsedLayers.Get("glazed")
glazedLayer, ok := parsedLayers.Get(settings.GlazedSlug)
if !ok {
return errors.New("glazed layer not found")
}
Expand Down
9 changes: 6 additions & 3 deletions pkg/glazed/handlers/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ func (h *QueryHandler) Handle(c echo.Context) error {
parsedLayers := layers.NewParsedLayers()

middlewares_ := append(
h.middlewares,
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
middlewares.SetFromDefaults(),
[]middlewares.Middleware{
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
},
h.middlewares...,
)
middlewares_ = append(middlewares_, middlewares.SetFromDefaults())

err := middlewares.ExecuteMiddlewares(description.Layers, parsedLayers, middlewares_...)
if err != nil {
return err
Expand Down
254 changes: 141 additions & 113 deletions pkg/glazed/handlers/output-file/output-file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,131 +2,159 @@ package output_file

import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"

"github.com/go-go-golems/glazed/pkg/cmds"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
"github.com/go-go-golems/glazed/pkg/helpers/list"
"github.com/go-go-golems/glazed/pkg/settings"
"github.com/go-go-golems/parka/pkg/glazed/handlers/glazed"
parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
)

// CreateGlazedFileHandler creates a handler that will run a glazed command and write the output
// with a Content-Disposition header to the response writer.
//
// If an output format requires writing to a temporary file locally, such as excel,
// the handler is wrapped in a temporary file handler.
func CreateGlazedFileHandler(
cmd cmds.GlazeCommand,
fileName string,
middlewares_ ...middlewares.Middleware,
) echo.HandlerFunc {
return func(c echo.Context) error {
glazedOverrides := map[string]interface{}{}
needsRealFileOutput := false

// create a temporary file for glazed output
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"
needsRealFileOutput = true
} else if strings.HasSuffix(fileName, ".txt") {
glazedOverrides["output"] = "table"
glazedOverrides["table-format"] = "ascii"
} else {
return errors.New("unsupported file format")
type QueryHandler struct {
cmd cmds.GlazeCommand
fileName string
middlewares []middlewares.Middleware
}

type QueryHandlerOption func(*QueryHandler)

func NewQueryHandler(cmd cmds.GlazeCommand, fileName string, options ...QueryHandlerOption) *QueryHandler {
h := &QueryHandler{
cmd: cmd,
fileName: fileName,
}

for _, option := range options {
option(h)
}

return h
}

func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption {
return func(handler *QueryHandler) {
handler.middlewares = middlewares
}
}

func (h *QueryHandler) Handle(c echo.Context) error {
glazedOverrides := map[string]interface{}{}
needsRealFileOutput := false

// create a temporary file for glazed output
if strings.HasSuffix(h.fileName, ".csv") {
glazedOverrides["output"] = "table"
glazedOverrides["table-format"] = "csv"
} else if strings.HasSuffix(h.fileName, ".tsv") {
glazedOverrides["output"] = "table"
glazedOverrides["table-format"] = "tsv"
} else if strings.HasSuffix(h.fileName, ".md") {
glazedOverrides["output"] = "table"
glazedOverrides["table-format"] = "markdown"
} else if strings.HasSuffix(h.fileName, ".html") {
glazedOverrides["output"] = "table"
glazedOverrides["table-format"] = "html"
} else if strings.HasSuffix(h.fileName, ".json") {
glazedOverrides["output"] = "json"
} else if strings.HasSuffix(h.fileName, ".yaml") {
glazedOverrides["output"] = "yaml"
} else if strings.HasSuffix(h.fileName, ".xlsx") {
glazedOverrides["output"] = "excel"
needsRealFileOutput = true
} else if strings.HasSuffix(h.fileName, ".txt") {
glazedOverrides["output"] = "table"
glazedOverrides["table-format"] = "ascii"
} else {
return errors.New("unsupported file format")
}

var tmpFile *os.File
var err error

glazedOverride := middlewares.UpdateFromMap(
map[string]map[string]interface{}{
settings.GlazedSlug: glazedOverrides,
},
parameters.WithParseStepSource("output-file-glazed-override"),
)

middlewares_ := append(
[]middlewares.Middleware{
glazedOverride,
},
h.middlewares...,
)

handler := glazed.NewQueryHandler(h.cmd,
glazed.WithMiddlewares(middlewares_...),
)

baseName := filepath.Base(h.fileName)
c.Response().Header().Set("Content-Disposition", "attachment; filename="+baseName)

// excel output needs a real output file, otherwise we can go stream to the HTTP response
if needsRealFileOutput {
tmpFile, err = os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", h.fileName))
if err != nil {
return errors.Wrap(err, "could not create temporary file")
}
defer func(name string) {
_ = os.Remove(name)
}(tmpFile.Name())

// now check file suffix for content-type
glazedOverrides["output-file"] = tmpFile.Name()

// here we have the output of the handler go to a request that we discard, and
// we instead copy the temporary file to the response writer
res := httptest.NewRecorder()
req := c.Request()
newCtx := c.Echo().NewContext(req, res)

var tmpFile *os.File
var err error

glazedOverride := middlewares.UpdateFromMap(
map[string]map[string]interface{}{
settings.GlazedSlug: glazedOverrides,
},
parameters.WithParseStepSource("output-file-glazed-override"),
)

handler := glazed.NewQueryHandler(cmd,
glazed.WithMiddlewares(
list.Prepend(middlewares_,
parka_middlewares.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
glazedOverride)...,
))

baseName := filepath.Base(fileName)
c.Response().Header().Set("Content-Disposition", "attachment; filename="+baseName)

// excel output needs a real output file, otherwise we can go stream to the HTTP response
if needsRealFileOutput {
tmpFile, err = os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName))
if err != nil {
return errors.Wrap(err, "could not create temporary file")
}
defer func(name string) {
_ = os.Remove(name)
}(tmpFile.Name())

// now check file suffix for content-type
glazedOverrides["output-file"] = tmpFile.Name()

// here we have the output of the handler go to a request that we discard, and
// we instead copy the temporary file to the response writer
res := httptest.NewRecorder()
req := c.Request()
newCtx := c.Echo().NewContext(req, res)

err = handler.Handle(newCtx)
if err != nil {
return err
}

// copy tmpFile to output
f, err := os.Open(tmpFile.Name())
if err != nil {
return errors.Wrap(err, "could not open temporary file")
}
defer func(f *os.File) {
_ = f.Close()
}(f)

c.Response().Header().Set("Content-Type", "application/octet-stream")
c.Response().WriteHeader(http.StatusOK)

_, err = io.Copy(c.Response().Writer, f)
if err != nil {
return err
}
} else {
err = handler.Handle(c)
if err != nil {
return err
}
err = handler.Handle(newCtx)
if err != nil {
return err
}

return nil
// copy tmpFile to output
f, err := os.Open(tmpFile.Name())
if err != nil {
return errors.Wrap(err, "could not open temporary file")
}
defer func(f *os.File) {
_ = f.Close()
}(f)

c.Response().Header().Set("Content-Type", "application/octet-stream")
c.Response().WriteHeader(http.StatusOK)

_, err = io.Copy(c.Response().Writer, f)
if err != nil {
return err
}
} else {
err = handler.Handle(c)
if err != nil {
return err
}
}

return nil
}

func CreateGlazedFileHandler(
cmd cmds.GlazeCommand,
fileName string,
options ...QueryHandlerOption,
) echo.HandlerFunc {
handler := NewQueryHandler(cmd, fileName, options...)
return handler.Handle
}
23 changes: 15 additions & 8 deletions pkg/glazed/handlers/sse/sse.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package sse

import (
"fmt"
"net/http"

"github.com/go-go-golems/glazed/pkg/cmds"
"github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
Expand All @@ -14,7 +16,6 @@ import (
middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares"
"github.com/kucherenkovova/safegroup"
"github.com/labstack/echo/v4"
"net/http"
)

type QueryHandler struct {
Expand Down Expand Up @@ -42,17 +43,24 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption {
}
}

var _ handlers.Handler = (*QueryHandler)(nil)

func (h *QueryHandler) Handle(c echo.Context) error {
description := h.cmd.Description()
parsedLayers := layers.NewParsedLayers()

middlewares_ := append([]middlewares.Middleware{
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
}, h.middlewares...)
middlewares_ := append(
[]middlewares.Middleware{
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
},
h.middlewares...,
)
middlewares_ = append(middlewares_, middlewares.SetFromDefaults())
err := middlewares.ExecuteMiddlewares(description.Layers, parsedLayers, middlewares_...)
if err != nil {
return err
}

c.Response().Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set("Cache-Control", "no-cache")
c.Response().Header().Set("Connection", "keep-alive")
Expand Down Expand Up @@ -148,15 +156,14 @@ func (h *QueryHandler) Handle(c echo.Context) error {
default:
return &handlers.UnsupportedCommandError{Command: h.cmd}
}

return nil
}

func CreateQueryHandler(
cmd cmds.Command,
middlewares_ ...middlewares.Middleware,
options ...QueryHandlerOption,
) echo.HandlerFunc {
handler := NewQueryHandler(cmd,
WithMiddlewares(middlewares_...),
)
handler := NewQueryHandler(cmd, options...)
return handler.Handle
}
Loading

0 comments on commit 52c569a

Please sign in to comment.