mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
add Context.ReadMultipartRelated as requested at #1787
This commit is contained in:
parent
43c85d0c85
commit
f28203f1be
|
@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and
|
||||||
|
|
||||||
## Fixes and Improvements
|
## Fixes and Improvements
|
||||||
|
|
||||||
|
- Add `Context.ReadMultipartRelated` as requested at: [issues/#1787](https://github.com/kataras/iris/issues/1787).
|
||||||
|
|
||||||
- Add `Container.DependencyMatcher` and `Dependency.Match` to implement the feature requested at [issues/#1842](https://github.com/kataras/iris/issues/1842).
|
- Add `Container.DependencyMatcher` and `Dependency.Match` to implement the feature requested at [issues/#1842](https://github.com/kataras/iris/issues/1842).
|
||||||
|
|
||||||
- Register [CORS middleware](middleware/cors) to the Application by default when `iris.Default()` is used instead of `iris.New()`.
|
- Register [CORS middleware](middleware/cors) to the Application by default when `iris.Default()` is used instead of `iris.New()`.
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -1783,15 +1784,16 @@ func GetForm(r *http.Request, postMaxMemory int64, resetBody bool) (form map[str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var bodyCopy []byte
|
|
||||||
|
|
||||||
if resetBody {
|
if resetBody {
|
||||||
// on POST, PUT and PATCH it will read the form values from request body otherwise from URL queries.
|
// on POST, PUT and PATCH it will read the form values from request body otherwise from URL queries.
|
||||||
if m := r.Method; m == "POST" || m == "PUT" || m == "PATCH" {
|
if m := r.Method; m == "POST" || m == "PUT" || m == "PATCH" {
|
||||||
bodyCopy, _ = GetBody(r, resetBody)
|
body, restoreBody, err := GetBody(r, resetBody)
|
||||||
if len(bodyCopy) == 0 {
|
if err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
setBody(r, body) // so the ctx.request.Body works
|
||||||
|
defer restoreBody() // so the next GetForm calls work.
|
||||||
|
|
||||||
// r.Body = ioutil.NopCloser(io.TeeReader(r.Body, buf))
|
// r.Body = ioutil.NopCloser(io.TeeReader(r.Body, buf))
|
||||||
} else {
|
} else {
|
||||||
resetBody = false
|
resetBody = false
|
||||||
|
@ -1803,9 +1805,9 @@ func GetForm(r *http.Request, postMaxMemory int64, resetBody bool) (form map[str
|
||||||
// After one call to ParseMultipartForm or ParseForm,
|
// After one call to ParseMultipartForm or ParseForm,
|
||||||
// subsequent calls have no effect, are idempotent.
|
// subsequent calls have no effect, are idempotent.
|
||||||
err := r.ParseMultipartForm(postMaxMemory)
|
err := r.ParseMultipartForm(postMaxMemory)
|
||||||
if resetBody {
|
// if resetBody {
|
||||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyCopy))
|
// r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyCopy))
|
||||||
}
|
// }
|
||||||
if err != nil && err != http.ErrNotMultipart {
|
if err != nil && err != http.ErrNotMultipart {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
@ -2247,20 +2249,28 @@ func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) {
|
||||||
ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes)
|
ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emptyFunc = func() {}
|
||||||
|
|
||||||
// GetBody reads and returns the request body.
|
// GetBody reads and returns the request body.
|
||||||
func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
func GetBody(r *http.Request, resetBody bool) ([]byte, func(), error) {
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resetBody {
|
if resetBody {
|
||||||
// * remember, Request.Body has no Bytes(), we have to consume them first
|
// * remember, Request.Body has no Bytes(), we have to consume them first
|
||||||
// and after re-set them to the body, this is the only solution.
|
// and after re-set them to the body, this is the only solution.
|
||||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
return data, func() {
|
||||||
|
setBody(r, data)
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, emptyFunc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBody(r *http.Request, data []byte) {
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
|
const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
|
||||||
|
@ -2285,7 +2295,12 @@ func (ctx *Context) IsRecordingBody() bool {
|
||||||
//
|
//
|
||||||
// However, whenever you can use the `ctx.Request().Body` instead.
|
// However, whenever you can use the `ctx.Request().Body` instead.
|
||||||
func (ctx *Context) GetBody() ([]byte, error) {
|
func (ctx *Context) GetBody() ([]byte, error) {
|
||||||
return GetBody(ctx.request, ctx.IsRecordingBody())
|
body, release, err := GetBody(ctx.request, ctx.IsRecordingBody())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
release()
|
||||||
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator is the validator for request body on Context methods such as
|
// Validator is the validator for request body on Context methods such as
|
||||||
|
@ -2452,10 +2467,11 @@ func (ctx *Context) ReadJSON(outPtr interface{}, opts ...JSONReader) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsRecordingBody() {
|
if ctx.IsRecordingBody() {
|
||||||
body, err := GetBody(ctx.request, true)
|
body, restoreBody, err := GetBody(ctx.request, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
restoreBody()
|
||||||
|
|
||||||
err = options.unmarshal(ctx.request.Context(), body, outPtr)
|
err = options.unmarshal(ctx.request.Context(), body, outPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2643,6 +2659,143 @@ func (ctx *Context) ReadForm(formObject interface{}) error {
|
||||||
return ctx.app.Validate(formObject)
|
return ctx.app.Validate(formObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// MultipartRelated is the result of the context.ReadMultipartRelated method.
|
||||||
|
MultipartRelated struct {
|
||||||
|
// ContentIDs keeps an ordered list of all the
|
||||||
|
// content-ids of the multipart related request.
|
||||||
|
ContentIDs []string
|
||||||
|
// Contents keeps each part's information by Content-ID.
|
||||||
|
// Contents holds each of the multipart/related part's data.
|
||||||
|
Contents map[string]MultipartRelatedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultipartRelatedContent holds a multipart/related part's id, header and body.
|
||||||
|
MultipartRelatedContent struct {
|
||||||
|
// ID holds the Content-ID.
|
||||||
|
ID string
|
||||||
|
// Headers holds the part's request headers.
|
||||||
|
Headers map[string][]string
|
||||||
|
// Body holds the part's body.
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadMultipartRelated returns a structure which contain
|
||||||
|
// information about each part (id, headers, body).
|
||||||
|
//
|
||||||
|
// Read more at: https://www.ietf.org/rfc/rfc2387.txt.
|
||||||
|
//
|
||||||
|
// Example request (2387/5.2 Text/X-Okie):
|
||||||
|
// Content-Type: Multipart/Related; boundary=example-2;
|
||||||
|
// start="<950118.AEBH@XIson.com>"
|
||||||
|
// type="Text/x-Okie"
|
||||||
|
//
|
||||||
|
// --example-2
|
||||||
|
// Content-Type: Text/x-Okie; charset=iso-8859-1;
|
||||||
|
// declaration="<950118.AEB0@XIson.com>"
|
||||||
|
// Content-ID: <950118.AEBH@XIson.com>
|
||||||
|
// Content-Description: Document
|
||||||
|
//
|
||||||
|
// {doc}
|
||||||
|
// This picture was taken by an automatic camera mounted ...
|
||||||
|
// {image file=cid:950118.AECB@XIson.com}
|
||||||
|
// {para}
|
||||||
|
// Now this is an enlargement of the area ...
|
||||||
|
// {image file=cid:950118:AFDH@XIson.com}
|
||||||
|
// {/doc}
|
||||||
|
// --example-2
|
||||||
|
// Content-Type: image/jpeg
|
||||||
|
// Content-ID: <950118.AFDH@XIson.com>
|
||||||
|
// Content-Transfer-Encoding: BASE64
|
||||||
|
// Content-Description: Picture A
|
||||||
|
//
|
||||||
|
// [encoded jpeg image]
|
||||||
|
// --example-2
|
||||||
|
// Content-Type: image/jpeg
|
||||||
|
// Content-ID: <950118.AECB@XIson.com>
|
||||||
|
// Content-Transfer-Encoding: BASE64
|
||||||
|
// Content-Description: Picture B
|
||||||
|
//
|
||||||
|
// [encoded jpeg image]
|
||||||
|
// --example-2--
|
||||||
|
func (ctx *Context) ReadMultipartRelated() (MultipartRelated, error) {
|
||||||
|
contentType, params, err := mime.ParseMediaType(ctx.GetHeader(ContentTypeHeaderKey))
|
||||||
|
if err != nil {
|
||||||
|
return MultipartRelated{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(contentType, ContentMultipartRelatedHeaderValue) {
|
||||||
|
return MultipartRelated{}, ErrEmptyForm
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
contentIDs []string
|
||||||
|
contents = make(map[string]MultipartRelatedContent)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx.IsRecordingBody() {
|
||||||
|
// * remember, Request.Body has no Bytes(), we have to consume them first
|
||||||
|
// and after re-set them to the body, this is the only solution.
|
||||||
|
body, restoreBody, err := GetBody(ctx.request, true)
|
||||||
|
if err != nil {
|
||||||
|
return MultipartRelated{}, fmt.Errorf("multipart related: body copy because of iris.Configuration.DisableBodyConsumptionOnUnmarshal: %w", err)
|
||||||
|
}
|
||||||
|
setBody(ctx.request, body) // so the ctx.request.Body works
|
||||||
|
defer restoreBody() // so the next ctx.GetBody calls work.
|
||||||
|
}
|
||||||
|
|
||||||
|
multipartReader := multipart.NewReader(ctx.request.Body, params["boundary"])
|
||||||
|
for {
|
||||||
|
part, err := multipartReader.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return MultipartRelated{}, fmt.Errorf("multipart related: next part: %w", err)
|
||||||
|
}
|
||||||
|
defer part.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return MultipartRelated{}, fmt.Errorf("multipart related: next part: read: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentID := part.Header.Get("Content-ID")
|
||||||
|
contentIDs = append(contentIDs, contentID)
|
||||||
|
contents[contentID] = MultipartRelatedContent{ // replace if same Content-ID appears, which it shouldn't.
|
||||||
|
ID: contentID,
|
||||||
|
Headers: http.Header(part.Header),
|
||||||
|
Body: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(contents) != len(contentIDs) {
|
||||||
|
contentIDs = distinctStrings(contentIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := MultipartRelated{
|
||||||
|
ContentIDs: contentIDs,
|
||||||
|
Contents: contents,
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func distinctStrings(values []string) (result []string) {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
|
||||||
|
for v := range values {
|
||||||
|
val := values[v]
|
||||||
|
if _, ok := seen[val]; !ok {
|
||||||
|
seen[val] = struct{}{}
|
||||||
|
result = append(result, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// ReadQuery binds URL Query to "ptr". The struct field tag is "url".
|
// ReadQuery binds URL Query to "ptr". The struct field tag is "url".
|
||||||
//
|
//
|
||||||
// Example: https://github.com/kataras/iris/blob/master/_examples/request-body/read-query/main.go
|
// Example: https://github.com/kataras/iris/blob/master/_examples/request-body/read-query/main.go
|
||||||
|
@ -2818,6 +2971,8 @@ func (ctx *Context) ReadBody(ptr interface{}) error {
|
||||||
return ctx.ReadYAML(ptr)
|
return ctx.ReadYAML(ptr)
|
||||||
case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
|
case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
|
||||||
return ctx.ReadForm(ptr)
|
return ctx.ReadForm(ptr)
|
||||||
|
case ContentMultipartRelatedHeaderValue:
|
||||||
|
return fmt.Errorf("context: read body: cannot bind multipart/related: use ReadMultipartRelated instead")
|
||||||
case ContentJSONHeaderValue:
|
case ContentJSONHeaderValue:
|
||||||
return ctx.ReadJSON(ptr)
|
return ctx.ReadJSON(ptr)
|
||||||
case ContentProtobufHeaderValue:
|
case ContentProtobufHeaderValue:
|
||||||
|
@ -3541,6 +3696,8 @@ const (
|
||||||
ContentFormHeaderValue = "application/x-www-form-urlencoded"
|
ContentFormHeaderValue = "application/x-www-form-urlencoded"
|
||||||
// ContentFormMultipartHeaderValue header value for post multipart form data.
|
// ContentFormMultipartHeaderValue header value for post multipart form data.
|
||||||
ContentFormMultipartHeaderValue = "multipart/form-data"
|
ContentFormMultipartHeaderValue = "multipart/form-data"
|
||||||
|
// ContentMultipartRelatedHeaderValue header value for multipart related data.
|
||||||
|
ContentMultipartRelatedHeaderValue = "multipart/related"
|
||||||
// ContentGRPCHeaderValue Content-Type header value for gRPC.
|
// ContentGRPCHeaderValue Content-Type header value for gRPC.
|
||||||
ContentGRPCHeaderValue = "application/grpc"
|
ContentGRPCHeaderValue = "application/grpc"
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/core/netutil"
|
"github.com/kataras/iris/v12/core/netutil"
|
||||||
|
@ -28,13 +27,8 @@ func WriteStartupLogOnServe(w io.Writer) func(TaskHost) {
|
||||||
addr = h.Supervisor.Server.Addr
|
addr = h.Supervisor.Server.Addr
|
||||||
}
|
}
|
||||||
listeningURI := netutil.ResolveURL(guessScheme, addr)
|
listeningURI := netutil.ResolveURL(guessScheme, addr)
|
||||||
interruptkey := "CTRL"
|
_, _ = fmt.Fprintf(w, "Now listening on: %s\nApplication started. Press CTRL/CMD+C to shut down.\n",
|
||||||
if runtime.GOOS == "darwin" {
|
listeningURI)
|
||||||
interruptkey = "CMD"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(w, "Now listening on: %s\nApplication started. Press %s+C to shut down.\n",
|
|
||||||
listeningURI, interruptkey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user