From 3e8c2fdbd87ab251257d26a335ce155a1787bfa3 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 16 Jun 2022 17:05:43 +0300 Subject: [PATCH 1/4] feat(http): pass multipart headers --- gen/_template/request_decode.tmpl | 9 +++++---- http/file.go | 33 +++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/gen/_template/request_decode.tmpl b/gen/_template/request_decode.tmpl index 603ba346a..24424e2ba 100644 --- a/gen/_template/request_decode.tmpl +++ b/gen/_template/request_decode.tmpl @@ -140,17 +140,18 @@ func (s *Server) decode{{ $op.Name }}Request(r *http.Request, span trace.Span) ( if !ok || len(files) < 1 { return errors.New("file is not set") } - header := files[0] + fh := files[0] - f, err := header.Open() + f, err := fh.Open() if err != nil { return errors.Wrap(err, "open") } closers = append(closers, f) {{ printf "req.%s" $param.Name }} = ht.MultipartFile{ - Name: header.Filename, - File: f, + Name: fh.Filename, + File: f, + Header: fh.Header, } return nil }(); err != nil { diff --git a/http/file.go b/http/file.go index fb78bc59f..8868ea872 100644 --- a/http/file.go +++ b/http/file.go @@ -1,22 +1,47 @@ package http import ( + "fmt" "io" "mime/multipart" + "net/textproto" + "strings" ) // MultipartFile is multipart form file. type MultipartFile struct { - Name string - File io.Reader + Name string + File io.Reader + Header textproto.MIMEHeader +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func (m MultipartFile) headers(fieldName string) (h textproto.MIMEHeader) { + h = make(textproto.MIMEHeader, len(m.Header)+2) + for k, v := range m.Header { + h[k] = v + } + + disposition := fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + escapeQuotes(fieldName), escapeQuotes(m.Name)) + h.Set("Content-Disposition", disposition) + if _, ok := h["Content-Type"]; !ok { + h.Set("Content-Type", "application/octet-stream") + } + return h } // WriteMultipart writes data from reader to given multipart.Writer as a form file. func (m MultipartFile) WriteMultipart(fieldName string, w *multipart.Writer) error { - fw, err := w.CreateFormFile(fieldName, m.Name) + p, err := w.CreatePart(m.headers(fieldName)) if err != nil { return err } - _, err = io.Copy(fw, m.File) + _, err = io.Copy(p, m.File) return err } From 26b926f4b4082e8cdcf44445651324b2ff4c9895 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 16 Jun 2022 17:05:51 +0300 Subject: [PATCH 2/4] chore: commit generated files --- internal/sample_api/oas_request_decoders_gen.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/sample_api/oas_request_decoders_gen.go b/internal/sample_api/oas_request_decoders_gen.go index 8ab3abb2b..ffc8e307e 100644 --- a/internal/sample_api/oas_request_decoders_gen.go +++ b/internal/sample_api/oas_request_decoders_gen.go @@ -1092,17 +1092,18 @@ func (s *Server) decodeTestMultipartUploadRequest(r *http.Request, span trace.Sp if !ok || len(files) < 1 { return errors.New("file is not set") } - header := files[0] + fh := files[0] - f, err := header.Open() + f, err := fh.Open() if err != nil { return errors.Wrap(err, "open") } closers = append(closers, f) req.FileName = ht.MultipartFile{ - Name: header.Filename, - File: f, + Name: fh.Filename, + File: f, + Header: fh.Header, } return nil }(); err != nil { From 460ea3fbfb0c5bcd0483bc0ffb6bfb6c737750af Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 16 Jun 2022 17:06:13 +0300 Subject: [PATCH 3/4] test: check multipart headers --- form_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/form_test.go b/form_test.go index 39299fe23..f4ed977b9 100644 --- a/form_test.go +++ b/form_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/textproto" "net/url" "strings" "testing" @@ -16,6 +17,7 @@ import ( ht "github.com/ogen-go/ogen/http" api "github.com/ogen-go/ogen/internal/sample_api" + "github.com/ogen-go/ogen/validate" ) func testForm() api.TestForm { @@ -62,8 +64,13 @@ func (s testFormServer) TestMultipart(ctx context.Context, req api.TestForm) (r } func (s testFormServer) TestMultipartUpload(ctx context.Context, req api.TestMultipartUploadReq) (r string, _ error) { + f := req.FileName + if val := f.Header.Get("Content-Type"); val != "image/jpeg" { + return "", validate.InvalidContentType(val) + } + var b strings.Builder - _, err := io.Copy(&b, req.FileName.File) + _, err := io.Copy(&b, f.File) return b.String(), err } @@ -143,6 +150,9 @@ func TestMultipartUploadE2E(t *testing.T) { FileName: ht.MultipartFile{ Name: "pablo.jpg", File: strings.NewReader(data), + Header: textproto.MIMEHeader{ + "Content-Type": []string{"image/jpeg"}, + }, }, }) a.NoError(err) From ad3595c0bd27463247746a475b67f5047e77b567 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 16 Jun 2022 17:17:09 +0300 Subject: [PATCH 4/4] chore: make linter happy --- .golangci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index b2cd45add..fb5135605 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -122,6 +122,11 @@ issues: linters: [ gosec ] text: G103 + # We are using quoting algorithm from mime/multipart package. False-positive. + - path: http(\/|\\)file\.go + linters: [ gocritic ] + text: sprintfQuotedString + # Intended design. - path: http source: Set @@ -134,3 +139,4 @@ issues: - linters: [ revive ] text: "if-return: redundant if ...; err != nil check, just return error instead." +