diff --git a/README.md b/README.md index cff9bc8a..a7bd2698 100644 --- a/README.md +++ b/README.md @@ -900,6 +900,32 @@ func main() { } ``` +### Serving data from reader + +```go +func main() { + router := gin.Default() + router.GET("/someDataFromReader", func(c *gin.Context) { + response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png") + if err != nil || response.StatusCode != http.StatusOK { + c.Status(http.StatusServiceUnavailable) + return + } + + reader := response.Body + contentLength := response.ContentLength + contentType := response.Header.Get("Content-Type") + + extraHeaders := map[string]string{ + "Content-Disposition": `attachment; filename="gopher.png"`, + } + + c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders) + }) + router.Run(":8080") +} +``` + ### HTML rendering Using LoadHTMLGlob() or LoadHTMLFiles() diff --git a/context.go b/context.go index be205f45..011b9ae1 100755 --- a/context.go +++ b/context.go @@ -716,6 +716,16 @@ func (c *Context) Data(code int, contentType string, data []byte) { }) } +// DataFromReader writes the specified reader into the body stream and updates the HTTP code. +func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) { + c.Render(code, render.Reader{ + Headers: extraHeaders, + ContentType: contentType, + ContentLength: contentLength, + Reader: reader, + }) +} + // File writes the specified file into the body stream in a efficient way. func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) diff --git a/context_test.go b/context_test.go index 841d8af0..303fbced 100644 --- a/context_test.go +++ b/context_test.go @@ -1391,3 +1391,22 @@ func TestContextGetRawData(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "Fetch binary post data", string(data)) } + +func TestContextRenderDataFromReader(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + body := "#!PNG some raw data" + reader := strings.NewReader(body) + contentLength := int64(len(body)) + contentType := "image/png" + extraHeaders := map[string]string{"Content-Disposition": `attachment; filename="gopher.png"`} + + c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, body, w.Body.String()) + assert.Equal(t, contentType, w.HeaderMap.Get("Content-Type")) + assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length")) + assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) +} diff --git a/render/reader.go b/render/reader.go new file mode 100644 index 00000000..2dd7e085 --- /dev/null +++ b/render/reader.go @@ -0,0 +1,36 @@ +package render + +import ( + "fmt" + "io" + "net/http" +) + +type Reader struct { + ContentType string + ContentLength int64 + Reader io.Reader + Headers map[string]string +} + +// Render (Reader) writes data with custom ContentType and headers. +func (r Reader) Render(w http.ResponseWriter) (err error) { + r.WriteContentType(w) + r.Headers["Content-Length"] = fmt.Sprintf("%d", r.ContentLength) + r.writeHeaders(w, r.Headers) + _, err = io.Copy(w, r.Reader) + return +} + +func (r Reader) WriteContentType(w http.ResponseWriter) { + writeContentType(w, []string{r.ContentType}) +} + +func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) { + header := w.Header() + for k, v := range headers { + if val := header[k]; len(val) == 0 { + header[k] = []string{v} + } + } +} diff --git a/render/render.go b/render/render.go index 578f2784..7caf9bba 100755 --- a/render/render.go +++ b/render/render.go @@ -25,6 +25,7 @@ var ( _ HTMLRender = HTMLProduction{} _ Render = YAML{} _ Render = MsgPack{} + _ Render = Reader{} ) func writeContentType(w http.ResponseWriter, value []string) { diff --git a/render/render_test.go b/render/render_test.go index c7b51ef4..34bffdac 100755 --- a/render/render_test.go +++ b/render/render_test.go @@ -11,8 +11,11 @@ import ( "html/template" "net/http" "net/http/httptest" + "strconv" "testing" + "strings" + "github.com/stretchr/testify/assert" "github.com/ugorji/go/codec" ) @@ -384,3 +387,24 @@ func TestRenderHTMLDebugPanics(t *testing.T) { } assert.Panics(t, func() { htmlRender.Instance("", nil) }) } + +func TestRenderReader(t *testing.T) { + w := httptest.NewRecorder() + + body := "#!PNG some raw data" + headers := make(map[string]string) + headers["Content-Disposition"] = `attachment; filename="filename.png"` + + err := (Reader{ + ContentLength: int64(len(body)), + ContentType: "image/png", + Reader: strings.NewReader(body), + Headers: headers, + }).Render(w) + + assert.NoError(t, err) + assert.Equal(t, body, w.Body.String()) + assert.Equal(t, "image/png", w.HeaderMap.Get("Content-Type")) + assert.Equal(t, strconv.Itoa(len(body)), w.HeaderMap.Get("Content-Length")) + assert.Equal(t, headers["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) +}