소스 검색

Serve easily dynamic files with `DataFromReader` context method (#1304)

* Add DataFromReader context method

* Replace fmt by strconv.FormatInt

* Add pull request link to README
Jean-Christophe Lebreton 7 년 전
부모
커밋
bf7803815b
6개의 변경된 파일116개의 추가작업 그리고 0개의 파일을 삭제
  1. 27 0
      README.md
  2. 10 0
      context.go
  3. 19 0
      context_test.go
  4. 36 0
      render/reader.go
  5. 1 0
      render/render.go
  6. 23 0
      render/render_test.go

+ 27 - 0
README.md

@@ -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)
@@ -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()

+ 10 - 0
context.go

@@ -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)

+ 19 - 0
context_test.go

@@ -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 - 0
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 - 0
render/render.go

@@ -25,6 +25,7 @@ var (
 	_ HTMLRender = HTMLProduction{}
 	_ Render     = YAML{}
 	_ Render     = MsgPack{}
+	_ Render     = Reader{}
 )
 
 func writeContentType(w http.ResponseWriter, value []string) {

+ 23 - 0
render/render_test.go

@@ -11,6 +11,8 @@ import (
 	"html/template"
 	"net/http"
 	"net/http/httptest"
+	"strconv"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -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"))
+}