forked from slok/go-copy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
271 lines (218 loc) · 7.06 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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// Here starts all the mess :)
//
// You need a client to work with all the time, the client has all the neccessary
// things to work with: URL, Session, result decoding logic...
//
// First create a client and the create services with the created client, these
// services will be the ones that retrieve data from the Copy servers
//
//
// The program has some global package variables
//
// defaultHttpClient: The default http client
// appTokenEnv: The copy app oauth token
// appSecretEnv: The copy app oauth secret
// accessTokenEnv : The user authorized oauth token for the app
// accessSecretEnv: The user authorized oauth secret for the app
// session: The session for the oauth hand shaking
// mux: the mux for the server mocking in the tests
// client: The mighty client for the job ;)
// server: The mock server for the tests
package copy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"strings"
)
// Client has the session (Session) for calling the REST API with Oauth
// the Http client and the URL to call
type Client struct {
session *Session
resourcesUrl string
httpClient *http.Client
}
const (
defaultResourcesUrl = "https://api.copy.com/rest"
)
var (
defaultHttpClient = http.DefaultClient
)
// Oauth handshake neccesary data
const (
appTokenEnv = "APP_TOKEN"
appSecretEnv = "APP_SECRET"
accessTokenEnv = "ACCESS_TOKEN"
accessSecretEnv = "ACCESS_SECRET"
)
// Global vars for the tokens
var (
appToken string
appSecret string
accessToken string
accessSecret string
)
// Creates a new client. If no http client and URL the client will use the
// default ones
func NewClient(httpClient *http.Client, resourcesUrl string,
appToken string, appSecret string,
accessToken string, accessSecret string) (*Client, error) {
c := new(Client)
if httpClient == nil {
c.httpClient = defaultHttpClient
} else {
c.httpClient = httpClient
}
if resourcesUrl == "" {
c.resourcesUrl = defaultResourcesUrl
} else {
c.resourcesUrl = resourcesUrl
}
session, err := NewSession(
AppToken{
Token: appToken,
Key: appSecret,
},
AccessToken{
Token: accessToken,
Key: accessSecret,
},
)
if err != nil || appToken == "" || appSecret == "" || accessToken == "" ||
accessSecret == "" {
return nil, errors.New("Could not create the client, Check access settings")
}
c.session = session
return c, nil
}
// Returns a default client, normally we will use this
func NewDefaultClient(appToken string, appSecret string,
accessToken string, accessSecret string) (*Client, error) {
return NewClient(nil, "", appToken, appSecret, accessToken, accessSecret)
}
// Makes the client request based on the url, method, values and returns
// the response is the response of the call
// the value is inside the v param (you should pass a pointer because will
// mutate inside the method)
func (c *Client) DoRequestDecoding(method string, urlStr string, form url.Values, v interface{}) (*http.Response, error) {
var resp *http.Response
var err error
endpoint := strings.Join([]string{c.resourcesUrl, urlStr}, "/")
switch method {
case "GET":
resp, err = c.session.Get(endpoint, form, c.httpClient)
case "POST":
resp, err = c.session.Post(endpoint, form, c.httpClient)
case "PUT":
resp, err = c.session.Put(endpoint, form, c.httpClient)
case "DELETE":
resp, err = c.session.Delete(endpoint, form, c.httpClient)
}
if err != nil || resp == nil {
return nil, errors.New("Error making the request")
}
defer resp.Body.Close()
// If v is nil that means that the caller doesn't need the response
if v != nil {
// response body to string
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return nil, err
}
respBody := buf.String()
// Decode to our structure
json.NewDecoder(strings.NewReader(respBody)).Decode(v)
}
if resp.StatusCode >= 400 { // 400s and 500s
return resp, errors.New(fmt.Sprintf("Client response: %d", resp.StatusCode))
}
return resp, nil
}
// Makes the client request based on the url.
//
// This will be binary data body so we don't process the request
func (c *Client) DoRequestContent(urlStr string, form url.Values) (*http.Response, error) {
var resp *http.Response
var err error
endpoint := strings.Join([]string{c.resourcesUrl, urlStr}, "/")
resp, err = c.session.Get(endpoint, form, c.httpClient)
if err != nil || resp == nil {
return nil, errors.New("Error making the request")
}
// Don't close the body is a chunked HTTP response
//defer resp.Body.Close()
if resp.StatusCode >= 400 { // 400s and 500s
return resp, errors.New(fmt.Sprintf("Client response: %d", resp.StatusCode))
}
return resp, nil
}
// Makes the client request for uploading multipart request
//
func (c *Client) DoRequestMultipart(filePath, uploadPath, filename, method string) (*http.Response, error) {
endpoint := strings.Join([]string{c.resourcesUrl, uploadPath}, "/")
// Do sequential upload (not all in memory)
// Get our file reader
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
// The key is to use a pipe
// (reading the file will feed the writer and then the reader will be in the multipart body
// so when the multipart request a chunk the reader will "call" the writer and this will
// read from the file)
//
// File -> FileReader -> Writer -> (Multipart wrapp magic) -> Reader -> Multipart body
/*reader, writer := io.Pipe()
multiWriter := multipart.NewWriter(writer)
// The sequential write (read from file) will be in a goroutine
go func() {
defer writer.Close()
defer file.Close()
part, _ := multiWriter.CreateFormFile("file", filename)
// Copy on demand
io.Copy(part, file)
multiWriter.Close()
}()
// This will be custom because the multipart is trickier thatn a normal request
req, err := http.NewRequest("POST", endpoint, reader)
if err != nil {
return nil, err
}*/
//-----------------------------------------------------------
// FIXME: See above, use pipes to read/write and don't load in memory
// From the docs: The maximum filesize of an upload is 1GB. An API endpoint
// supporting chunked file uploading is planned for circumventing this limitation.
//defer file.Close()
body := &bytes.Buffer{}
multiWriter := multipart.NewWriter(body)
part, err := multiWriter.CreateFormFile("file", filename)
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
multiWriter.Close()
req, err := http.NewRequest(method, endpoint, body)
if err != nil {
return nil, err
}
//-----------------------------------------------------------
fileInfo, _ := file.Stat()
req.Header.Set("Authorization", c.session.OauthClient.AuthorizationHeader(&c.session.TokenCreds, method, req.URL, nil))
req.Header.Set("Content-Type", multiWriter.FormDataContentType())
req.Header.Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
resp, err := c.session.Do(req, c.httpClient)
if err != nil || resp == nil {
return nil, errors.New("Error making the request")
}
if resp.StatusCode >= 400 { // 400s and 500s
return resp, errors.New(fmt.Sprintf("Client response: %d", resp.StatusCode))
}
return resp, nil
}