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{}