diff options
| author | Shulhan <ms@kilabit.info> | 2022-01-30 17:17:58 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2022-01-30 17:17:58 +0700 |
| commit | d5c58233592b7642980790754fba015fe3fb04dc (patch) | |
| tree | 5c3d5a7abe3024fa6196ecbc15f3c08f2eb97ec7 | |
| parent | d60135a97783a59dce0a4552ceded1f53172f25c (diff) | |
| download | pakakeh.go-d5c58233592b7642980790754fba015fe3fb04dc.tar.xz | |
lib/http: implement method Download() on Client
The Download method get a resource from remote server and write it into
DownloadRequest.Output (a io.Writer).
| -rw-r--r-- | lib/http/client.go | 55 | ||||
| -rw-r--r-- | lib/http/client_request.go | 151 | ||||
| -rw-r--r-- | lib/http/client_test.go | 93 | ||||
| -rw-r--r-- | lib/http/download_request.go | 17 | ||||
| -rw-r--r-- | lib/http/http.go | 2 | ||||
| -rw-r--r-- | lib/http/http_test.go | 40 |
6 files changed, 358 insertions, 0 deletions
diff --git a/lib/http/client.go b/lib/http/client.go index b4b7667f..964674d1 100644 --- a/lib/http/client.go +++ b/lib/http/client.go @@ -153,6 +153,61 @@ func (client *Client) Do(httpRequest *http.Request) ( } // +// Download a resource from remote server and write it into +// DownloadRequest.Output. +// +// If the DownloadRequest.Output is nil, it will return an error +// ErrClientDownloadNoOutput. +// If server return HTTP code beside 200, it will return non-nil +// http.Response with an error. +// +func (client *Client) Download(req DownloadRequest) (httpRes *http.Response, err error) { + var ( + logp = "Download" + httpReq *http.Request + tee io.Reader + errClose error + ) + + if req.Output == nil { + return nil, fmt.Errorf("%s: %w", logp, ErrClientDownloadNoOutput) + } + + httpReq, err = req.toHttpRequest(client) + if err != nil { + return nil, fmt.Errorf("%s: %s", logp, err) + } + + httpRes, err = client.Client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + if httpRes.StatusCode != http.StatusOK { + err = fmt.Errorf("%s: %s", logp, httpRes.Status) + goto out + } + + tee = io.TeeReader(httpRes.Body, req.Output) + + _, err = io.ReadAll(tee) + if err != nil { + err = fmt.Errorf("%s: %w", logp, err) + } +out: + errClose = httpRes.Body.Close() + if errClose != nil { + if err == nil { + err = fmt.Errorf("%s: %w", logp, errClose) + } else { + err = fmt.Errorf("%w: %s", err, errClose) + } + } + + return httpRes, err +} + +// // GenerateHttpRequest generate http.Request from method, path, requestType, // headers, and params. // diff --git a/lib/http/client_request.go b/lib/http/client_request.go new file mode 100644 index 00000000..1e887c6a --- /dev/null +++ b/lib/http/client_request.go @@ -0,0 +1,151 @@ +// Copyright 2022, 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" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// ClientRequest define the parameters for each Client methods. +type ClientRequest struct { + // Headers additional header to be send on request. + // This field is optional. + Headers http.Header + + // + // Params define parameter to be send on request. + // This field is optional. + // + // For Method GET, CONNECT, DELETE, HEAD, OPTIONS, or TRACE; the + // params value should be nil or url.Values. + // If its url.Values, then the params will be encoded as query + // parameters. + // + // For Method PATCH, POST, or PUT; the Params will converted based on + // Type rules below, + // + // * If Type is RequestTypeQuery and Params is url.Values it will be + // added as query parameters in the Path. + // + // * If Type is RequestTypeForm and Params is url.Values it will be + // added as URL encoded in the body. + // + // * If Type is RequestTypeMultipartForm and Params is + // map[string][]byte, then it will be converted as multipart form in + // the body. + // + // * If Type is RequestTypeJSON and Params is not nil, the params will + // be encoded as JSON in body using json.Encode(). + // + Params interface{} + + // The Path to resource on the server. + // This field is required, if its empty default to "/". + Path string + + // The HTTP method of request. + // This field is optional, if its empty default to RequestMethodGet + // (GET). + Method RequestMethod + + // The Type of request. + // This field is optional, it's affect how the Params field encoded in + // the path or body. + Type RequestType +} + +// +// toHttpRequest convert the ClientRequest into the standard http.Request. +// +func (creq *ClientRequest) toHttpRequest(client *Client) (httpReq *http.Request, err error) { + var ( + logp = "toHttpRequest" + paramsAsUrlValues url.Values + paramsAsJSON []byte + contentType = creq.Type.String() + path strings.Builder + body io.Reader + strBody string + isParamsUrlValues bool + ) + + if client != nil { + path.WriteString(client.opts.ServerUrl) + } + path.WriteString(creq.Path) + paramsAsUrlValues, isParamsUrlValues = creq.Params.(url.Values) + + switch creq.Method { + case RequestMethodGet, + RequestMethodConnect, + RequestMethodDelete, + RequestMethodHead, + RequestMethodOptions, + RequestMethodTrace: + + if isParamsUrlValues { + path.WriteString("?") + path.WriteString(paramsAsUrlValues.Encode()) + } + + case RequestMethodPatch, + RequestMethodPost, + RequestMethodPut: + switch creq.Type { + case RequestTypeQuery: + if isParamsUrlValues { + path.WriteString("?") + path.WriteString(paramsAsUrlValues.Encode()) + } + + case RequestTypeForm: + if isParamsUrlValues { + strBody = paramsAsUrlValues.Encode() + body = strings.NewReader(strBody) + } + + case RequestTypeMultipartForm: + paramsAsMultipart, ok := creq.Params.(map[string][]byte) + if ok { + contentType, strBody, err = generateFormData(paramsAsMultipart) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + body = strings.NewReader(strBody) + } + + case RequestTypeJSON: + if creq.Params != nil { + paramsAsJSON, err = json.Marshal(creq.Params) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + body = bytes.NewReader(paramsAsJSON) + } + } + } + + httpReq, err = http.NewRequest(creq.Method.String(), path.String(), body) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + if client != nil { + setHeaders(httpReq, client.opts.Headers) + } + setHeaders(httpReq, creq.Headers) + + if len(contentType) > 0 { + httpReq.Header.Set(HeaderContentType, contentType) + } + + return httpReq, nil +} diff --git a/lib/http/client_test.go b/lib/http/client_test.go new file mode 100644 index 00000000..f9065cef --- /dev/null +++ b/lib/http/client_test.go @@ -0,0 +1,93 @@ +// Copyright 2022, 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" + "testing" + + "github.com/shuLhan/share/lib/test" +) + +func TestClient_Download(t *testing.T) { + var ( + logp = "Download" + out bytes.Buffer + err error + + clientOpts = ClientOptions{ + ServerUrl: fmt.Sprintf("http://%s", testServer.Options.Address), + } + client = NewClient(&clientOpts) + ) + + cases := []struct { + desc string + expError string + req DownloadRequest + }{{ + desc: "With nil Output", + req: DownloadRequest{ + ClientRequest: ClientRequest{ + Path: "/redirect/downloads", + }, + }, + expError: fmt.Sprintf("%s: %s", logp, ErrClientDownloadNoOutput), + }, { + desc: "With invalid path", + req: DownloadRequest{ + ClientRequest: ClientRequest{ + Path: "/redirect/downloads", + }, + Output: &out, + }, + expError: fmt.Sprintf("%s: 404 Not Found", logp), + }, { + desc: "With redirect", + req: DownloadRequest{ + ClientRequest: ClientRequest{ + Path: "/redirect/download", + }, + Output: &out, + }, + }, { + desc: "With redirect and trailing slash", + req: DownloadRequest{ + ClientRequest: ClientRequest{ + Path: "/redirect/download/", + }, + Output: &out, + }, + }, { + desc: "With direct path", + req: DownloadRequest{ + ClientRequest: ClientRequest{ + Path: "/download", + }, + Output: &out, + }, + }, { + desc: "With direct path and trailing slash", + req: DownloadRequest{ + ClientRequest: ClientRequest{ + Path: "/download/", + }, + Output: &out, + }, + }} + + for _, c := range cases { + out.Reset() + + _, err = client.Download(c.req) + if err != nil { + test.Assert(t, c.desc+" - error", c.expError, err.Error()) + continue + } + + test.Assert(t, c.desc, testDownloadBody, out.Bytes()) + } +} diff --git a/lib/http/download_request.go b/lib/http/download_request.go new file mode 100644 index 00000000..5c7838a4 --- /dev/null +++ b/lib/http/download_request.go @@ -0,0 +1,17 @@ +// Copyright 2022, 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 "io" + +// DownloadRequest define the parameter for Client's Download() method. +type DownloadRequest struct { + // Output define where the downloaded resource from server will be + // writen. + // This field is required. + Output io.Writer + + ClientRequest +} diff --git a/lib/http/http.go b/lib/http/http.go index 26f233fd..60094576 100644 --- a/lib/http/http.go +++ b/lib/http/http.go @@ -238,6 +238,8 @@ const ( ) var ( + ErrClientDownloadNoOutput = errors.New("invalid or empty client download output") + // // ErrEndpointAmbiguous define an error when registering path that // already exist. For example, after registering "/:x", registering diff --git a/lib/http/http_test.go b/lib/http/http_test.go index 42404e51..707b383f 100644 --- a/lib/http/http_test.go +++ b/lib/http/http_test.go @@ -66,6 +66,8 @@ func TestMain(m *testing.M) { log.Fatal(err) } + registerEndpoints() + go func() { err := testServer.Start() if err != nil { @@ -82,3 +84,41 @@ func TestMain(m *testing.M) { os.Exit(status) } + +var ( + testDownloadBody []byte +) + +func registerEndpoints() { + var err error + + testDownloadBody, err = os.ReadFile("client.go") + if err != nil { + log.Fatalf("TestMain: %s", err) + } + + // Endpoint to test the client Download(). + err = testServer.RegisterEndpoint(&Endpoint{ + Path: "/download", + ResponseType: ResponseTypePlain, + Call: func(epr *EndpointRequest) ([]byte, error) { + return testDownloadBody, nil + }, + }) + if err != nil { + log.Fatalf("TestMain: %s", err) + } + + // Endpoint to test the client Download() with HTTP 302 redirect. + err = testServer.RegisterEndpoint(&Endpoint{ + Path: "/redirect/download", + ResponseType: ResponseTypePlain, + Call: func(epr *EndpointRequest) ([]byte, error) { + http.Redirect(epr.HttpWriter, epr.HttpRequest, "/download", http.StatusFound) + return nil, nil + }, + }) + if err != nil { + log.Fatalf("TestMain: %s", err) + } +} |
