This repository has been archived by the owner on Nov 2, 2018. It is now read-only.
/
client.go
208 lines (182 loc) · 5.51 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/NebulousLabs/Sia/node/api"
"github.com/NebulousLabs/errors"
)
// A Client makes requests to the siad HTTP API.
type Client struct {
// Address is the API address of the siad server.
Address string
// Password must match the password of the siad server.
Password string
// UserAgent must match the User-Agent required by the siad server. If not
// set, it defaults to "Sia-Agent".
UserAgent string
}
// New creates a new Client using the provided address.
func New(address string) *Client {
return &Client{
Address: address,
}
}
// NewRequest constructs a request to the siad HTTP API, setting the correct
// User-Agent and Basic Auth. The resource path must begin with /.
func (c *Client) NewRequest(method, resource string, body io.Reader) (*http.Request, error) {
url := "http://" + c.Address + resource
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
agent := c.UserAgent
if agent == "" {
agent = "Sia-Agent"
}
req.Header.Set("User-Agent", agent)
if c.Password != "" {
req.SetBasicAuth("", c.Password)
}
return req, nil
}
// drainAndClose reads rc until EOF and then closes it. drainAndClose should
// always be called on HTTP response bodies, because if the body is not fully
// read, the underlying connection can't be reused.
func drainAndClose(rc io.ReadCloser) {
io.Copy(ioutil.Discard, rc)
rc.Close()
}
// readAPIError decodes and returns an api.Error.
func readAPIError(r io.Reader) error {
var apiErr api.Error
if err := json.NewDecoder(r).Decode(&apiErr); err != nil {
return errors.AddContext(err, "could not read error response")
}
return apiErr
}
// getRawResponse requests the specified resource. The response, if provided,
// will be returned in a byte slice
func (c *Client) getRawResponse(resource string) ([]byte, error) {
req, err := c.NewRequest("GET", resource, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.AddContext(err, "request failed")
}
defer drainAndClose(res.Body)
if res.StatusCode == http.StatusNotFound {
return nil, errors.New("API call not recognized: " + resource)
}
// If the status code is not 2xx, decode and return the accompanying
// api.Error.
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, readAPIError(res.Body)
}
if res.StatusCode == http.StatusNoContent {
// no reason to read the response
return []byte{}, nil
}
return ioutil.ReadAll(res.Body)
}
// getRawResponse requests part of the specified resource. The response, if
// provided, will be returned in a byte slice
func (c *Client) getRawPartialResponse(resource string, from, to uint64) ([]byte, error) {
req, err := c.NewRequest("GET", resource, nil)
if err != nil {
return nil, err
}
req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", from, to))
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.AddContext(err, "request failed")
}
defer drainAndClose(res.Body)
if res.StatusCode == http.StatusNotFound {
return nil, errors.New("API call not recognized: " + resource)
}
// If the status code is not 2xx, decode and return the accompanying
// api.Error.
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, readAPIError(res.Body)
}
if res.StatusCode == http.StatusNoContent {
// no reason to read the response
return []byte{}, nil
}
return ioutil.ReadAll(res.Body)
}
// get requests the specified resource. The response, if provided, will be
// decoded into obj. The resource path must begin with /.
func (c *Client) get(resource string, obj interface{}) error {
// Request resource
data, err := c.getRawResponse(resource)
if err != nil {
return err
}
if obj == nil {
// No need to decode response
return nil
}
// Decode response
buf := bytes.NewBuffer(data)
err = json.NewDecoder(buf).Decode(obj)
if err != nil {
return errors.AddContext(err, "could not read response")
}
return nil
}
// postRawResponse requests the specified resource. The response, if provided,
// will be returned in a byte slice
func (c *Client) postRawResponse(resource string, data string) ([]byte, error) {
req, err := c.NewRequest("POST", resource, strings.NewReader(data))
if err != nil {
return nil, err
}
// TODO: is this necessary?
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.AddContext(err, "request failed")
}
defer drainAndClose(res.Body)
if res.StatusCode == http.StatusNotFound {
return nil, errors.New("API call not recognized: " + resource)
}
// If the status code is not 2xx, decode and return the accompanying
// api.Error.
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, readAPIError(res.Body)
}
if res.StatusCode == http.StatusNoContent {
// no reason to read the response
return []byte{}, nil
}
return ioutil.ReadAll(res.Body)
}
// post makes a POST request to the resource at `resource`, using `data` as the
// request body. The response, if provided, will be decoded into `obj`.
func (c *Client) post(resource string, data string, obj interface{}) error {
// Request resource
body, err := c.postRawResponse(resource, data)
if err != nil {
return err
}
if obj == nil {
// No need to decode response
return nil
}
// Decode response
buf := bytes.NewBuffer(body)
err = json.NewDecoder(buf).Decode(obj)
if err != nil {
return errors.AddContext(err, "could not read response")
}
return nil
}