Skip to content

Commit

Permalink
Serve easily dynamic files with DataFromReader context method (#1304)
Browse files Browse the repository at this point in the history
* Add DataFromReader context method

* Replace fmt by strconv.FormatInt

* Add pull request link to README
  • Loading branch information
jclebreton authored and appleboy committed May 12, 2018
1 parent 5636afe commit bf78038
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 0 deletions.
27 changes: 27 additions & 0 deletions README.md
Expand Up @@ -40,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [XML, JSON and YAML rendering](#xml-json-and-yaml-rendering)
- [JSONP rendering](#jsonp)
- [Serving static files](#serving-static-files)
- [Serving data from reader](#serving-data-from-reader)
- [HTML rendering](#html-rendering)
- [Multitemplate](#multitemplate)
- [Redirects](#redirects)
Expand Down Expand Up @@ -901,6 +902,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()
Expand Down
10 changes: 10 additions & 0 deletions context.go
Expand Up @@ -741,6 +741,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)
Expand Down
19 changes: 19 additions & 0 deletions context_test.go
Expand Up @@ -1471,3 +1471,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"))
}
36 changes: 36 additions & 0 deletions render/reader.go
@@ -0,0 +1,36 @@
package render

import (
"io"
"net/http"
"strconv"
)

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"] = strconv.FormatInt(r.ContentLength, 10)
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}
}
}
}
1 change: 1 addition & 0 deletions render/render.go
Expand Up @@ -25,6 +25,7 @@ var (
_ HTMLRender = HTMLProduction{}
_ Render = YAML{}
_ Render = MsgPack{}
_ Render = Reader{}
)

func writeContentType(w http.ResponseWriter, value []string) {
Expand Down
23 changes: 23 additions & 0 deletions render/render_test.go
Expand Up @@ -11,6 +11,8 @@ import (
"html/template"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -384,3 +386,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"))
}

0 comments on commit bf78038

Please sign in to comment.