├── LICENSE ├── Readme.md ├── client.go ├── client_test.go ├── config.go ├── doc.go ├── error.go ├── example_test.go ├── files.go ├── files_test.go ├── sharing.go ├── sharing_test.go ├── users.go └── users_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 TJ Holowaychuk <tj@tjholowaychuk.coma> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | [![GoDoc](https://godoc.org/github.com/tj/go-dropbox?status.svg)](https://godoc.org/github.com/tj/go-dropbox) [![Build Status](https://semaphoreci.com/api/v1/projects/bc0bfd8b-73c9-45ba-b988-00f9e285e6ef/617305/badge.svg)](https://semaphoreci.com/tj/go-dropbox) 3 | 4 | # Dropbox 5 | 6 | Simple Dropbox v2 client for Go. 7 | 8 | For a higher level client take a look at [go-dropy](https://github.com/tj/go-dropy). 9 | 10 | ## About 11 | 12 | Modelled more or less 1:1 with the API for consistency and parity with the [official documentation](https://www.dropbox.com/developers/documentation/http). More sugar should be implemented on top. 13 | 14 | ## Testing 15 | 16 | To manually run tests use the test account access token: 17 | 18 | ``` 19 | $ export DROPBOX_ACCESS_TOKEN=oENFkq_oIVAAAAAAAAAAC8gE3wIUFMEraPBL-D71Aq2C4zuh1l4oDn5FiWSdVVlL 20 | $ go test -v 21 | ``` 22 | 23 | # License 24 | 25 | MIT -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // Client implements a Dropbox client. You may use the Files and Users 13 | // clients directly if preferred, however Client exposes them both. 14 | type Client struct { 15 | *Config 16 | Users *Users 17 | Files *Files 18 | Sharing *Sharing 19 | } 20 | 21 | // New client. 22 | func New(config *Config) *Client { 23 | c := &Client{Config: config} 24 | c.Users = &Users{c} 25 | c.Files = &Files{c} 26 | c.Sharing = &Sharing{c} 27 | return c 28 | } 29 | 30 | // call rpc style endpoint. 31 | func (c *Client) call(path string, in interface{}) (io.ReadCloser, error) { 32 | url := "https://api.dropboxapi.com/2" + path 33 | 34 | body, err := json.Marshal(in) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | req, err := http.NewRequest("POST", url, bytes.NewReader(body)) 40 | if err != nil { 41 | return nil, err 42 | } 43 | req.Header.Set("Authorization", "Bearer "+c.AccessToken) 44 | req.Header.Set("Content-Type", "application/json") 45 | 46 | r, _, err := c.do(req) 47 | return r, err 48 | } 49 | 50 | // download style endpoint. 51 | func (c *Client) download(path string, in interface{}, r io.Reader) (io.ReadCloser, int64, error) { 52 | url := "https://content.dropboxapi.com/2" + path 53 | 54 | body, err := json.Marshal(in) 55 | if err != nil { 56 | return nil, 0, err 57 | } 58 | 59 | req, err := http.NewRequest("POST", url, r) 60 | if err != nil { 61 | return nil, 0, err 62 | } 63 | req.Header.Set("Authorization", "Bearer "+c.AccessToken) 64 | req.Header.Set("Dropbox-API-Arg", string(body)) 65 | 66 | if r != nil { 67 | req.Header.Set("Content-Type", "application/octet-stream") 68 | } 69 | 70 | return c.do(req) 71 | } 72 | 73 | // perform the request. 74 | func (c *Client) do(req *http.Request) (io.ReadCloser, int64, error) { 75 | res, err := c.HTTPClient.Do(req) 76 | if err != nil { 77 | return nil, 0, err 78 | } 79 | 80 | if res.StatusCode < 400 { 81 | return res.Body, res.ContentLength, err 82 | } 83 | 84 | defer res.Body.Close() 85 | 86 | e := &Error{ 87 | Status: http.StatusText(res.StatusCode), 88 | StatusCode: res.StatusCode, 89 | } 90 | 91 | kind := res.Header.Get("Content-Type") 92 | 93 | if strings.Contains(kind, "text/plain") { 94 | if b, err := ioutil.ReadAll(res.Body); err == nil { 95 | e.Summary = string(b) 96 | return nil, 0, e 97 | } 98 | return nil, 0, err 99 | } 100 | 101 | if err := json.NewDecoder(res.Body).Decode(e); err != nil { 102 | return nil, 0, err 103 | } 104 | 105 | return nil, 0, e 106 | } 107 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/segmentio/go-env" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func client() *Client { 11 | token := env.MustGet("DROPBOX_ACCESS_TOKEN") 12 | return New(NewConfig(token)) 13 | } 14 | 15 | func TestClient_error_text(t *testing.T) { 16 | c := client() 17 | 18 | _, err := c.Files.Download(&DownloadInput{ 19 | Path: "asdfasdfasdf", 20 | }) 21 | 22 | assert.Error(t, err) 23 | 24 | e := err.(*Error) 25 | assert.Contains(t, e.Error(), "Error in call") 26 | assert.Equal(t, "Bad Request", e.Status) 27 | assert.Equal(t, 400, e.StatusCode) 28 | } 29 | 30 | func TestClient_error_json(t *testing.T) { 31 | c := client() 32 | 33 | _, err := c.Files.Download(&DownloadInput{"/nothing"}) 34 | assert.Error(t, err) 35 | 36 | e := err.(*Error) 37 | assert.Contains(t, e.Error(), "path/not_found") 38 | assert.Equal(t, "Conflict", e.Status) 39 | assert.Equal(t, 409, e.StatusCode) 40 | } 41 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Config for the Dropbox clients. 8 | type Config struct { 9 | HTTPClient *http.Client 10 | AccessToken string 11 | } 12 | 13 | // NewConfig with the given access token. 14 | func NewConfig(accessToken string) *Config { 15 | return &Config{ 16 | HTTPClient: http.DefaultClient, 17 | AccessToken: accessToken, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package dropbox implements a simple v2 client. 2 | package dropbox 3 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | // Error response. 4 | type Error struct { 5 | Status string 6 | StatusCode int 7 | Summary string `json:"error_summary"` 8 | } 9 | 10 | // Error string. 11 | func (e *Error) Error() string { 12 | return e.Summary 13 | } 14 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package dropbox_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/tj/go-dropbox" 9 | ) 10 | 11 | // Example using the Client, which provides both User and File clients. 12 | func Example() { 13 | d := dropbox.New(dropbox.NewConfig("")) 14 | 15 | file, _ := os.Open("Readme.md") 16 | 17 | d.Files.Upload(&dropbox.UploadInput{ 18 | Path: "Readme.md", 19 | Reader: file, 20 | Mute: true, 21 | }) 22 | } 23 | 24 | // Example using the Files client directly. 25 | func Example_files() { 26 | files := dropbox.NewFiles(dropbox.NewConfig("")) 27 | 28 | out, _ := files.Download(&dropbox.DownloadInput{ 29 | Path: "Readme.md", 30 | }) 31 | 32 | io.Copy(os.Stdout, out.Body) 33 | } 34 | 35 | // Example using the Users client directly. 36 | func Example_users() { 37 | users := dropbox.NewUsers(dropbox.NewConfig("")) 38 | out, _ := users.GetCurrentAccount() 39 | fmt.Printf("%v\n", out) 40 | } 41 | -------------------------------------------------------------------------------- /files.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "time" 10 | ) 11 | 12 | // Files client for files and folders. 13 | type Files struct { 14 | *Client 15 | } 16 | 17 | // NewFiles client. 18 | func NewFiles(config *Config) *Files { 19 | return &Files{ 20 | Client: &Client{ 21 | Config: config, 22 | }, 23 | } 24 | } 25 | 26 | // WriteMode determines what to do if the file already exists. 27 | type WriteMode string 28 | 29 | // Supported write modes. 30 | const ( 31 | WriteModeAdd WriteMode = "add" 32 | WriteModeOverwrite = "overwrite" 33 | ) 34 | 35 | // Dimensions specifies the dimensions of a photo or video. 36 | type Dimensions struct { 37 | Width uint64 `json:"width"` 38 | Height uint64 `json:"height"` 39 | } 40 | 41 | // GPSCoordinates specifies the GPS coordinate of a photo or video. 42 | type GPSCoordinates struct { 43 | Latitude float64 `json:"latitude"` 44 | Longitude float64 `json:"longitude"` 45 | } 46 | 47 | // PhotoMetadata specifies metadata for a photo. 48 | type PhotoMetadata struct { 49 | Dimensions *Dimensions `json:"dimensions,omitempty"` 50 | Location *GPSCoordinates `json:"location,omitempty"` 51 | TimeTaken time.Time `json:"time_taken,omitempty"` 52 | } 53 | 54 | // VideoMetadata specifies metadata for a video. 55 | type VideoMetadata struct { 56 | Dimensions *Dimensions `json:"dimensions,omitempty"` 57 | Location *GPSCoordinates `json:"location,omitempty"` 58 | TimeTaken time.Time `json:"time_taken,omitempty"` 59 | Duration uint64 `json:"duration,omitempty"` 60 | } 61 | 62 | // MediaMetadata provides metadata for a photo or video. 63 | type MediaMetadata struct { 64 | Photo *PhotoMetadata `json:"photo,omitempty"` 65 | Video *VideoMetadata `json:"video,omitempty"` 66 | } 67 | 68 | // MediaInfo provides additional information for a photo or video file. 69 | type MediaInfo struct { 70 | Pending bool `json:"pending"` 71 | Metadata *MediaMetadata `json:"metadata,omitempty"` 72 | } 73 | 74 | // FileSharingInfo for a file which is contained in a shared folder. 75 | type FileSharingInfo struct { 76 | ReadOnly bool `json:"read_only"` 77 | ParentSharedFolderID string `json:"parent_shared_folder_id"` 78 | ModifiedBy string `json:"modified_by,omitempty"` 79 | } 80 | 81 | // Metadata for a file or folder. 82 | type Metadata struct { 83 | Tag string `json:".tag"` 84 | Name string `json:"name"` 85 | PathLower string `json:"path_lower"` 86 | PathDisplay string `json:"path_display"` 87 | ClientModified time.Time `json:"client_modified"` 88 | ServerModified time.Time `json:"server_modified"` 89 | Rev string `json:"rev"` 90 | Size uint64 `json:"size"` 91 | ID string `json:"id"` 92 | MediaInfo *MediaInfo `json:"media_info,omitempty"` 93 | SharingInfo *FileSharingInfo `json:"sharing_info,omitempty"` 94 | ContentHash string `json:"content_hash,omitempty"` 95 | } 96 | 97 | // GetMetadataInput request input. 98 | type GetMetadataInput struct { 99 | Path string `json:"path"` 100 | IncludeMediaInfo bool `json:"include_media_info"` 101 | } 102 | 103 | // GetMetadataOutput request output. 104 | type GetMetadataOutput struct { 105 | Metadata 106 | } 107 | 108 | // GetMetadata returns the metadata for a file or folder. 109 | func (c *Files) GetMetadata(in *GetMetadataInput) (out *GetMetadataOutput, err error) { 110 | body, err := c.call("/files/get_metadata", in) 111 | if err != nil { 112 | return 113 | } 114 | defer body.Close() 115 | 116 | err = json.NewDecoder(body).Decode(&out) 117 | return 118 | } 119 | 120 | // CreateFolderInput request input. 121 | type CreateFolderInput struct { 122 | Path string `json:"path"` 123 | } 124 | 125 | // CreateFolderOutput request output. 126 | type CreateFolderOutput struct { 127 | Name string `json:"name"` 128 | PathLower string `json:"path_lower"` 129 | ID string `json:"id"` 130 | } 131 | 132 | // CreateFolder creates a folder. 133 | func (c *Files) CreateFolder(in *CreateFolderInput) (out *CreateFolderOutput, err error) { 134 | body, err := c.call("/files/create_folder", in) 135 | if err != nil { 136 | return 137 | } 138 | defer body.Close() 139 | 140 | err = json.NewDecoder(body).Decode(&out) 141 | return 142 | } 143 | 144 | // DeleteInput request input. 145 | type DeleteInput struct { 146 | Path string `json:"path"` 147 | } 148 | 149 | // DeleteOutput request output. 150 | type DeleteOutput struct { 151 | Metadata 152 | } 153 | 154 | // Delete a file or folder and its contents. 155 | func (c *Files) Delete(in *DeleteInput) (out *DeleteOutput, err error) { 156 | body, err := c.call("/files/delete", in) 157 | if err != nil { 158 | return 159 | } 160 | defer body.Close() 161 | 162 | err = json.NewDecoder(body).Decode(&out) 163 | return 164 | } 165 | 166 | // PermanentlyDeleteInput request input. 167 | type PermanentlyDeleteInput struct { 168 | Path string `json:"path"` 169 | } 170 | 171 | // PermanentlyDelete a file or folder and its contents. 172 | func (c *Files) PermanentlyDelete(in *PermanentlyDeleteInput) (err error) { 173 | body, err := c.call("/files/delete", in) 174 | if err != nil { 175 | return 176 | } 177 | defer body.Close() 178 | 179 | return 180 | } 181 | 182 | // CopyInput request input. 183 | type CopyInput struct { 184 | FromPath string `json:"from_path"` 185 | ToPath string `json:"to_path"` 186 | } 187 | 188 | // CopyOutput request output. 189 | type CopyOutput struct { 190 | Metadata 191 | } 192 | 193 | // Copy a file or folder to a different location. 194 | func (c *Files) Copy(in *CopyInput) (out *CopyOutput, err error) { 195 | body, err := c.call("/files/copy", in) 196 | if err != nil { 197 | return 198 | } 199 | defer body.Close() 200 | 201 | err = json.NewDecoder(body).Decode(&out) 202 | return 203 | } 204 | 205 | // MoveInput request input. 206 | type MoveInput struct { 207 | FromPath string `json:"from_path"` 208 | ToPath string `json:"to_path"` 209 | } 210 | 211 | // MoveOutput request output. 212 | type MoveOutput struct { 213 | Metadata 214 | } 215 | 216 | // Move a file or folder to a different location. 217 | func (c *Files) Move(in *MoveInput) (out *MoveOutput, err error) { 218 | body, err := c.call("/files/move", in) 219 | if err != nil { 220 | return 221 | } 222 | defer body.Close() 223 | 224 | err = json.NewDecoder(body).Decode(&out) 225 | return 226 | } 227 | 228 | // RestoreInput request input. 229 | type RestoreInput struct { 230 | Path string `json:"path"` 231 | Rev string `json:"rev"` 232 | } 233 | 234 | // RestoreOutput request output. 235 | type RestoreOutput struct { 236 | Metadata 237 | } 238 | 239 | // Restore a file to a specific revision. 240 | func (c *Files) Restore(in *RestoreInput) (out *RestoreOutput, err error) { 241 | body, err := c.call("/files/restore", in) 242 | if err != nil { 243 | return 244 | } 245 | defer body.Close() 246 | 247 | err = json.NewDecoder(body).Decode(&out) 248 | return 249 | } 250 | 251 | // ListFolderInput request input. 252 | type ListFolderInput struct { 253 | Path string `json:"path"` 254 | Recursive bool `json:"recursive"` 255 | IncludeMediaInfo bool `json:"include_media_info"` 256 | IncludeDeleted bool `json:"include_deleted"` 257 | } 258 | 259 | // ListFolderOutput request output. 260 | type ListFolderOutput struct { 261 | Cursor string `json:"cursor"` 262 | HasMore bool `json:"has_more"` 263 | Entries []*Metadata 264 | } 265 | 266 | // ListFolder returns the metadata for a file or folder. 267 | func (c *Files) ListFolder(in *ListFolderInput) (out *ListFolderOutput, err error) { 268 | in.Path = normalizePath(in.Path) 269 | 270 | body, err := c.call("/files/list_folder", in) 271 | if err != nil { 272 | return 273 | } 274 | defer body.Close() 275 | 276 | err = json.NewDecoder(body).Decode(&out) 277 | return 278 | } 279 | 280 | // ListFolderContinueInput request input. 281 | type ListFolderContinueInput struct { 282 | Cursor string `json:"cursor"` 283 | } 284 | 285 | // ListFolderContinue pagenates using the cursor from ListFolder. 286 | func (c *Files) ListFolderContinue(in *ListFolderContinueInput) (out *ListFolderOutput, err error) { 287 | body, err := c.call("/files/list_folder/continue", in) 288 | if err != nil { 289 | return 290 | } 291 | defer body.Close() 292 | 293 | err = json.NewDecoder(body).Decode(&out) 294 | return 295 | } 296 | 297 | // SearchMode determines how a search is performed. 298 | type SearchMode string 299 | 300 | // Supported search modes. 301 | const ( 302 | SearchModeFilename SearchMode = "filename" 303 | SearchModeFilenameAndContent = "filename_and_content" 304 | SearchModeDeletedFilename = "deleted_filename" 305 | ) 306 | 307 | // SearchMatchType represents the type of match made. 308 | type SearchMatchType string 309 | 310 | // Supported search match types. 311 | const ( 312 | SearchMatchFilename SearchMatchType = "filename" 313 | SearchMatchContent = "content" 314 | SearchMatchBoth = "both" 315 | ) 316 | 317 | // SearchMatch represents a matched file or folder. 318 | type SearchMatch struct { 319 | MatchType struct { 320 | Tag SearchMatchType `json:".tag"` 321 | } `json:"match_type"` 322 | Metadata *Metadata `json:"metadata"` 323 | } 324 | 325 | // SearchInput request input. 326 | type SearchInput struct { 327 | Path string `json:"path"` 328 | Query string `json:"query"` 329 | Start uint64 `json:"start,omitempty"` 330 | MaxResults uint64 `json:"max_results,omitempty"` 331 | Mode SearchMode `json:"mode"` 332 | } 333 | 334 | // SearchOutput request output. 335 | type SearchOutput struct { 336 | Matches []*SearchMatch `json:"matches"` 337 | More bool `json:"more"` 338 | Start uint64 `json:"start"` 339 | } 340 | 341 | // Search for files and folders. 342 | func (c *Files) Search(in *SearchInput) (out *SearchOutput, err error) { 343 | in.Path = normalizePath(in.Path) 344 | 345 | if in.Mode == "" { 346 | in.Mode = SearchModeFilename 347 | } 348 | 349 | body, err := c.call("/files/search", in) 350 | if err != nil { 351 | return 352 | } 353 | defer body.Close() 354 | 355 | err = json.NewDecoder(body).Decode(&out) 356 | return 357 | } 358 | 359 | // UploadInput request input. 360 | type UploadInput struct { 361 | Path string `json:"path"` 362 | Mode WriteMode `json:"mode"` 363 | AutoRename bool `json:"autorename"` 364 | Mute bool `json:"mute"` 365 | ClientModified string `json:"client_modified,omitempty"` 366 | Reader io.Reader `json:"-"` 367 | } 368 | 369 | // UploadOutput request output. 370 | type UploadOutput struct { 371 | Metadata 372 | } 373 | 374 | // Upload a file smaller than 150MB. 375 | func (c *Files) Upload(in *UploadInput) (out *UploadOutput, err error) { 376 | body, _, err := c.download("/files/upload", in, in.Reader) 377 | if err != nil { 378 | return 379 | } 380 | defer body.Close() 381 | 382 | err = json.NewDecoder(body).Decode(&out) 383 | return 384 | } 385 | 386 | // DownloadInput request input. 387 | type DownloadInput struct { 388 | Path string `json:"path"` 389 | } 390 | 391 | // DownloadOutput request output. 392 | type DownloadOutput struct { 393 | Body io.ReadCloser 394 | Length int64 395 | } 396 | 397 | // Download a file. 398 | func (c *Files) Download(in *DownloadInput) (out *DownloadOutput, err error) { 399 | body, l, err := c.download("/files/download", in, nil) 400 | if err != nil { 401 | return 402 | } 403 | 404 | out = &DownloadOutput{body, l} 405 | return 406 | } 407 | 408 | // ThumbnailFormat determines the format of the thumbnail. 409 | type ThumbnailFormat string 410 | 411 | const ( 412 | // GetThumbnailFormatJPEG specifies a JPG thumbnail 413 | GetThumbnailFormatJPEG ThumbnailFormat = "jpeg" 414 | // GetThumbnailFormatPNG specifies a PNG thumbnail 415 | GetThumbnailFormatPNG = "png" 416 | ) 417 | 418 | // ThumbnailSize determines the size of the thumbnail. 419 | type ThumbnailSize string 420 | 421 | const ( 422 | // GetThumbnailSizeW32H32 specifies a size of 32 by 32 px 423 | GetThumbnailSizeW32H32 ThumbnailSize = "w32h32" 424 | // GetThumbnailSizeW64H64 specifies a size of 64 by 64 px 425 | GetThumbnailSizeW64H64 = "w64h64" 426 | // GetThumbnailSizeW128H128 specifies a size of 128 by 128 px 427 | GetThumbnailSizeW128H128 = "w128h128" 428 | // GetThumbnailSizeW640H480 specifies a size of 640 by 480 px 429 | GetThumbnailSizeW640H480 = "w640h480" 430 | // GetThumbnailSizeW1024H768 specifies a size of 1024 by 768 px 431 | GetThumbnailSizeW1024H768 = "w1024h768" 432 | ) 433 | 434 | // GetThumbnailInput request input. 435 | type GetThumbnailInput struct { 436 | Path string `json:"path"` 437 | Format ThumbnailFormat `json:"format"` 438 | Size ThumbnailSize `json:"size"` 439 | } 440 | 441 | // GetThumbnailOutput request output. 442 | type GetThumbnailOutput struct { 443 | Body io.ReadCloser 444 | Length int64 445 | } 446 | 447 | // GetThumbnail a thumbnail for a file. Currently thumbnails are only generated for the 448 | // files with the following extensions: png, jpeg, png, tiff, tif, gif and bmp. 449 | func (c *Files) GetThumbnail(in *GetThumbnailInput) (out *GetThumbnailOutput, err error) { 450 | body, l, err := c.download("/files/get_thumbnail", in, nil) 451 | if err != nil { 452 | return 453 | } 454 | 455 | out = &GetThumbnailOutput{body, l} 456 | return 457 | } 458 | 459 | // GetPreviewInput request input. 460 | type GetPreviewInput struct { 461 | Path string `json:"path"` 462 | } 463 | 464 | // GetPreviewOutput request output. 465 | type GetPreviewOutput struct { 466 | Body io.ReadCloser 467 | Length int64 468 | } 469 | 470 | // GetPreview a preview for a file. Currently previews are only generated for the 471 | // files with the following extensions: .doc, .docx, .docm, .ppt, .pps, .ppsx, 472 | // .ppsm, .pptx, .pptm, .xls, .xlsx, .xlsm, .rtf 473 | func (c *Files) GetPreview(in *GetPreviewInput) (out *GetPreviewOutput, err error) { 474 | body, l, err := c.download("/files/get_preview", in, nil) 475 | if err != nil { 476 | return 477 | } 478 | 479 | out = &GetPreviewOutput{body, l} 480 | return 481 | } 482 | 483 | // ListRevisionsInput request input. 484 | type ListRevisionsInput struct { 485 | Path string `json:"path"` 486 | Limit uint64 `json:"limit,omitempty"` 487 | } 488 | 489 | // ListRevisionsOutput request output. 490 | type ListRevisionsOutput struct { 491 | IsDeleted bool 492 | Entries []*Metadata 493 | } 494 | 495 | // ListRevisions gets the revisions of the specified file. 496 | func (c *Files) ListRevisions(in *ListRevisionsInput) (out *ListRevisionsOutput, err error) { 497 | body, err := c.call("/files/list_revisions", in) 498 | if err != nil { 499 | return 500 | } 501 | defer body.Close() 502 | 503 | err = json.NewDecoder(body).Decode(&out) 504 | return 505 | } 506 | 507 | // Normalize path so people can use "/" as they expect. 508 | func normalizePath(s string) string { 509 | if s == "/" { 510 | return "" 511 | } 512 | return s 513 | } 514 | 515 | const hashBlockSize = 4 * 1024 * 1024 516 | 517 | // ContentHash returns the Dropbox content_hash for a io.Reader. 518 | // See https://www.dropbox.com/developers/reference/content-hash 519 | func ContentHash(r io.Reader) (string, error) { 520 | buf := make([]byte, hashBlockSize) 521 | resultHash := sha256.New() 522 | n, err := r.Read(buf) 523 | if err != nil && err != io.EOF { 524 | return "", err 525 | } 526 | if n > 0 { 527 | bufHash := sha256.Sum256(buf[:n]) 528 | resultHash.Write(bufHash[:]) 529 | } 530 | for n == hashBlockSize && err == nil { 531 | n, err = r.Read(buf) 532 | if err != nil && err != io.EOF { 533 | return "", err 534 | } 535 | if n > 0 { 536 | bufHash := sha256.Sum256(buf[:n]) 537 | resultHash.Write(bufHash[:]) 538 | } 539 | } 540 | return fmt.Sprintf("%x", resultHash.Sum(nil)), nil 541 | } 542 | 543 | // FileContentHash returns the Dropbox content_hash for a local file. 544 | // See https://www.dropbox.com/developers/reference/content-hash 545 | func FileContentHash(filename string) (string, error) { 546 | f, err := os.Open(filename) 547 | if err != nil { 548 | return "", err 549 | } 550 | defer f.Close() 551 | return ContentHash(f) 552 | } 553 | -------------------------------------------------------------------------------- /files_test.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/ungerik/go-dry" 12 | ) 13 | 14 | func TestFiles_Upload(t *testing.T) { 15 | c := client() 16 | 17 | file, err := os.Open("Readme.md") 18 | assert.NoError(t, err, "error opening file") 19 | defer file.Close() 20 | 21 | out, err := c.Files.Upload(&UploadInput{ 22 | Mute: true, 23 | Mode: WriteModeOverwrite, 24 | Path: "/Readme.md", 25 | Reader: file, 26 | }) 27 | 28 | assert.NoError(t, err, "error uploading file") 29 | assert.Equal(t, "/readme.md", out.PathLower) 30 | } 31 | 32 | func TestFiles_Download(t *testing.T) { 33 | c := client() 34 | 35 | out, err := c.Files.Download(&DownloadInput{"/Readme.md"}) 36 | 37 | assert.NoError(t, err, "error downloading") 38 | defer out.Body.Close() 39 | 40 | fi, err := os.Lstat("Readme.md") 41 | assert.NoError(t, err, "error getting local file info") 42 | assert.Equal(t, fi.Size(), out.Length, "Readme.md length mismatch") 43 | 44 | remote, err := ioutil.ReadAll(out.Body) 45 | assert.NoError(t, err, "error reading remote") 46 | 47 | local, err := ioutil.ReadFile("Readme.md") 48 | assert.NoError(t, err, "error reading local") 49 | 50 | assert.Equal(t, local, remote, "Readme.md mismatch") 51 | } 52 | 53 | func TestFiles_GetMetadata(t *testing.T) { 54 | c := client() 55 | 56 | out, err := c.Files.GetMetadata(&GetMetadataInput{ 57 | Path: "/Readme.md", 58 | }) 59 | assert.NoError(t, err) 60 | assert.Equal(t, "file", out.Tag) 61 | } 62 | 63 | func TestFiles_ListFolder(t *testing.T) { 64 | t.Parallel() 65 | c := client() 66 | 67 | out, err := c.Files.ListFolder(&ListFolderInput{ 68 | Path: "/list", 69 | }) 70 | 71 | assert.NoError(t, err) 72 | assert.Equal(t, 2000, len(out.Entries)) 73 | assert.True(t, out.HasMore) 74 | } 75 | 76 | func TestFiles_ListFolder_root(t *testing.T) { 77 | t.Parallel() 78 | c := client() 79 | 80 | _, err := c.Files.ListFolder(&ListFolderInput{ 81 | Path: "/", 82 | }) 83 | 84 | assert.NoError(t, err) 85 | } 86 | 87 | func TestFiles_Search(t *testing.T) { 88 | c := client() 89 | 90 | out, err := c.Files.Search(&SearchInput{ 91 | Path: "/", 92 | Query: "hello", 93 | }) 94 | 95 | assert.NoError(t, err) 96 | assert.Equal(t, 2, len(out.Matches)) 97 | } 98 | 99 | func TestFiles_Delete(t *testing.T) { 100 | c := client() 101 | 102 | out, err := c.Files.Delete(&DeleteInput{ 103 | Path: "/Readme.md", 104 | }) 105 | 106 | assert.NoError(t, err) 107 | assert.Equal(t, "/readme.md", out.PathLower) 108 | } 109 | 110 | // A gray, 64 by 64 px PNG 111 | var grayPng = []byte{ 112 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 113 | 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x40, 0x08, 0x02, 114 | 0x00, 0x00, 0x00, 0x25, 0x0b, 0xe6, 0x89, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 115 | 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 116 | 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 117 | 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 118 | 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 119 | 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x70, 0x61, 0x69, 0x6e, 120 | 0x74, 0x2e, 0x6e, 0x65, 0x74, 0x20, 0x34, 0x2e, 0x30, 0x2e, 0x36, 0xfc, 0x8c, 121 | 0x63, 0xdf, 0x00, 0x00, 0x00, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x68, 0xde, 0xed, 122 | 0xcf, 0x31, 0x0d, 0x00, 0x00, 0x08, 0x03, 0x30, 0xe6, 0x7e, 0xb2, 0x91, 0xc0, 123 | 0x4d, 0xd2, 0x3a, 0x68, 0xda, 0xce, 0x67, 0x11, 0x10, 0x10, 0x10, 0x10, 0x10, 124 | 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 125 | 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 126 | 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 127 | 0x10, 0x10, 0xb8, 0x2c, 0x4a, 0x27, 0x66, 0x41, 0xb9, 0xd3, 0xef, 0xa3, 0x00, 128 | 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, 129 | } 130 | 131 | func TestFiles_GetThumbnail(t *testing.T) { 132 | c := client() 133 | // REVIEW(bg): This feels a bit sloppy... 134 | { 135 | buf := bytes.NewBuffer(grayPng) 136 | _, err := c.Files.Upload(&UploadInput{ 137 | Mute: true, 138 | Mode: WriteModeOverwrite, 139 | Path: "/gray.png", 140 | Reader: buf, 141 | }) 142 | assert.NoError(t, err, "error uploading file") 143 | } 144 | out, err := c.Files.GetThumbnail(&GetThumbnailInput{"/gray.png", GetThumbnailFormatJPEG, GetThumbnailSizeW32H32}) 145 | assert.NoError(t, err) 146 | if err != nil { 147 | return 148 | } 149 | defer out.Body.Close() 150 | 151 | assert.NotEmpty(t, out.Length, "length should not be 0") 152 | 153 | buf := make([]byte, 11) 154 | _, err = out.Body.Read(buf) 155 | assert.NoError(t, err) 156 | assert.Equal(t, []byte{ 157 | 0xff, 0xd8, // JPEG SOI marker 158 | 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, // JFIF tag 159 | }, buf, "should have jpeg header") 160 | } 161 | 162 | func TestFiles_GetPreview(t *testing.T) { 163 | c := client() 164 | 165 | out, err := c.Files.GetPreview(&GetPreviewInput{"/sample.ppt"}) 166 | defer out.Body.Close() 167 | 168 | assert.NoError(t, err) 169 | 170 | assert.NotEmpty(t, out.Length, "length should not be 0") 171 | 172 | buf := make([]byte, 4) 173 | _, err = out.Body.Read(buf) 174 | assert.NoError(t, err) 175 | assert.Equal(t, []byte{0x25, 0x50, 0x44, 0x46}, buf, "should have pdf magic number") 176 | } 177 | 178 | func TestFiles_ListRevisions(t *testing.T) { 179 | c := client() 180 | 181 | out, err := c.Files.ListRevisions(&ListRevisionsInput{Path: "/sample.ppt"}) 182 | 183 | assert.NoError(t, err) 184 | assert.NotEmpty(t, out.Entries) 185 | assert.False(t, out.IsDeleted) 186 | } 187 | 188 | func TestFiles_ContentHash(t *testing.T) { 189 | data, err := dry.FileGetBytes("https://www.dropbox.com/static/images/developers/milky-way-nasa.jpg", time.Second*5) 190 | assert.NoError(t, err) 191 | 192 | hash, err := ContentHash(bytes.NewBuffer(data)) 193 | assert.NoError(t, err) 194 | 195 | assert.Equal(t, "485291fa0ee50c016982abbfa943957bcd231aae0492ccbaa22c58e3997b35e0", hash) 196 | } 197 | -------------------------------------------------------------------------------- /sharing.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Sharing client. 9 | type Sharing struct { 10 | *Client 11 | } 12 | 13 | // NewSharing client. 14 | func NewSharing(config *Config) *Sharing { 15 | return &Sharing{ 16 | Client: &Client{ 17 | Config: config, 18 | }, 19 | } 20 | } 21 | 22 | // CreateSharedLinkInput request input. 23 | type CreateSharedLinkInput struct { 24 | Path string `json:"path"` 25 | } 26 | 27 | // CreateSharedLinkOutput request output. 28 | type CreateSharedLinkOutput struct { 29 | URL string `json:"url"` 30 | Path string `json:"path"` 31 | VisibilityModel struct { 32 | Tag VisibilityType `json:".tag"` 33 | } `json:"visibility"` 34 | Expires time.Time `json:"expires,omitempty"` 35 | } 36 | 37 | // VisibilityType determines who can access the link. 38 | type VisibilityType string 39 | 40 | // Visibility types supported. 41 | const ( 42 | Public VisibilityType = "public" 43 | TeamOnly = "team_only" 44 | Password = "password" 45 | TeamAndPassword = "team_and_password" 46 | SharedFolderOnly = "shared_folder_only" 47 | ) 48 | 49 | // CreateSharedLink returns a shared link. 50 | func (c *Sharing) CreateSharedLink(in *CreateSharedLinkInput) (out *CreateSharedLinkOutput, err error) { 51 | body, err := c.call("/sharing/create_shared_link_with_settings", in) 52 | if err != nil { 53 | return 54 | } 55 | defer body.Close() 56 | 57 | err = json.NewDecoder(body).Decode(&out) 58 | return 59 | } 60 | 61 | // ListShareLinksInput request input. 62 | type ListShareLinksInput struct { 63 | Path string `json:"path"` 64 | } 65 | 66 | // SharedLinkOutput request output. 67 | type SharedLinkOutput struct { 68 | URL string `json:"url"` 69 | Path string `json:"path"` 70 | VisibilityModel struct { 71 | Tag VisibilityType `json:".tag"` 72 | } `json:"visibility"` 73 | Expires time.Time `json:"expires,omitempty"` 74 | } 75 | 76 | // ListShareLinksOutput request output. 77 | type ListShareLinksOutput struct { 78 | Links []SharedLinkOutput `json:"links"` 79 | } 80 | 81 | // ListSharedLinks gets shared links of input. 82 | func (c *Sharing) ListSharedLinks(in *ListShareLinksInput) (out *ListShareLinksOutput, err error) { 83 | endpoint := "/sharing/list_shared_links" 84 | body, err := c.call(endpoint, in) 85 | if err != nil { 86 | return 87 | } 88 | defer body.Close() 89 | 90 | err = json.NewDecoder(body).Decode(&out) 91 | return 92 | } 93 | 94 | // ListSharedFolderInput request input. 95 | type ListSharedFolderInput struct { 96 | Limit uint64 `json:"limit"` 97 | Actions []FolderAction `json:"actions,omitempty"` 98 | } 99 | 100 | // FolderAction defines actions that may be taken on shared folders. 101 | type FolderAction struct { 102 | ChangeOptions string 103 | } 104 | 105 | // ListSharedFolderOutput lists metadata about shared folders with a cursor to retrieve the next page. 106 | type ListSharedFolderOutput struct { 107 | Entries []SharedFolderMetadata `json:"entries"` 108 | Cursor string `json:"cursor"` 109 | } 110 | 111 | // ListSharedFolders returns the list of all shared folders the current user has access to. 112 | func (c *Sharing) ListSharedFolders(in *ListSharedFolderInput) (out *ListSharedFolderOutput, err error) { 113 | body, err := c.call("/sharing/list_folders", in) 114 | if err != nil { 115 | return 116 | } 117 | defer body.Close() 118 | 119 | err = json.NewDecoder(body).Decode(&out) 120 | return 121 | } 122 | 123 | // ListSharedFolderContinueInput request input. 124 | type ListSharedFolderContinueInput struct { 125 | Cursor string `json:"cursor"` 126 | } 127 | 128 | // ListSharedFoldersContinue returns the list of all shared folders the current user has access to. 129 | func (c *Sharing) ListSharedFoldersContinue(in *ListSharedFolderContinueInput) (out *ListSharedFolderOutput, err error) { 130 | body, err := c.call("/sharing/list_folders/continue", in) 131 | if err != nil { 132 | return 133 | } 134 | defer body.Close() 135 | 136 | err = json.NewDecoder(body).Decode(&out) 137 | return 138 | } 139 | 140 | // SharedFolderMetadata includes basic information about the shared folder. 141 | type SharedFolderMetadata struct { 142 | AccessType struct { 143 | Tag AccessType `json:".tag"` 144 | } `json:"access_type"` 145 | IsTeamFolder bool `json:"is_team_folder"` 146 | Policy FolderPolicy `json:"policy"` 147 | Name string `json:"name"` 148 | SharedFolderID string `json:"shared_folder_id"` 149 | TimeInvited time.Time `json:"time_invited"` 150 | OwnerTeam struct { 151 | ID string `json:"id"` 152 | Name string `json:"name"` 153 | } `json:"owner_team"` 154 | ParentSharedFolderID string `json:"parent_shared_folder_id"` 155 | PathLower string `json:"path_lower"` 156 | Permissions []string `json:"permissions"` 157 | } 158 | 159 | // FolderPolicy enumerates the policies governing this shared folder. 160 | type FolderPolicy struct { 161 | ACLUpdatePolicy struct { 162 | Tag ACLUpdatePolicy `json:".tag"` 163 | } `json:"acl_update_policy"` 164 | SharedLinkPolicy struct { 165 | Tag SharedLinkPolicy `json:".tag"` 166 | } `json:"shared_link_policy"` 167 | MemberPolicy struct { 168 | Tag MemberPolicy `json:".tag"` 169 | } `json:"member_policy"` 170 | ResolvedMemberPolicy struct { 171 | Tag MemberPolicy `json:".tag"` 172 | } `json:"resolved_member_policy"` 173 | } 174 | 175 | // AccessType determines the level of access to a shared folder. 176 | type AccessType string 177 | 178 | // Access types supported. 179 | const ( 180 | Owner AccessType = "owner" 181 | Editor = "editor" 182 | Viewer = "viewer" 183 | ViewerNoComment = "viewer_no_comment" 184 | ) 185 | 186 | // ACLUpdatePolicy determines who can add and remove members from this shared folder. 187 | type ACLUpdatePolicy string 188 | 189 | // ACLUpdatePolicy types supported. 190 | const ( 191 | ACLUpdatePolicyOwner ACLUpdatePolicy = "owner" 192 | ACLUpdatePolicyEditors = "editors" 193 | ) 194 | 195 | // SharedLinkPolicy governs who can view shared links. 196 | type SharedLinkPolicy string 197 | 198 | // SharedLinkPolicy types supported. 199 | const ( 200 | SharedLinkPolicyAnyone SharedLinkPolicy = "anyone" 201 | SharedLinkPolicyMembers = "members" 202 | ) 203 | 204 | // MemberPolicy determines who can be a member of this shared folder, as set on the folder itself. 205 | type MemberPolicy string 206 | 207 | // MemberPolicy types supported. 208 | const ( 209 | MemberPolicyTeam MemberPolicy = "team" 210 | MemberPolicyAnyone = "anyone" 211 | ) 212 | -------------------------------------------------------------------------------- /sharing_test.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSharing_CreateSharedLink(t *testing.T) { 10 | c := client() 11 | out, err := c.Sharing.CreateSharedLink(&CreateSharedLinkInput{ 12 | Path: "/hello.txt", 13 | }) 14 | 15 | assert.NoError(t, err, "error sharing file") 16 | assert.Equal(t, "/hello.txt", out.Path) 17 | } 18 | 19 | func TestSharing_ListSharedFolder(t *testing.T) { 20 | c := client() 21 | out, err := c.Sharing.ListSharedFolders(&ListSharedFolderInput{ 22 | Limit: 1, 23 | }) 24 | 25 | assert.NoError(t, err, "listing shared folders") 26 | assert.NotEmpty(t, out.Entries, "output should be non-empty") 27 | 28 | for out.Cursor != "" { 29 | out, err = c.Sharing.ListSharedFoldersContinue(&ListSharedFolderContinueInput{ 30 | Cursor: out.Cursor, 31 | }) 32 | 33 | assert.NoError(t, err, "listing shared folders") 34 | assert.NotEmpty(t, out.Entries, "output should be non-empty") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Users client for user accounts. 8 | type Users struct { 9 | *Client 10 | } 11 | 12 | // NewUsers client. 13 | func NewUsers(config *Config) *Users { 14 | return &Users{ 15 | Client: &Client{ 16 | Config: config, 17 | }, 18 | } 19 | } 20 | 21 | // GetAccountInput request input. 22 | type GetAccountInput struct { 23 | AccountID string `json:"account_id"` 24 | } 25 | 26 | // GetAccountOutput request output. 27 | type GetAccountOutput struct { 28 | AccountID string `json:"account_id"` 29 | Name struct { 30 | GivenName string `json:"given_name"` 31 | Surname string `json:"surname"` 32 | FamiliarName string `json:"familiar_name"` 33 | DisplayName string `json:"display_name"` 34 | } `json:"name"` 35 | } 36 | 37 | // GetAccount returns information about a user's account. 38 | func (c *Users) GetAccount(in *GetAccountInput) (out *GetAccountOutput, err error) { 39 | body, err := c.call("/users/get_account", in) 40 | if err != nil { 41 | return 42 | } 43 | defer body.Close() 44 | 45 | err = json.NewDecoder(body).Decode(&out) 46 | return 47 | } 48 | 49 | // GetCurrentAccountOutput request output. 50 | type GetCurrentAccountOutput struct { 51 | AccountID string `json:"account_id"` 52 | Name struct { 53 | GivenName string `json:"given_name"` 54 | Surname string `json:"surname"` 55 | FamiliarName string `json:"familiar_name"` 56 | DisplayName string `json:"display_name"` 57 | } `json:"name"` 58 | Email string `json:"email"` 59 | Locale string `json:"locale"` 60 | ReferralLink string `json:"referral_link"` 61 | IsPaired bool `json:"is_paired"` 62 | AccountType struct { 63 | Tag string `json:".tag"` 64 | } `json:"account_type"` 65 | Country string `json:"country"` 66 | } 67 | 68 | // GetCurrentAccount returns information about the current user's account. 69 | func (c *Users) GetCurrentAccount() (out *GetCurrentAccountOutput, err error) { 70 | body, err := c.call("/users/get_current_account", nil) 71 | if err != nil { 72 | return 73 | } 74 | defer body.Close() 75 | 76 | err = json.NewDecoder(body).Decode(&out) 77 | return 78 | } 79 | 80 | // GetSpaceUsageOutput request output. 81 | type GetSpaceUsageOutput struct { 82 | Used uint64 `json:"used"` 83 | Allocation struct { 84 | Used uint64 `json:"used"` 85 | Allocated uint64 `json:"allocated"` 86 | } `json:"allocation"` 87 | } 88 | 89 | // GetSpaceUsage returns space usage information for the current user's account. 90 | func (c *Users) GetSpaceUsage() (out *GetSpaceUsageOutput, err error) { 91 | body, err := c.call("/users/get_space_usage", nil) 92 | if err != nil { 93 | return 94 | } 95 | defer body.Close() 96 | 97 | err = json.NewDecoder(body).Decode(&out) 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /users_test.go: -------------------------------------------------------------------------------- 1 | package dropbox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUsers_GetCurrentAccount(t *testing.T) { 10 | c := client() 11 | _, err := c.Users.GetCurrentAccount() 12 | assert.NoError(t, err) 13 | } 14 | --------------------------------------------------------------------------------