master
go 148 lines 3.66 KB
Raw
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{}