summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2024-04-23 19:15:54 +0700
committerShulhan <ms@kilabit.info>2024-04-24 14:57:02 +0700
commit072a5866613aab933d03bef7df5a2dcb3a0855e4 (patch)
tree79f185033d4e43fde44ded7bb4e7e59d047e4941
parent225b6372d0592c2291ff4b9301a68c80d4660d77 (diff)
downloadpakakeh.go-072a5866613aab933d03bef7df5a2dcb3a0855e4.tar.xz
lib/http: refactoring "multipart/form-data" parameters in ClientRequest
Previously, ClientRequest with type RequestTypeMultipartForm pass the type "map[string][]byte" in Params. This type hold the file upload, where key is the file name and []byte is content of file. Unfortunately, this model does not correct because a "multipart/form-data" can contains different field name and file name, for example --boundary Content-Disposition: form-data; name="field0"; filename="file0" Content-Type: application/octet-stream <Content of file0> This changes fix this by changing the parameter type for RequestTypeMultipartForm to [*multipart.Form], which affect several functions including [Client.PutFormData] and [GenerateFormData]. We also add new function [CreateMultipartFileHeader] to help creating [multipart.FileHeader] from raw bytes, that can be assigned to [*multipart.Form].
-rw-r--r--lib/http/client.go58
-rw-r--r--lib/http/client_example_test.go9
-rw-r--r--lib/http/client_request.go11
-rw-r--r--lib/http/multipart_form.go168
-rw-r--r--lib/http/multipart_form_test.go84
-rw-r--r--lib/http/testdata/GenerateFormData_test.txt22
6 files changed, 295 insertions, 57 deletions
diff --git a/lib/http/client.go b/lib/http/client.go
index 10ee1a2c..751b1de3 100644
--- a/lib/http/client.go
+++ b/lib/http/client.go
@@ -21,7 +21,6 @@ import (
"net"
"net/http"
"path"
- "sort"
"strings"
"time"
@@ -193,7 +192,7 @@ out:
// - If Type is [RequestTypeForm] and Params is [url.Values] it
// will be send as URL encoded in the body.
// - If Type is [RequestTypeMultipartForm] and Params type is
-// map[string][]byte, then it will send as multipart form in the
+// [*multipart.Form], then it will send as multipart form in the
// body.
// - If Type is [RequestTypeJSON] and Params is not nil, the Params
// will be encoded as JSON in the body.
@@ -224,15 +223,15 @@ func (client *Client) GenerateHTTPRequest(req ClientRequest) (httpReq *http.Requ
case RequestTypeMultipartForm:
var (
- paramsAsMultipart map[string][]byte
- ok bool
+ mpform *multipart.Form
+ ok bool
)
- paramsAsMultipart, ok = req.Params.(map[string][]byte)
+ mpform, ok = req.Params.(*multipart.Form)
if ok {
var strBody string
- contentType, strBody, err = GenerateFormData(paramsAsMultipart)
+ contentType, strBody, err = GenerateFormData(mpform)
if err != nil {
return nil, fmt.Errorf(`%s: %w`, logp, err)
}
@@ -331,7 +330,7 @@ func (client *Client) PostFormData(req ClientRequest) (res *ClientResponse, err
var (
logp = `PostFormData`
- params map[string][]byte
+ params *multipart.Form
)
req.contentType = req.Type.String()
@@ -405,8 +404,9 @@ func (client *Client) PutForm(req ClientRequest) (*ClientResponse, error) {
// "multipart/form-data".
func (client *Client) PutFormData(req ClientRequest) (res *ClientResponse, err error) {
var (
- logp = `PutFormData`
- params map[string][]byte
+ logp = `PutFormData`
+
+ params *multipart.Form
)
req.contentType = req.Type.String()
@@ -574,43 +574,3 @@ func (client *Client) uncompress(res *http.Response, body []byte) (
return out, err
}
-
-// GenerateFormData generate multipart/form-data body from params.
-func GenerateFormData(params map[string][]byte) (contentType, body string, err error) {
- var (
- sb = new(strings.Builder)
- w = multipart.NewWriter(sb)
- listKey = make([]string, 0, len(params))
-
- k string
- )
- for k = range params {
- listKey = append(listKey, k)
- }
- sort.Strings(listKey)
-
- var (
- part io.Writer
- v []byte
- )
- for _, k = range listKey {
- part, err = w.CreateFormField(k)
- if err != nil {
- return "", "", err
- }
- v = params[k]
- _, err = part.Write(v)
- if err != nil {
- return "", "", err
- }
- }
-
- err = w.Close()
- if err != nil {
- return "", "", err
- }
-
- contentType = w.FormDataContentType()
-
- return contentType, sb.String(), nil
-}
diff --git a/lib/http/client_example_test.go b/lib/http/client_example_test.go
index 7b72cba1..22b24285 100644
--- a/lib/http/client_example_test.go
+++ b/lib/http/client_example_test.go
@@ -4,6 +4,7 @@ import (
"crypto/rand"
"fmt"
"log"
+ "mime/multipart"
"strings"
libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http"
@@ -15,9 +16,11 @@ func ExampleGenerateFormData() {
// NOTE: do not do this on real code.
rand.Reader = mock.NewRandReader([]byte(`randomseed`))
- var data = map[string][]byte{
- `name`: []byte(`test.txt`),
- `size`: []byte(`42`),
+ var data = &multipart.Form{
+ Value: map[string][]string{
+ `name`: []string{`test.txt`},
+ `size`: []string{`42`},
+ },
}
var (
diff --git a/lib/http/client_request.go b/lib/http/client_request.go
index 454aeae1..55cf3ab3 100644
--- a/lib/http/client_request.go
+++ b/lib/http/client_request.go
@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "mime/multipart"
"net/http"
"net/url"
"strings"
@@ -111,7 +112,7 @@ func (creq *ClientRequest) toHTTPRequest(client *Client) (httpReq *http.Request,
}
case RequestTypeMultipartForm:
- paramsAsMultipart, ok := creq.Params.(map[string][]byte)
+ paramsAsMultipart, ok := creq.Params.(*multipart.Form)
if ok {
contentType, strBody, err = GenerateFormData(paramsAsMultipart)
if err != nil {
@@ -170,16 +171,16 @@ func (creq *ClientRequest) paramsAsURLEncoded() string {
return params.Encode()
}
-// paramsAsMultipart convert the Params as "map[string][]byte" and return the
-// content type and body.
-func (creq *ClientRequest) paramsAsMultipart() (params map[string][]byte) {
+// paramsAsMultipart convert the Params as [*multipart.Form] or nil if its
+// empty.
+func (creq *ClientRequest) paramsAsMultipart() (params *multipart.Form) {
if creq.Params == nil {
return nil
}
var ok bool
- params, ok = creq.Params.(map[string][]byte)
+ params, ok = creq.Params.(*multipart.Form)
if !ok {
return nil
}
diff --git a/lib/http/multipart_form.go b/lib/http/multipart_form.go
new file mode 100644
index 00000000..bd535cfe
--- /dev/null
+++ b/lib/http/multipart_form.go
@@ -0,0 +1,168 @@
+// Copyright 2024, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "sort"
+ "strings"
+)
+
+// CreateMultipartFileHeader create [multipart.FileHeader] from raw content
+// with optional filename.
+func CreateMultipartFileHeader(filename string, content []byte) (fh *multipart.FileHeader, err error) {
+ var (
+ logp = `NewMultipartFormFile`
+ boundary = `__boundary__`
+ fieldname = `__fieldname__`
+
+ buf bytes.Buffer
+ )
+
+ var wrt = multipart.NewWriter(&buf)
+
+ err = wrt.SetBoundary(boundary)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ var formfile io.Writer
+
+ formfile, err = wrt.CreateFormFile(fieldname, filename)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ _, err = formfile.Write(content)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ err = wrt.Close()
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ var (
+ rdr = multipart.NewReader(&buf, boundary)
+
+ form *multipart.Form
+ )
+
+ form, err = rdr.ReadForm(0)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ var (
+ listFH []*multipart.FileHeader
+ ok bool
+ )
+
+ listFH, ok = form.File[fieldname]
+ if !ok {
+ return nil, fmt.Errorf(`%s: missing generated file %s`, logp, filename)
+ }
+ if len(listFH) == 0 {
+ return nil, fmt.Errorf(`%s: empty generated FileHeader`, logp)
+ }
+
+ fh = listFH[0]
+
+ return fh, nil
+}
+
+// GenerateFormData generate "multipart/form-data" body from mpform.
+func GenerateFormData(mpform *multipart.Form) (contentType, body string, err error) {
+ if mpform == nil {
+ return ``, ``, nil
+ }
+
+ var (
+ logp = `GenerateFormData`
+ sb = new(strings.Builder)
+ w = multipart.NewWriter(sb)
+ listKey = make([]string, 0, len(mpform.File))
+
+ k string
+ )
+
+ // Generate files part.
+
+ for k = range mpform.File {
+ listKey = append(listKey, k)
+ }
+ sort.Strings(listKey)
+
+ var (
+ listFH []*multipart.FileHeader
+ fh *multipart.FileHeader
+ part io.Writer
+ file multipart.File
+ content []byte
+ )
+ for k, listFH = range mpform.File {
+ for _, fh = range listFH {
+ part, err = w.CreateFormFile(k, fh.Filename)
+ if err != nil {
+ return ``, ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ file, err = fh.Open()
+ if err != nil {
+ return ``, ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ content, err = io.ReadAll(file)
+ if err != nil {
+ return ``, ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ _, err = part.Write(content)
+ if err != nil {
+ return ``, ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ }
+ }
+
+ // Generate values part.
+
+ listKey = listKey[:0]
+ for k = range mpform.Value {
+ listKey = append(listKey, k)
+ }
+ sort.Strings(listKey)
+
+ var (
+ listValue []string
+ v string
+ )
+ for _, k = range listKey {
+ listValue = mpform.Value[k]
+ for _, v = range listValue {
+ part, err = w.CreateFormField(k)
+ if err != nil {
+ return ``, ``, err
+ }
+
+ _, err = part.Write([]byte(v))
+ if err != nil {
+ return ``, ``, err
+ }
+ }
+ }
+
+ err = w.Close()
+ if err != nil {
+ return ``, ``, err
+ }
+
+ contentType = w.FormDataContentType()
+
+ return contentType, sb.String(), nil
+}
diff --git a/lib/http/multipart_form_test.go b/lib/http/multipart_form_test.go
new file mode 100644
index 00000000..8562acc7
--- /dev/null
+++ b/lib/http/multipart_form_test.go
@@ -0,0 +1,84 @@
+// Copyright 2024, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "crypto/rand"
+ "mime/multipart"
+ "strings"
+ "testing"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test/mock"
+)
+
+func TestGenerateFormData(t *testing.T) {
+ type testcase struct {
+ form multipart.Form
+ tagOutput string
+ field2filename map[string]string
+ }
+
+ rand.Reader = mock.NewRandReader([]byte(`randomseed`))
+
+ var (
+ tdata *test.Data
+ err error
+ )
+ tdata, err = test.LoadData(`testdata/GenerateFormData_test.txt`)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var listcase = []testcase{{
+ form: multipart.Form{
+ Value: map[string][]string{
+ `field1`: []string{`value1`, `value1.1`},
+ },
+ File: map[string][]*multipart.FileHeader{},
+ },
+ field2filename: map[string]string{
+ `field0`: `file0`,
+ },
+ tagOutput: `file0`,
+ }}
+
+ var (
+ tcase testcase
+ listFH []*multipart.FileHeader
+ fh *multipart.FileHeader
+
+ fieldname string
+ filename string
+ gotContentType string
+ gotBody string
+ tag string
+ )
+
+ for _, tcase = range listcase {
+ for fieldname, filename = range tcase.field2filename {
+ fh, err = CreateMultipartFileHeader(filename, tdata.Input[filename])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ listFH = tcase.form.File[fieldname]
+ listFH = append(listFH, fh)
+ tcase.form.File[fieldname] = listFH
+ }
+
+ gotContentType, gotBody, err = GenerateFormData(&tcase.form)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tag = tcase.tagOutput + `.ContentType`
+ test.Assert(t, tag, string(tdata.Output[tag]), gotContentType)
+
+ gotBody = strings.ReplaceAll(gotBody, "\r\n", "\n")
+ tag = tcase.tagOutput + `.Body`
+ test.Assert(t, tag, string(tdata.Output[tag]), gotBody)
+ }
+}
diff --git a/lib/http/testdata/GenerateFormData_test.txt b/lib/http/testdata/GenerateFormData_test.txt
new file mode 100644
index 00000000..b209e07f
--- /dev/null
+++ b/lib/http/testdata/GenerateFormData_test.txt
@@ -0,0 +1,22 @@
+
+>>> file0
+Content of file0.
+
+<<< file0.ContentType
+multipart/form-data; boundary=616e646f6d73656564616e646f6d73656564616e646f6d73656564616e64
+
+<<< file0.Body
+--616e646f6d73656564616e646f6d73656564616e646f6d73656564616e64
+Content-Disposition: form-data; name="field0"; filename="file0"
+Content-Type: application/octet-stream
+
+Content of file0.
+--616e646f6d73656564616e646f6d73656564616e646f6d73656564616e64
+Content-Disposition: form-data; name="field1"
+
+value1
+--616e646f6d73656564616e646f6d73656564616e646f6d73656564616e64
+Content-Disposition: form-data; name="field1"
+
+value1.1
+--616e646f6d73656564616e646f6d73656564616e646f6d73656564616e64--