diff --git a/README.md b/README.md index f6bdb47..b2f90ea 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ import "github.com/duke-git/lancet/convertor" - [DecodeByte](https://github.com/duke-git/lancet/blob/v1/docs/convertor.md#DecodeByte) - [DeepClone](https://github.com/duke-git/lancet/blob/v1/docs/convertor.md#DeepClone) - [CopyProperties](https://github.com/duke-git/lancet/blob/v1/docs/convertor.md#CopyProperties) +- [ToInterface](https://github.com/duke-git/lancet/blob/v1/docs/convertor.md#ToInterface) +- [Utf8ToGbk](https://github.com/duke-git/lancet/blob/v1/docs/convertor.md#Utf8ToGbk) +- [GbkToUtf8](https://github.com/duke-git/lancet/blob/v1/docs/convertor.md#GbkToUtf8) ### 3. Cryptor package is for data encryption and decryption. @@ -215,6 +218,7 @@ import "github.com/duke-git/lancet/fileutil" - [MTime](https://github.com/duke-git/lancet/blob/v1/docs/fileutil.md#MTime) - [Sha](https://github.com/duke-git/lancet/blob/v1/docs/fileutil.md#Sha) - [ReadCsvFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil.md#ReadCsvFile) +- [WriteCsvFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil.md#WriteCsvFile) - [WriteStringToFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil.md#WriteStringToFile) - [WriteBytesToFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil.md#WriteBytesToFile) @@ -275,6 +279,7 @@ import "github.com/duke-git/lancet/mathutil" - [LCM](https://github.com/duke-git/lancet/blob/v1/docs/mathutil.md#LCM) - [Cos](https://github.com/duke-git/lancet/blob/v1/docs/mathutil.md#Cos) - [Sin](https://github.com/duke-git/lancet/blob/v1/docs/mathutil.md#Sin) +- [Log](https://github.com/duke-git/lancet/blob/v1/docs/mathutil.md#Log) ### 9. Netutil package contains functions to get net information and send http request. @@ -320,6 +325,7 @@ import "github.com/duke-git/lancet/random" - [RandNumeral](https://github.com/duke-git/lancet/blob/v1/docs/random.md#RandNumeral) - [RandNumeralOrLetter](https://github.com/duke-git/lancet/blob/v1/docs/random.md#RandNumeralOrLetter) - [UUIdV4](https://github.com/duke-git/lancet/blob/v1/docs/random.md#UUIdV4) +- [RandUniqueIntSlice](https://github.com/duke-git/lancet/blob/v1/docs/random.md#RandUniqueIntSlice) ### 11. Retry package is for executing a function repeatedly until it was successful or canceled by the context. @@ -427,7 +433,7 @@ import "github.com/duke-git/lancet/strutil" - [Trim](https://github.com/duke-git/lancet/blob/v1/docs/strutil.md#Trim) - [SplitAndTrim](https://github.com/duke-git/lancet/blob/v1/docs/strutil.md#SplitAndTrim) - [HideString](https://github.com/duke-git/lancet/blob/v1/docs/strutil.md#HideString) - +- [RemoveWhiteSpace](https://github.com/duke-git/lancet/blob/v1/docs/strutil.md#RemoveWhiteSpace) ### 14. System package contain some functions about os, runtime, shell command. ```go diff --git a/README_zh-CN.md b/README_zh-CN.md index 84daf05..79d7f72 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -99,7 +99,9 @@ import "github.com/duke-git/lancet/convertor" - [DecodeByte](https://github.com/duke-git/lancet/blob/v1/docs/convertor_zh-CN.md#DecodeByte) - [DeepClone](https://github.com/duke-git/lancet/blob/v1/docs/convertor_zh-CN.md#DeepClone) - [CopyProperties](https://github.com/duke-git/lancet/blob/v1/docs/convertor_zh-CN.md#CopyProperties) - +- [ToInterface](https://github.com/duke-git/lancet/blob/v1/docs/convertor_zh-CN.md#ToInterface) +- [Utf8ToGbk](https://github.com/duke-git/lancet/blob/v1/docs/convertor_zh-CN.md#Utf8ToGbk) +- [GbkToUtf8](https://github.com/duke-git/lancet/blob/v1/docs/convertor_zh-CN.md#GbkToUtf8) ### 3. cryptor 加密包支持数据加密和解密,获取 md5,hash 值。支持 base64, md5, hmac, aes, des, rsa。 ```go @@ -214,6 +216,7 @@ import "github.com/duke-git/lancet/fileutil" - [MTime](https://github.com/duke-git/lancet/blob/v1/docs/fileutil_zh-CN.md#MTime) - [Sha](https://github.com/duke-git/lancet/blob/v1/docs/fileutil_zh-CN.md#Sha) - [ReadCsvFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil_zh-CN.md#ReadCsvFile) +- [WriteCsvFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil_zh-CN.md#WriteCsvFile) - [WriteStringToFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil_zh-CN.md#WriteStringToFile) - [WriteBytesToFile](https://github.com/duke-git/lancet/blob/v1/docs/fileutil_zh-CN.md#WriteBytesToFile) @@ -274,7 +277,7 @@ import "github.com/duke-git/lancet/mathutil" - [LCM](https://github.com/duke-git/lancet/blob/v1/docs/mathutil_zh-CN.md#LCM) - [Cos](https://github.com/duke-git/lancet/blob/v1/docs/mathutil_zh-CN.md#Cos) - [Sin](https://github.com/duke-git/lancet/blob/v1/docs/mathutil_zh-CN.md#Sin) - +- [Log](https://github.com/duke-git/lancet/blob/v1/docs/mathutil_zh-CN.md#Log) ### 9. netutil 网络包支持获取 ip 地址,发送 http 请求。 ```go @@ -319,6 +322,7 @@ import "github.com/duke-git/lancet/random" - [RandNumeral](https://github.com/duke-git/lancet/blob/v1/docs/random_zh-CN.md#RandNumeral) - [RandNumeralOrLetter](https://github.com/duke-git/lancet/blob/v1/docs/random_zh-CN.md#RandNumeralOrLetter) - [UUIdV4](https://github.com/duke-git/lancet/blob/v1/docs/random.md#UUIdV4) +- [RandUniqueIntSlice](https://github.com/duke-git/lancet/blob/v1/docs/random.md#RandUniqueIntSlice) ### 11. retry 重试执行函数直到函数运行成功或被 context cancel。 @@ -426,6 +430,7 @@ import "github.com/duke-git/lancet/strutil" - [Trim](https://github.com/duke-git/lancet/blob/v1/docs/strutil_zh-CN.md#Trim) - [SplitAndTrim](https://github.com/duke-git/lancet/blob/v1/docs/strutil_zh-CN.md#SplitAndTrim) - [HideString](https://github.com/duke-git/lancet/blob/v1/docs/strutil_zh-CN.md#HideString) +- [RemoveWhiteSpace](https://github.com/duke-git/lancet/blob/v1/docs/strutil_zh-CN.md#RemoveWhiteSpace) ### 14. system 包含 os, runtime, shell command 相关函数。 diff --git a/netutil/http.go b/netutil/http.go index 9e93edf..263a55b 100644 --- a/netutil/http.go +++ b/netutil/http.go @@ -18,9 +18,11 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" + "mime/multipart" "net/http" "net/url" + "os" "reflect" "regexp" "sort" @@ -93,6 +95,7 @@ type HttpRequest struct { Headers http.Header QueryParams url.Values FormData url.Values + File *File Body []byte } @@ -180,7 +183,11 @@ func (client *HttpClient) SendRequest(request *HttpRequest) (*http.Response, err client.setQueryParam(req, rawUrl, request.QueryParams) if request.FormData != nil { - client.setFormData(req, request.FormData) + if request.File != nil { + err = client.setFormData(req, request.FormData, setFile(request.File)) + } else { + err = client.setFormData(req, request.FormData, nil) + } } client.Request = req @@ -244,10 +251,79 @@ func (client *HttpClient) setQueryParam(req *http.Request, reqUrl string, queryP return nil } -func (client *HttpClient) setFormData(req *http.Request, values url.Values) { - formData := []byte(values.Encode()) - req.Body = ioutil.NopCloser(bytes.NewReader(formData)) - req.ContentLength = int64(len(formData)) +func (client *HttpClient) setFormData(req *http.Request, values url.Values, setFile SetFileFunc) error { + if setFile != nil { + err := setFile(req, values) + if err != nil { + return err + } + } else { + formData := []byte(values.Encode()) + req.Body = io.NopCloser(bytes.NewReader(formData)) + req.ContentLength = int64(len(formData)) + } + return nil +} + +type SetFileFunc func(req *http.Request, values url.Values) error + +// File struct is a combination of file attributes +type File struct { + Content []byte + Path string + FieldName string + FileName string +} + +// setFile set parameters for http request formdata file upload +func setFile(f *File) SetFileFunc { + return func(req *http.Request, values url.Values) error { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + for key, vals := range values { + for _, val := range vals { + err := writer.WriteField(key, val) + if err != nil { + return err + } + } + } + + if f.Content != nil { + part, err := writer.CreateFormFile(f.FieldName, f.FileName) + if err != nil { + return err + } + part.Write(f.Content) + } else if f.Path != "" { + file, err := os.Open(f.Path) + if err != nil { + return err + } + defer file.Close() + + part, err := writer.CreateFormFile(f.FieldName, f.FileName) + if err != nil { + return err + } + _, err = io.Copy(part, file) + if err != nil { + return err + } + } + + err := writer.Close() + if err != nil { + return err + } + + req.Body = io.NopCloser(body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.ContentLength = int64(body.Len()) + + return nil + } } // validateRequest check if a request has url, and valid method. diff --git a/netutil/http_test.go b/netutil/http_test.go index 2b8fc87..1be7738 100644 --- a/netutil/http_test.go +++ b/netutil/http_test.go @@ -1,12 +1,15 @@ package netutil import ( + "bytes" "encoding/json" "io" "io/ioutil" "log" "net/http" + "net/http/httptest" "net/url" + "os" "testing" "github.com/duke-git/lancet/internal" @@ -250,3 +253,108 @@ func TestStructToUrlValues(t *testing.T) { assert.Equal("456", queryValues2.Get("userId")) assert.Equal("", queryValues2.Get("name")) } + +func handleFileRequest(t *testing.T, w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(1024) + if err != nil { + t.Fatal(err) + } + + key1 := r.FormValue("key1") + expectedKey1 := "value1" + if key1 != expectedKey1 { + t.Fatalf("expected %s, got %s", expectedKey1, key1) + } + + key2 := r.FormValue("key2") + expectedKey2 := "value2" + if key2 != expectedKey2 { + t.Fatalf("expected %s, got %s", expectedKey2, key2) + } + + file, header, err := r.FormFile("image") + if err != nil { + t.Fatal(err) + } + + expectedFileName := "testImage.jpg" + if header.Filename != expectedFileName { + t.Fatalf("expected %s, got %s", expectedFileName, header.Filename) + } + + defer file.Close() + + content, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) + } + + expectedContent := []byte("file content") + if !bytes.Equal(content, expectedContent) { + t.Fatalf("expected %s, got %s", string(expectedContent), string(content)) + } +} + +func TestSendRequestWithFileContent(t *testing.T) { + handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + handleFileRequest(t, writer, request) + }) + + server := httptest.NewServer(handler) + defer server.Close() + + client := NewHttpClient() + request := &HttpRequest{ + RawURL: server.URL, + Method: "POST", + File: &File{Content: []byte("file content"), FieldName: "image", FileName: "testImage.jpg"}, + FormData: url.Values{"key1": {"value1"}, "key2": {"value2"}}, + } + + resp, err := client.SendRequest(request) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected %d, got %d", http.StatusOK, resp.StatusCode) + } +} + +func TestSendRequestWithFilePath(t *testing.T) { + handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + handleFileRequest(t, writer, request) + }) + + server := httptest.NewServer(handler) + defer server.Close() + + tmpFile, err := ioutil.TempFile("", "testImage.jpg") + if err != nil { + t.Fatal(err) + } + + defer os.Remove(tmpFile.Name()) + + tmpFile.Write([]byte("file content")) + tmpFile.Close() + + client := NewHttpClient() + request := &HttpRequest{ + RawURL: server.URL, + Method: "POST", + File: &File{Path: tmpFile.Name(), FieldName: "image", FileName: "testImage.jpg"}, + FormData: url.Values{"key1": {"value1"}, "key2": {"value2"}}, + } + + resp, err := client.SendRequest(request) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected %d, got %d", http.StatusOK, resp.StatusCode) + } +}