-
Notifications
You must be signed in to change notification settings - Fork 8
/
api.go
141 lines (120 loc) · 4.11 KB
/
api.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
/*
Submit information about discovered devices to other services via REST API
endpoints.
Upon discovering and/or identifying a device, submit a POST request to a URL
like https://my-other-system.com/device/, optionally presenting an authorization
token. This API endpoint should behave like an "upsert" request, i.e., it
should update itself rather than bail when it receives a clue about a device it
has been told about before.
Concurrent requests to this API endpoint are limited according to the apiLimit
parameter of NewAPIClient(). If apiLimit number of requests are currently in
flight, requests will be canceled until there are slots available.
Helpful documentation about requests in Go:
- https://golang.org/pkg/net/http/
- http://polyglot.ninja/golang-making-http-requests/
*/
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"
"time"
)
// An APIClient holds state and credentials related to uploading Asset
// information to a REST API endpoint.
type APIClient struct {
url string
authToken string
clientID string
enabled bool
semaphore chan bool
}
// NewAPIClient creates a new APIClient.
func NewAPIClient(
apiURL string,
apiToken string,
clientID string,
apiLimit int,
enabled bool,
) *APIClient {
apiClient := new(APIClient)
apiClient.url = apiURL
apiClient.authToken = apiToken
apiClient.clientID = clientID
apiClient.enabled = enabled
// A channel will act as a semaphore with the desired level of concurrency
// Full example here: http://jmoiron.net/blog/limiting-concurrency-in-go/
apiClient.semaphore = make(chan bool, apiLimit)
return apiClient
}
// Upload sends asset information to a REST API.
//
// Returns response data, if any, and error, either of which may be nil.
func (apiClient *APIClient) Upload(asset *Asset) (map[string]interface{}, error) {
asset.ClientID = apiClient.clientID
// Handle API throttling. If the number of outstanding requests exceeds the
// limit, return an error.
if len(apiClient.semaphore) == cap(apiClient.semaphore) {
logger.Printf("Ignoring API request due to throttling")
return nil, fmt.Errorf("Ignoring API request due to throttling")
}
apiClient.semaphore <- true
defer func() { <-apiClient.semaphore }()
// Build JSON string
bytesRepresentation, err := json.Marshal(asset)
if err != nil {
return nil, fmt.Errorf("Error marshalling JSON: %s", err)
}
// Build request object
httpClient := http.Client{
Timeout: time.Duration(5 * time.Second),
}
request, err := http.NewRequest(
http.MethodPost,
apiClient.url,
bytes.NewBuffer(bytesRepresentation),
)
if err != nil {
return nil, fmt.Errorf("Error building request object: %s", err)
}
request.Header.Set("Content-Type", "application/json")
if apiClient.authToken != "" {
tokHdr := fmt.Sprintf("Token %s", apiClient.authToken)
request.Header.Set("Authorization", tokHdr)
}
requestDump, err := httputil.DumpRequestOut(request, true)
if err != nil {
return nil, fmt.Errorf("Error dumping request: %s", err)
}
logger.Printf("DEBUG request:\n%s\n", string(requestDump))
// Send request
response, err := httpClient.Do(request)
if err != nil {
// An error is returned if caused by client policy (such as CheckRedirect),
// or failure to speak HTTP (such as a network connectivity problem). A
// non-2xx status code doesn't cause an error.
return nil, fmt.Errorf("Error making request: %s", err)
}
// Check status code of response
logger.Println("DEBUG response:", response.Status)
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("Error API non-2xx response: %s", response.Status)
}
// Dump response for debugging
defer response.Body.Close()
responseDump, err := httputil.DumpResponse(response, true)
if err != nil {
return nil, fmt.Errorf("Error dumping response: %s", err)
}
logger.Printf("DEBUG response:\n%s\n", responseDump)
// Decode response JSON
var result map[string]interface{}
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("Error decoding response:: %s", err)
}
logger.Println("DEBUG response JSON:", result)
// Success
return result, nil
}