Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pull client functionality and CLI command #148

Merged
merged 14 commits into from
Nov 15, 2023
8 changes: 7 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type RequestOptions struct {

type RequestResponse struct {
StatusCode int
Headers http.Header
// ChangeID is typically set when an AsyncRequest type is performed. The
// change id allows for introspection and progress tracking of the request.
ChangeID string
Expand Down Expand Up @@ -321,7 +322,11 @@ func (rq *defaultRequester) Do(ctx context.Context, opts *RequestOptions) (*Requ

// Is the result expecting a caller-managed raw body?
if opts.Type == RawRequest {
return &RequestResponse{Body: httpResp.Body}, nil
return &RequestResponse{
StatusCode: httpResp.StatusCode,
anpep marked this conversation as resolved.
Show resolved Hide resolved
Headers: httpResp.Header,
Body: httpResp.Body,
}, nil
}

defer httpResp.Body.Close()
Expand Down Expand Up @@ -375,6 +380,7 @@ func (rq *defaultRequester) Do(ctx context.Context, opts *RequestOptions) (*Requ
// Common response
return &RequestResponse{
StatusCode: serverResp.StatusCode,
Headers: httpResp.Header,
ChangeID: serverResp.Change,
Result: serverResp.Result,
}, nil
Expand Down
92 changes: 92 additions & 0 deletions client/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/textproto"
"net/url"
Expand Down Expand Up @@ -512,3 +513,94 @@ func (client *Client) Push(opts *PushOptions) error {

return nil
}

type PullOptions struct {
// Path indicates the absolute path of the file in the remote system
// (required).
Path string

// Target is the destination io.Writer that will receive the data (required).
// During a call to Pull, Target may be written to even if an error is returned.
Target io.Writer
}

// Pull retrieves a file from the remote system.
func (client *Client) Pull(opts *PullOptions) error {
resp, err := client.Requester().Do(context.Background(), &RequestOptions{
Type: RawRequest,
Method: "GET",
Path: "/v1/files",
Query: map[string][]string{
"action": {"read"},
"path": {opts.Path},
},
Headers: map[string]string{
"Accept": "multipart/form-data",
},
})
if err != nil {
return err
}
defer resp.Body.Close()

// Obtain Content-Type to check for a multipart payload and parse its value
// in order to obtain the multipart boundary.
mediaType, params, err := mime.ParseMediaType(resp.Headers.Get("Content-Type"))
if err != nil {
return fmt.Errorf("cannot parse Content-Type: %w", err)
}
if mediaType != "multipart/form-data" {
// Not an error response after all.
return fmt.Errorf("expected a multipart response, got %q", mediaType)
}

mr := multipart.NewReader(resp.Body, params["boundary"])
filesPart, err := mr.NextPart()
if err != nil {
return fmt.Errorf("cannot decode multipart payload: %w", err)
}
defer filesPart.Close()

if filesPart.FormName() != "files" {
return fmt.Errorf(`expected first field name to be "files", got %q`, filesPart.FormName())
}
if _, err := io.Copy(opts.Target, filesPart); err != nil {
return fmt.Errorf("cannot write to target: %w", err)
}

responsePart, err := mr.NextPart()
if err != nil {
return fmt.Errorf("cannot decode multipart payload: %w", err)
}
defer responsePart.Close()
if responsePart.FormName() != "response" {
return fmt.Errorf(`expected second field name to be "response", got %q`, responsePart.FormName())
}

// Process response metadata (see defaultRequester.Do)
var multipartResp response
if err := decodeInto(responsePart, &multipartResp); err != nil {
return err
}
if err := multipartResp.err(); err != nil {
return err
}
if multipartResp.Type != "sync" {
return fmt.Errorf("expected sync response, got %q", multipartResp.Type)
}

requestResponse := &RequestResponse{Result: multipartResp.Result}

// Decode response result.
var fr []fileResult
if err := requestResponse.DecodeResult(&fr); err != nil {
return fmt.Errorf("cannot unmarshal result: %w", err)
}
if len(fr) != 1 {
return fmt.Errorf("expected exactly one result from API, got %d", len(fr))
}
if fr[0].Error != nil {
return fr[0].Error
}
return nil
}
Loading
Loading