From e87ba00764b3eb61c73a29aa8debd0980e3e3ca5 Mon Sep 17 00:00:00 2001 From: wklken Date: Thu, 10 Feb 2022 23:01:57 +0800 Subject: [PATCH] feat(sendfile): enable custom Content-Type for SendFile 1) code copy and change from https://github.com/parnurzeal/gorequest/pull/213 2)fix issue https://github.com/parnurzeal/gorequest/issues/208 --- gorequest.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- gorequest_test.go | 27 +++++++++++++++++++++++++++ util.go | 22 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/gorequest.go b/gorequest.go index f9c2ce0..37c203b 100644 --- a/gorequest.go +++ b/gorequest.go @@ -6,6 +6,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -808,10 +809,11 @@ func (s *SuperAgent) SendString(content string) *SuperAgent { type File struct { Filename string Fieldname string + MimeType string Data []byte } -// SendFile function works only with type "multipart". The function accepts one mandatory and up to two optional arguments. The mandatory (first) argument is the file. +// SendFile function works only with type "multipart". The function accepts one mandatory and up to three optional arguments. The mandatory (first) argument is the file. // The function accepts a path to a file as string: // // gorequest.New(). @@ -859,11 +861,32 @@ type File struct { // SendFile(b, "", "my_custom_fieldname"). // filename left blank, will become "example_file.ext" // End() // +// The third optional argument (fourth argument overall) is a bool value skipFileNumbering. It defaults to "false", +// if fieldname is "file" and skipFileNumbering is set to "false", the fieldname will be automatically set to +// fileNUMBER, where number is the greatest existing number+1. +// +// b, _ := ioutil.ReadFile("./example_file.ext") +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile(b, "filename", "my_custom_fieldname", false). +// End() +// +// The fourth optional argument (fifth argument overall) is the mimetype request form-data part. It defaults to "application/octet-stream". +// +// b, _ := ioutil.ReadFile("./example_file.ext") +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile(b, "filename", "my_custom_fieldname", false, "mime_type"). +// End() +// func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent { filename := "" fieldname := "file" skipFileNumbering := false + fileType := "application/octet-stream" if len(args) >= 1 { argFilename := fmt.Sprintf("%v", args[0]) @@ -886,6 +909,17 @@ func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent } } + if len(args) >= 4 { + argFileType := fmt.Sprintf("%v", args[3]) + if len(argFileType) > 0 { + fileType = strings.TrimSpace(argFileType) + } + if fileType == "" { + s.Errors = append(s.Errors, errors.New("the fifth SendFile method argument for MIME type cannot be an empty string")) + return s + } + } + if (fieldname == "file" && !skipFileNumbering) || fieldname == "" { fieldname = "file" + strconv.Itoa(len(s.FileData)+1) } @@ -908,6 +942,7 @@ func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent s.FileData = append(s.FileData, File{ Filename: filename, Fieldname: fieldname, + MimeType: fileType, Data: data, }) case reflect.Slice: @@ -918,6 +953,7 @@ func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent f := File{ Filename: filename, Fieldname: fieldname, + MimeType: fileType, Data: make([]byte, len(slice)), } for i := range slice { @@ -934,6 +970,9 @@ func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent if len(args) == 3 { return s.SendFile(v.Elem().Interface(), args[0], args[1], args[2]) } + if len(args) == 4 { + return s.SendFile(v.Elem().Interface(), args[0], args[1], args[2], args[3]) + } return s.SendFile(v.Elem().Interface()) default: if v.Type() == reflect.TypeOf(os.File{}) { @@ -949,6 +988,7 @@ func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent s.FileData = append(s.FileData, File{ Filename: filename, Fieldname: fieldname, + MimeType: fileType, Data: data, }) return s @@ -1340,7 +1380,7 @@ func (s *SuperAgent) MakeRequest() (*http.Request, error) { // add the files if len(s.FileData) != 0 { for _, file := range s.FileData { - fw, _ := mw.CreateFormFile(file.Fieldname, file.Filename) + fw, _ := CreateFormFile(mw, file.Fieldname, file.Filename, file.MimeType) fw.Write(file.Data) } contentReader = buf diff --git a/gorequest_test.go b/gorequest_test.go index 4757f52..0460734 100644 --- a/gorequest_test.go +++ b/gorequest_test.go @@ -1203,6 +1203,7 @@ func TestMultipartRequest(t *testing.T) { const case10b_send_file_by_path_pointer = "/send_file_by_path_pointer" const case11_send_file_by_path_without_name = "/send_file_by_path_without_name" const case12_send_file_by_path_without_name_but_with_fieldname = "/send_file_by_path_without_name_but_with_fieldname" + const case121_send_file_by_path_with_name_and_fieldname_and_mimetype = "/send_file_by_path_with_name_and_fieldname_and_mimetype" const case13_send_file_by_content_without_name = "/send_file_by_content_without_name" const case13a_send_file_by_content_without_name_pointer = "/send_file_by_content_without_name_pointer" @@ -1210,6 +1211,7 @@ func TestMultipartRequest(t *testing.T) { const case15_send_file_by_content_without_name_but_with_fieldname = "/send_file_by_content_without_name_but_with_fieldname" const case16_send_file_by_content_with_name_and_with_fieldname = "/send_file_by_content_with_name_and_with_fieldname" + const case161_send_file_by_content_with_name_and_fieldname_and_mimetype = "/send_file_by_content_with_name_and_fieldname_and_mimetype" const case17_send_file_multiple_by_path_and_content_without_name = "/send_file_multiple_by_path_and_content_without_name" const case18_send_file_multiple_by_path_and_content_with_name = "/send_file_multiple_by_path_and_content_with_name" @@ -1441,6 +1443,21 @@ func TestMultipartRequest(t *testing.T) { t.Error("Expected Header:Content-Type:application/octet-stream", "| but got", r.MultipartForm.File["my_fieldname"][0].Header["Content-Type"]) } checkFile(t, r.MultipartForm.File["my_fieldname"][0]) + case case161_send_file_by_content_with_name_and_fieldname_and_mimetype, case121_send_file_by_path_with_name_and_fieldname_and_mimetype: + if len(r.MultipartForm.File) != 1 { + t.Error("Expected length of files:[] == 1", "| but got", len(r.MultipartForm.File)) + } + if _, ok := r.MultipartForm.File["my_fieldname"]; !ok { + keys := reflect.ValueOf(r.MultipartForm.File).MapKeys() + t.Error("Expected Fieldname:my_fieldname", "| but got", keys) + } + if r.MultipartForm.File["my_fieldname"][0].Filename != "MY_LICENSE" { + t.Error("Expected Filename:MY_LICENSE", "| but got", r.MultipartForm.File["my_fieldname"][0].Filename) + } + if r.MultipartForm.File["my_fieldname"][0].Header["Content-Type"][0] != "application/json" { + t.Error("Expected Header:Content-Type:application/json", "| but got", r.MultipartForm.File["my_fieldname"][0].Header["Content-Type"]) + } + checkFile(t, r.MultipartForm.File["my_fieldname"][0]) case case17_send_file_multiple_by_path_and_content_without_name: if len(r.MultipartForm.File) != 2 { t.Error("Expected length of files:[] == 2", "| but got", len(r.MultipartForm.File)) @@ -1661,6 +1678,11 @@ func TestMultipartRequest(t *testing.T) { SendFile(fileByPath, "", "my_fieldname"). End() + New().Post(ts.URL+case121_send_file_by_path_with_name_and_fieldname_and_mimetype). + Type("multipart"). + SendFile(fileByPath, "MY_LICENSE", "my_fieldname", false, "application/json"). + End() + b, _ := ioutil.ReadFile("./LICENSE") New().Post(ts.URL + case13_send_file_by_content_without_name). Type("multipart"). @@ -1687,6 +1709,11 @@ func TestMultipartRequest(t *testing.T) { SendFile(b, "MY_LICENSE", "my_fieldname"). End() + New().Post(ts.URL+case161_send_file_by_content_with_name_and_fieldname_and_mimetype). + Type("multipart"). + SendFile(b, "MY_LICENSE", "my_fieldname", false, "application/json"). + End() + New().Post(ts.URL + case17_send_file_multiple_by_path_and_content_without_name). Type("multipart"). SendFile("./LICENSE"). diff --git a/util.go b/util.go index 8472e52..d0065b8 100644 --- a/util.go +++ b/util.go @@ -1,7 +1,12 @@ package gorequest import ( + "fmt" + "io" + "mime/multipart" "net/http" + "net/textproto" + "strings" "unsafe" ) @@ -104,3 +109,20 @@ func filterFlags(content string) string { } return content } + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +// CreateFormFile is a convenience wrapper around CreatePart. It creates +// a new form-data header with the provided field name and file name. +func CreateFormFile(w *multipart.Writer, fieldname, filename string, contenttype string) (io.Writer, error) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", + fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + escapeQuotes(fieldname), escapeQuotes(filename))) + h.Set("Content-Type", contenttype) + return w.CreatePart(h) +}