/ client / rpc / requestbuilder.go
requestbuilder.go
  1  package rpc
  2  
  3  import (
  4  	"bytes"
  5  	"context"
  6  	"fmt"
  7  	"io"
  8  	"strconv"
  9  	"strings"
 10  
 11  	"github.com/blang/semver/v4"
 12  	"github.com/ipfs/boxo/files"
 13  )
 14  
 15  type RequestBuilder interface {
 16  	Arguments(args ...string) RequestBuilder
 17  	BodyString(body string) RequestBuilder
 18  	BodyBytes(body []byte) RequestBuilder
 19  	Body(body io.Reader) RequestBuilder
 20  	FileBody(body io.Reader) RequestBuilder
 21  	Option(key string, value any) RequestBuilder
 22  	Header(name, value string) RequestBuilder
 23  	Send(ctx context.Context) (*Response, error)
 24  	Exec(ctx context.Context, res any) error
 25  }
 26  
 27  // encodedAbsolutePathVersion is the version from which the absolute path header in
 28  // multipart requests is %-encoded. Before this version, its sent raw.
 29  var encodedAbsolutePathVersion = semver.MustParse("0.23.0-dev")
 30  
 31  // requestBuilder is an IPFS commands request builder.
 32  type requestBuilder struct {
 33  	command    string
 34  	args       []string
 35  	opts       map[string]string
 36  	headers    map[string]string
 37  	body       io.Reader
 38  	buildError error
 39  
 40  	shell *HttpApi
 41  }
 42  
 43  // Arguments adds the arguments to the args.
 44  func (r *requestBuilder) Arguments(args ...string) RequestBuilder {
 45  	r.args = append(r.args, args...)
 46  	return r
 47  }
 48  
 49  // BodyString sets the request body to the given string.
 50  func (r *requestBuilder) BodyString(body string) RequestBuilder {
 51  	return r.Body(strings.NewReader(body))
 52  }
 53  
 54  // BodyBytes sets the request body to the given buffer.
 55  func (r *requestBuilder) BodyBytes(body []byte) RequestBuilder {
 56  	return r.Body(bytes.NewReader(body))
 57  }
 58  
 59  // Body sets the request body to the given reader.
 60  func (r *requestBuilder) Body(body io.Reader) RequestBuilder {
 61  	r.body = body
 62  	return r
 63  }
 64  
 65  // FileBody sets the request body to the given reader wrapped into multipartreader.
 66  func (r *requestBuilder) FileBody(body io.Reader) RequestBuilder {
 67  	pr, _ := files.NewReaderPathFile("/dev/stdin", io.NopCloser(body), nil)
 68  	d := files.NewMapDirectory(map[string]files.Node{"": pr})
 69  
 70  	version, err := r.shell.loadRemoteVersion()
 71  	if err != nil {
 72  		// Unfortunately, we cannot return an error here. Changing this API is also
 73  		// not the best since we would otherwise have an inconsistent RequestBuilder.
 74  		// We save the error and return it when calling [requestBuilder.Send].
 75  		r.buildError = err
 76  		return r
 77  	}
 78  
 79  	useEncodedAbsPaths := version.LT(encodedAbsolutePathVersion)
 80  	r.body = files.NewMultiFileReader(d, false, useEncodedAbsPaths)
 81  
 82  	return r
 83  }
 84  
 85  // Option sets the given option.
 86  func (r *requestBuilder) Option(key string, value any) RequestBuilder {
 87  	var s string
 88  	switch v := value.(type) {
 89  	case bool:
 90  		s = strconv.FormatBool(v)
 91  	case string:
 92  		s = v
 93  	case []byte:
 94  		s = string(v)
 95  	default:
 96  		// slow case.
 97  		s = fmt.Sprint(value)
 98  	}
 99  	if r.opts == nil {
100  		r.opts = make(map[string]string, 1)
101  	}
102  	r.opts[key] = s
103  	return r
104  }
105  
106  // Header sets the given header.
107  func (r *requestBuilder) Header(name, value string) RequestBuilder {
108  	if r.headers == nil {
109  		r.headers = make(map[string]string, 1)
110  	}
111  	r.headers[name] = value
112  	return r
113  }
114  
115  // Send sends the request and return the response.
116  func (r *requestBuilder) Send(ctx context.Context) (*Response, error) {
117  	if r.buildError != nil {
118  		return nil, r.buildError
119  	}
120  
121  	r.shell.applyGlobal(r)
122  
123  	req := NewRequest(ctx, r.shell.url, r.command, r.args...)
124  	req.Opts = r.opts
125  	req.Headers = r.headers
126  	req.Body = r.body
127  	return req.Send(&r.shell.httpcli)
128  }
129  
130  // Exec sends the request a request and decodes the response.
131  func (r *requestBuilder) Exec(ctx context.Context, res any) error {
132  	httpRes, err := r.Send(ctx)
133  	if err != nil {
134  		return err
135  	}
136  
137  	if res == nil {
138  		lateErr := httpRes.Close()
139  		if httpRes.Error != nil {
140  			return httpRes.Error
141  		}
142  		return lateErr
143  	}
144  
145  	return httpRes.decode(res)
146  }
147  
148  var _ RequestBuilder = &requestBuilder{}