forked from mncaudill/go-flickr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
flickr.go
228 lines (184 loc) · 4.83 KB
/
flickr.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
package flickr
import (
"bytes"
"crypto/md5"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"sort"
)
const (
endpoint = "https://api.flickr.com/services/rest/?"
uploadEndpoint = "https://api.flickr.com/services/upload/"
replaceEndpoint = "https://api.flickr.com/services/replace/"
apiHost = "api.flickr.com"
)
type Request struct {
ApiKey string
Method string
Args map[string]string
}
type Response struct {
Status string `xml:"stat,attr"`
Error *ResponseError `xml:"err"`
Payload string `xml:",innerxml"`
}
type ResponseError struct {
Code string `xml:"code,attr"`
Message string `xml:"msg,attr"`
}
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error { return nil }
type Error string
func (e Error) Error() string {
return string(e)
}
func (request *Request) Sign(secret string) {
args := request.Args
// Remove api_sig
delete(args, "api_sig")
sorted_keys := make([]string, len(args)+2)
args["api_key"] = request.ApiKey
args["method"] = request.Method
// Sort array keys
i := 0
for k := range args {
sorted_keys[i] = k
i++
}
sort.Strings(sorted_keys)
// Build out ordered key-value string prefixed by secret
s := secret
for _, key := range sorted_keys {
if args[key] != "" {
s += fmt.Sprintf("%s%s", key, args[key])
}
}
// Since we're only adding two keys, it's easier
// and more space-efficient to just delete them
// them copy the whole map
delete(args, "api_key")
delete(args, "method")
// Have the full string, now hash
hash := md5.New()
hash.Write([]byte(s))
// Add api_sig as one of the args
args["api_sig"] = fmt.Sprintf("%x", hash.Sum(nil))
}
func (request *Request) URL() string {
args := request.Args
args["api_key"] = request.ApiKey
args["method"] = request.Method
s := endpoint + encodeQuery(args)
return s
}
func (request *Request) Execute() (response string, ret error) {
if request.ApiKey == "" || request.Method == "" {
return "", Error("Need both API key and method")
}
s := request.URL()
res, err := http.Get(s)
defer res.Body.Close()
if err != nil {
return "", err
}
body, _ := ioutil.ReadAll(res.Body)
return string(body), nil
}
func encodeQuery(args map[string]string) string {
i := 0
s := bytes.NewBuffer(nil)
for k, v := range args {
if i != 0 {
s.WriteString("&")
}
i++
s.WriteString(k + "=" + url.QueryEscape(v))
}
return s.String()
}
func (request *Request) buildPost(url_ string, filename string, filetype string) (*http.Request, error) {
real_url, _ := url.Parse(url_)
f, err := os.Open(filename)
if err != nil {
return nil, err
}
stat, err := f.Stat()
if err != nil {
return nil, err
}
f_size := stat.Size()
request.Args["api_key"] = request.ApiKey
boundary, end := "----###---###--flickr-go-rules", "\r\n"
// Build out all of POST body sans file
header := bytes.NewBuffer(nil)
for k, v := range request.Args {
header.WriteString("--" + boundary + end)
header.WriteString("Content-Disposition: form-data; name=\"" + k + "\"" + end + end)
header.WriteString(v + end)
}
header.WriteString("--" + boundary + end)
header.WriteString("Content-Disposition: form-data; name=\"photo\"; filename=\"photo.jpg\"" + end)
header.WriteString("Content-Type: " + filetype + end + end)
footer := bytes.NewBufferString(end + "--" + boundary + "--" + end)
body_len := int64(header.Len()) + int64(footer.Len()) + f_size
r, w := io.Pipe()
go func() {
pieces := []io.Reader{header, f, footer}
for _, k := range pieces {
_, err = io.Copy(w, k)
if err != nil {
w.CloseWithError(nil)
return
}
}
f.Close()
w.Close()
}()
http_header := make(http.Header)
http_header.Add("Content-Type", "multipart/form-data; boundary="+boundary)
postRequest := &http.Request{
Method: "POST",
URL: real_url,
Host: apiHost,
Header: http_header,
Body: r,
ContentLength: body_len,
}
return postRequest, nil
}
// Example:
// r.Upload("thumb.jpg", "image/jpeg")
func (request *Request) Upload(filename string, filetype string) (response *Response, err error) {
postRequest, err := request.buildPost(uploadEndpoint, filename, filetype)
if err != nil {
return nil, err
}
return sendPost(postRequest)
}
func (request *Request) Replace(filename string, filetype string) (response *Response, err error) {
postRequest, err := request.buildPost(replaceEndpoint, filename, filetype)
if err != nil {
return nil, err
}
return sendPost(postRequest)
}
func sendPost(postRequest *http.Request) (response *Response, err error) {
// Create and use TCP connection (lifted mostly wholesale from http.send)
client := http.DefaultClient
resp, err := client.Do(postRequest)
if err != nil {
return nil, err
}
rawBody, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
var r Response
err = xml.Unmarshal(rawBody, &r)
return &r, err
}