Archived
1
0
This repository has been archived on 2021-02-07. You can view files and clone it, but cannot push or open issues or pull requests.
dropbox/dropbox.go

861 lines
27 KiB
Go
Raw Normal View History

2014-02-19 23:15:46 +01:00
/*
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
** SUCH DAMAGE.
*/
// Package dropbox implements the Dropbox core and datastore API.
2014-02-19 23:15:46 +01:00
package dropbox
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
2014-07-29 22:00:07 +02:00
"github.com/golang/oauth2"
2014-02-19 23:15:46 +01:00
)
2014-02-24 19:22:35 +01:00
// ErrNotAuth is the error returned when the OAuth token is not provided
var ErrNotAuth = errors.New("authentication required")
2014-02-19 23:15:46 +01:00
2014-02-24 19:22:35 +01:00
// Account represents information about the user account.
2014-02-19 23:15:46 +01:00
type Account struct {
2014-03-23 18:48:54 +01:00
ReferralLink string `json:"referral_link,omitempty"` // URL for referral.
DisplayName string `json:"display_name,omitempty"` // User name.
UID int `json:"uid,omitempty"` // User account ID.
Country string `json:"country,omitempty"` // Country ISO code.
2014-02-19 23:15:46 +01:00
QuotaInfo struct {
2014-03-23 18:48:54 +01:00
Shared int64 `json:"shared,omitempty"` // Quota for shared files.
Quota int64 `json:"quota,omitempty"` // Quota in bytes.
Normal int64 `json:"normal,omitempty"` // Quota for non-shared files.
2014-02-19 23:15:46 +01:00
} `json:"quota_info"`
}
2014-03-23 18:48:54 +01:00
// CopyRef represents the reply of CopyRef.
2014-02-19 23:15:46 +01:00
type CopyRef struct {
CopyRef string `json:"copy_ref"` // Reference to use on fileops/copy.
Expires string `json:"expires"` // Expiration date.
}
2014-02-24 19:22:35 +01:00
// DeltaPage represents the reply of delta.
2014-02-19 23:15:46 +01:00
type DeltaPage struct {
Reset bool // if true the local state must be cleared.
HasMore bool // if true an other call to delta should be made.
Cursor string // Tag of the current state.
Entries []DeltaEntry // List of changed entries.
}
2014-03-23 18:48:54 +01:00
// DeltaEntry represents the list of changes for a given path.
2014-02-19 23:15:46 +01:00
type DeltaEntry struct {
Path string // Path of this entry in lowercase.
Entry *Entry // nil when this entry does not exists.
}
2014-02-24 19:22:35 +01:00
// DeltaPoll represents the reply of longpoll_delta.
2014-02-19 23:15:46 +01:00
type DeltaPoll struct {
Changes bool `json:"changes"` // true if the polled path has changed.
Backoff int `json:"backoff"` // time in second before calling poll again.
}
2014-02-24 19:22:35 +01:00
// ChunkUploadResponse represents the reply of chunked_upload.
2014-02-19 23:15:46 +01:00
type ChunkUploadResponse struct {
2014-02-24 19:22:35 +01:00
UploadID string `json:"upload_id"` // Unique ID of this upload.
2014-02-19 23:15:46 +01:00
Offset int `json:"offset"` // Size in bytes of already sent data.
2014-03-23 18:48:54 +01:00
Expires DBTime `json:"expires"` // Expiration time of this upload.
2014-02-19 23:15:46 +01:00
}
2014-03-23 18:48:54 +01:00
// Format of reply when http error code is not 200.
2014-02-19 23:15:46 +01:00
// Format may be:
// {"error": "reason"}
// {"error": {"param": "reason"}}
2014-02-24 19:22:35 +01:00
type requestError struct {
2014-02-19 23:15:46 +01:00
Error interface{} `json:"error"` // Description of this error.
}
const (
2014-03-23 18:48:54 +01:00
// PollMinTimeout is the minimum timeout for longpoll.
2014-02-24 19:22:35 +01:00
PollMinTimeout = 30
2014-03-23 18:48:54 +01:00
// PollMaxTimeout is the maximum timeout for longpoll.
2014-02-24 19:22:35 +01:00
PollMaxTimeout = 480
// DefaultChunkSize is the maximum size of a file sendable using files_put.
DefaultChunkSize = 4 * 1024 * 1024
// MaxPutFileSize is the maximum size of a file sendable using files_put.
MaxPutFileSize = 150 * 1024 * 1024
// MetadataLimitMax is the maximum number of entries returned by metadata.
MetadataLimitMax = 25000
// MetadataLimitDefault is the default number of entries returned by metadata.
MetadataLimitDefault = 10000
// RevisionsLimitMax is the maximum number of revisions returned by revisions.
RevisionsLimitMax = 1000
// RevisionsLimitDefault is the default number of revisions returned by revisions.
RevisionsLimitDefault = 10
// SearchLimitMax is the maximum number of entries returned by search.
SearchLimitMax = 1000
// SearchLimitDefault is the default number of entries returned by search.
SearchLimitDefault = 1000
// DateFormat is the format to use when decoding a time.
DateFormat = time.RFC1123Z
2014-02-19 23:15:46 +01:00
)
2014-03-23 18:48:54 +01:00
// DBTime allow marshalling and unmarshalling of time.
type DBTime time.Time
// UnmarshalJSON unmarshals a time according to the Dropbox format.
func (dbt *DBTime) UnmarshalJSON(data []byte) error {
var s string
var err error
var t time.Time
if err = json.Unmarshal(data, &s); err != nil {
return err
}
if t, err = time.ParseInLocation(DateFormat, s, time.UTC); err != nil {
return err
}
if t.IsZero() {
*dbt = DBTime(time.Time{})
} else {
*dbt = DBTime(t)
}
return nil
}
// MarshalJSON marshals a time according to the Dropbox format.
func (dbt DBTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(dbt).Format(DateFormat))
}
2014-02-24 19:22:35 +01:00
// Entry represents the metadata of a file or folder.
2014-02-19 23:15:46 +01:00
type Entry struct {
2014-03-23 18:48:54 +01:00
Bytes int `json:"bytes,omitempty"` // Size of the file in bytes.
ClientMtime DBTime `json:"client_mtime,omitempty"` // Modification time set by the client when added.
Contents []Entry `json:"contents,omitempty"` // List of children for a directory.
Hash string `json:"hash,omitempty"` // Hash of this entry.
Icon string `json:"icon,omitempty"` // Name of the icon displayed for this entry.
IsDeleted bool `json:"is_deleted,omitempty"` // true if this entry was deleted.
IsDir bool `json:"is_dir,omitempty"` // true if this entry is a directory.
MimeType string `json:"mime_type,omitempty"` // MimeType of this entry.
Modified DBTime `json:"modified,omitempty"` // Date of last modification.
Path string `json:"path,omitempty"` // Absolute path of this entry.
Revision string `json:"rev,omitempty"` // Unique ID for this file revision.
Root string `json:"root,omitempty"` // dropbox or sandbox.
Size string `json:"size,omitempty"` // Size of the file humanized/localized.
ThumbExists bool `json:"thumb_exists,omitempty"` // true if a thumbnail is available for this entry.
2014-02-19 23:15:46 +01:00
}
// Link for sharing a file.
type Link struct {
2014-03-23 18:48:54 +01:00
Expires DBTime `json:"expires"` // Expiration date of this link.
2014-02-19 23:15:46 +01:00
URL string `json:"url"` // URL to share.
}
// Dropbox client.
type Dropbox struct {
2014-07-29 22:00:07 +02:00
RootDirectory string // dropbox or sandbox.
Locale string // Locale sent to the API to translate/format messages.
APIURL string // Normal API URL.
APIContentURL string // URL for transferring files.
APINotifyURL string // URL for realtime notification.
config *oauth2.Config
token *oauth2.Token
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// NewDropbox returns a new Dropbox configured.
2014-02-19 23:15:46 +01:00
func NewDropbox() *Dropbox {
2014-07-29 22:00:07 +02:00
db := &Dropbox{
2014-02-19 23:15:46 +01:00
RootDirectory: "dropbox", // dropbox or sandbox.
Locale: "en",
APIURL: "https://api.dropbox.com/1",
APIContentURL: "https://api-content.dropbox.com/1",
APINotifyURL: "https://api-notify.dropbox.com/1",
}
2014-07-29 22:00:07 +02:00
return db
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// SetAppInfo sets the clientid (app_key) and clientsecret (app_secret).
2014-02-19 23:15:46 +01:00
// You have to register an application on https://www.dropbox.com/developers/apps.
2014-07-29 22:00:07 +02:00
func (db *Dropbox) SetAppInfo(clientid, clientsecret string) error {
var err error
db.config, err = oauth2.NewConfig(
&oauth2.Options{
ClientID: clientid,
ClientSecret: clientsecret,
},
"https://www.dropbox.com/1/oauth2/authorize",
"https://api.dropbox.com/1/oauth2/token")
return err
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// SetAccessToken sets access token to avoid calling Auth method.
func (db *Dropbox) SetAccessToken(accesstoken string) {
2014-07-29 22:00:07 +02:00
db.token = &oauth2.Token{AccessToken: accesstoken}
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// AccessToken returns the OAuth access token.
func (db *Dropbox) AccessToken() string {
2014-07-29 22:00:07 +02:00
return db.token.AccessToken
}
func (db *Dropbox) client() *http.Client {
2014-08-14 22:45:47 +02:00
var t *oauth2.Transport
2014-07-29 22:00:07 +02:00
t = db.config.NewTransport()
t.SetToken(db.token)
return &http.Client{Transport: t}
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// Auth displays the URL to authorize this application to connect to your account.
func (db *Dropbox) Auth() error {
2014-02-19 23:15:46 +01:00
var code string
2014-08-14 22:45:47 +02:00
var t *oauth2.Transport
2014-07-29 22:00:07 +02:00
var err error
2014-02-19 23:15:46 +01:00
fmt.Printf("Please visit:\n%s\nEnter the code: ",
2014-09-07 18:54:24 +02:00
db.config.AuthCodeURL("", "", ""))
2014-02-19 23:15:46 +01:00
fmt.Scanln(&code)
2014-07-29 22:00:07 +02:00
if t, err = db.config.NewTransportWithCode(code); err != nil {
return err
}
db.token = t.Token()
db.token.TokenType = "Bearer"
return nil
2014-02-19 23:15:46 +01:00
}
// Error - all errors generated by HTTP transactions are of this type.
// Other error may be passed on from library functions though.
type Error struct {
StatusCode int // HTTP status code
Text string
}
// Error satisfy the error interface.
func (e *Error) Error() string {
return e.Text
}
// newError make a new error from a string.
func newError(StatusCode int, Text string) *Error {
return &Error{
StatusCode: StatusCode,
Text: Text,
}
}
// newErrorf makes a new error from sprintf parameters.
func newErrorf(StatusCode int, Text string, Parameters ...interface{}) *Error {
return newError(StatusCode, fmt.Sprintf(Text, Parameters...))
}
func getResponse(r *http.Response) ([]byte, error) {
var e requestError
var b []byte
var err error
if b, err = ioutil.ReadAll(r.Body); err != nil {
return nil, err
}
if r.StatusCode == http.StatusOK {
return b, nil
}
if err = json.Unmarshal(b, &e); err == nil {
switch v := e.Error.(type) {
case string:
return nil, newErrorf(r.StatusCode, "%s", v)
case map[string]interface{}:
for param, reason := range v {
if reasonstr, ok := reason.(string); ok {
return nil, newErrorf(r.StatusCode, "%s: %s", param, reasonstr)
}
}
return nil, newErrorf(r.StatusCode, "wrong parameter")
}
}
2014-07-13 12:13:36 +02:00
return nil, newErrorf(r.StatusCode, "unexpected HTTP status code %d", r.StatusCode)
}
2014-07-29 00:53:25 +02:00
// urlEncode encodes s for url
func urlEncode(s string) string {
// Would like to call url.escape(value, encodePath) here
encoded := url.QueryEscape(s)
encoded = strings.Replace(encoded, "+", "%20", -1)
return encoded
}
2014-02-24 19:22:35 +01:00
// CommitChunkedUpload ends the chunked upload by giving a name to the UploadID.
func (db *Dropbox) CommitChunkedUpload(uploadid, dst string, overwrite bool, parentRev string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var err error
var rawurl string
var response *http.Response
var params *url.Values
var body []byte
var rv Entry
if dst[0] == '/' {
dst = dst[1:]
}
params = &url.Values{
2014-02-24 19:22:35 +01:00
"locale": {db.Locale},
2014-02-19 23:15:46 +01:00
"upload_id": {uploadid},
"overwrite": {strconv.FormatBool(overwrite)},
}
if len(parentRev) != 0 {
params.Set("parent_rev", parentRev)
}
2014-07-29 00:53:25 +02:00
rawurl = fmt.Sprintf("%s/commit_chunked_upload/%s/%s?%s", db.APIContentURL, db.RootDirectory, urlEncode(dst), params.Encode())
2014-02-19 23:15:46 +01:00
2014-07-29 22:00:07 +02:00
if response, err = db.client().Post(rawurl, "", nil); err != nil {
2014-02-19 23:15:46 +01:00
return nil, err
}
defer response.Body.Close()
if body, err = getResponse(response); err != nil {
return nil, err
2014-02-19 23:15:46 +01:00
}
err = json.Unmarshal(body, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// ChunkedUpload sends a chunk with a maximum size of chunksize, if there is no session a new one is created.
func (db *Dropbox) ChunkedUpload(session *ChunkUploadResponse, input io.ReadCloser, chunksize int) (*ChunkUploadResponse, error) {
2014-02-19 23:15:46 +01:00
var err error
var rawurl string
var cur ChunkUploadResponse
var response *http.Response
var body []byte
var r *io.LimitedReader
if chunksize <= 0 {
2014-02-20 20:41:00 +01:00
chunksize = DefaultChunkSize
} else if chunksize > MaxPutFileSize {
chunksize = MaxPutFileSize
2014-02-19 23:15:46 +01:00
}
if session != nil {
2014-02-24 19:22:35 +01:00
rawurl = fmt.Sprintf("%s/chunked_upload?upload_id=%s&offset=%d", db.APIContentURL, session.UploadID, session.Offset)
2014-02-19 23:15:46 +01:00
} else {
2014-02-24 19:22:35 +01:00
rawurl = fmt.Sprintf("%s/chunked_upload", db.APIContentURL)
2014-02-19 23:15:46 +01:00
}
r = &io.LimitedReader{R: input, N: int64(chunksize)}
2014-07-29 22:00:07 +02:00
if response, err = db.client().Post(rawurl, "application/octet-stream", r); err != nil {
2014-02-19 23:15:46 +01:00
return nil, err
}
defer response.Body.Close()
if body, err = getResponse(response); err != nil {
return nil, err
2014-02-19 23:15:46 +01:00
}
err = json.Unmarshal(body, &cur)
if r.N != 0 {
err = io.EOF
}
2014-02-19 23:15:46 +01:00
return &cur, err
}
2014-02-24 19:22:35 +01:00
// UploadByChunk uploads data from the input reader to the dst path on Dropbox by sending chunks of chunksize.
func (db *Dropbox) UploadByChunk(input io.ReadCloser, chunksize int, dst string, overwrite bool, parentRev string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var err error
var cur *ChunkUploadResponse
for err == nil {
2014-02-24 19:22:35 +01:00
if cur, err = db.ChunkedUpload(cur, input, chunksize); err != nil && err != io.EOF {
2014-02-19 23:15:46 +01:00
return nil, err
}
}
2014-02-24 19:22:35 +01:00
return db.CommitChunkedUpload(cur.UploadID, dst, overwrite, parentRev)
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// FilesPut uploads size bytes from the input reader to the dst path on Dropbox.
func (db *Dropbox) FilesPut(input io.ReadCloser, size int64, dst string, overwrite bool, parentRev string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var err error
var rawurl string
var rv Entry
var request *http.Request
var response *http.Response
var params *url.Values
var body []byte
2014-02-20 20:41:00 +01:00
if size > MaxPutFileSize {
2014-02-24 19:22:35 +01:00
return nil, fmt.Errorf("could not upload files bigger than 150MB using this method, use UploadByChunk instead")
2014-02-19 23:15:46 +01:00
}
if dst[0] == '/' {
dst = dst[1:]
}
2014-03-12 19:24:36 +01:00
params = &url.Values{"overwrite": {strconv.FormatBool(overwrite)}, "locale": {db.Locale}}
2014-02-19 23:15:46 +01:00
if len(parentRev) != 0 {
params.Set("parent_rev", parentRev)
}
2014-07-29 00:53:25 +02:00
rawurl = fmt.Sprintf("%s/files_put/%s/%s?%s", db.APIContentURL, db.RootDirectory, urlEncode(dst), params.Encode())
2014-02-19 23:15:46 +01:00
if request, err = http.NewRequest("PUT", rawurl, input); err != nil {
return nil, err
}
request.Header.Set("Content-Length", strconv.FormatInt(size, 10))
2014-07-29 22:00:07 +02:00
if response, err = db.client().Do(request); err != nil {
2014-02-19 23:15:46 +01:00
return nil, err
}
defer response.Body.Close()
if body, err = getResponse(response); err != nil {
return nil, err
2014-02-19 23:15:46 +01:00
}
err = json.Unmarshal(body, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// UploadFile uploads the file located in the src path on the local disk to the dst path on Dropbox.
func (db *Dropbox) UploadFile(src, dst string, overwrite bool, parentRev string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var err error
var fd *os.File
var fsize int64
if fd, err = os.Open(src); err != nil {
return nil, err
}
defer fd.Close()
if fi, err := fd.Stat(); err == nil {
fsize = fi.Size()
} else {
return nil, err
}
2014-02-24 19:22:35 +01:00
return db.FilesPut(fd, fsize, dst, overwrite, parentRev)
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// Thumbnails gets a thumbnail for an image.
func (db *Dropbox) Thumbnails(src, format, size string) (io.ReadCloser, int64, *Entry, error) {
2014-02-19 23:15:46 +01:00
var response *http.Response
var rawurl string
var err error
var entry Entry
switch format {
case "":
format = "jpeg"
case "jpeg", "png":
break
default:
2014-02-24 19:22:35 +01:00
return nil, 0, nil, fmt.Errorf("unsupported format '%s' must be jpeg or png", format)
2014-02-19 23:15:46 +01:00
}
switch size {
case "":
size = "s"
case "xs", "s", "m", "l", "xl":
break
default:
2014-02-24 19:22:35 +01:00
return nil, 0, nil, fmt.Errorf("unsupported size '%s' must be xs, s, m, l or xl", size)
2014-02-19 23:15:46 +01:00
}
if src[0] == '/' {
src = src[1:]
}
2014-07-29 00:53:25 +02:00
rawurl = fmt.Sprintf("%s/thumbnails/%s/%s?format=%s&size=%s", db.APIContentURL, db.RootDirectory, urlEncode(src), urlEncode(format), urlEncode(size))
2014-07-29 22:00:07 +02:00
if response, err = db.client().Get(rawurl); err != nil {
2014-02-19 23:15:46 +01:00
return nil, 0, nil, err
}
2014-07-13 12:13:36 +02:00
if response.StatusCode == http.StatusOK {
json.Unmarshal([]byte(response.Header.Get("x-dropbox-metadata")), &entry)
return response.Body, response.ContentLength, &entry, err
}
response.Body.Close()
2014-02-19 23:15:46 +01:00
switch response.StatusCode {
case http.StatusNotFound:
return nil, 0, nil, os.ErrNotExist
case http.StatusUnsupportedMediaType:
return nil, 0, nil, newErrorf(response.StatusCode, "the image located at '%s' cannot be converted to a thumbnail", src)
2014-07-13 12:13:36 +02:00
default:
return nil, 0, nil, newErrorf(response.StatusCode, "unexpected HTTP status code %d", response.StatusCode)
2014-02-19 23:15:46 +01:00
}
}
2014-02-24 19:22:35 +01:00
// ThumbnailsToFile downloads the file located in the src path on the Dropbox to the dst file on the local disk.
func (db *Dropbox) ThumbnailsToFile(src, dst, format, size string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var input io.ReadCloser
var fd *os.File
var err error
var entry *Entry
if fd, err = os.Create(dst); err != nil {
return nil, err
}
defer fd.Close()
2014-02-24 19:22:35 +01:00
if input, _, entry, err = db.Thumbnails(src, format, size); err != nil {
2014-02-19 23:15:46 +01:00
os.Remove(dst)
return nil, err
}
defer input.Close()
if _, err = io.Copy(fd, input); err != nil {
os.Remove(dst)
}
return entry, err
}
2014-02-24 19:22:35 +01:00
// Download requests the file located at src, the specific revision may be given.
2014-02-19 23:15:46 +01:00
// offset is used in case the download was interrupted.
// A io.ReadCloser to get the file ans its size is returned.
2014-02-24 19:22:35 +01:00
func (db *Dropbox) Download(src, rev string, offset int) (io.ReadCloser, int64, error) {
2014-02-19 23:15:46 +01:00
var request *http.Request
var response *http.Response
var rawurl string
var err error
if src[0] == '/' {
src = src[1:]
}
2014-07-29 00:53:25 +02:00
rawurl = fmt.Sprintf("%s/files/%s/%s", db.APIContentURL, db.RootDirectory, urlEncode(src))
2014-02-19 23:15:46 +01:00
if len(rev) != 0 {
rawurl += fmt.Sprintf("?rev=%s", rev)
}
if request, err = http.NewRequest("GET", rawurl, nil); err != nil {
return nil, 0, err
}
if offset != 0 {
request.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
}
2014-07-29 22:00:07 +02:00
if response, err = db.client().Do(request); err != nil {
2014-02-19 23:15:46 +01:00
return nil, 0, err
}
2014-07-13 12:13:36 +02:00
if response.StatusCode == http.StatusOK {
return response.Body, response.ContentLength, err
}
response.Body.Close()
switch response.StatusCode {
case http.StatusNotFound:
2014-02-19 23:15:46 +01:00
return nil, 0, os.ErrNotExist
2014-07-13 12:13:36 +02:00
default:
return nil, 0, newErrorf(response.StatusCode, "unexpected HTTP status code %d", response.StatusCode)
2014-02-19 23:15:46 +01:00
}
}
2014-02-24 19:22:35 +01:00
// DownloadToFileResume resumes the download of the file located in the src path on the Dropbox to the dst file on the local disk.
func (db *Dropbox) DownloadToFileResume(src, dst, rev string) error {
2014-02-19 23:15:46 +01:00
var input io.ReadCloser
2014-02-24 19:22:35 +01:00
var fi os.FileInfo
2014-02-19 23:15:46 +01:00
var fd *os.File
var offset int
var err error
if fd, err = os.OpenFile(dst, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
return err
}
2014-02-24 19:22:35 +01:00
defer fd.Close()
if fi, err = fd.Stat(); err != nil {
2014-02-19 23:15:46 +01:00
return err
}
2014-02-24 19:22:35 +01:00
offset = int(fi.Size())
2014-02-19 23:15:46 +01:00
2014-02-24 19:22:35 +01:00
if input, _, err = db.Download(src, rev, offset); err != nil {
2014-02-19 23:15:46 +01:00
return err
}
defer input.Close()
_, err = io.Copy(fd, input)
return err
}
2014-02-24 19:22:35 +01:00
// DownloadToFile downloads the file located in the src path on the Dropbox to the dst file on the local disk.
2014-02-19 23:15:46 +01:00
// If the destination file exists it will be truncated.
2014-02-24 19:22:35 +01:00
func (db *Dropbox) DownloadToFile(src, dst, rev string) error {
2014-02-19 23:15:46 +01:00
var input io.ReadCloser
var fd *os.File
var err error
if fd, err = os.Create(dst); err != nil {
return err
}
defer fd.Close()
2014-02-24 19:22:35 +01:00
if input, _, err = db.Download(src, rev, 0); err != nil {
2014-02-19 23:15:46 +01:00
os.Remove(dst)
return err
}
defer input.Close()
if _, err = io.Copy(fd, input); err != nil {
os.Remove(dst)
}
return err
}
2014-02-24 19:22:35 +01:00
func (db *Dropbox) doRequest(method, path string, params *url.Values, receiver interface{}) error {
2014-02-19 23:15:46 +01:00
var body []byte
var rawurl string
var response *http.Response
var request *http.Request
var err error
if params == nil {
2014-02-24 19:22:35 +01:00
params = &url.Values{"locale": {db.Locale}}
2014-03-12 19:24:36 +01:00
} else {
params.Set("locale", db.Locale)
2014-02-19 23:15:46 +01:00
}
2014-07-29 00:53:25 +02:00
rawurl = fmt.Sprintf("%s/%s?%s", db.APIURL, urlEncode(path), params.Encode())
2014-02-19 23:15:46 +01:00
if request, err = http.NewRequest(method, rawurl, nil); err != nil {
return err
}
2014-07-29 22:00:07 +02:00
if response, err = db.client().Do(request); err != nil {
2014-02-19 23:15:46 +01:00
return err
}
defer response.Body.Close()
if body, err = getResponse(response); err != nil {
2014-02-19 23:15:46 +01:00
return err
}
err = json.Unmarshal(body, receiver)
return err
}
2014-02-24 19:22:35 +01:00
// GetAccountInfo gets account information for the user currently authenticated.
func (db *Dropbox) GetAccountInfo() (*Account, error) {
2014-02-19 23:15:46 +01:00
var rv Account
2014-02-24 19:22:35 +01:00
err := db.doRequest("GET", "account/info", nil, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Shares shares a file.
func (db *Dropbox) Shares(path string, shortURL bool) (*Link, error) {
2014-02-19 23:15:46 +01:00
var rv Link
var params *url.Values
2014-02-24 19:22:35 +01:00
if shortURL {
params = &url.Values{"short_url": {strconv.FormatBool(shortURL)}}
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"shares", db.RootDirectory, path}, "/")
err := db.doRequest("POST", act, params, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Media shares a file for streaming (direct access).
func (db *Dropbox) Media(path string) (*Link, error) {
2014-02-19 23:15:46 +01:00
var rv Link
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"media", db.RootDirectory, path}, "/")
err := db.doRequest("POST", act, nil, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Search searches the entries matching all the words contained in query in the given path.
2014-02-19 23:15:46 +01:00
// The maximum number of entries and whether to consider deleted file may be given.
func (db *Dropbox) Search(path, query string, fileLimit int, includeDeleted bool) ([]Entry, error) {
2014-02-19 23:15:46 +01:00
var rv []Entry
var params *url.Values
2014-02-20 20:41:00 +01:00
if fileLimit <= 0 || fileLimit > SearchLimitMax {
fileLimit = SearchLimitDefault
2014-02-19 23:15:46 +01:00
}
params = &url.Values{
"query": {query},
"file_limit": {strconv.FormatInt(int64(fileLimit), 10)},
"include_deleted": {strconv.FormatBool(includeDeleted)},
}
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"search", db.RootDirectory, path}, "/")
err := db.doRequest("GET", act, params, &rv)
return rv, err
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// Delta gets modifications since the cursor.
func (db *Dropbox) Delta(cursor, pathPrefix string) (*DeltaPage, error) {
2014-02-19 23:15:46 +01:00
var rv DeltaPage
var params *url.Values
type deltaPageParser struct {
Reset bool `json:"reset"` // if true the local state must be cleared.
HasMore bool `json:"has_more"` // if true an other call to delta should be made.
Cursor string `json:"cursor"` // Tag of the current state.
Entries [][]json.RawMessage `json:"entries"` // List of changed entries.
}
var dpp deltaPageParser
params = &url.Values{}
if len(cursor) != 0 {
params.Set("cursor", cursor)
}
if len(pathPrefix) != 0 {
params.Set("path_prefix", pathPrefix)
}
2014-02-24 19:22:35 +01:00
err := db.doRequest("POST", "delta", params, &dpp)
2014-02-19 23:15:46 +01:00
rv = DeltaPage{Reset: dpp.Reset, HasMore: dpp.HasMore, Cursor: dpp.Cursor}
rv.Entries = make([]DeltaEntry, 0, len(dpp.Entries))
for _, jentry := range dpp.Entries {
var path string
var entry Entry
if len(jentry) != 2 {
2014-02-24 19:22:35 +01:00
return nil, fmt.Errorf("malformed reply")
2014-02-19 23:15:46 +01:00
}
if err = json.Unmarshal(jentry[0], &path); err != nil {
return nil, err
}
if err = json.Unmarshal(jentry[1], &entry); err != nil {
return nil, err
}
if entry.Path == "" {
rv.Entries = append(rv.Entries, DeltaEntry{Path: path, Entry: nil})
} else {
rv.Entries = append(rv.Entries, DeltaEntry{Path: path, Entry: &entry})
}
}
return &rv, err
}
2014-02-24 19:22:35 +01:00
// LongPollDelta waits for a notification to happen.
func (db *Dropbox) LongPollDelta(cursor string, timeout int) (*DeltaPoll, error) {
2014-02-19 23:15:46 +01:00
var rv DeltaPoll
var params *url.Values
var body []byte
var rawurl string
var response *http.Response
var err error
var client http.Client
params = &url.Values{}
if timeout != 0 {
2014-02-20 20:41:00 +01:00
if timeout < PollMinTimeout || timeout > PollMaxTimeout {
2014-02-24 19:22:35 +01:00
return nil, fmt.Errorf("timeout out of range [%d; %d]", PollMinTimeout, PollMaxTimeout)
2014-02-19 23:15:46 +01:00
}
params.Set("timeout", strconv.FormatInt(int64(timeout), 10))
}
params.Set("cursor", cursor)
2014-02-24 19:22:35 +01:00
rawurl = fmt.Sprintf("%s/longpoll_delta?%s", db.APINotifyURL, params.Encode())
2014-02-19 23:15:46 +01:00
if response, err = client.Get(rawurl); err != nil {
return nil, err
}
defer response.Body.Close()
if body, err = getResponse(response); err != nil {
2014-02-19 23:15:46 +01:00
return nil, err
}
err = json.Unmarshal(body, &rv)
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Metadata gets the metadata for a file or a directory.
2014-02-19 23:15:46 +01:00
// If list is true and src is a directory, immediate child will be sent in the Contents field.
// If include_deleted is true, entries deleted will be sent.
// hash is the hash of the contents of a directory, it is used to avoid sending data when directory did not change.
// rev is the specific revision to get the metadata from.
// limit is the maximum number of entries requested.
2014-02-24 19:22:35 +01:00
func (db *Dropbox) Metadata(src string, list bool, includeDeleted bool, hash, rev string, limit int) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var rv Entry
var params *url.Values
if limit <= 0 {
2014-02-20 20:41:00 +01:00
limit = MetadataLimitDefault
} else if limit > MetadataLimitMax {
limit = MetadataLimitMax
2014-02-19 23:15:46 +01:00
}
params = &url.Values{
"list": {strconv.FormatBool(list)},
"include_deleted": {strconv.FormatBool(includeDeleted)},
"file_limit": {strconv.FormatInt(int64(limit), 10)},
}
if len(rev) != 0 {
params.Set("rev", rev)
}
if len(hash) != 0 {
params.Set("hash", hash)
}
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"metadata", db.RootDirectory, src}, "/")
err := db.doRequest("GET", act, params, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// CopyRef gets a reference to a file.
2014-02-19 23:15:46 +01:00
// This reference can be used to copy this file to another user's Dropbox by passing it to the Copy method.
2014-02-24 19:22:35 +01:00
func (db *Dropbox) CopyRef(src string) (*CopyRef, error) {
2014-02-19 23:15:46 +01:00
var rv CopyRef
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"copy_ref", db.RootDirectory, src}, "/")
err := db.doRequest("GET", act, nil, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Revisions gets the list of revisions for a file.
func (db *Dropbox) Revisions(src string, revLimit int) ([]Entry, error) {
2014-02-19 23:15:46 +01:00
var rv []Entry
if revLimit <= 0 {
2014-02-20 20:41:00 +01:00
revLimit = RevisionsLimitDefault
} else if revLimit > RevisionsLimitMax {
revLimit = RevisionsLimitMax
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"revisions", db.RootDirectory, src}, "/")
err := db.doRequest("GET", act,
2014-02-19 23:15:46 +01:00
&url.Values{"rev_limit": {strconv.FormatInt(int64(revLimit), 10)}}, &rv)
return rv, err
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
// Restore restores a deleted file at the corresponding revision.
func (db *Dropbox) Restore(src string, rev string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var rv Entry
2014-02-24 19:22:35 +01:00
act := strings.Join([]string{"restore", db.RootDirectory, src}, "/")
err := db.doRequest("POST", act, &url.Values{"rev": {rev}}, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Copy copies a file.
2014-02-19 23:15:46 +01:00
// If isRef is true src must be a reference from CopyRef instead of a path.
2014-02-24 19:22:35 +01:00
func (db *Dropbox) Copy(src, dst string, isRef bool) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var rv Entry
2014-02-24 19:22:35 +01:00
params := &url.Values{"root": {db.RootDirectory}, "to_path": {dst}}
2014-02-19 23:15:46 +01:00
if isRef {
params.Set("from_copy_ref", src)
2014-03-12 19:22:05 +01:00
} else {
params.Set("from_path", src)
2014-02-19 23:15:46 +01:00
}
2014-02-24 19:22:35 +01:00
err := db.doRequest("POST", "fileops/copy", params, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// CreateFolder creates a new directory.
func (db *Dropbox) CreateFolder(path string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var rv Entry
2014-02-24 19:22:35 +01:00
err := db.doRequest("POST", "fileops/create_folder",
&url.Values{"root": {db.RootDirectory}, "path": {path}}, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Delete removes a file or directory (it is a recursive delete).
func (db *Dropbox) Delete(path string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var rv Entry
2014-02-24 19:22:35 +01:00
err := db.doRequest("POST", "fileops/delete",
&url.Values{"root": {db.RootDirectory}, "path": {path}}, &rv)
2014-02-19 23:15:46 +01:00
return &rv, err
}
2014-02-24 19:22:35 +01:00
// Move moves a file or directory.
func (db *Dropbox) Move(src, dst string) (*Entry, error) {
2014-02-19 23:15:46 +01:00
var rv Entry
2014-02-24 19:22:35 +01:00
err := db.doRequest("POST", "fileops/move",
&url.Values{"root": {db.RootDirectory},
2014-02-19 23:15:46 +01:00
"from_path": {src},
"to_path": {dst}}, &rv)
return &rv, err
}