├── LICENSE ├── Readme.md ├── client.go ├── client_test.go ├── example_test.go ├── file.go └── file_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-dropy?status.svg)](https://godoc.org/github.com/tj/go-dropy) [![Build Status](https://semaphoreci.com/api/v1/projects/486c0583-68ae-465b-a25e-422dd8760f6e/617435/badge.svg)](https://semaphoreci.com/tj/go-dropy) 3 | 4 | 5 | # Dropy 6 | 7 | High level Dropbox v2 client for Go built on top of [go-dropbox](https://github.com/tj/go-dropbox). 8 | 9 | ## Example 10 | 11 | ```go 12 | token := os.Getenv("DROPBOX_ACCESS_TOKEN") 13 | client := dropy.New(dropbox.New(dropbox.NewConfig(token))) 14 | 15 | client.Upload("/demo.txt", strings.NewReader("Hello World")) 16 | io.Copy(os.Stdout, client.Open("/demo.txt")) 17 | ``` 18 | 19 | ## Testing 20 | 21 | To manually run tests use the test account access token: 22 | 23 | ``` 24 | $ export DROPBOX_ACCESS_TOKEN=oENFkq_oIVAAAAAAAAAABqI2Nor2e9_ORA3oAZDQexMgJocCQX4aOFXZuDc1t-Sx 25 | $ go test -v 26 | ``` 27 | 28 | # License 29 | 30 | MIT -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package dropy implements a higher-level Dropbox API on top of go-dropbox. 2 | package dropy 3 | 4 | import ( 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/tj/go-dropbox" 10 | ) 11 | 12 | // Client wraps dropbox.Client to provide higher level sugar. 13 | type Client struct { 14 | *dropbox.Client 15 | } 16 | 17 | // New client. 18 | func New(d *dropbox.Client) *Client { 19 | return &Client{ 20 | Client: d, 21 | } 22 | } 23 | 24 | // Stat returns file and directory meta-data for `name`. 25 | func (c *Client) Stat(name string) (os.FileInfo, error) { 26 | out, err := c.Files.GetMetadata(&dropbox.GetMetadataInput{ 27 | Path: name, 28 | }) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &FileInfo{&out.Metadata}, nil 35 | } 36 | 37 | // ListN returns entries in dir `name`. Up to `n` entries, or all when `n` <= 0. 38 | func (c *Client) ListN(name string, n int) (list []os.FileInfo, err error) { 39 | var cursor string 40 | 41 | if n <= 0 { 42 | n = -1 43 | } 44 | 45 | for { 46 | var out *dropbox.ListFolderOutput 47 | 48 | if cursor == "" { 49 | if out, err = c.Files.ListFolder(&dropbox.ListFolderInput{Path: name}); err != nil { 50 | return 51 | } 52 | cursor = out.Cursor 53 | } else { 54 | if out, err = c.Files.ListFolderContinue(&dropbox.ListFolderContinueInput{cursor}); err != nil { 55 | return 56 | } 57 | cursor = out.Cursor 58 | } 59 | 60 | if err != nil { 61 | return 62 | } 63 | 64 | for _, ent := range out.Entries { 65 | list = append(list, &FileInfo{ent}) 66 | } 67 | 68 | if n >= 0 && len(list) >= n { 69 | list = list[:n] 70 | break 71 | } 72 | 73 | if !out.HasMore { 74 | break 75 | } 76 | } 77 | 78 | if n >= 0 && len(list) == 0 { 79 | err = io.EOF 80 | return 81 | } 82 | 83 | return 84 | } 85 | 86 | // List returns all entries in dir `name`. 87 | func (c *Client) List(name string) ([]os.FileInfo, error) { 88 | return c.ListN(name, 0) 89 | } 90 | 91 | // ListFilter returns all entries in dir `name` filtered by `filter`. 92 | func (c *Client) ListFilter(name string, filter func(info os.FileInfo) bool) (ret []os.FileInfo, err error) { 93 | ents, err := c.ListN(name, 0) 94 | if err != nil { 95 | return 96 | } 97 | 98 | for _, ent := range ents { 99 | if filter(ent) { 100 | ret = append(ret, ent) 101 | } 102 | } 103 | 104 | return 105 | } 106 | 107 | // ListFolders returns all folders in dir `name`. 108 | func (c *Client) ListFolders(name string) ([]os.FileInfo, error) { 109 | return c.ListFilter(name, func(info os.FileInfo) bool { 110 | return info.IsDir() 111 | }) 112 | } 113 | 114 | // ListFiles returns all files in dir `name`. 115 | func (c *Client) ListFiles(name string) ([]os.FileInfo, error) { 116 | return c.ListFilter(name, func(info os.FileInfo) bool { 117 | return !info.IsDir() 118 | }) 119 | } 120 | 121 | // Open returns a File for reading and writing. 122 | func (c *Client) Open(name string) *File { 123 | r, w := io.Pipe() 124 | return &File{ 125 | Name: name, 126 | c: c, 127 | pipeR: r, 128 | pipeW: w, 129 | } 130 | } 131 | 132 | // Read returns the contents of `name`. 133 | func (c *Client) Read(name string) ([]byte, error) { 134 | f := c.Open(name) 135 | defer f.Close() 136 | return ioutil.ReadAll(f) 137 | } 138 | 139 | // Download returns the contents of `name`. 140 | func (c *Client) Download(name string) (io.ReadCloser, error) { 141 | out, err := c.Files.Download(&dropbox.DownloadInput{name}) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | return out.Body, nil 147 | } 148 | 149 | // Preview returns the PDF preview of `name`. 150 | func (c *Client) Preview(name string) (io.ReadCloser, error) { 151 | out, err := c.Files.GetPreview(&dropbox.GetPreviewInput{name}) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | return out.Body, nil 157 | } 158 | 159 | // Mkdir creates folder `name`. 160 | func (c *Client) Mkdir(name string) error { 161 | _, err := c.Files.CreateFolder(&dropbox.CreateFolderInput{name}) 162 | return err 163 | } 164 | 165 | // Delete file `name`. 166 | func (c *Client) Delete(name string) error { 167 | _, err := c.Files.Delete(&dropbox.DeleteInput{name}) 168 | return err 169 | } 170 | 171 | // Copy file from `src` to `dst`. 172 | func (c *Client) Copy(src, dst string) error { 173 | _, err := c.Files.Copy(&dropbox.CopyInput{ 174 | FromPath: src, 175 | ToPath: dst, 176 | }) 177 | return err 178 | } 179 | 180 | // Move file from `src` to `dst`. 181 | func (c *Client) Move(src, dst string) error { 182 | _, err := c.Files.Move(&dropbox.MoveInput{ 183 | FromPath: src, 184 | ToPath: dst, 185 | }) 186 | return err 187 | } 188 | 189 | // Search return results for a search against `path` with the given `query`. 190 | func (c *Client) Search(path, query string) (list []os.FileInfo, err error) { 191 | var start uint64 192 | 193 | more: 194 | out, err := c.Files.Search(&dropbox.SearchInput{ 195 | Mode: dropbox.SearchModeFilename, 196 | Path: path, 197 | Query: query, 198 | Start: start, 199 | }) 200 | 201 | if err != nil { 202 | return 203 | } 204 | 205 | for _, match := range out.Matches { 206 | list = append(list, &FileInfo{match.Metadata}) 207 | } 208 | 209 | if out.More { 210 | start = out.Start 211 | goto more 212 | } 213 | 214 | return 215 | } 216 | 217 | // Upload reader to path. 218 | func (c *Client) Upload(path string, r io.Reader) error { 219 | _, err := c.Files.Upload(&dropbox.UploadInput{ 220 | Mode: dropbox.WriteModeOverwrite, 221 | Path: path, 222 | Reader: r, 223 | Mute: true, 224 | }) 225 | 226 | return err 227 | } 228 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package dropy 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/segmentio/go-env" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/tj/go-dropbox" 12 | ) 13 | 14 | func client() *Client { 15 | token := env.MustGet("DROPBOX_ACCESS_TOKEN") 16 | return New(dropbox.New(dropbox.NewConfig(token))) 17 | } 18 | 19 | func TestClient_Stat(t *testing.T) { 20 | t.Parallel() 21 | c := client() 22 | info, err := c.Stat("/hello.txt") 23 | assert.NoError(t, err) 24 | assert.Equal(t, false, info.IsDir()) 25 | assert.Equal(t, false, info.Mode().IsDir()) 26 | assert.Equal(t, true, info.Mode().IsRegular()) 27 | assert.Equal(t, "hello.txt", info.Name()) 28 | assert.Equal(t, int64(5), info.Size()) 29 | } 30 | 31 | func TestClient_Mkdir(t *testing.T) { 32 | t.Parallel() 33 | c := client() 34 | c.Delete("/some-new-dir") 35 | assert.NoError(t, c.Mkdir("/some-new-dir")) 36 | stat, err := c.Stat("/some-new-dir") 37 | assert.NoError(t, err) 38 | assert.True(t, stat.IsDir()) 39 | } 40 | 41 | func TestClient_List(t *testing.T) { 42 | t.Parallel() 43 | c := client() 44 | ents, err := c.List("/list") 45 | assert.NoError(t, err) 46 | assert.Equal(t, 5000, len(ents)) 47 | } 48 | 49 | func TestClient_ListN_missing(t *testing.T) { 50 | t.Parallel() 51 | c := client() 52 | _, err := c.ListN("/notfound", 0) 53 | assert.Error(t, err) 54 | } 55 | 56 | func TestClient_ListN_zero(t *testing.T) { 57 | t.Parallel() 58 | c := client() 59 | ents, err := c.ListN("/list", 0) 60 | assert.NoError(t, err) 61 | assert.Equal(t, 5000, len(ents)) 62 | } 63 | 64 | func TestClient_ListN_subzero(t *testing.T) { 65 | t.Parallel() 66 | c := client() 67 | ents, err := c.ListN("/list", -5) 68 | assert.NoError(t, err) 69 | assert.Equal(t, 5000, len(ents)) 70 | } 71 | 72 | func TestClient_ListN_count(t *testing.T) { 73 | t.Parallel() 74 | c := client() 75 | ents, err := c.ListN("/list", 1234) 76 | assert.NoError(t, err) 77 | assert.Equal(t, 1234, len(ents)) 78 | } 79 | 80 | func TestClient_ListFilter(t *testing.T) { 81 | t.Parallel() 82 | c := client() 83 | ents, err := c.ListFilter("/list-types", func(info os.FileInfo) bool { 84 | return info.IsDir() 85 | }) 86 | assert.NoError(t, err) 87 | assert.Equal(t, 3, len(ents)) 88 | } 89 | 90 | func TestClient_ListFolders(t *testing.T) { 91 | t.Parallel() 92 | c := client() 93 | ents, err := c.ListFolders("/list-types") 94 | assert.NoError(t, err) 95 | assert.Equal(t, 3, len(ents)) 96 | assert.Equal(t, "one", ents[0].Name()) 97 | } 98 | 99 | func TestClient_ListFiles(t *testing.T) { 100 | t.Parallel() 101 | c := client() 102 | ents, err := c.ListFiles("/list-types") 103 | assert.NoError(t, err) 104 | assert.Equal(t, 3, len(ents)) 105 | assert.Equal(t, "one.txt", ents[0].Name()) 106 | } 107 | 108 | func TestClient_Open(t *testing.T) { 109 | t.Parallel() 110 | c := client() 111 | 112 | f := c.Open("/hello.txt") 113 | 114 | b, err := ioutil.ReadAll(f) 115 | assert.NoError(t, err) 116 | 117 | assert.Equal(t, "world", string(b)) 118 | } 119 | 120 | func TestCient_Open_missing(t *testing.T) { 121 | t.Parallel() 122 | c := client() 123 | 124 | f := c.Open("/dev/null") 125 | 126 | _, err := ioutil.ReadAll(f) 127 | assert.EqualError(t, err, "open /dev/null: no such file or directory") 128 | } 129 | 130 | func TestClient_Read(t *testing.T) { 131 | t.Parallel() 132 | c := client() 133 | b, err := c.Read("/hello.txt") 134 | assert.NoError(t, err) 135 | assert.Equal(t, "world", string(b)) 136 | } 137 | 138 | func TestClient_Delete(t *testing.T) { 139 | t.Parallel() 140 | c := client() 141 | assert.NoError(t, c.Mkdir("/delete-me")) 142 | assert.NoError(t, c.Delete("/delete-me")) 143 | } 144 | 145 | func TestClient_Search(t *testing.T) { 146 | t.Parallel() 147 | c := client() 148 | 149 | list, err := c.Search("/list", "100") 150 | assert.NoError(t, err) 151 | 152 | assert.Equal(t, 11, len(list)) 153 | } 154 | 155 | func TestClient_Search_more(t *testing.T) { 156 | t.Parallel() 157 | c := client() 158 | 159 | list, err := c.Search("/list", "10") 160 | assert.NoError(t, err) 161 | 162 | assert.Equal(t, 111, len(list)) 163 | } 164 | 165 | func TestClient_Upload(t *testing.T) { 166 | t.Parallel() 167 | 168 | c := client() 169 | err := c.Upload("/upload-1.txt", strings.NewReader("one")) 170 | assert.NoError(t, err, "error uploading") 171 | 172 | b, err := c.Read("/upload-1.txt") 173 | assert.NoError(t, err, "error reading") 174 | 175 | assert.Equal(t, "one", string(b)) 176 | } 177 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package dropy_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | 8 | "github.com/segmentio/go-env" 9 | "github.com/tj/go-dropbox" 10 | "github.com/tj/go-dropy" 11 | ) 12 | 13 | // Upload and read a file. 14 | func Example() { 15 | token := env.MustGet("DROPBOX_ACCESS_TOKEN") 16 | client := dropy.New(dropbox.New(dropbox.NewConfig(token))) 17 | 18 | client.Upload("/demo.txt", strings.NewReader("Hello World")) 19 | io.Copy(os.Stdout, client.Open("/demo.txt")) 20 | // Output: Hello World 21 | } 22 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package dropy 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/tj/go-dropbox" 11 | ) 12 | 13 | // FileInfo wraps Dropbox file MetaData to implement os.FileInfo. 14 | type FileInfo struct { 15 | meta *dropbox.Metadata 16 | } 17 | 18 | // Name of the file. 19 | func (f *FileInfo) Name() string { 20 | return f.meta.Name 21 | } 22 | 23 | // Size of the file. 24 | func (f *FileInfo) Size() int64 { 25 | return int64(f.meta.Size) 26 | } 27 | 28 | // IsDir returns true if the file is a directory. 29 | func (f *FileInfo) IsDir() bool { 30 | return f.meta.Tag == "folder" 31 | } 32 | 33 | // Sys is not implemented. 34 | func (f *FileInfo) Sys() interface{} { 35 | return nil 36 | } 37 | 38 | // ModTime returns the modification time. 39 | func (f *FileInfo) ModTime() time.Time { 40 | return f.meta.ServerModified 41 | } 42 | 43 | // Mode returns the file mode flags. 44 | func (f *FileInfo) Mode() os.FileMode { 45 | var m os.FileMode 46 | 47 | if f.IsDir() { 48 | m |= os.ModeDir 49 | } 50 | 51 | return m 52 | } 53 | 54 | // File implements an io.ReadWriteCloser for Dropbox files. 55 | type File struct { 56 | Name string 57 | closed bool 58 | writing bool 59 | reader io.ReadCloser 60 | pipeR *io.PipeReader 61 | pipeW *io.PipeWriter 62 | c *Client 63 | } 64 | 65 | // Read implements io.Reader 66 | func (f *File) Read(b []byte) (int, error) { 67 | if f.reader == nil { 68 | if err := f.download(); err != nil { 69 | return 0, err 70 | } 71 | } 72 | 73 | return f.reader.Read(b) 74 | } 75 | 76 | // Write implements io.Writer. 77 | func (f *File) Write(b []byte) (int, error) { 78 | if !f.writing { 79 | f.writing = true 80 | 81 | go func() { 82 | _, err := f.c.Files.Upload(&dropbox.UploadInput{ 83 | Mode: dropbox.WriteModeOverwrite, 84 | Path: f.Name, 85 | Mute: true, 86 | Reader: f.pipeR, 87 | }) 88 | 89 | f.pipeR.CloseWithError(err) 90 | }() 91 | } 92 | 93 | return f.pipeW.Write(b) 94 | } 95 | 96 | // Close implements io.Closer. 97 | func (f *File) Close() error { 98 | if f.closed { 99 | return &os.PathError{"close", f.Name, syscall.EINVAL} 100 | } 101 | f.closed = true 102 | 103 | if f.writing { 104 | if err := f.pipeW.Close(); err != nil { 105 | return err 106 | } 107 | } 108 | 109 | if f.reader != nil { 110 | if err := f.reader.Close(); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // download the file. 119 | func (f *File) download() error { 120 | out, err := f.c.Files.Download(&dropbox.DownloadInput{f.Name}) 121 | if err != nil { 122 | if strings.HasPrefix(err.Error(), "path/not_found/") { 123 | return &os.PathError{"open", f.Name, syscall.ENOENT} 124 | } 125 | return err 126 | } 127 | 128 | f.reader = out.Body 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | package dropy 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFile_Open(t *testing.T) { 11 | t.Parallel() 12 | c := client() 13 | 14 | f := c.Open("/hello.txt") 15 | 16 | b, err := ioutil.ReadAll(f) 17 | assert.NoError(t, err) 18 | 19 | assert.Equal(t, "world", string(b)) 20 | } 21 | 22 | func TestFile_Close(t *testing.T) { 23 | t.Parallel() 24 | c := client() 25 | 26 | f := c.Open("/hello.txt") 27 | assert.NoError(t, f.Close()) 28 | } 29 | 30 | func TestFile_Close_inval(t *testing.T) { 31 | t.Parallel() 32 | c := client() 33 | 34 | f := c.Open("/hello.txt") 35 | assert.NoError(t, f.Close()) 36 | assert.EqualError(t, f.Close(), "close /hello.txt: invalid argument") 37 | } 38 | 39 | func TestFile_Read(t *testing.T) { 40 | t.Parallel() 41 | c := client() 42 | 43 | f := c.Open("/hello.txt") 44 | 45 | b := make([]byte, 5) 46 | n, err := f.Read(b) 47 | assert.Equal(t, 5, n) 48 | assert.EqualError(t, err, "EOF") 49 | assert.Equal(t, "world", string(b)) 50 | 51 | assert.NoError(t, f.Close()) 52 | } 53 | 54 | func TestFile_Write(t *testing.T) { 55 | t.Parallel() 56 | c := client() 57 | 58 | f := c.Open("/hello-world-1.txt") 59 | 60 | _, err := f.Write([]byte("Hello")) 61 | assert.NoError(t, err) 62 | 63 | _, err = f.Write([]byte(" Wor")) 64 | assert.NoError(t, err) 65 | 66 | _, err = f.Write([]byte("ld")) 67 | assert.NoError(t, err) 68 | 69 | assert.NoError(t, f.Close()) 70 | 71 | b, err := c.Read("/hello-world-1.txt") 72 | assert.NoError(t, err) 73 | 74 | assert.Equal(t, "Hello World", string(b)) 75 | } 76 | --------------------------------------------------------------------------------