Files
2024-09-17 23:59:38 +03:00

196 lines
5.4 KiB
Go

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
}