-
Notifications
You must be signed in to change notification settings - Fork 25
/
upload.go
195 lines (165 loc) · 5.44 KB
/
upload.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
package flickr
import (
"crypto/rand"
"crypto/tls"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
// generate a random multipart boundary string,
// shamelessly copypasted from the std library
func randomBoundary() string {
var buf [30]byte
_, err := io.ReadFull(rand.Reader, buf[:])
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", buf[:])
}
// Encode the file and request parameters in a multipart body.
// File contents are streamed into the request using an io.Pipe in a separated goroutine
func streamUploadBody(client *FlickrClient, photo io.Reader, body *io.PipeWriter, fileName string, boundary string) {
// multipart writer to fill the body
defer body.Close()
writer := multipart.NewWriter(body)
writer.SetBoundary(boundary)
// create the "photo" field
part, err := writer.CreateFormFile("photo", filepath.Base(fileName))
if err != nil {
log.Fatal(err)
return
}
// fill the photo field
_, err = io.Copy(part, photo)
if err != nil {
log.Fatal(err)
return
}
// dump other params
for key, val := range client.Args {
_ = writer.WriteField(key, val[0])
}
// close the form writer
err = writer.Close()
if err != nil {
log.Fatal(err)
return
}
}
// UploadParams is a convenience struct wrapping all optional upload parameters
type UploadParams struct {
Title, Description string
Tags []string
IsPublic, IsFamily, IsFriend bool
ContentType int
Hidden int
SafetyLevel int
}
// NewUploadParams provides meaningful default values
func NewUploadParams() *UploadParams {
ret := &UploadParams{}
ret.ContentType = 1 // photo
ret.Hidden = 2 // hidden from public searchesi
ret.SafetyLevel = 1 // safe
return ret
}
// UploadResponse is a type representing a successful upload response from the api
type UploadResponse struct {
BasicResponse
ID string `xml:"photoid"`
}
// Set client query arguments based on the contents of the UploadParams struct
func fillArgsWithParams(client *FlickrClient, params *UploadParams) {
if params.Title != "" {
client.Args.Set("title", params.Title)
}
if params.Description != "" {
client.Args.Set("description", params.Description)
}
if len(params.Tags) > 0 {
client.Args.Set("tags", strings.Join(params.Tags, " "))
}
var boolString = func(b bool) string {
if b {
return "1"
}
return "0"
}
client.Args.Set("is_public", boolString(params.IsPublic))
client.Args.Set("is_friend", boolString(params.IsFriend))
client.Args.Set("is_family", boolString(params.IsFamily))
if params.ContentType >= 1 && params.ContentType <= 3 {
client.Args.Set("content_type", strconv.Itoa(params.ContentType))
}
if params.Hidden >= 1 && params.Hidden <= 2 {
client.Args.Set("hidden", strconv.Itoa(params.Hidden))
}
if params.SafetyLevel >= 1 && params.SafetyLevel <= 3 {
client.Args.Set("safety_level", strconv.Itoa(params.SafetyLevel))
}
}
// UploadFile performs a file upload using the Flickr API. If optionalParams is nil,
// no parameters will be added to the request and Flickr will set User's
// default preferences.
// This call must be signed with write permissions
func UploadFile(client *FlickrClient, path string, optionalParams *UploadParams) (*UploadResponse, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return UploadReader(client, file, file.Name(), optionalParams)
}
// UploadReader does same as UploadFile but the photo file is passed as an io.Reader instead of a file path
func UploadReader(client *FlickrClient, photoReader io.Reader, name string, optionalParams *UploadParams) (*UploadResponse, error) {
return UploadReaderWithClient(client, photoReader, name, optionalParams, nil)
}
// UploadReaderWithClient does same as UploadReader but allows passing a custom httpClient
func UploadReaderWithClient(client *FlickrClient, photoReader io.Reader, name string, optionalParams *UploadParams, httpClient *http.Client) (*UploadResponse, error) {
client.Init()
client.EndpointUrl = UPLOAD_ENDPOINT
client.HTTPVerb = "POST"
if optionalParams != nil {
fillArgsWithParams(client, optionalParams)
}
client.OAuthSign()
// write request body in a Pipe
boundary := randomBoundary()
r, w := io.Pipe()
go streamUploadBody(client, photoReader, w, name, boundary)
// create an HTTP Request
req, err := http.NewRequest("POST", client.EndpointUrl, r)
if err != nil {
return nil, err
}
// set content-type
req.Header.Set("content-type", "multipart/form-data; boundary="+boundary)
req.ContentLength = -1 // unknown
if (httpClient == nil) {
// Create a Transport to explicitly use the http1.1 client
// TODO: for some reason, when we use the http2 client flickr API responds
// with HTTP: 411 (No Content Length : POST) whereas it should be ok to
// upload using chunks. Explicitly setting `req.Header.Set("transfer-encoding", "chunked")`
// does not help and try to compute the request size isn't the right thing to do IMHO.
// We should investigate why this happens instead of forcing the downgrade to http1.1.
tr := &http.Transport{
TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
}
// instance an HTTP client
httpClient = &http.Client{Transport: tr}
}
// perform upload request streaming the file
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
apiResp := &UploadResponse{}
err = parseApiResponse(resp, apiResp)
return apiResp, err
}