2019-08-06 14:43:14 +02:00
|
|
|
package putio
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
defaultUserAgent = "go-putio"
|
|
|
|
defaultMediaType = "application/json"
|
|
|
|
defaultBaseURL = "https://api.put.io"
|
|
|
|
defaultUploadURL = "https://upload.put.io"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Client manages communication with Put.io v2 API.
|
|
|
|
type Client struct {
|
|
|
|
// HTTP client used to communicate with Put.io API
|
|
|
|
client *http.Client
|
|
|
|
|
|
|
|
// Base URL for API requests
|
|
|
|
BaseURL *url.URL
|
|
|
|
|
|
|
|
// base url for upload requests
|
|
|
|
uploadURL *url.URL
|
|
|
|
|
|
|
|
// User agent for client
|
|
|
|
UserAgent string
|
|
|
|
|
|
|
|
// ExtraHeaders are passed to the API server on every request.
|
|
|
|
ExtraHeaders http.Header
|
|
|
|
|
|
|
|
// Services used for communicating with the API
|
|
|
|
Account *AccountService
|
|
|
|
Files *FilesService
|
|
|
|
Transfers *TransfersService
|
|
|
|
Zips *ZipsService
|
|
|
|
Friends *FriendsService
|
|
|
|
Events *EventsService
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient returns a new Put.io API client, using the htttpClient, which must
|
|
|
|
// be a new Oauth2 enabled http.Client. If httpClient is not defined, default
|
|
|
|
// HTTP client is used.
|
|
|
|
func NewClient(httpClient *http.Client) *Client {
|
|
|
|
if httpClient == nil {
|
|
|
|
httpClient = http.DefaultClient
|
|
|
|
}
|
|
|
|
|
|
|
|
baseURL, _ := url.Parse(defaultBaseURL)
|
|
|
|
uploadURL, _ := url.Parse(defaultUploadURL)
|
|
|
|
c := &Client{
|
|
|
|
client: httpClient,
|
|
|
|
BaseURL: baseURL,
|
|
|
|
uploadURL: uploadURL,
|
|
|
|
UserAgent: defaultUserAgent,
|
|
|
|
ExtraHeaders: make(http.Header),
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Account = &AccountService{client: c}
|
|
|
|
c.Files = &FilesService{client: c}
|
|
|
|
c.Transfers = &TransfersService{client: c}
|
|
|
|
c.Zips = &ZipsService{client: c}
|
|
|
|
c.Friends = &FriendsService{client: c}
|
|
|
|
c.Events = &EventsService{client: c}
|
|
|
|
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) ValidateToken(ctx context.Context) (userID *int64, err error) {
|
|
|
|
req, err := c.NewRequest(ctx, "GET", "/v2/oauth2/validate", nil)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var r struct {
|
|
|
|
UserID *int64 `json:"user_id"`
|
|
|
|
}
|
|
|
|
_, err = c.Do(req, &r)
|
|
|
|
return r.UserID, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewRequest creates an API request. A relative URL can be provided via
|
|
|
|
// relURL, which will be resolved to the BaseURL of the Client.
|
|
|
|
func (c *Client) NewRequest(ctx context.Context, method, relURL string, body io.Reader) (*http.Request, error) {
|
|
|
|
rel, err := url.Parse(relURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var u *url.URL
|
|
|
|
// XXX: workaroud for upload endpoint. upload method has a different base url,
|
|
|
|
// so we've a special case for testing purposes.
|
|
|
|
if relURL == "/v2/files/upload" {
|
|
|
|
u = c.uploadURL.ResolveReference(rel)
|
|
|
|
} else {
|
|
|
|
u = c.BaseURL.ResolveReference(rel)
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(method, u.String(), body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
req.Header.Set("Accept", defaultMediaType)
|
|
|
|
req.Header.Set("User-Agent", c.UserAgent)
|
|
|
|
|
|
|
|
// merge headers with extra headers
|
|
|
|
for header, values := range c.ExtraHeaders {
|
|
|
|
for _, value := range values {
|
|
|
|
req.Header.Add(header, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do sends an API request and returns the API response. The API response is
|
|
|
|
// JSON decoded and stored in the value pointed to by v, or returned as an
|
|
|
|
// error if an API error has occurred. Response body is closed at all cases except
|
|
|
|
// v is nil. If v is nil, response body is not closed and the body can be used
|
|
|
|
// for streaming.
|
|
|
|
func (c *Client) Do(r *http.Request, v interface{}) (*http.Response, error) {
|
|
|
|
resp, err := c.client.Do(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = checkResponse(resp)
|
|
|
|
if err != nil {
|
|
|
|
// close the body at all times if there is an http error
|
|
|
|
resp.Body.Close()
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if v == nil {
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// close the body for all cases from here
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
err = json.NewDecoder(resp.Body).Decode(v)
|
|
|
|
if err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// checkResponse is the entrypoint to reading the API response. If the response
|
|
|
|
// status code is not in success range, it will try to return a structured
|
|
|
|
// error.
|
|
|
|
func checkResponse(r *http.Response) error {
|
|
|
|
status := r.StatusCode
|
|
|
|
switch {
|
|
|
|
case status >= 200 && status <= 399:
|
|
|
|
return nil
|
|
|
|
case status >= 400 && status <= 599:
|
|
|
|
// server returns json
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unexpected status code: %d", status)
|
|
|
|
}
|
|
|
|
errorResponse := &ErrorResponse{Response: r}
|
|
|
|
data, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("body read error: %s. status: %v. Details: %v:", err, status, string(data[:250]))
|
|
|
|
}
|
|
|
|
if len(data) > 0 {
|
|
|
|
err = json.Unmarshal(data, errorResponse)
|
|
|
|
if err != nil {
|
2019-08-26 19:00:17 +02:00
|
|
|
return fmt.Errorf("json decode error: %s. status: %v. Details: %v:", err, status, string(data[:250]))
|
2019-08-06 14:43:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return errorResponse
|
|
|
|
}
|