summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-01-30 17:17:58 +0700
committerShulhan <ms@kilabit.info>2022-01-30 17:17:58 +0700
commitd5c58233592b7642980790754fba015fe3fb04dc (patch)
tree5c3d5a7abe3024fa6196ecbc15f3c08f2eb97ec7
parentd60135a97783a59dce0a4552ceded1f53172f25c (diff)
downloadpakakeh.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.go55
-rw-r--r--lib/http/client_request.go151
-rw-r--r--lib/http/client_test.go93
-rw-r--r--lib/http/download_request.go17
-rw-r--r--lib/http/http.go2
-rw-r--r--lib/http/http_test.go40
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)
+ }
+}