├── .travis.yml ├── LICENSE ├── README.md ├── datastores_parser_test.go ├── crypto.go ├── datastores_parser.go ├── datastores_requests.go ├── datastores_test.go ├── datastores_changes.go ├── datastores.go ├── dropbox_test.go └── dropbox.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.1.2 5 | - 1.2.2 6 | - 1.3 7 | - tip 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Arnaud Ysmal 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dropbox 2 | ======= 3 | Go client library for the Dropbox core and Datastore API with support for uploading and downloading encrypted files. 4 | 5 | Support of the Datastore API should be considered as a beta version. 6 | 7 | Prerequisite 8 | ------------ 9 | To use this library, you must have a valid client ID (app key) and client secret (app secret) provided by Dropbox.
10 | To register a new client application, please visit https://www.dropbox.com/developers/apps/create 11 | 12 | Installation 13 | ------------ 14 | This library depends on the oauth2 package, it can be installed with the go get command: 15 | 16 | $ go get golang.org/x/oauth2 17 | 18 | This package can be installed with the go get command: 19 | 20 | $ go get github.com/stacktic/dropbox 21 | 22 | 23 | Examples 24 | -------- 25 | This simple 4-step example will show you how to create a folder: 26 | 27 | package main 28 | 29 | import ( 30 | "dropbox" 31 | "fmt" 32 | ) 33 | 34 | func main() { 35 | var err error 36 | var db *dropbox.Dropbox 37 | 38 | var clientid, clientsecret string 39 | var token string 40 | 41 | clientid = "xxxxx" 42 | clientsecret = "yyyyy" 43 | token = "zzzz" 44 | 45 | // 1. Create a new dropbox object. 46 | db = dropbox.NewDropbox() 47 | 48 | // 2. Provide your clientid and clientsecret (see prerequisite). 49 | db.SetAppInfo(clientid, clientsecret) 50 | 51 | // 3. Provide the user token. 52 | db.SetAccessToken(token) 53 | 54 | // 4. Send your commands. 55 | // In this example, you will create a new folder named "demo". 56 | folder := "demo" 57 | if _, err = db.CreateFolder(folder); err != nil { 58 | fmt.Printf("Error creating folder %s: %s\n", folder, err) 59 | } else { 60 | fmt.Printf("Folder %s successfully created\n", folder) 61 | } 62 | } 63 | 64 | If you do not know the user token, you can replace step 3 by a call to the Auth method: 65 | 66 | // This method will ask the user to visit an URL and paste the generated code. 67 | if err = db.Auth(); err != nil { 68 | fmt.Println(err) 69 | return 70 | } 71 | // You can now retrieve the token if you want. 72 | token = db.AccessToken() 73 | 74 | If you want a more complete example, please check the following project: https://github.com/stacktic/dbox. 75 | 76 | Documentation 77 | ------------- 78 | 79 | API documentation can be found here: http://godoc.org/github.com/stacktic/dropbox. 80 | -------------------------------------------------------------------------------- /datastores_parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "encoding/json" 30 | "fmt" 31 | "math" 32 | "reflect" 33 | "testing" 34 | "time" 35 | ) 36 | 37 | type equaller interface { 38 | equals(equaller) bool 39 | } 40 | 41 | func (s *atom) equals(o *atom) bool { 42 | switch v1 := s.Value.(type) { 43 | case float64: 44 | if v2, ok := o.Value.(float64); ok { 45 | if math.IsNaN(v1) && math.IsNaN(v2) { 46 | return true 47 | } 48 | return v1 == v2 49 | } 50 | default: 51 | return reflect.DeepEqual(s, o) 52 | } 53 | return false 54 | } 55 | 56 | func (s *value) equals(o *value) bool { 57 | return reflect.DeepEqual(s, o) 58 | } 59 | 60 | func (s Fields) equals(o Fields) bool { 61 | return reflect.DeepEqual(s, o) 62 | } 63 | 64 | func (s opDict) equals(o opDict) bool { 65 | return reflect.DeepEqual(s, o) 66 | } 67 | 68 | func (s *change) equals(o *change) bool { 69 | return reflect.DeepEqual(s, o) 70 | } 71 | 72 | func (s *fieldOp) equals(o *fieldOp) bool { 73 | return reflect.DeepEqual(s, o) 74 | } 75 | 76 | func testDSAtom(t *testing.T, c *atom, e string) { 77 | var c2 atom 78 | var err error 79 | var js []byte 80 | 81 | if js, err = json.Marshal(c); err != nil { 82 | t.Errorf("%s", err) 83 | } 84 | if err = c2.UnmarshalJSON(js); err != nil { 85 | t.Errorf("%s", err) 86 | } 87 | if !c.equals(&c2) { 88 | t.Errorf("expected %#v type %s got %#v of type %s", c.Value, reflect.TypeOf(c.Value).Name(), c2.Value, reflect.TypeOf(c2.Value).Name()) 89 | } 90 | c2 = atom{} 91 | if err = c2.UnmarshalJSON([]byte(e)); err != nil { 92 | t.Errorf("%s", err) 93 | } 94 | if !c.equals(&c2) { 95 | t.Errorf("expected %#v type %s got %#v of type %s", c.Value, reflect.TypeOf(c.Value).Name(), c2.Value, reflect.TypeOf(c2.Value).Name()) 96 | } 97 | } 98 | 99 | func TestDSAtomUnmarshalJSON(t *testing.T) { 100 | testDSAtom(t, &atom{Value: 32.5}, `32.5`) 101 | testDSAtom(t, &atom{Value: true}, `true`) 102 | testDSAtom(t, &atom{Value: int64(42)}, `{"I":"42"}`) 103 | testDSAtom(t, &atom{Value: math.NaN()}, `{"N":"nan"}`) 104 | testDSAtom(t, &atom{Value: math.Inf(1)}, `{"N":"+inf"}`) 105 | testDSAtom(t, &atom{Value: math.Inf(-1)}, `{"N":"-inf"}`) 106 | testDSAtom(t, &atom{Value: []byte(`random string converted to bytes`)}, `{"B":"cmFuZG9tIHN0cmluZyBjb252ZXJ0ZWQgdG8gYnl0ZXM="}`) 107 | 108 | now := time.Now().Round(time.Millisecond) 109 | js := fmt.Sprintf(`{"T": "%d"}`, now.UnixNano()/int64(time.Millisecond)) 110 | testDSAtom(t, &atom{Value: now}, js) 111 | } 112 | 113 | func testDSChange(t *testing.T, c *change, e string) { 114 | var c2 change 115 | var err error 116 | var js []byte 117 | 118 | if js, err = json.Marshal(c); err != nil { 119 | t.Errorf("%s", err) 120 | } 121 | if err = c2.UnmarshalJSON(js); err != nil { 122 | t.Errorf("%s", err) 123 | } 124 | if !c.equals(&c2) { 125 | t.Errorf("mismatch: got:\n\t%#v\nexpected:\n\t%#v", c2, *c) 126 | } 127 | c2 = change{} 128 | if err = c2.UnmarshalJSON([]byte(e)); err != nil { 129 | t.Errorf("%s", err) 130 | } 131 | if !c.equals(&c2) { 132 | t.Errorf("mismatch") 133 | } 134 | } 135 | 136 | func TestDSChangeUnmarshalJSON(t *testing.T) { 137 | testDSChange(t, 138 | &change{ 139 | Op: recordInsert, 140 | TID: "dropbox", 141 | RecordID: "test", 142 | Data: Fields{"float": value{values: []interface{}{float64(42)}, isList: false}}, 143 | }, `["I","dropbox","test",{"float":42}]`) 144 | testDSChange(t, 145 | &change{ 146 | Op: recordUpdate, 147 | TID: "dropbox", 148 | RecordID: "test", 149 | Ops: opDict{"field": fieldOp{Op: fieldPut, Data: value{values: []interface{}{float64(42)}, isList: false}}}, 150 | }, `["U","dropbox","test",{"field":["P", 42]}]`) 151 | testDSChange(t, 152 | &change{ 153 | Op: recordUpdate, 154 | TID: "dropbox", 155 | RecordID: "test", 156 | Ops: opDict{"field": fieldOp{Op: listCreate}}, 157 | }, `["U","dropbox","test",{"field":["LC"]}]`) 158 | 159 | testDSChange(t, 160 | &change{ 161 | Op: recordDelete, 162 | TID: "dropbox", 163 | RecordID: "test", 164 | }, `["D","dropbox","test"]`) 165 | } 166 | 167 | func testCheckfieldOp(t *testing.T, fo *fieldOp, e string) { 168 | var fo2 fieldOp 169 | var js []byte 170 | var err error 171 | 172 | if js, err = json.Marshal(fo); err != nil { 173 | t.Errorf("%s", err) 174 | } 175 | if string(js) != e { 176 | t.Errorf("marshalling error got %s expected %s", string(js), e) 177 | } 178 | if err = json.Unmarshal(js, &fo2); err != nil { 179 | t.Errorf("%s %s", err, string(js)) 180 | } 181 | if !fo.equals(&fo2) { 182 | t.Errorf("%#v != %#v\n", fo, fo2) 183 | } 184 | fo2 = fieldOp{} 185 | if err = json.Unmarshal([]byte(e), &fo2); err != nil { 186 | t.Errorf("%s %s", err, string(js)) 187 | } 188 | if !fo.equals(&fo2) { 189 | t.Errorf("%#v != %#v\n", fo, fo2) 190 | } 191 | } 192 | 193 | func TestDSfieldOpMarshalling(t *testing.T) { 194 | testCheckfieldOp(t, &fieldOp{Op: "P", Data: value{values: []interface{}{"bar"}, isList: false}}, `["P","bar"]`) 195 | testCheckfieldOp(t, &fieldOp{Op: "P", Data: value{values: []interface{}{"ga", "bu", "zo", "meuh", int64(42), 4.5, true}, isList: true}}, `["P",["ga","bu","zo","meuh",{"I":"42"},4.5,true]]`) 196 | testCheckfieldOp(t, &fieldOp{Op: "D"}, `["D"]`) 197 | testCheckfieldOp(t, &fieldOp{Op: "LC"}, `["LC"]`) 198 | testCheckfieldOp(t, &fieldOp{Op: "LP", Index: 1, Data: value{values: []interface{}{"baz"}}}, `["LP",1,"baz"]`) 199 | testCheckfieldOp(t, &fieldOp{Op: "LI", Index: 1, Data: value{values: []interface{}{"baz"}}}, `["LI",1,"baz"]`) 200 | testCheckfieldOp(t, &fieldOp{Op: "LD", Index: 1}, `["LD",1]`) 201 | testCheckfieldOp(t, &fieldOp{Op: "LM", Index: 1, Index2: 2}, `["LM",1,2]`) 202 | } 203 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "crypto/aes" 30 | "crypto/cipher" 31 | "crypto/rand" 32 | "io" 33 | "os" 34 | ) 35 | 36 | // GenerateKey generates a key by reading length bytes from /dev/random 37 | func GenerateKey(length int) ([]byte, error) { 38 | var err error 39 | var fd io.Reader 40 | var rv []byte 41 | 42 | if fd, err = os.Open("/dev/random"); err != nil { 43 | return nil, err 44 | } 45 | rv = make([]byte, length) 46 | _, err = io.ReadFull(fd, rv) 47 | return rv, err 48 | } 49 | 50 | func newCrypter(key []byte, in io.Reader, size int, newCipher func(key []byte) (cipher.Block, error)) (io.ReadCloser, int, error) { 51 | var block cipher.Block 52 | var err error 53 | 54 | if block, err = newCipher(key); err != nil { 55 | return nil, 0, err 56 | } 57 | outsize := size - size%block.BlockSize() + 2*block.BlockSize() 58 | 59 | rd, wr := io.Pipe() 60 | go encrypt(block, in, size, wr) 61 | return rd, outsize, nil 62 | } 63 | 64 | func newDecrypter(key []byte, in io.Reader, size int, newCipher func(key []byte) (cipher.Block, error)) (io.ReadCloser, error) { 65 | var block cipher.Block 66 | var err error 67 | 68 | if block, err = newCipher(key); err != nil { 69 | return nil, err 70 | } 71 | 72 | rd, wr := io.Pipe() 73 | go decrypt(block, in, size, wr) 74 | return rd, nil 75 | } 76 | 77 | // NewAESDecrypterReader creates and returns a new io.ReadCloser to decrypt the given io.Reader containing size bytes with the given AES key. 78 | // The AES key should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. 79 | func NewAESDecrypterReader(key []byte, input io.Reader, size int) (io.ReadCloser, error) { 80 | return newDecrypter(key, input, size, aes.NewCipher) 81 | } 82 | 83 | // NewAESCrypterReader creates and returns a new io.ReadCloser to encrypt the given io.Reader containing size bytes with the given AES key. 84 | // The AES key should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. 85 | func NewAESCrypterReader(key []byte, input io.Reader, size int) (io.ReadCloser, int, error) { 86 | return newCrypter(key, input, size, aes.NewCipher) 87 | } 88 | 89 | func encrypt(block cipher.Block, in io.Reader, size int, out io.WriteCloser) error { 90 | var err error 91 | var rd int 92 | var buf []byte 93 | var last bool 94 | var encrypter cipher.BlockMode 95 | 96 | defer out.Close() 97 | 98 | buf = make([]byte, block.BlockSize()) 99 | 100 | if _, err = io.ReadFull(rand.Reader, buf); err != nil { 101 | return err 102 | } 103 | encrypter = cipher.NewCBCEncrypter(block, buf) 104 | 105 | if _, err = out.Write(buf); err != nil { 106 | return err 107 | } 108 | for !last { 109 | if rd, err = io.ReadFull(in, buf); err != nil { 110 | if err == io.ErrUnexpectedEOF || err == io.EOF { 111 | buf = buf[:rd] 112 | buf = append(buf, 0x80) 113 | for len(buf) < block.BlockSize() { 114 | buf = append(buf, 0x00) 115 | } 116 | last = true 117 | } else { 118 | return err 119 | } 120 | } 121 | encrypter.CryptBlocks(buf, buf) 122 | if _, err = out.Write(buf); err != nil { 123 | return err 124 | } 125 | } 126 | return nil 127 | } 128 | 129 | func decrypt(block cipher.Block, in io.Reader, size int, out io.WriteCloser) error { 130 | var err error 131 | var buf []byte 132 | var count int 133 | var decrypter cipher.BlockMode 134 | 135 | defer out.Close() 136 | 137 | buf = make([]byte, block.BlockSize()) 138 | if _, err = io.ReadFull(in, buf); err != nil { 139 | return err 140 | } 141 | decrypter = cipher.NewCBCDecrypter(block, buf) 142 | 143 | count = (size - block.BlockSize()) / block.BlockSize() 144 | for count > 0 && err == nil { 145 | if _, err = io.ReadFull(in, buf); err == nil { 146 | decrypter.CryptBlocks(buf, buf) 147 | if count == 1 { 148 | for count = block.BlockSize() - 1; buf[count] == 0x00; count-- { 149 | continue 150 | } 151 | if buf[count] == 0x80 { 152 | buf = buf[:count] 153 | } 154 | } 155 | _, err = out.Write(buf) 156 | } 157 | count-- 158 | } 159 | if err == io.EOF { 160 | return nil 161 | } 162 | return err 163 | } 164 | 165 | // FilesPutAES uploads and encrypts size bytes from the input reader to the dst path on Dropbox. 166 | func (db *Dropbox) FilesPutAES(key []byte, input io.ReadCloser, size int64, dst string, overwrite bool, parentRev string) (*Entry, error) { 167 | var encreader io.ReadCloser 168 | var outsize int 169 | var err error 170 | 171 | if encreader, outsize, err = NewAESCrypterReader(key, input, int(size)); err != nil { 172 | return nil, err 173 | } 174 | return db.FilesPut(encreader, int64(outsize), dst, overwrite, parentRev) 175 | } 176 | 177 | // UploadFileAES uploads and encrypts the file located in the src path on the local disk to the dst path on Dropbox. 178 | func (db *Dropbox) UploadFileAES(key []byte, src, dst string, overwrite bool, parentRev string) (*Entry, error) { 179 | var err error 180 | var fd *os.File 181 | var fsize int64 182 | 183 | if fd, err = os.Open(src); err != nil { 184 | return nil, err 185 | } 186 | defer fd.Close() 187 | 188 | if fi, err := fd.Stat(); err == nil { 189 | fsize = fi.Size() 190 | } else { 191 | return nil, err 192 | } 193 | return db.FilesPutAES(key, fd, fsize, dst, overwrite, parentRev) 194 | } 195 | 196 | // DownloadAES downloads and decrypts the file located in the src path on Dropbox and returns a io.ReadCloser. 197 | func (db *Dropbox) DownloadAES(key []byte, src, rev string, offset int) (io.ReadCloser, error) { 198 | var in io.ReadCloser 199 | var size int64 200 | var err error 201 | 202 | if in, size, err = db.Download(src, rev, offset); err != nil { 203 | return nil, err 204 | } 205 | return NewAESDecrypterReader(key, in, int(size)) 206 | } 207 | 208 | // DownloadToFileAES downloads and decrypts the file located in the src path on Dropbox to the dst file on the local disk. 209 | func (db *Dropbox) DownloadToFileAES(key []byte, src, dst, rev string) error { 210 | var input io.ReadCloser 211 | var fd *os.File 212 | var err error 213 | 214 | if fd, err = os.Create(dst); err != nil { 215 | return err 216 | } 217 | defer fd.Close() 218 | 219 | if input, err = db.DownloadAES(key, src, rev, 0); err != nil { 220 | os.Remove(dst) 221 | return err 222 | } 223 | defer input.Close() 224 | if _, err = io.Copy(fd, input); err != nil { 225 | os.Remove(dst) 226 | } 227 | return err 228 | } 229 | -------------------------------------------------------------------------------- /datastores_parser.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "encoding/base64" 30 | "encoding/json" 31 | "fmt" 32 | "math" 33 | "strconv" 34 | "strings" 35 | "time" 36 | ) 37 | 38 | type atom struct { 39 | Value interface{} 40 | } 41 | 42 | func encodeDBase64(b []byte) string { 43 | return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") 44 | } 45 | 46 | func decodeDBase64(s string) ([]byte, error) { 47 | pad := 4 - len(s)%4 48 | if pad != 4 { 49 | s += strings.Repeat("=", pad) 50 | } 51 | return base64.URLEncoding.DecodeString(s) 52 | } 53 | 54 | // MarshalJSON returns the JSON encoding of a. 55 | func (a atom) MarshalJSON() ([]byte, error) { 56 | switch v := a.Value.(type) { 57 | case bool, string: 58 | return json.Marshal(v) 59 | case float64: 60 | if math.IsNaN(v) { 61 | return []byte(`{"N": "nan"}`), nil 62 | } else if math.IsInf(v, 1) { 63 | return []byte(`{"N": "+inf"}`), nil 64 | } else if math.IsInf(v, -1) { 65 | return []byte(`{"N": "-inf"}`), nil 66 | } 67 | return json.Marshal(v) 68 | case time.Time: 69 | return []byte(fmt.Sprintf(`{"T": "%d"}`, v.UnixNano()/int64(time.Millisecond))), nil 70 | case int, int32, int64: 71 | return []byte(fmt.Sprintf(`{"I": "%d"}`, v)), nil 72 | case []byte: 73 | return []byte(fmt.Sprintf(`{"B": "%s"}`, encodeDBase64(v))), nil 74 | } 75 | return nil, fmt.Errorf("wrong format") 76 | } 77 | 78 | // UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by a. 79 | func (a *atom) UnmarshalJSON(data []byte) error { 80 | var i interface{} 81 | var err error 82 | 83 | if err = json.Unmarshal(data, &i); err != nil { 84 | return err 85 | } 86 | switch v := i.(type) { 87 | case bool, int, int32, int64, float32, float64, string: 88 | a.Value = v 89 | return nil 90 | case map[string]interface{}: 91 | for key, rval := range v { 92 | val, ok := rval.(string) 93 | if !ok { 94 | return fmt.Errorf("could not parse atom") 95 | } 96 | switch key { 97 | case "I": 98 | a.Value, err = strconv.ParseInt(val, 10, 64) 99 | return nil 100 | case "N": 101 | switch val { 102 | case "nan": 103 | a.Value = math.NaN() 104 | return nil 105 | case "+inf": 106 | a.Value = math.Inf(1) 107 | return nil 108 | case "-inf": 109 | a.Value = math.Inf(-1) 110 | return nil 111 | default: 112 | return fmt.Errorf("unknown special type %s", val) 113 | } 114 | case "T": 115 | t, err := strconv.ParseInt(val, 10, 64) 116 | if err != nil { 117 | return fmt.Errorf("could not parse atom") 118 | } 119 | a.Value = time.Unix(t/1000, (t%1000)*int64(time.Millisecond)) 120 | return nil 121 | 122 | case "B": 123 | a.Value, err = decodeDBase64(val) 124 | return err 125 | } 126 | } 127 | } 128 | return fmt.Errorf("could not parse atom") 129 | } 130 | 131 | // MarshalJSON returns the JSON encoding of v. 132 | func (v value) MarshalJSON() ([]byte, error) { 133 | if v.isList { 134 | var a []atom 135 | 136 | a = make([]atom, len(v.values)) 137 | for i := range v.values { 138 | a[i].Value = v.values[i] 139 | } 140 | return json.Marshal(a) 141 | } 142 | return json.Marshal(atom{Value: v.values[0]}) 143 | } 144 | 145 | // UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by v. 146 | func (v *value) UnmarshalJSON(data []byte) error { 147 | var isArray bool 148 | var err error 149 | var a atom 150 | var as []atom 151 | 152 | for _, d := range data { 153 | if d == ' ' { 154 | continue 155 | } 156 | if d == '[' { 157 | isArray = true 158 | } 159 | break 160 | } 161 | if isArray { 162 | if err = json.Unmarshal(data, &as); err != nil { 163 | return err 164 | } 165 | v.values = make([]interface{}, len(as)) 166 | for i, at := range as { 167 | v.values[i] = at.Value 168 | } 169 | v.isList = true 170 | return nil 171 | } 172 | if err = json.Unmarshal(data, &a); err != nil { 173 | return err 174 | } 175 | v.values = make([]interface{}, 1) 176 | v.values[0] = a.Value 177 | return nil 178 | } 179 | 180 | // UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by f. 181 | func (f *fieldOp) UnmarshalJSON(data []byte) error { 182 | var i []json.RawMessage 183 | var err error 184 | 185 | if err = json.Unmarshal(data, &i); err != nil { 186 | return err 187 | } 188 | 189 | if err = json.Unmarshal(i[0], &f.Op); err != nil { 190 | return err 191 | } 192 | switch f.Op { 193 | case fieldPut: 194 | if len(i) != 2 { 195 | return fmt.Errorf("wrong format") 196 | } 197 | return json.Unmarshal(i[1], &f.Data) 198 | case fieldDelete, listCreate: 199 | if len(i) != 1 { 200 | return fmt.Errorf("wrong format") 201 | } 202 | case listInsert, listPut: 203 | if len(i) != 3 { 204 | return fmt.Errorf("wrong format") 205 | } 206 | if err = json.Unmarshal(i[1], &f.Index); err != nil { 207 | return err 208 | } 209 | return json.Unmarshal(i[2], &f.Data) 210 | case listDelete: 211 | if len(i) != 2 { 212 | return fmt.Errorf("wrong format") 213 | } 214 | return json.Unmarshal(i[1], &f.Index) 215 | case listMove: 216 | if len(i) != 3 { 217 | return fmt.Errorf("wrong format") 218 | } 219 | if err = json.Unmarshal(i[1], &f.Index); err != nil { 220 | return err 221 | } 222 | return json.Unmarshal(i[2], &f.Index2) 223 | default: 224 | return fmt.Errorf("wrong format") 225 | } 226 | return nil 227 | } 228 | 229 | // MarshalJSON returns the JSON encoding of f. 230 | func (f fieldOp) MarshalJSON() ([]byte, error) { 231 | switch f.Op { 232 | case fieldPut: 233 | return json.Marshal([]interface{}{f.Op, f.Data}) 234 | case fieldDelete, listCreate: 235 | return json.Marshal([]interface{}{f.Op}) 236 | case listInsert, listPut: 237 | return json.Marshal([]interface{}{f.Op, f.Index, f.Data}) 238 | case listDelete: 239 | return json.Marshal([]interface{}{f.Op, f.Index}) 240 | case listMove: 241 | return json.Marshal([]interface{}{f.Op, f.Index, f.Index2}) 242 | } 243 | return nil, fmt.Errorf("could not marshal Change type") 244 | } 245 | 246 | // UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by c. 247 | func (c *change) UnmarshalJSON(data []byte) error { 248 | var i []json.RawMessage 249 | var err error 250 | 251 | if err = json.Unmarshal(data, &i); err != nil { 252 | return err 253 | } 254 | if len(i) < 3 { 255 | return fmt.Errorf("wrong format") 256 | } 257 | 258 | if err = json.Unmarshal(i[0], &c.Op); err != nil { 259 | return err 260 | } 261 | if err = json.Unmarshal(i[1], &c.TID); err != nil { 262 | return err 263 | } 264 | if err = json.Unmarshal(i[2], &c.RecordID); err != nil { 265 | return err 266 | } 267 | switch c.Op { 268 | case recordInsert: 269 | if len(i) != 4 { 270 | return fmt.Errorf("wrong format") 271 | } 272 | if err = json.Unmarshal(i[3], &c.Data); err != nil { 273 | return err 274 | } 275 | case recordUpdate: 276 | if len(i) != 4 { 277 | return fmt.Errorf("wrong format") 278 | } 279 | if err = json.Unmarshal(i[3], &c.Ops); err != nil { 280 | return err 281 | } 282 | case recordDelete: 283 | if len(i) != 3 { 284 | return fmt.Errorf("wrong format") 285 | } 286 | default: 287 | return fmt.Errorf("wrong format") 288 | } 289 | return nil 290 | } 291 | 292 | // MarshalJSON returns the JSON encoding of c. 293 | func (c change) MarshalJSON() ([]byte, error) { 294 | switch c.Op { 295 | case recordInsert: 296 | return json.Marshal([]interface{}{recordInsert, c.TID, c.RecordID, c.Data}) 297 | case recordUpdate: 298 | return json.Marshal([]interface{}{recordUpdate, c.TID, c.RecordID, c.Ops}) 299 | case recordDelete: 300 | return json.Marshal([]interface{}{recordDelete, c.TID, c.RecordID}) 301 | } 302 | return nil, fmt.Errorf("could not marshal Change type") 303 | } 304 | -------------------------------------------------------------------------------- /datastores_requests.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "crypto/rand" 30 | "crypto/sha256" 31 | "encoding/json" 32 | "fmt" 33 | "io" 34 | "net/url" 35 | "strconv" 36 | "time" 37 | ) 38 | 39 | type row struct { 40 | TID string `json:"tid"` 41 | RowID string `json:"rowid"` 42 | Data Fields `json:"data"` 43 | } 44 | 45 | type infoDict struct { 46 | Title string `json:"title"` 47 | MTime struct { 48 | Time DBTime `json:"T"` 49 | } `json:"mtime"` 50 | } 51 | 52 | type datastoreInfo struct { 53 | ID string `json:"dsid"` 54 | Handle string `json:"handle"` 55 | Revision int `json:"rev"` 56 | Info infoDict `json:"info"` 57 | } 58 | 59 | func (db *Dropbox) openOrCreateDatastore(dsID string) (int, string, bool, error) { 60 | var r struct { 61 | Revision int `json:"rev"` 62 | Handle string `json:"handle"` 63 | Created bool `json:"created"` 64 | } 65 | 66 | err := db.doRequest("POST", "datastores/get_or_create_datastore", &url.Values{"dsid": {dsID}}, &r) 67 | return r.Revision, r.Handle, r.Created, err 68 | } 69 | 70 | func (db *Dropbox) listDatastores() ([]DatastoreInfo, string, error) { 71 | var rv []DatastoreInfo 72 | 73 | var dl struct { 74 | Info []datastoreInfo `json:"datastores"` 75 | Token string `json:"token"` 76 | } 77 | 78 | if err := db.doRequest("GET", "datastores/list_datastores", nil, &dl); err != nil { 79 | return nil, "", err 80 | } 81 | rv = make([]DatastoreInfo, len(dl.Info)) 82 | for i, di := range dl.Info { 83 | rv[i] = DatastoreInfo{ 84 | ID: di.ID, 85 | handle: di.Handle, 86 | revision: di.Revision, 87 | title: di.Info.Title, 88 | mtime: time.Time(di.Info.MTime.Time), 89 | } 90 | } 91 | return rv, dl.Token, nil 92 | } 93 | 94 | func (db *Dropbox) deleteDatastore(handle string) (*string, error) { 95 | var r struct { 96 | NotFound string `json:"notfound"` 97 | OK string `json:"ok"` 98 | } 99 | 100 | if err := db.doRequest("POST", "datastores/delete_datastore", &url.Values{"handle": {handle}}, &r); err != nil { 101 | return nil, err 102 | } 103 | if len(r.NotFound) != 0 { 104 | return nil, fmt.Errorf(r.NotFound) 105 | } 106 | return &r.OK, nil 107 | } 108 | 109 | func generateDatastoreID() (string, error) { 110 | var b []byte 111 | var blen int 112 | 113 | b = make([]byte, 1) 114 | _, err := io.ReadFull(rand.Reader, b) 115 | if err != nil { 116 | return "", err 117 | } 118 | blen = (int(b[0]) % maxGlobalIDLength) + 1 119 | b = make([]byte, blen) 120 | _, err = io.ReadFull(rand.Reader, b) 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | return encodeDBase64(b), nil 126 | } 127 | 128 | func (db *Dropbox) createDatastore(key string) (int, string, bool, error) { 129 | var r struct { 130 | Revision int `json:"rev"` 131 | Handle string `json:"handle"` 132 | Created bool `json:"created"` 133 | NotFound string `json:"notfound"` 134 | } 135 | var b64key string 136 | var err error 137 | 138 | if len(key) != 0 { 139 | b64key = encodeDBase64([]byte(key)) 140 | } else { 141 | b64key, err = generateDatastoreID() 142 | if err != nil { 143 | return 0, "", false, err 144 | } 145 | } 146 | hash := sha256.New() 147 | hash.Write([]byte(b64key)) 148 | rhash := hash.Sum(nil) 149 | dsID := "." + encodeDBase64(rhash[:]) 150 | 151 | params := &url.Values{ 152 | "key": {b64key}, 153 | "dsid": {dsID}, 154 | } 155 | if err := db.doRequest("POST", "datastores/create_datastore", params, &r); err != nil { 156 | return 0, "", false, err 157 | } 158 | if len(r.NotFound) != 0 { 159 | return 0, "", false, fmt.Errorf("%s", r.NotFound) 160 | } 161 | return r.Revision, r.Handle, r.Created, nil 162 | } 163 | 164 | func (db *Dropbox) putDelta(handle string, rev int, changes listOfChanges) (int, error) { 165 | var r struct { 166 | Revision int `json:"rev"` 167 | NotFound string `json:"notfound"` 168 | Conflict string `json:"conflict"` 169 | Error string `json:"error"` 170 | } 171 | var js []byte 172 | var err error 173 | 174 | if len(changes) == 0 { 175 | return rev, nil 176 | } 177 | 178 | if js, err = json.Marshal(changes); err != nil { 179 | return 0, err 180 | } 181 | 182 | params := &url.Values{ 183 | "handle": {handle}, 184 | "rev": {strconv.FormatInt(int64(rev), 10)}, 185 | "changes": {string(js)}, 186 | } 187 | 188 | if err = db.doRequest("POST", "datastores/put_delta", params, &r); err != nil { 189 | return 0, err 190 | } 191 | if len(r.NotFound) != 0 { 192 | return 0, fmt.Errorf("%s", r.NotFound) 193 | } 194 | if len(r.Conflict) != 0 { 195 | return 0, fmt.Errorf("%s", r.Conflict) 196 | } 197 | if len(r.Error) != 0 { 198 | return 0, fmt.Errorf("%s", r.Error) 199 | } 200 | return r.Revision, nil 201 | } 202 | 203 | func (db *Dropbox) getDelta(handle string, rev int) ([]datastoreDelta, error) { 204 | var rv struct { 205 | Deltas []datastoreDelta `json:"deltas"` 206 | NotFound string `json:"notfound"` 207 | } 208 | err := db.doRequest("GET", "datastores/get_deltas", 209 | &url.Values{ 210 | "handle": {handle}, 211 | "rev": {strconv.FormatInt(int64(rev), 10)}, 212 | }, &rv) 213 | 214 | if len(rv.NotFound) != 0 { 215 | return nil, fmt.Errorf("%s", rv.NotFound) 216 | } 217 | return rv.Deltas, err 218 | } 219 | 220 | func (db *Dropbox) getSnapshot(handle string) ([]row, int, error) { 221 | var r struct { 222 | Rows []row `json:"rows"` 223 | Revision int `json:"rev"` 224 | NotFound string `json:"notfound"` 225 | } 226 | 227 | if err := db.doRequest("GET", "datastores/get_snapshot", 228 | &url.Values{"handle": {handle}}, &r); err != nil { 229 | return nil, 0, err 230 | } 231 | if len(r.NotFound) != 0 { 232 | return nil, 0, fmt.Errorf("%s", r.NotFound) 233 | } 234 | return r.Rows, r.Revision, nil 235 | } 236 | 237 | func (db *Dropbox) await(cursors []*Datastore, token string) (string, []DatastoreInfo, map[string][]datastoreDelta, error) { 238 | var params *url.Values 239 | var dis []DatastoreInfo 240 | var dd map[string][]datastoreDelta 241 | 242 | type awaitResult struct { 243 | Deltas struct { 244 | Results map[string]struct { 245 | Deltas []datastoreDelta `json:"deltas"` 246 | NotFound string `json:"notfound"` 247 | } `json:"deltas"` 248 | } `json:"get_deltas"` 249 | Datastores struct { 250 | Info []datastoreInfo `json:"datastores"` 251 | Token string `json:"token"` 252 | } `json:"list_datastores"` 253 | } 254 | var r awaitResult 255 | if len(token) == 0 && len(cursors) == 0 { 256 | return "", nil, nil, fmt.Errorf("at least one parameter required") 257 | } 258 | params = &url.Values{} 259 | if len(token) != 0 { 260 | js, err := json.Marshal(map[string]string{"token": token}) 261 | if err != nil { 262 | return "", nil, nil, err 263 | } 264 | params.Set("list_datastores", string(js)) 265 | } 266 | if len(cursors) != 0 { 267 | m := make(map[string]int) 268 | for _, ds := range cursors { 269 | m[ds.info.handle] = ds.info.revision 270 | } 271 | js, err := json.Marshal(map[string]map[string]int{"cursors": m}) 272 | if err != nil { 273 | return "", nil, nil, err 274 | } 275 | params.Set("get_deltas", string(js)) 276 | } 277 | if err := db.doRequest("GET", "datastores/await", params, &r); err != nil { 278 | return "", nil, nil, err 279 | } 280 | if len(r.Deltas.Results) == 0 && len(r.Datastores.Info) == 0 { 281 | return token, nil, nil, fmt.Errorf("await timed out") 282 | } 283 | if len(r.Datastores.Token) != 0 { 284 | token = r.Datastores.Token 285 | } 286 | if len(r.Deltas.Results) != 0 { 287 | dd = make(map[string][]datastoreDelta) 288 | for k, v := range r.Deltas.Results { 289 | dd[k] = v.Deltas 290 | } 291 | } 292 | if len(r.Datastores.Info) != 0 { 293 | dis = make([]DatastoreInfo, len(r.Datastores.Info)) 294 | for i, di := range r.Datastores.Info { 295 | dis[i] = DatastoreInfo{ 296 | ID: di.ID, 297 | handle: di.Handle, 298 | revision: di.Revision, 299 | title: di.Info.Title, 300 | mtime: time.Time(di.Info.MTime.Time), 301 | } 302 | } 303 | } 304 | return token, dis, dd, nil 305 | } 306 | -------------------------------------------------------------------------------- /datastores_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "encoding/json" 30 | "testing" 31 | ) 32 | 33 | func checkList(t *testing.T, l *List, e []interface{}) { 34 | var elt1 interface{} 35 | var err error 36 | 37 | if l.Size() != len(e) { 38 | t.Errorf("wrong size") 39 | } 40 | for i := range e { 41 | if elt1, err = l.Get(i); err != nil { 42 | t.Errorf("%s", err) 43 | } 44 | if elt1 != e[i] { 45 | t.Errorf("position %d mismatch got %#v, expected %#v", i, elt1, e[i]) 46 | } 47 | } 48 | } 49 | 50 | func newDatastore(t *testing.T) *Datastore { 51 | var ds *Datastore 52 | 53 | ds = &Datastore{ 54 | manager: newDropbox(t).NewDatastoreManager(), 55 | info: DatastoreInfo{ 56 | ID: "dummyID", 57 | handle: "dummyHandle", 58 | title: "dummyTitle", 59 | revision: 0, 60 | }, 61 | tables: make(map[string]*Table), 62 | changesQueue: make(chan changeWork), 63 | } 64 | go ds.doHandleChange() 65 | return ds 66 | } 67 | 68 | func TestList(t *testing.T) { 69 | var tbl *Table 70 | var r *Record 71 | var ds *Datastore 72 | var l *List 73 | var err error 74 | 75 | ds = newDatastore(t) 76 | 77 | if tbl, err = ds.GetTable("dummyTable"); err != nil { 78 | t.Errorf("%s", err) 79 | } 80 | if r, err = tbl.GetOrInsert("dummyRecord"); err != nil { 81 | t.Errorf("%s", err) 82 | } 83 | if l, err = r.GetOrCreateList("dummyList"); err != nil { 84 | t.Errorf("%s", err) 85 | } 86 | for i := 0; i < 10; i++ { 87 | if err = l.Add(i); err != nil { 88 | t.Errorf("%s", err) 89 | } 90 | } 91 | if ftype, err := r.GetFieldType("dummyList"); err != nil || ftype != TypeList { 92 | t.Errorf("wrong type") 93 | } 94 | 95 | ftype, err := l.GetType(0) 96 | if err != nil { 97 | t.Errorf("%s", err) 98 | } 99 | if ftype != TypeInteger { 100 | t.Errorf("wrong type") 101 | } 102 | 103 | checkList(t, l, []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 104 | 105 | if err = l.Remove(5); err != nil { 106 | t.Errorf("could not remove element 5") 107 | } 108 | checkList(t, l, []interface{}{0, 1, 2, 3, 4, 6, 7, 8, 9}) 109 | 110 | if err = l.Remove(0); err != nil { 111 | t.Errorf("could not remove element 0") 112 | } 113 | checkList(t, l, []interface{}{1, 2, 3, 4, 6, 7, 8, 9}) 114 | 115 | if err = l.Remove(7); err != nil { 116 | t.Errorf("could not remove element 7") 117 | } 118 | checkList(t, l, []interface{}{1, 2, 3, 4, 6, 7, 8}) 119 | 120 | if err = l.Remove(7); err == nil { 121 | t.Errorf("out of bound index must return an error") 122 | } 123 | checkList(t, l, []interface{}{1, 2, 3, 4, 6, 7, 8}) 124 | 125 | if err = l.Move(3, 6); err != nil { 126 | t.Errorf("could not move element 3 to position 6") 127 | } 128 | checkList(t, l, []interface{}{1, 2, 3, 6, 7, 8, 4}) 129 | 130 | if err = l.Move(3, 9); err == nil { 131 | t.Errorf("out of bound index must return an error") 132 | } 133 | checkList(t, l, []interface{}{1, 2, 3, 6, 7, 8, 4}) 134 | 135 | if err = l.Move(6, 3); err != nil { 136 | t.Errorf("could not move element 6 to position 3") 137 | } 138 | checkList(t, l, []interface{}{1, 2, 3, 4, 6, 7, 8}) 139 | 140 | if err = l.AddAtPos(0, 0); err != nil { 141 | t.Errorf("could not insert element at position 0") 142 | } 143 | checkList(t, l, []interface{}{0, 1, 2, 3, 4, 6, 7, 8}) 144 | 145 | if err = l.Add(9); err != nil { 146 | t.Errorf("could not append element") 147 | } 148 | checkList(t, l, []interface{}{0, 1, 2, 3, 4, 6, 7, 8, 9}) 149 | 150 | if err = l.AddAtPos(5, 5); err != nil { 151 | t.Errorf("could not insert element at position 5") 152 | } 153 | checkList(t, l, []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 154 | 155 | if err = l.Set(0, 3); err != nil { 156 | t.Errorf("could not update element at position 0") 157 | } 158 | checkList(t, l, []interface{}{3, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 159 | 160 | if err = l.Set(9, 2); err != nil { 161 | t.Errorf("could not update element at position 9") 162 | } 163 | checkList(t, l, []interface{}{3, 1, 2, 3, 4, 5, 6, 7, 8, 2}) 164 | 165 | if err = l.Set(10, 11); err == nil { 166 | t.Errorf("out of bound index must return an error") 167 | } 168 | checkList(t, l, []interface{}{3, 1, 2, 3, 4, 5, 6, 7, 8, 2}) 169 | } 170 | 171 | func TestGenerateID(t *testing.T) { 172 | f, err := generateDatastoreID() 173 | if err != nil { 174 | t.Errorf("%s", err) 175 | } 176 | if !isValidDatastoreID(f) { 177 | t.Errorf("generated ID is not correct") 178 | } 179 | } 180 | 181 | func TestUnmarshalAwait(t *testing.T) { 182 | type awaitResult struct { 183 | Deltas struct { 184 | Results map[string]struct { 185 | Deltas []datastoreDelta `json:"deltas"` 186 | } `json:"deltas"` 187 | } `json:"get_deltas"` 188 | Datastores struct { 189 | Info []datastoreInfo `json:"datastores"` 190 | Token string `json:"token"` 191 | } `json:"list_datastores"` 192 | } 193 | var r awaitResult 194 | var datastoreID string 195 | var res []datastoreDelta 196 | 197 | js := `{"get_deltas":{"deltas":{"12345678901234567890":{"deltas":[{"changes":[["I","dummyTable","dummyRecord",{}]],"nonce":"","rev":0},{"changes":[["U","dummyTable","dummyRecord",{"name":["P","dummy"]}],["U","dummyTable","dummyRecord",{"dummyList":["LC"]}]],"nonce":"","rev":1},{"changes":[["U","dummyTable","dummyRecord",{"dummyList":["LI",0,{"I":"0"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",1,{"I":"1"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",2,{"I":"2"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",3,{"I":"3"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",4,{"I":"4"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",5,{"I":"5"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",6,{"I":"6"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",7,{"I":"7"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",8,{"I":"8"}]}],["U","dummyTable","dummyRecord",{"dummyList":["LI",9,{"I":"9"}]}]],"nonce":"","rev":2},{"changes":[["D","dummyTable","dummyRecord"]],"nonce":"","rev":3}]}}}}` 198 | datastoreID = "12345678901234567890" 199 | 200 | expected := []datastoreDelta{ 201 | datastoreDelta{ 202 | Revision: 0, 203 | Changes: listOfChanges{ 204 | &change{Op: recordInsert, TID: "dummyTable", RecordID: "dummyRecord", Data: Fields{}}, 205 | }, 206 | }, 207 | datastoreDelta{ 208 | Revision: 1, 209 | Changes: listOfChanges{ 210 | &change{Op: "U", TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"name": fieldOp{Op: "P", Index: 0, Data: value{values: []interface{}{"dummy"}}}}}, 211 | &change{Op: "U", TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: "LC"}}}, 212 | }, 213 | }, 214 | datastoreDelta{ 215 | Revision: 2, 216 | Changes: listOfChanges{ 217 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 0, Data: value{values: []interface{}{int64(0)}}}}}, 218 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 1, Data: value{values: []interface{}{int64(1)}}}}}, 219 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 2, Data: value{values: []interface{}{int64(2)}}}}}, 220 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 3, Data: value{values: []interface{}{int64(3)}}}}}, 221 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 4, Data: value{values: []interface{}{int64(4)}}}}}, 222 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 5, Data: value{values: []interface{}{int64(5)}}}}}, 223 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 6, Data: value{values: []interface{}{int64(6)}}}}}, 224 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 7, Data: value{values: []interface{}{int64(7)}}}}}, 225 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 8, Data: value{values: []interface{}{int64(8)}}}}}, 226 | &change{Op: recordUpdate, TID: "dummyTable", RecordID: "dummyRecord", Ops: opDict{"dummyList": fieldOp{Op: listInsert, Index: 9, Data: value{values: []interface{}{int64(9)}}}}}, 227 | }, 228 | }, 229 | datastoreDelta{ 230 | Revision: 3, 231 | Changes: listOfChanges{ 232 | &change{Op: "D", TID: "dummyTable", RecordID: "dummyRecord"}, 233 | }, 234 | }, 235 | } 236 | err := json.Unmarshal([]byte(js), &r) 237 | if err != nil { 238 | t.Errorf("%s", err) 239 | } 240 | if len(r.Deltas.Results) != 1 { 241 | t.Errorf("wrong number of datastoreDelta") 242 | } 243 | 244 | if tmp, ok := r.Deltas.Results[datastoreID]; !ok { 245 | t.Fatalf("wrong datastore ID") 246 | } else { 247 | res = tmp.Deltas 248 | } 249 | if len(res) != len(expected) { 250 | t.Fatalf("got %d results expected %d", len(res), len(expected)) 251 | } 252 | for i, d := range res { 253 | ed := expected[i] 254 | if d.Revision != ed.Revision { 255 | t.Errorf("wrong revision got %d expected %d", d.Revision, expected[i].Revision) 256 | } 257 | for j, c := range d.Changes { 258 | if !c.equals(ed.Changes[j]) { 259 | t.Errorf("wrong change: got: %+v expected: %+v", *c, *ed.Changes[j]) 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /datastores_changes.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "fmt" 30 | "reflect" 31 | ) 32 | 33 | type value struct { 34 | values []interface{} 35 | isList bool 36 | } 37 | 38 | type fieldOp struct { 39 | Op string 40 | Index int 41 | Index2 int 42 | Data value 43 | } 44 | 45 | type opDict map[string]fieldOp 46 | 47 | type change struct { 48 | Op string 49 | TID string 50 | RecordID string 51 | Ops opDict 52 | Data Fields 53 | Revert *change 54 | } 55 | type listOfChanges []*change 56 | 57 | type changeWork struct { 58 | c *change 59 | out chan error 60 | } 61 | 62 | const ( 63 | recordDelete = "D" 64 | recordInsert = "I" 65 | recordUpdate = "U" 66 | fieldDelete = "D" 67 | fieldPut = "P" 68 | listCreate = "LC" 69 | listDelete = "LD" 70 | listInsert = "LI" 71 | listMove = "LM" 72 | listPut = "LP" 73 | ) 74 | 75 | func newValueFromInterface(i interface{}) *value { 76 | if a, ok := i.([]byte); ok { 77 | return &value{ 78 | values: []interface{}{a}, 79 | isList: false, 80 | } 81 | } 82 | if reflect.TypeOf(i).Kind() == reflect.Slice || reflect.TypeOf(i).Kind() == reflect.Array { 83 | val := reflect.ValueOf(i) 84 | v := &value{ 85 | values: make([]interface{}, val.Len()), 86 | isList: true, 87 | } 88 | for i := range v.values { 89 | v.values[i] = val.Index(i).Interface() 90 | } 91 | return v 92 | } 93 | return &value{ 94 | values: []interface{}{i}, 95 | isList: false, 96 | } 97 | } 98 | 99 | func newValue(v *value) *value { 100 | var nv *value 101 | 102 | nv = &value{ 103 | values: make([]interface{}, len(v.values)), 104 | isList: v.isList, 105 | } 106 | copy(nv.values, v.values) 107 | return nv 108 | } 109 | 110 | func newFields(f Fields) Fields { 111 | var n Fields 112 | 113 | n = make(Fields) 114 | for k, v := range f { 115 | n[k] = *newValue(&v) 116 | } 117 | return n 118 | } 119 | 120 | func (ds *Datastore) deleteRecord(table, record string) error { 121 | return ds.handleChange(&change{ 122 | Op: recordDelete, 123 | TID: table, 124 | RecordID: record, 125 | }) 126 | } 127 | 128 | func (ds *Datastore) insertRecord(table, record string, values Fields) error { 129 | return ds.handleChange(&change{ 130 | Op: recordInsert, 131 | TID: table, 132 | RecordID: record, 133 | Data: newFields(values), 134 | }) 135 | } 136 | 137 | func (ds *Datastore) updateFields(table, record string, values map[string]interface{}) error { 138 | var dsval opDict 139 | 140 | dsval = make(opDict) 141 | for k, v := range values { 142 | dsval[k] = fieldOp{ 143 | Op: fieldPut, 144 | Data: *newValueFromInterface(v), 145 | } 146 | } 147 | return ds.handleChange(&change{ 148 | Op: recordUpdate, 149 | TID: table, 150 | RecordID: record, 151 | Ops: dsval, 152 | }) 153 | } 154 | 155 | func (ds *Datastore) updateField(table, record, field string, i interface{}) error { 156 | return ds.updateFields(table, record, map[string]interface{}{field: i}) 157 | } 158 | 159 | func (ds *Datastore) deleteField(table, record, field string) error { 160 | return ds.handleChange(&change{ 161 | Op: recordUpdate, 162 | TID: table, 163 | RecordID: record, 164 | Ops: opDict{ 165 | field: fieldOp{ 166 | Op: fieldDelete, 167 | }, 168 | }, 169 | }) 170 | } 171 | 172 | func (ds *Datastore) listCreate(table, record, field string) error { 173 | return ds.handleChange(&change{ 174 | Op: recordUpdate, 175 | TID: table, 176 | RecordID: record, 177 | Ops: opDict{ 178 | field: fieldOp{ 179 | Op: listCreate, 180 | }, 181 | }, 182 | }) 183 | } 184 | 185 | func (ds *Datastore) listDelete(table, record, field string, pos int) error { 186 | return ds.handleChange(&change{ 187 | Op: recordUpdate, 188 | TID: table, 189 | RecordID: record, 190 | Ops: opDict{ 191 | field: fieldOp{ 192 | Op: listDelete, 193 | Index: pos, 194 | }, 195 | }, 196 | }) 197 | } 198 | 199 | func (ds *Datastore) listInsert(table, record, field string, pos int, i interface{}) error { 200 | return ds.handleChange(&change{ 201 | Op: recordUpdate, 202 | TID: table, 203 | RecordID: record, 204 | Ops: opDict{ 205 | field: fieldOp{ 206 | Op: listInsert, 207 | Index: pos, 208 | Data: *newValueFromInterface(i), 209 | }, 210 | }, 211 | }) 212 | } 213 | 214 | func (ds *Datastore) listMove(table, record, field string, from, to int) error { 215 | return ds.handleChange(&change{ 216 | Op: recordUpdate, 217 | TID: table, 218 | RecordID: record, 219 | Ops: opDict{ 220 | field: fieldOp{ 221 | Op: listMove, 222 | Index: from, 223 | Index2: to, 224 | }, 225 | }, 226 | }) 227 | } 228 | 229 | func (ds *Datastore) listPut(table, record, field string, pos int, i interface{}) error { 230 | return ds.handleChange(&change{ 231 | Op: recordUpdate, 232 | TID: table, 233 | RecordID: record, 234 | Ops: opDict{ 235 | field: fieldOp{ 236 | Op: listPut, 237 | Index: pos, 238 | Data: *newValueFromInterface(i), 239 | }, 240 | }, 241 | }) 242 | } 243 | 244 | func (ds *Datastore) handleChange(c *change) error { 245 | var out chan error 246 | 247 | if ds.changesQueue == nil { 248 | return fmt.Errorf("datastore is closed") 249 | } 250 | out = make(chan error) 251 | ds.changesQueue <- changeWork{ 252 | c: c, 253 | out: out, 254 | } 255 | return <-out 256 | } 257 | 258 | func (ds *Datastore) doHandleChange() { 259 | var err error 260 | var c *change 261 | 262 | q := ds.changesQueue 263 | for cw := range q { 264 | c = cw.c 265 | 266 | if err = ds.validateChange(c); err != nil { 267 | cw.out <- err 268 | continue 269 | } 270 | if c.Revert, err = ds.inverseChange(c); err != nil { 271 | cw.out <- err 272 | continue 273 | } 274 | 275 | if err = ds.applyChange(c); err != nil { 276 | cw.out <- err 277 | continue 278 | } 279 | 280 | ds.changes = append(ds.changes, c) 281 | 282 | if ds.autoCommit { 283 | if err = ds.Commit(); err != nil { 284 | cw.out <- err 285 | } 286 | } 287 | close(cw.out) 288 | } 289 | } 290 | 291 | func (ds *Datastore) validateChange(c *change) error { 292 | var t *Table 293 | var r *Record 294 | var ok bool 295 | 296 | if t, ok = ds.tables[c.TID]; !ok { 297 | t = &Table{ 298 | datastore: ds, 299 | tableID: c.TID, 300 | records: make(map[string]*Record), 301 | } 302 | } 303 | 304 | r = t.records[c.RecordID] 305 | 306 | switch c.Op { 307 | case recordInsert, recordDelete: 308 | return nil 309 | case recordUpdate: 310 | if r == nil { 311 | return fmt.Errorf("no such record: %s", c.RecordID) 312 | } 313 | for field, op := range c.Ops { 314 | if op.Op == fieldPut || op.Op == fieldDelete { 315 | continue 316 | } 317 | v, ok := r.fields[field] 318 | if op.Op == listCreate { 319 | if ok { 320 | return fmt.Errorf("field %s already exists", field) 321 | } 322 | continue 323 | } 324 | if !ok { 325 | return fmt.Errorf("no such field: %s", field) 326 | } 327 | if !v.isList { 328 | return fmt.Errorf("field %s is not a list", field) 329 | } 330 | maxIndex := len(v.values) - 1 331 | if op.Op == listInsert { 332 | maxIndex++ 333 | } 334 | if op.Index > maxIndex { 335 | return fmt.Errorf("out of bound access index %d on [0:%d]", op.Index, maxIndex) 336 | } 337 | if op.Index2 > maxIndex { 338 | return fmt.Errorf("out of bound access index %d on [0:%d]", op.Index, maxIndex) 339 | } 340 | } 341 | } 342 | return nil 343 | } 344 | 345 | func (ds *Datastore) applyChange(c *change) error { 346 | var t *Table 347 | var r *Record 348 | var ok bool 349 | 350 | if t, ok = ds.tables[c.TID]; !ok { 351 | t = &Table{ 352 | datastore: ds, 353 | tableID: c.TID, 354 | records: make(map[string]*Record), 355 | } 356 | ds.tables[c.TID] = t 357 | } 358 | 359 | r = t.records[c.RecordID] 360 | 361 | switch c.Op { 362 | case recordInsert: 363 | t.records[c.RecordID] = &Record{ 364 | table: t, 365 | recordID: c.RecordID, 366 | fields: newFields(c.Data), 367 | } 368 | case recordDelete: 369 | if r == nil { 370 | return nil 371 | } 372 | r.isDeleted = true 373 | delete(t.records, c.RecordID) 374 | case recordUpdate: 375 | for field, op := range c.Ops { 376 | v, ok := r.fields[field] 377 | switch op.Op { 378 | case fieldPut: 379 | r.fields[field] = *newValue(&op.Data) 380 | case fieldDelete: 381 | if ok { 382 | delete(r.fields, field) 383 | } 384 | case listCreate: 385 | if !ok { 386 | r.fields[field] = value{isList: true} 387 | } 388 | case listDelete: 389 | copy(v.values[op.Index:], v.values[op.Index+1:]) 390 | v.values = v.values[:len(v.values)-1] 391 | r.fields[field] = v 392 | case listInsert: 393 | v.values = append(v.values, op.Data) 394 | copy(v.values[op.Index+1:], v.values[op.Index:len(v.values)-1]) 395 | v.values[op.Index] = op.Data.values[0] 396 | r.fields[field] = v 397 | case listMove: 398 | val := v.values[op.Index] 399 | if op.Index < op.Index2 { 400 | copy(v.values[op.Index:op.Index2], v.values[op.Index+1:op.Index2+1]) 401 | } else { 402 | copy(v.values[op.Index2+1:op.Index+1], v.values[op.Index2:op.Index]) 403 | } 404 | v.values[op.Index2] = val 405 | r.fields[field] = v 406 | case listPut: 407 | r.fields[field].values[op.Index] = op.Data.values[0] 408 | } 409 | } 410 | } 411 | return nil 412 | } 413 | 414 | func (ds *Datastore) inverseChange(c *change) (*change, error) { 415 | var t *Table 416 | var r *Record 417 | var ok bool 418 | var rev *change 419 | 420 | if t, ok = ds.tables[c.TID]; !ok { 421 | t = &Table{ 422 | datastore: ds, 423 | tableID: c.TID, 424 | records: make(map[string]*Record), 425 | } 426 | ds.tables[c.TID] = t 427 | } 428 | 429 | r = t.records[c.RecordID] 430 | 431 | switch c.Op { 432 | case recordInsert: 433 | return &change{ 434 | Op: recordDelete, 435 | TID: c.TID, 436 | RecordID: c.RecordID, 437 | }, nil 438 | case recordDelete: 439 | if r == nil { 440 | return nil, nil 441 | } 442 | return &change{ 443 | Op: recordInsert, 444 | TID: c.TID, 445 | RecordID: c.RecordID, 446 | Data: newFields(r.fields), 447 | }, nil 448 | case recordUpdate: 449 | rev = &change{ 450 | Op: recordUpdate, 451 | TID: c.TID, 452 | RecordID: c.RecordID, 453 | Ops: make(opDict), 454 | } 455 | for field, op := range c.Ops { 456 | switch op.Op { 457 | case fieldPut: 458 | if v, ok := r.fields[field]; ok { 459 | rev.Ops[field] = fieldOp{ 460 | Op: fieldPut, 461 | Data: *newValue(&v), 462 | } 463 | } else { 464 | rev.Ops[field] = fieldOp{ 465 | Op: fieldDelete, 466 | } 467 | } 468 | case fieldDelete: 469 | if v, ok := r.fields[field]; ok { 470 | rev.Ops[field] = fieldOp{ 471 | Op: fieldPut, 472 | Data: *newValue(&v), 473 | } 474 | } 475 | case listCreate: 476 | if _, ok := r.fields[field]; !ok { 477 | rev.Ops[field] = fieldOp{ 478 | Op: fieldDelete, 479 | } 480 | } 481 | case listDelete: 482 | v := r.fields[field] 483 | rev.Ops[field] = fieldOp{ 484 | Op: listInsert, 485 | Index: op.Index, 486 | Data: value{ 487 | values: []interface{}{v.values[op.Index]}, 488 | isList: false, 489 | }, 490 | } 491 | case listInsert: 492 | rev.Ops[field] = fieldOp{ 493 | Op: listDelete, 494 | Index: op.Index, 495 | } 496 | case listMove: 497 | rev.Ops[field] = fieldOp{ 498 | Op: listMove, 499 | Index: op.Index2, 500 | Index2: op.Index, 501 | } 502 | case listPut: 503 | v := r.fields[field] 504 | rev.Ops[field] = fieldOp{ 505 | Op: listPut, 506 | Index: op.Index, 507 | Data: value{ 508 | values: []interface{}{v.values[op.Index]}, 509 | isList: false, 510 | }, 511 | } 512 | } 513 | } 514 | } 515 | return rev, nil 516 | } 517 | -------------------------------------------------------------------------------- /datastores.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "fmt" 30 | "reflect" 31 | "regexp" 32 | "time" 33 | ) 34 | 35 | // List represents a value of type list. 36 | type List struct { 37 | record *Record 38 | field string 39 | values []interface{} 40 | } 41 | 42 | // Fields represents a record. 43 | type Fields map[string]value 44 | 45 | // Record represents an entry in a table. 46 | type Record struct { 47 | table *Table 48 | recordID string 49 | fields Fields 50 | isDeleted bool 51 | } 52 | 53 | // Table represents a list of records. 54 | type Table struct { 55 | datastore *Datastore 56 | tableID string 57 | records map[string]*Record 58 | } 59 | 60 | // DatastoreInfo represents the information about a datastore. 61 | type DatastoreInfo struct { 62 | ID string 63 | handle string 64 | revision int 65 | title string 66 | mtime time.Time 67 | } 68 | 69 | type datastoreDelta struct { 70 | Revision int `json:"rev"` 71 | Changes listOfChanges `json:"changes"` 72 | Nonce *string `json:"nonce"` 73 | } 74 | 75 | type listOfDelta []datastoreDelta 76 | 77 | // Datastore represents a datastore. 78 | type Datastore struct { 79 | manager *DatastoreManager 80 | info DatastoreInfo 81 | changes listOfChanges 82 | tables map[string]*Table 83 | isDeleted bool 84 | autoCommit bool 85 | changesQueue chan changeWork 86 | } 87 | 88 | // DatastoreManager represents all datastores linked to the current account. 89 | type DatastoreManager struct { 90 | dropbox *Dropbox 91 | datastores []*Datastore 92 | token string 93 | } 94 | 95 | const ( 96 | defaultDatastoreID = "default" 97 | maxGlobalIDLength = 63 98 | maxIDLength = 64 99 | 100 | localIDPattern = `[a-z0-9_-]([a-z0-9._-]{0,62}[a-z0-9_-])?` 101 | globalIDPattern = `.[A-Za-z0-9_-]{1,63}` 102 | fieldsIDPattern = `[A-Za-z0-9._+/=-]{1,64}` 103 | fieldsSpecialIDPattern = `:[A-Za-z0-9._+/=-]{1,63}` 104 | ) 105 | 106 | var ( 107 | localIDRegexp *regexp.Regexp 108 | globalIDRegexp *regexp.Regexp 109 | fieldsIDRegexp *regexp.Regexp 110 | fieldsSpecialIDRegexp *regexp.Regexp 111 | ) 112 | 113 | func init() { 114 | var err error 115 | if localIDRegexp, err = regexp.Compile(localIDPattern); err != nil { 116 | fmt.Println(err) 117 | } 118 | if globalIDRegexp, err = regexp.Compile(globalIDPattern); err != nil { 119 | fmt.Println(err) 120 | } 121 | if fieldsIDRegexp, err = regexp.Compile(fieldsIDPattern); err != nil { 122 | fmt.Println(err) 123 | } 124 | if fieldsSpecialIDRegexp, err = regexp.Compile(fieldsSpecialIDPattern); err != nil { 125 | fmt.Println(err) 126 | } 127 | } 128 | 129 | func isValidDatastoreID(ID string) bool { 130 | if ID[0] == '.' { 131 | return globalIDRegexp.MatchString(ID) 132 | } 133 | return localIDRegexp.MatchString(ID) 134 | } 135 | 136 | func isValidID(ID string) bool { 137 | if ID[0] == ':' { 138 | return fieldsSpecialIDRegexp.MatchString(ID) 139 | } 140 | return fieldsIDRegexp.MatchString(ID) 141 | } 142 | 143 | const ( 144 | // TypeBoolean is the returned type when the value is a bool 145 | TypeBoolean AtomType = iota 146 | // TypeInteger is the returned type when the value is an int 147 | TypeInteger 148 | // TypeDouble is the returned type when the value is a float 149 | TypeDouble 150 | // TypeString is the returned type when the value is a string 151 | TypeString 152 | // TypeBytes is the returned type when the value is a []byte 153 | TypeBytes 154 | // TypeDate is the returned type when the value is a Date 155 | TypeDate 156 | // TypeList is the returned type when the value is a List 157 | TypeList 158 | ) 159 | 160 | // AtomType represents the type of the value. 161 | type AtomType int 162 | 163 | // NewDatastoreManager returns a new DatastoreManager linked to the current account. 164 | func (db *Dropbox) NewDatastoreManager() *DatastoreManager { 165 | return &DatastoreManager{ 166 | dropbox: db, 167 | } 168 | } 169 | 170 | // OpenDatastore opens or creates a datastore. 171 | func (dmgr *DatastoreManager) OpenDatastore(dsID string) (*Datastore, error) { 172 | rev, handle, _, err := dmgr.dropbox.openOrCreateDatastore(dsID) 173 | if err != nil { 174 | return nil, err 175 | } 176 | rv := &Datastore{ 177 | manager: dmgr, 178 | info: DatastoreInfo{ 179 | ID: dsID, 180 | handle: handle, 181 | revision: rev, 182 | }, 183 | tables: make(map[string]*Table), 184 | changesQueue: make(chan changeWork), 185 | } 186 | if rev > 0 { 187 | err = rv.LoadSnapshot() 188 | } 189 | go rv.doHandleChange() 190 | return rv, err 191 | } 192 | 193 | // OpenDefaultDatastore opens the default datastore. 194 | func (dmgr *DatastoreManager) OpenDefaultDatastore() (*Datastore, error) { 195 | return dmgr.OpenDatastore(defaultDatastoreID) 196 | } 197 | 198 | // ListDatastores lists all datastores. 199 | func (dmgr *DatastoreManager) ListDatastores() ([]DatastoreInfo, error) { 200 | info, _, err := dmgr.dropbox.listDatastores() 201 | return info, err 202 | } 203 | 204 | // DeleteDatastore deletes a datastore. 205 | func (dmgr *DatastoreManager) DeleteDatastore(dsID string) error { 206 | _, err := dmgr.dropbox.deleteDatastore(dsID) 207 | return err 208 | } 209 | 210 | // CreateDatastore creates a global datastore with a unique ID, empty string for a random key. 211 | func (dmgr *DatastoreManager) CreateDatastore(dsID string) (*Datastore, error) { 212 | rev, handle, _, err := dmgr.dropbox.createDatastore(dsID) 213 | if err != nil { 214 | return nil, err 215 | } 216 | return &Datastore{ 217 | manager: dmgr, 218 | info: DatastoreInfo{ 219 | ID: dsID, 220 | handle: handle, 221 | revision: rev, 222 | }, 223 | tables: make(map[string]*Table), 224 | changesQueue: make(chan changeWork), 225 | }, nil 226 | } 227 | 228 | // AwaitDeltas awaits for deltas and applies them. 229 | func (ds *Datastore) AwaitDeltas() error { 230 | if len(ds.changes) != 0 { 231 | return fmt.Errorf("changes already pending") 232 | } 233 | _, _, deltas, err := ds.manager.dropbox.await([]*Datastore{ds}, "") 234 | if err != nil { 235 | return err 236 | } 237 | changes, ok := deltas[ds.info.handle] 238 | if !ok || len(changes) == 0 { 239 | return nil 240 | } 241 | return ds.applyDelta(changes) 242 | } 243 | 244 | func (ds *Datastore) applyDelta(dds []datastoreDelta) error { 245 | if len(ds.changes) != 0 { 246 | return fmt.Errorf("changes already pending") 247 | } 248 | for _, d := range dds { 249 | if d.Revision < ds.info.revision { 250 | continue 251 | } 252 | for _, c := range d.Changes { 253 | ds.applyChange(c) 254 | } 255 | } 256 | return nil 257 | } 258 | 259 | // Close closes the datastore. 260 | func (ds *Datastore) Close() { 261 | close(ds.changesQueue) 262 | } 263 | 264 | // Delete deletes the datastore. 265 | func (ds *Datastore) Delete() error { 266 | return ds.manager.DeleteDatastore(ds.info.ID) 267 | } 268 | 269 | // SetTitle sets the datastore title to the given string. 270 | func (ds *Datastore) SetTitle(t string) error { 271 | if len(ds.info.title) == 0 { 272 | return ds.insertRecord(":info", "info", Fields{ 273 | "title": value{ 274 | values: []interface{}{t}, 275 | }, 276 | }) 277 | } 278 | return ds.updateField(":info", "info", "title", t) 279 | } 280 | 281 | // SetMTime sets the datastore mtime to the given time. 282 | func (ds *Datastore) SetMTime(t time.Time) error { 283 | if time.Time(ds.info.mtime).IsZero() { 284 | return ds.insertRecord(":info", "info", Fields{ 285 | "mtime": value{ 286 | values: []interface{}{t}, 287 | }, 288 | }) 289 | } 290 | return ds.updateField(":info", "info", "mtime", t) 291 | } 292 | 293 | // Rollback reverts all local changes and discards them. 294 | func (ds *Datastore) Rollback() error { 295 | if len(ds.changes) == 0 { 296 | return nil 297 | } 298 | for i := len(ds.changes) - 1; i >= 0; i-- { 299 | ds.applyChange(ds.changes[i].Revert) 300 | } 301 | ds.changes = ds.changes[:0] 302 | return nil 303 | } 304 | 305 | // GetTable returns the requested table. 306 | func (ds *Datastore) GetTable(tableID string) (*Table, error) { 307 | if !isValidID(tableID) { 308 | return nil, fmt.Errorf("invalid table ID %s", tableID) 309 | } 310 | t, ok := ds.tables[tableID] 311 | if ok { 312 | return t, nil 313 | } 314 | t = &Table{ 315 | datastore: ds, 316 | tableID: tableID, 317 | records: make(map[string]*Record), 318 | } 319 | ds.tables[tableID] = t 320 | return t, nil 321 | } 322 | 323 | // Commit commits the changes registered by sending them to the server. 324 | func (ds *Datastore) Commit() error { 325 | rev, err := ds.manager.dropbox.putDelta(ds.info.handle, ds.info.revision, ds.changes) 326 | if err != nil { 327 | return err 328 | } 329 | ds.changes = ds.changes[:0] 330 | ds.info.revision = rev 331 | return nil 332 | } 333 | 334 | // LoadSnapshot updates the state of the datastore from the server. 335 | func (ds *Datastore) LoadSnapshot() error { 336 | if len(ds.changes) != 0 { 337 | return fmt.Errorf("could not load snapshot when there are pending changes") 338 | } 339 | rows, rev, err := ds.manager.dropbox.getSnapshot(ds.info.handle) 340 | if err != nil { 341 | return err 342 | } 343 | 344 | ds.tables = make(map[string]*Table) 345 | for _, r := range rows { 346 | if _, ok := ds.tables[r.TID]; !ok { 347 | ds.tables[r.TID] = &Table{ 348 | datastore: ds, 349 | tableID: r.TID, 350 | records: make(map[string]*Record), 351 | } 352 | } 353 | ds.tables[r.TID].records[r.RowID] = &Record{ 354 | table: ds.tables[r.TID], 355 | recordID: r.RowID, 356 | fields: r.Data, 357 | } 358 | } 359 | ds.info.revision = rev 360 | return nil 361 | } 362 | 363 | // GetDatastore returns the datastore associated with this table. 364 | func (t *Table) GetDatastore() *Datastore { 365 | return t.datastore 366 | } 367 | 368 | // GetID returns the ID of this table. 369 | func (t *Table) GetID() string { 370 | return t.tableID 371 | } 372 | 373 | // Get returns the record with this ID. 374 | func (t *Table) Get(recordID string) (*Record, error) { 375 | if !isValidID(recordID) { 376 | return nil, fmt.Errorf("invalid record ID %s", recordID) 377 | } 378 | return t.records[recordID], nil 379 | } 380 | 381 | // GetOrInsert gets the requested record. 382 | func (t *Table) GetOrInsert(recordID string) (*Record, error) { 383 | if !isValidID(recordID) { 384 | return nil, fmt.Errorf("invalid record ID %s", recordID) 385 | } 386 | return t.GetOrInsertWithFields(recordID, nil) 387 | } 388 | 389 | // GetOrInsertWithFields gets the requested table. 390 | func (t *Table) GetOrInsertWithFields(recordID string, fields Fields) (*Record, error) { 391 | if !isValidID(recordID) { 392 | return nil, fmt.Errorf("invalid record ID %s", recordID) 393 | } 394 | if r, ok := t.records[recordID]; ok { 395 | return r, nil 396 | } 397 | if fields == nil { 398 | fields = make(Fields) 399 | } 400 | if err := t.datastore.insertRecord(t.tableID, recordID, fields); err != nil { 401 | return nil, err 402 | } 403 | return t.records[recordID], nil 404 | } 405 | 406 | // Query returns a list of records matching all the given fields. 407 | func (t *Table) Query(fields Fields) ([]*Record, error) { 408 | var records []*Record 409 | 410 | next: 411 | for _, record := range t.records { 412 | for qf, qv := range fields { 413 | if rv, ok := record.fields[qf]; !ok || !reflect.DeepEqual(qv, rv) { 414 | continue next 415 | } 416 | } 417 | records = append(records, record) 418 | } 419 | return records, nil 420 | } 421 | 422 | // GetTable returns the table associated with this record. 423 | func (r *Record) GetTable() *Table { 424 | return r.table 425 | } 426 | 427 | // GetID returns the ID of this record. 428 | func (r *Record) GetID() string { 429 | return r.recordID 430 | } 431 | 432 | // IsDeleted returns whether this record was deleted. 433 | func (r *Record) IsDeleted() bool { 434 | return r.isDeleted 435 | } 436 | 437 | // DeleteRecord deletes this record. 438 | func (r *Record) DeleteRecord() { 439 | r.table.datastore.deleteRecord(r.table.tableID, r.recordID) 440 | } 441 | 442 | // HasField returns whether this field exists. 443 | func (r *Record) HasField(field string) (bool, error) { 444 | if !isValidID(field) { 445 | return false, fmt.Errorf("invalid field %s", field) 446 | } 447 | _, ok := r.fields[field] 448 | return ok, nil 449 | } 450 | 451 | // Get gets the current value of this field. 452 | func (r *Record) Get(field string) (interface{}, bool, error) { 453 | if !isValidID(field) { 454 | return nil, false, fmt.Errorf("invalid field %s", field) 455 | } 456 | v, ok := r.fields[field] 457 | if !ok { 458 | return nil, false, nil 459 | } 460 | if v.isList { 461 | return &List{ 462 | record: r, 463 | field: field, 464 | values: v.values, 465 | }, true, nil 466 | } 467 | return v.values[0], true, nil 468 | } 469 | 470 | // GetOrCreateList gets the current value of this field. 471 | func (r *Record) GetOrCreateList(field string) (*List, error) { 472 | if !isValidID(field) { 473 | return nil, fmt.Errorf("invalid field %s", field) 474 | } 475 | v, ok := r.fields[field] 476 | if ok && !v.isList { 477 | return nil, fmt.Errorf("not a list") 478 | } 479 | if !ok { 480 | if err := r.table.datastore.listCreate(r.table.tableID, r.recordID, field); err != nil { 481 | return nil, err 482 | } 483 | v = r.fields[field] 484 | } 485 | return &List{ 486 | record: r, 487 | field: field, 488 | values: v.values, 489 | }, nil 490 | } 491 | 492 | func getType(i interface{}) (AtomType, error) { 493 | switch i.(type) { 494 | case bool: 495 | return TypeBoolean, nil 496 | case int, int32, int64: 497 | return TypeInteger, nil 498 | case float32, float64: 499 | return TypeDouble, nil 500 | case string: 501 | return TypeString, nil 502 | case []byte: 503 | return TypeBytes, nil 504 | case time.Time: 505 | return TypeDate, nil 506 | } 507 | return 0, fmt.Errorf("type %s not supported", reflect.TypeOf(i).Name()) 508 | } 509 | 510 | // GetFieldType returns the type of the given field. 511 | func (r *Record) GetFieldType(field string) (AtomType, error) { 512 | if !isValidID(field) { 513 | return 0, fmt.Errorf("invalid field %s", field) 514 | } 515 | v, ok := r.fields[field] 516 | if !ok { 517 | return 0, fmt.Errorf("no such field: %s", field) 518 | } 519 | if v.isList { 520 | return TypeList, nil 521 | } 522 | return getType(v.values[0]) 523 | } 524 | 525 | // Set sets the value of a field. 526 | func (r *Record) Set(field string, value interface{}) error { 527 | if !isValidID(field) { 528 | return fmt.Errorf("invalid field %s", field) 529 | } 530 | return r.table.datastore.updateField(r.table.tableID, r.recordID, field, value) 531 | } 532 | 533 | // DeleteField deletes the given field from this record. 534 | func (r *Record) DeleteField(field string) error { 535 | if !isValidID(field) { 536 | return fmt.Errorf("invalid field %s", field) 537 | } 538 | return r.table.datastore.deleteField(r.table.tableID, r.recordID, field) 539 | } 540 | 541 | // FieldNames returns a list of fields names. 542 | func (r *Record) FieldNames() []string { 543 | var rv []string 544 | 545 | rv = make([]string, 0, len(r.fields)) 546 | for k := range r.fields { 547 | rv = append(rv, k) 548 | } 549 | return rv 550 | } 551 | 552 | // IsEmpty returns whether the list contains an element. 553 | func (l *List) IsEmpty() bool { 554 | return len(l.values) == 0 555 | } 556 | 557 | // Size returns the number of elements in the list. 558 | func (l *List) Size() int { 559 | return len(l.values) 560 | } 561 | 562 | // GetType gets the type of the n-th element in the list. 563 | func (l *List) GetType(n int) (AtomType, error) { 564 | if n >= len(l.values) { 565 | return 0, fmt.Errorf("out of bound index") 566 | } 567 | return getType(l.values[n]) 568 | } 569 | 570 | // Get gets the n-th element in the list. 571 | func (l *List) Get(n int) (interface{}, error) { 572 | if n >= len(l.values) { 573 | return 0, fmt.Errorf("out of bound index") 574 | } 575 | return l.values[n], nil 576 | } 577 | 578 | // AddAtPos inserts the item at the n-th position in the list. 579 | func (l *List) AddAtPos(n int, i interface{}) error { 580 | if n > len(l.values) { 581 | return fmt.Errorf("out of bound index") 582 | } 583 | err := l.record.table.datastore.listInsert(l.record.table.tableID, l.record.recordID, l.field, n, i) 584 | if err != nil { 585 | return err 586 | } 587 | l.values = l.record.fields[l.field].values 588 | return nil 589 | } 590 | 591 | // Add adds the item at the end of the list. 592 | func (l *List) Add(i interface{}) error { 593 | return l.AddAtPos(len(l.values), i) 594 | } 595 | 596 | // Set sets the value of the n-th element of the list. 597 | func (l *List) Set(n int, i interface{}) error { 598 | if n >= len(l.values) { 599 | return fmt.Errorf("out of bound index") 600 | } 601 | return l.record.table.datastore.listPut(l.record.table.tableID, l.record.recordID, l.field, n, i) 602 | } 603 | 604 | // Remove removes the n-th element of the list. 605 | func (l *List) Remove(n int) error { 606 | if n >= len(l.values) { 607 | return fmt.Errorf("out of bound index") 608 | } 609 | err := l.record.table.datastore.listDelete(l.record.table.tableID, l.record.recordID, l.field, n) 610 | l.values = l.record.fields[l.field].values 611 | return err 612 | } 613 | 614 | // Move moves the element from the from-th position to the to-th. 615 | func (l *List) Move(from, to int) error { 616 | if from >= len(l.values) || to >= len(l.values) { 617 | return fmt.Errorf("out of bound index") 618 | } 619 | return l.record.table.datastore.listMove(l.record.table.tableID, l.record.recordID, l.field, from, to) 620 | } 621 | -------------------------------------------------------------------------------- /dropbox_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | package dropbox 27 | 28 | import ( 29 | "bytes" 30 | "encoding/json" 31 | "fmt" 32 | "io/ioutil" 33 | "net/http" 34 | "reflect" 35 | "strconv" 36 | "testing" 37 | "time" 38 | ) 39 | 40 | var dirEntry = Entry{Size: "0 bytes", Revision: "1f477dd351f", ThumbExists: false, Bytes: 0, 41 | Modified: DBTime(time.Date(2011, time.August, 10, 18, 21, 30, 0, time.UTC)), 42 | Path: "/testdir", IsDir: true, Icon: "folder", Root: "auto"} 43 | 44 | var fileEntry = Entry{Size: "0 bytes", Revision: "1f33043551f", ThumbExists: false, Bytes: 0, 45 | Modified: DBTime(time.Date(2011, time.August, 10, 18, 21, 30, 0, time.UTC)), 46 | Path: "/testfile", IsDir: false, Icon: "page_white_text", 47 | Root: "auto", MimeType: "text/plain"} 48 | 49 | type FakeHTTP struct { 50 | t *testing.T 51 | Method string 52 | Host string 53 | Path string 54 | Params map[string]string 55 | RequestData []byte 56 | ResponseData []byte 57 | } 58 | 59 | func (f FakeHTTP) RoundTrip(req *http.Request) (resp *http.Response, err error) { 60 | if resp, err = f.checkRequest(req); err != nil { 61 | f.t.Errorf("%s", err) 62 | } 63 | return resp, err 64 | } 65 | 66 | func (f FakeHTTP) checkRequest(r *http.Request) (*http.Response, error) { 67 | var va []string 68 | var ok bool 69 | 70 | if r.Method != f.Method { 71 | return nil, fmt.Errorf("wrong method") 72 | } 73 | if r.URL.Scheme != "https" || r.URL.Host != f.Host || r.URL.Path != f.Path { 74 | return nil, fmt.Errorf("wrong URL %s://%s%s", r.URL.Scheme, r.URL.Host, r.URL.Path) 75 | } 76 | vals := r.URL.Query() 77 | if len(vals) != len(f.Params) { 78 | return nil, fmt.Errorf("wrong number of parameters got %d expected %d", len(vals), len(f.Params)) 79 | } 80 | for k, v := range f.Params { 81 | if va, ok = vals[k]; !ok || len(va) != 1 { 82 | return nil, fmt.Errorf("wrong parameters %s", k) 83 | } else if va[0] != v { 84 | return nil, fmt.Errorf("wrong parameters %s expected %s received %s", k, v, va[0]) 85 | } 86 | } 87 | if len(f.RequestData) != 0 { 88 | var buf []byte 89 | var err error 90 | 91 | if buf, err = ioutil.ReadAll(r.Body); err != nil { 92 | return nil, err 93 | } 94 | if !bytes.Equal(buf, f.RequestData) { 95 | return nil, fmt.Errorf("wrong request body") 96 | } 97 | } 98 | 99 | return &http.Response{Status: "200 OK", StatusCode: 200, 100 | Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, 101 | ContentLength: int64(len(f.ResponseData)), Body: ioutil.NopCloser(bytes.NewReader(f.ResponseData))}, nil 102 | } 103 | 104 | // Downloading a file 105 | func Example() { 106 | db := NewDropbox() 107 | db.SetAppInfo("application id", "application secret") 108 | db.SetAccessToken("your secret token for this application") 109 | db.DownloadToFile("file on Dropbox", "local destination", "revision of the file on Dropbox") 110 | } 111 | 112 | func newDropbox(t *testing.T) *Dropbox { 113 | db := NewDropbox() 114 | db.SetAppInfo("dummyappkey", "dummyappsecret") 115 | db.SetAccessToken("dummyoauthtoken") 116 | return db 117 | } 118 | 119 | func TestAccountInfo(t *testing.T) { 120 | var err error 121 | var db *Dropbox 122 | var received *Account 123 | 124 | db = newDropbox(t) 125 | 126 | expected := Account{ReferralLink: "https://www.dropbox.com/referrals/r1a2n3d4m5s6t7", DisplayName: "John P. User", Country: "US", UID: 12345678} 127 | expected.QuotaInfo.Shared = 253738410565 128 | expected.QuotaInfo.Quota = 107374182400000 129 | expected.QuotaInfo.Normal = 680031877871 130 | js, err := json.Marshal(expected) 131 | if err != nil { 132 | t.Fatalf("could not run test marshalling issue") 133 | } 134 | 135 | http.DefaultClient = &http.Client{ 136 | Transport: FakeHTTP{ 137 | t: t, 138 | Method: "GET", 139 | Host: "api.dropbox.com", 140 | Path: "/1/account/info", 141 | Params: map[string]string{"locale": "en"}, 142 | ResponseData: js, 143 | }, 144 | } 145 | 146 | if received, err = db.GetAccountInfo(); err != nil { 147 | t.Errorf("API error: %s", err) 148 | } else if !reflect.DeepEqual(expected, *received) { 149 | t.Errorf("got %#v expected %#v", *received, expected) 150 | } 151 | } 152 | 153 | func TestCopy(t *testing.T) { 154 | var err error 155 | var db *Dropbox 156 | var received *Entry 157 | var from, to string 158 | var fake FakeHTTP 159 | 160 | expected := fileEntry 161 | from = expected.Path[1:] 162 | to = from + ".1" 163 | expected.Path = to 164 | 165 | js, err := json.Marshal(expected) 166 | if err != nil { 167 | t.Fatalf("could not run test marshalling issue") 168 | } 169 | 170 | fake = FakeHTTP{ 171 | t: t, 172 | Method: "POST", 173 | Host: "api.dropbox.com", 174 | Path: "/1/fileops/copy", 175 | Params: map[string]string{ 176 | "root": "auto", 177 | "from_path": from, 178 | "to_path": to, 179 | "locale": "en", 180 | }, 181 | ResponseData: js, 182 | } 183 | db = newDropbox(t) 184 | http.DefaultClient = &http.Client{ 185 | Transport: fake, 186 | } 187 | 188 | if received, err = db.Copy(from, to, false); err != nil { 189 | t.Errorf("API error: %s", err) 190 | } else if !reflect.DeepEqual(expected, *received) { 191 | t.Errorf("got %#v expected %#v", *received, expected) 192 | } 193 | 194 | delete(fake.Params, "from_path") 195 | fake.Params["from_copy_ref"] = from 196 | if received, err = db.Copy(from, to, true); err != nil { 197 | t.Errorf("API error: %s", err) 198 | } else if !reflect.DeepEqual(expected, *received) { 199 | t.Errorf("got %#v expected %#v", *received, expected) 200 | } 201 | } 202 | 203 | func TestCopyRef(t *testing.T) { 204 | var err error 205 | var db *Dropbox 206 | var received *CopyRef 207 | var filename string 208 | 209 | filename = "dummyfile" 210 | db = newDropbox(t) 211 | 212 | expected := CopyRef{CopyRef: "z1X6ATl6aWtzOGq0c3g5Ng", Expires: "Fri, 31 Jan 2042 21:01:05 +0000"} 213 | js, err := json.Marshal(expected) 214 | if err != nil { 215 | t.Fatalf("could not run test due to marshalling issue") 216 | } 217 | 218 | http.DefaultClient = &http.Client{ 219 | Transport: FakeHTTP{ 220 | Method: "GET", 221 | Host: "api.dropbox.com", 222 | Path: "/1/copy_ref/auto/" + filename, 223 | t: t, 224 | Params: map[string]string{"locale": "en"}, 225 | ResponseData: js, 226 | }, 227 | } 228 | if received, err = db.CopyRef(filename); err != nil { 229 | t.Errorf("API error: %s", err) 230 | } else if !reflect.DeepEqual(expected, *received) { 231 | t.Errorf("got %#v expected %#v", *received, expected) 232 | } 233 | } 234 | 235 | func TestCreateFolder(t *testing.T) { 236 | var err error 237 | var db *Dropbox 238 | var received *Entry 239 | var foldername string 240 | 241 | expected := dirEntry 242 | foldername = expected.Path[1:] 243 | 244 | js, err := json.Marshal(expected) 245 | if err != nil { 246 | t.Fatalf("could not run test due to marshalling issue") 247 | } 248 | 249 | db = newDropbox(t) 250 | http.DefaultClient = &http.Client{ 251 | Transport: FakeHTTP{ 252 | Method: "POST", 253 | Host: "api.dropbox.com", 254 | Path: "/1/fileops/create_folder", 255 | Params: map[string]string{ 256 | "root": "auto", 257 | "path": foldername, 258 | "locale": "en", 259 | }, 260 | t: t, 261 | ResponseData: js, 262 | }, 263 | } 264 | if received, err = db.CreateFolder(foldername); err != nil { 265 | t.Errorf("API error: %s", err) 266 | } else if !reflect.DeepEqual(expected, *received) { 267 | t.Errorf("got %#v expected %#v", *received, expected) 268 | } 269 | } 270 | 271 | func TestDelete(t *testing.T) { 272 | var err error 273 | var db *Dropbox 274 | var received *Entry 275 | var path string 276 | 277 | expected := dirEntry 278 | expected.IsDeleted = true 279 | path = expected.Path[1:] 280 | 281 | js, err := json.Marshal(expected) 282 | if err != nil { 283 | t.Fatalf("could not run test marshalling issue") 284 | } 285 | 286 | db = newDropbox(t) 287 | http.DefaultClient = &http.Client{ 288 | Transport: FakeHTTP{ 289 | t: t, 290 | Method: "POST", 291 | Host: "api.dropbox.com", 292 | Path: "/1/fileops/delete", 293 | Params: map[string]string{ 294 | "root": "auto", 295 | "path": path, 296 | "locale": "en", 297 | }, 298 | ResponseData: js, 299 | }, 300 | } 301 | if received, err = db.Delete(path); err != nil { 302 | t.Errorf("API error: %s", err) 303 | } else if !reflect.DeepEqual(expected, *received) { 304 | t.Errorf("got %#v expected %#v", *received, expected) 305 | } 306 | } 307 | 308 | func TestFilesPut(t *testing.T) { 309 | var err error 310 | var db *Dropbox 311 | var received *Entry 312 | var filename string 313 | var content, js []byte 314 | var fake FakeHTTP 315 | 316 | filename = "test.txt" 317 | content = []byte("file content") 318 | 319 | expected := Entry{Size: strconv.FormatInt(int64(len(content)), 10), Revision: "35e97029684fe", ThumbExists: false, Bytes: len(content), 320 | Modified: DBTime(time.Date(2011, time.July, 19, 21, 55, 38, 0, time.UTC)), Path: "/" + filename, IsDir: false, Icon: "page_white_text", 321 | Root: "auto", MimeType: "text/plain"} 322 | 323 | js, err = json.Marshal(expected) 324 | if err != nil { 325 | t.Fatalf("could not run test marshalling issue") 326 | } 327 | 328 | fake = FakeHTTP{ 329 | t: t, 330 | Method: "PUT", 331 | Host: "api-content.dropbox.com", 332 | Path: "/1/files_put/auto/" + filename, 333 | Params: map[string]string{ 334 | "locale": "en", 335 | "overwrite": "false", 336 | }, 337 | ResponseData: js, 338 | RequestData: content, 339 | } 340 | 341 | db = newDropbox(t) 342 | http.DefaultClient = &http.Client{ 343 | Transport: fake, 344 | } 345 | 346 | received, err = db.FilesPut(ioutil.NopCloser(bytes.NewBuffer(content)), int64(len(content)), filename, false, "") 347 | if err != nil { 348 | t.Errorf("API error: %s", err) 349 | } else if !reflect.DeepEqual(expected, *received) { 350 | t.Errorf("got %#v expected %#v", *received, expected) 351 | } 352 | 353 | fake.Params["parent_rev"] = "12345" 354 | received, err = db.FilesPut(ioutil.NopCloser(bytes.NewBuffer(content)), int64(len(content)), filename, false, "12345") 355 | if err != nil { 356 | t.Errorf("API error: %s", err) 357 | } else if !reflect.DeepEqual(expected, *received) { 358 | t.Errorf("got %#v expected %#v", *received, expected) 359 | } 360 | 361 | fake.Params["overwrite"] = "true" 362 | received, err = db.FilesPut(ioutil.NopCloser(bytes.NewBuffer(content)), int64(len(content)), filename, true, "12345") 363 | if err != nil { 364 | t.Errorf("API error: %s", err) 365 | } else if !reflect.DeepEqual(expected, *received) { 366 | t.Errorf("got %#v expected %#v", *received, expected) 367 | } 368 | 369 | _, err = db.FilesPut(ioutil.NopCloser(bytes.NewBuffer(content)), int64(MaxPutFileSize+1), filename, true, "12345") 370 | if err == nil { 371 | t.Errorf("size > %d bytes must returns an error", MaxPutFileSize) 372 | } 373 | } 374 | 375 | func TestMedia(t *testing.T) { 376 | var err error 377 | var db *Dropbox 378 | var received *Link 379 | var filename string 380 | 381 | filename = "dummyfile" 382 | db = newDropbox(t) 383 | 384 | expected := Link{Expires: DBTime(time.Date(2011, time.August, 10, 18, 21, 30, 0, time.UTC)), URL: "https://dl.dropboxusercontent.com/1/view/abcdefghijk/example"} 385 | js, err := json.Marshal(expected) 386 | if err != nil { 387 | t.Fatalf("could not run test due to marshalling issue: %s", err) 388 | } 389 | 390 | http.DefaultClient = &http.Client{ 391 | Transport: FakeHTTP{ 392 | Method: "POST", 393 | Host: "api.dropbox.com", 394 | Path: "/1/media/auto/" + filename, 395 | Params: map[string]string{"locale": "en"}, 396 | t: t, 397 | ResponseData: js, 398 | }, 399 | } 400 | if received, err = db.Media(filename); err != nil { 401 | t.Errorf("API error: %s", err) 402 | } else if !reflect.DeepEqual(expected, *received) { 403 | t.Errorf("got %#v expected %#v", *received, expected) 404 | } 405 | } 406 | 407 | func TestMetadata(t *testing.T) { 408 | var err error 409 | var db *Dropbox 410 | var received *Entry 411 | var path string 412 | var fake FakeHTTP 413 | 414 | expected := fileEntry 415 | path = expected.Path[1:] 416 | 417 | js, err := json.Marshal(expected) 418 | if err != nil { 419 | t.Fatalf("could not run test marshalling issue") 420 | } 421 | 422 | fake = FakeHTTP{ 423 | t: t, 424 | Method: "GET", 425 | Host: "api.dropbox.com", 426 | Path: "/1/metadata/auto/" + path, 427 | Params: map[string]string{ 428 | "list": "false", 429 | "include_deleted": "false", 430 | "file_limit": "10", 431 | "locale": "en", 432 | }, 433 | ResponseData: js, 434 | } 435 | db = newDropbox(t) 436 | http.DefaultClient = &http.Client{ 437 | Transport: fake, 438 | } 439 | 440 | if received, err = db.Metadata(path, false, false, "", "", 10); err != nil { 441 | t.Errorf("API error: %s", err) 442 | } else if !reflect.DeepEqual(expected, *received) { 443 | t.Errorf("got %#v expected %#v", *received, expected) 444 | } 445 | 446 | fake.Params["list"] = "true" 447 | if received, err = db.Metadata(path, true, false, "", "", 10); err != nil { 448 | t.Errorf("API error: %s", err) 449 | } else if !reflect.DeepEqual(expected, *received) { 450 | t.Errorf("got %#v expected %#v", *received, expected) 451 | } 452 | 453 | fake.Params["include_deleted"] = "true" 454 | if received, err = db.Metadata(path, true, true, "", "", 10); err != nil { 455 | t.Errorf("API error: %s", err) 456 | } else if !reflect.DeepEqual(expected, *received) { 457 | t.Errorf("got %#v expected %#v", *received, expected) 458 | } 459 | 460 | fake.Params["file_limit"] = "20" 461 | if received, err = db.Metadata(path, true, true, "", "", 20); err != nil { 462 | t.Errorf("API error: %s", err) 463 | } else if !reflect.DeepEqual(expected, *received) { 464 | t.Errorf("got %#v expected %#v", *received, expected) 465 | } 466 | 467 | fake.Params["rev"] = "12345" 468 | if received, err = db.Metadata(path, true, true, "", "12345", 20); err != nil { 469 | t.Errorf("API error: %s", err) 470 | } else if !reflect.DeepEqual(expected, *received) { 471 | t.Errorf("got %#v expected %#v", *received, expected) 472 | } 473 | 474 | fake.Params["hash"] = "6789" 475 | if received, err = db.Metadata(path, true, true, "6789", "12345", 20); err != nil { 476 | t.Errorf("API error: %s", err) 477 | } else if !reflect.DeepEqual(expected, *received) { 478 | t.Errorf("got %#v expected %#v", *received, expected) 479 | } 480 | 481 | fake.Params["file_limit"] = "10000" 482 | if received, err = db.Metadata(path, true, true, "6789", "12345", 0); err != nil { 483 | t.Errorf("API error: %s", err) 484 | } else if !reflect.DeepEqual(expected, *received) { 485 | t.Errorf("got %#v expected %#v", *received, expected) 486 | } 487 | 488 | fake.Params["file_limit"] = strconv.FormatInt(int64(MetadataLimitMax), 10) 489 | if received, err = db.Metadata(path, true, true, "6789", "12345", MetadataLimitMax+1); err != nil { 490 | t.Errorf("API error: %s", err) 491 | } else if !reflect.DeepEqual(expected, *received) { 492 | t.Errorf("got %#v expected %#v", *received, expected) 493 | } 494 | } 495 | 496 | func TestMove(t *testing.T) { 497 | var err error 498 | var db *Dropbox 499 | var received *Entry 500 | var from, to string 501 | 502 | expected := fileEntry 503 | from = expected.Path[1:] 504 | to = from + ".1" 505 | expected.Path = to 506 | 507 | js, err := json.Marshal(expected) 508 | if err != nil { 509 | t.Fatalf("could not run test marshalling issue") 510 | } 511 | 512 | db = newDropbox(t) 513 | http.DefaultClient = &http.Client{ 514 | Transport: FakeHTTP{ 515 | t: t, 516 | Method: "POST", 517 | Host: "api.dropbox.com", 518 | Path: "/1/fileops/move", 519 | Params: map[string]string{ 520 | "root": "auto", 521 | "from_path": from, 522 | "to_path": to, 523 | "locale": "en", 524 | }, 525 | ResponseData: js, 526 | }, 527 | } 528 | if received, err = db.Move(from, to); err != nil { 529 | t.Errorf("API error: %s", err) 530 | } else if !reflect.DeepEqual(expected, *received) { 531 | t.Errorf("got %#v expected %#v", *received, expected) 532 | } 533 | } 534 | 535 | func TestRestore(t *testing.T) { 536 | var err error 537 | var db *Dropbox 538 | var received *Entry 539 | var path string 540 | 541 | expected := fileEntry 542 | path = expected.Path[1:] 543 | 544 | js, err := json.Marshal(expected) 545 | if err != nil { 546 | t.Fatalf("could not run test marshalling issue") 547 | } 548 | 549 | db = newDropbox(t) 550 | http.DefaultClient = &http.Client{ 551 | Transport: FakeHTTP{ 552 | t: t, 553 | Method: "POST", 554 | Host: "api.dropbox.com", 555 | Path: "/1/restore/auto/" + path, 556 | Params: map[string]string{ 557 | "rev": expected.Revision, 558 | "locale": "en", 559 | }, 560 | ResponseData: js, 561 | }, 562 | } 563 | if received, err = db.Restore(path, expected.Revision); err != nil { 564 | t.Errorf("API error: %s", err) 565 | } else if !reflect.DeepEqual(expected, *received) { 566 | t.Errorf("got %#v expected %#v", *received, expected) 567 | } 568 | } 569 | 570 | func TestRevisions(t *testing.T) { 571 | var err error 572 | var db *Dropbox 573 | var received []Entry 574 | var path string 575 | var fake FakeHTTP 576 | 577 | expected := []Entry{fileEntry} 578 | path = expected[0].Path[1:] 579 | 580 | js, err := json.Marshal(expected) 581 | if err != nil { 582 | t.Fatalf("could not run test marshalling issue") 583 | } 584 | 585 | fake = FakeHTTP{ 586 | t: t, 587 | Method: "GET", 588 | Host: "api.dropbox.com", 589 | Path: "/1/revisions/auto/" + path, 590 | Params: map[string]string{ 591 | "rev_limit": "10", 592 | "locale": "en", 593 | }, 594 | ResponseData: js, 595 | } 596 | db = newDropbox(t) 597 | http.DefaultClient = &http.Client{ 598 | Transport: fake, 599 | } 600 | 601 | if received, err = db.Revisions(path, 10); err != nil { 602 | t.Errorf("API error: %s", err) 603 | } else if !reflect.DeepEqual(expected, received) { 604 | t.Errorf("got %#v expected %#v", received, expected) 605 | } 606 | 607 | fake.Params["rev_limit"] = strconv.FormatInt(int64(RevisionsLimitDefault), 10) 608 | if received, err = db.Revisions(path, 0); err != nil { 609 | t.Errorf("API error: %s", err) 610 | } else if !reflect.DeepEqual(expected, received) { 611 | t.Errorf("got %#v expected %#v", received, expected) 612 | } 613 | 614 | fake.Params["rev_limit"] = strconv.FormatInt(int64(RevisionsLimitMax), 10) 615 | if received, err = db.Revisions(path, RevisionsLimitMax+1); err != nil { 616 | t.Errorf("API error: %s", err) 617 | } else if !reflect.DeepEqual(expected, received) { 618 | t.Errorf("got %#v expected %#v", received, expected) 619 | } 620 | } 621 | 622 | func TestSearch(t *testing.T) { 623 | var err error 624 | var db *Dropbox 625 | var received []Entry 626 | var dirname string 627 | 628 | dirname = "dummy" 629 | db = newDropbox(t) 630 | 631 | expected := []Entry{Entry{Size: "0 bytes", Revision: "35c1f029684fe", ThumbExists: false, Bytes: 0, 632 | Modified: DBTime(time.Date(2011, time.August, 10, 18, 21, 30, 0, time.UTC)), Path: "/" + dirname + "/dummyfile", IsDir: false, Icon: "page_white_text", 633 | Root: "auto", MimeType: "text/plain"}} 634 | js, err := json.Marshal(expected) 635 | if err != nil { 636 | t.Fatalf("could not run test due to marshalling issue") 637 | } 638 | 639 | fake := FakeHTTP{ 640 | Method: "GET", 641 | Host: "api.dropbox.com", 642 | Path: "/1/search/auto/" + dirname, 643 | t: t, 644 | Params: map[string]string{ 645 | "locale": "en", 646 | "query": "foo bar", 647 | "file_limit": "10", 648 | "include_deleted": "false", 649 | }, 650 | ResponseData: js, 651 | } 652 | http.DefaultClient = &http.Client{ 653 | Transport: fake, 654 | } 655 | 656 | if received, err = db.Search(dirname, "foo bar", 10, false); err != nil { 657 | t.Errorf("API error: %s", err) 658 | } else if !reflect.DeepEqual(expected, received) { 659 | t.Errorf("got %#v expected %#v", received, expected) 660 | } 661 | 662 | fake.Params["include_deleted"] = "true" 663 | if received, err = db.Search(dirname, "foo bar", 10, true); err != nil { 664 | t.Errorf("API error: %s", err) 665 | } else if !reflect.DeepEqual(expected, received) { 666 | t.Errorf("got %#v expected %#v", received, expected) 667 | } 668 | 669 | fake.Params["file_limit"] = strconv.FormatInt(int64(SearchLimitDefault), 10) 670 | if received, err = db.Search(dirname, "foo bar", 0, true); err != nil { 671 | t.Errorf("API error: %s", err) 672 | } else if !reflect.DeepEqual(expected, received) { 673 | t.Errorf("got %#v expected %#v", received, expected) 674 | } 675 | 676 | if received, err = db.Search(dirname, "foo bar", SearchLimitMax+1, true); err != nil { 677 | t.Errorf("API error: %s", err) 678 | } else if !reflect.DeepEqual(expected, received) { 679 | t.Errorf("got %#v expected %#v", received, expected) 680 | } 681 | } 682 | 683 | func TestShares(t *testing.T) { 684 | var err error 685 | var db *Dropbox 686 | var received *Link 687 | var filename string 688 | 689 | filename = "dummyfile" 690 | db = newDropbox(t) 691 | 692 | expected := Link{Expires: DBTime(time.Date(2011, time.August, 10, 18, 21, 30, 0, time.UTC)), URL: "https://db.tt/c0mFuu1Y"} 693 | js, err := json.Marshal(expected) 694 | if err != nil { 695 | t.Fatalf("could not run test due to marshalling issue") 696 | } 697 | fake := FakeHTTP{ 698 | Method: "POST", 699 | Host: "api.dropbox.com", 700 | Path: "/1/shares/auto/" + filename, 701 | Params: map[string]string{ 702 | "locale": "en", 703 | "short_url": "false", 704 | }, 705 | t: t, 706 | ResponseData: js, 707 | } 708 | http.DefaultClient = &http.Client{ 709 | Transport: fake, 710 | } 711 | 712 | if received, err = db.Shares(filename, false); err != nil { 713 | t.Errorf("API error: %s", err) 714 | } else if !reflect.DeepEqual(expected, *received) { 715 | t.Errorf("got %#v expected %#v", *received, expected) 716 | } 717 | 718 | fake.Params["short_url"] = "true" 719 | if received, err = db.Shares(filename, true); err != nil { 720 | t.Errorf("API error: %s", err) 721 | } else if !reflect.DeepEqual(expected, *received) { 722 | t.Errorf("got %#v expected %#v", *received, expected) 723 | } 724 | } 725 | 726 | func TestLatestCursor(t *testing.T) { 727 | tab := []struct { 728 | prefix string 729 | mediaInfo bool 730 | }{ 731 | { 732 | prefix: "", 733 | mediaInfo: false, 734 | }, 735 | { 736 | prefix: "/some", 737 | mediaInfo: false, 738 | }, 739 | { 740 | prefix: "", 741 | mediaInfo: true, 742 | }, 743 | { 744 | prefix: "/some", 745 | mediaInfo: true, 746 | }, 747 | } 748 | 749 | expected := Cursor{Cursor: "some"} 750 | cur, err := json.Marshal(expected) 751 | if err != nil { 752 | t.Fatal("Failed to JSON encode Cursor") 753 | } 754 | 755 | for _, testCase := range tab { 756 | db := newDropbox(t) 757 | fake := FakeHTTP{ 758 | Method: "POST", 759 | Host: "api.dropbox.com", 760 | Path: "/1/delta/latest_cursor", 761 | t: t, 762 | Params: map[string]string{ 763 | "locale": "en", 764 | }, 765 | ResponseData: cur, 766 | } 767 | 768 | if testCase.prefix != "" { 769 | fake.Params["path_prefix"] = testCase.prefix 770 | } 771 | 772 | if testCase.mediaInfo { 773 | fake.Params["include_media_info"] = "true" 774 | } 775 | 776 | http.DefaultClient = &http.Client{ 777 | Transport: fake, 778 | } 779 | 780 | if received, err := db.LatestCursor(testCase.prefix, testCase.mediaInfo); err != nil { 781 | t.Errorf("API error: %s", err) 782 | } else if !reflect.DeepEqual(expected, *received) { 783 | t.Errorf("got %#v expected %#v", *received, expected) 784 | } 785 | } 786 | } 787 | -------------------------------------------------------------------------------- /dropbox.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved. 3 | ** 4 | ** Redistribution and use in source and binary forms, with or without 5 | ** modification, are permitted provided that the following conditions 6 | ** are met: 7 | ** 1. Redistributions of source code must retain the above copyright 8 | ** notice, this list of conditions and the following disclaimer. 9 | ** 2. Redistributions in binary form must reproduce the above copyright 10 | ** notice, this list of conditions and the following disclaimer in the 11 | ** documentation and/or other materials provided with the distribution. 12 | ** 13 | ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 14 | ** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | ** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | ** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | ** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | ** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | ** SUCH DAMAGE. 24 | */ 25 | 26 | // Package dropbox implements the Dropbox core and datastore API. 27 | package dropbox 28 | 29 | import ( 30 | "encoding/json" 31 | "errors" 32 | "fmt" 33 | "io" 34 | "io/ioutil" 35 | "net/http" 36 | "net/url" 37 | "os" 38 | "strconv" 39 | "strings" 40 | "time" 41 | 42 | "golang.org/x/net/context" 43 | "golang.org/x/oauth2" 44 | ) 45 | 46 | // ErrNotAuth is the error returned when the OAuth token is not provided 47 | var ErrNotAuth = errors.New("authentication required") 48 | 49 | // Account represents information about the user account. 50 | type Account struct { 51 | ReferralLink string `json:"referral_link,omitempty"` // URL for referral. 52 | DisplayName string `json:"display_name,omitempty"` // User name. 53 | UID int `json:"uid,omitempty"` // User account ID. 54 | Country string `json:"country,omitempty"` // Country ISO code. 55 | QuotaInfo struct { 56 | Shared int64 `json:"shared,omitempty"` // Quota for shared files. 57 | Quota int64 `json:"quota,omitempty"` // Quota in bytes. 58 | Normal int64 `json:"normal,omitempty"` // Quota for non-shared files. 59 | } `json:"quota_info"` 60 | } 61 | 62 | // CopyRef represents the reply of CopyRef. 63 | type CopyRef struct { 64 | CopyRef string `json:"copy_ref"` // Reference to use on fileops/copy. 65 | Expires string `json:"expires"` // Expiration date. 66 | } 67 | 68 | // DeltaPage represents the reply of delta. 69 | type DeltaPage struct { 70 | Reset bool // if true the local state must be cleared. 71 | HasMore bool // if true an other call to delta should be made. 72 | Cursor // Tag of the current state. 73 | Entries []DeltaEntry // List of changed entries. 74 | } 75 | 76 | // DeltaEntry represents the list of changes for a given path. 77 | type DeltaEntry struct { 78 | Path string // Path of this entry in lowercase. 79 | Entry *Entry // nil when this entry does not exists. 80 | } 81 | 82 | // DeltaPoll represents the reply of longpoll_delta. 83 | type DeltaPoll struct { 84 | Changes bool `json:"changes"` // true if the polled path has changed. 85 | Backoff int `json:"backoff"` // time in second before calling poll again. 86 | } 87 | 88 | // ChunkUploadResponse represents the reply of chunked_upload. 89 | type ChunkUploadResponse struct { 90 | UploadID string `json:"upload_id"` // Unique ID of this upload. 91 | Offset int `json:"offset"` // Size in bytes of already sent data. 92 | Expires DBTime `json:"expires"` // Expiration time of this upload. 93 | } 94 | 95 | // Cursor represents the tag of a server state at a given moment. 96 | type Cursor struct { 97 | Cursor string `json:"cursor"` 98 | } 99 | 100 | // Format of reply when http error code is not 200. 101 | // Format may be: 102 | // {"error": "reason"} 103 | // {"error": {"param": "reason"}} 104 | type requestError struct { 105 | Error interface{} `json:"error"` // Description of this error. 106 | } 107 | 108 | const ( 109 | // PollMinTimeout is the minimum timeout for longpoll. 110 | PollMinTimeout = 30 111 | // PollMaxTimeout is the maximum timeout for longpoll. 112 | PollMaxTimeout = 480 113 | // DefaultChunkSize is the maximum size of a file sendable using files_put. 114 | DefaultChunkSize = 4 * 1024 * 1024 115 | // MaxPutFileSize is the maximum size of a file sendable using files_put. 116 | MaxPutFileSize = 150 * 1024 * 1024 117 | // MetadataLimitMax is the maximum number of entries returned by metadata. 118 | MetadataLimitMax = 25000 119 | // MetadataLimitDefault is the default number of entries returned by metadata. 120 | MetadataLimitDefault = 10000 121 | // RevisionsLimitMax is the maximum number of revisions returned by revisions. 122 | RevisionsLimitMax = 1000 123 | // RevisionsLimitDefault is the default number of revisions returned by revisions. 124 | RevisionsLimitDefault = 10 125 | // SearchLimitMax is the maximum number of entries returned by search. 126 | SearchLimitMax = 1000 127 | // SearchLimitDefault is the default number of entries returned by search. 128 | SearchLimitDefault = 1000 129 | // DateFormat is the format to use when decoding a time. 130 | DateFormat = time.RFC1123Z 131 | ) 132 | 133 | // DBTime allow marshalling and unmarshalling of time. 134 | type DBTime time.Time 135 | 136 | // UnmarshalJSON unmarshals a time according to the Dropbox format. 137 | func (dbt *DBTime) UnmarshalJSON(data []byte) error { 138 | var s string 139 | var err error 140 | var t time.Time 141 | 142 | if err = json.Unmarshal(data, &s); err != nil { 143 | return err 144 | } 145 | if t, err = time.ParseInLocation(DateFormat, s, time.UTC); err != nil { 146 | return err 147 | } 148 | if t.IsZero() { 149 | *dbt = DBTime(time.Time{}) 150 | } else { 151 | *dbt = DBTime(t) 152 | } 153 | return nil 154 | } 155 | 156 | // MarshalJSON marshals a time according to the Dropbox format. 157 | func (dbt DBTime) MarshalJSON() ([]byte, error) { 158 | return json.Marshal(time.Time(dbt).Format(DateFormat)) 159 | } 160 | 161 | // Modifier represents the user who made a change on a particular file 162 | type Modifier struct { 163 | UID int64 `json:"uid"` 164 | DisplayName string `json:"display_name"` 165 | } 166 | 167 | // Entry represents the metadata of a file or folder. 168 | type Entry struct { 169 | Bytes int `json:"bytes,omitempty"` // Size of the file in bytes. 170 | ClientMtime DBTime `json:"client_mtime,omitempty"` // Modification time set by the client when added. 171 | Contents []Entry `json:"contents,omitempty"` // List of children for a directory. 172 | Hash string `json:"hash,omitempty"` // Hash of this entry. 173 | Icon string `json:"icon,omitempty"` // Name of the icon displayed for this entry. 174 | IsDeleted bool `json:"is_deleted,omitempty"` // true if this entry was deleted. 175 | IsDir bool `json:"is_dir,omitempty"` // true if this entry is a directory. 176 | MimeType string `json:"mime_type,omitempty"` // MimeType of this entry. 177 | Modified DBTime `json:"modified,omitempty"` // Date of last modification. 178 | Path string `json:"path,omitempty"` // Absolute path of this entry. 179 | Revision string `json:"rev,omitempty"` // Unique ID for this file revision. 180 | Root string `json:"root,omitempty"` // dropbox or sandbox. 181 | Size string `json:"size,omitempty"` // Size of the file humanized/localized. 182 | ThumbExists bool `json:"thumb_exists,omitempty"` // true if a thumbnail is available for this entry. 183 | Modifier *Modifier `json:"modifier"` // last user to edit the file if in a shared folder 184 | ParentSharedFolderID string `json:"parent_shared_folder_id,omitempty"` 185 | } 186 | 187 | // Link for sharing a file. 188 | type Link struct { 189 | Expires DBTime `json:"expires"` // Expiration date of this link. 190 | URL string `json:"url"` // URL to share. 191 | } 192 | 193 | // User represents a Dropbox user. 194 | type User struct { 195 | UID int64 `json:"uid"` 196 | DisplayName string `json:"display_name"` 197 | } 198 | 199 | // SharedFolderMember represents access right associated with a Dropbox user. 200 | type SharedFolderMember struct { 201 | User User `json:"user"` 202 | Active bool `json:"active"` 203 | AccessType string `json:"access_type"` 204 | } 205 | 206 | // SharedFolder reprensents a directory with a specific sharing policy. 207 | type SharedFolder struct { 208 | SharedFolderID string `json:"shared_folder_id"` 209 | SharedFolderName string `json:"shared_folder_name"` 210 | Path string `json:"path"` 211 | AccessType string `json:"access_type"` 212 | SharedLinkPolicy string `json:"shared_link_policy"` 213 | Owner User `json:"owner"` 214 | Membership []SharedFolderMember `json:"membership"` 215 | } 216 | 217 | // Dropbox client. 218 | type Dropbox struct { 219 | RootDirectory string // dropbox or sandbox. 220 | Locale string // Locale sent to the API to translate/format messages. 221 | APIURL string // Normal API URL. 222 | APIContentURL string // URL for transferring files. 223 | APINotifyURL string // URL for realtime notification. 224 | config *oauth2.Config 225 | token *oauth2.Token 226 | ctx context.Context 227 | } 228 | 229 | // NewDropbox returns a new Dropbox configured. 230 | func NewDropbox() *Dropbox { 231 | db := &Dropbox{ 232 | RootDirectory: "auto", // auto (recommended), dropbox or sandbox. 233 | Locale: "en", 234 | APIURL: "https://api.dropbox.com/1", 235 | APIContentURL: "https://api-content.dropbox.com/1", 236 | APINotifyURL: "https://api-notify.dropbox.com/1", 237 | ctx: oauth2.NoContext, 238 | } 239 | return db 240 | } 241 | 242 | // SetAppInfo sets the clientid (app_key) and clientsecret (app_secret). 243 | // You have to register an application on https://www.dropbox.com/developers/apps. 244 | func (db *Dropbox) SetAppInfo(clientid, clientsecret string) error { 245 | 246 | db.config = &oauth2.Config{ 247 | ClientID: clientid, 248 | ClientSecret: clientsecret, 249 | Endpoint: oauth2.Endpoint{ 250 | AuthURL: "https://www.dropbox.com/1/oauth2/authorize", 251 | TokenURL: "https://api.dropbox.com/1/oauth2/token", 252 | }, 253 | } 254 | return nil 255 | } 256 | 257 | // SetAccessToken sets access token to avoid calling Auth method. 258 | func (db *Dropbox) SetAccessToken(accesstoken string) { 259 | db.token = &oauth2.Token{AccessToken: accesstoken} 260 | } 261 | 262 | // SetContext allow to set a custom context. 263 | func (db *Dropbox) SetContext(ctx context.Context) { 264 | db.ctx = ctx 265 | } 266 | 267 | // AccessToken returns the OAuth access token. 268 | func (db *Dropbox) AccessToken() string { 269 | return db.token.AccessToken 270 | } 271 | 272 | // SetRedirectURL updates the configuration with the given redirection URL. 273 | func (db *Dropbox) SetRedirectURL(url string) { 274 | db.config.RedirectURL = url 275 | } 276 | 277 | func (db *Dropbox) client() *http.Client { 278 | return db.config.Client(db.ctx, db.token) 279 | } 280 | 281 | // Auth displays the URL to authorize this application to connect to your account. 282 | func (db *Dropbox) Auth() error { 283 | var code string 284 | 285 | fmt.Printf("Please visit:\n%s\nEnter the code: ", 286 | db.config.AuthCodeURL("")) 287 | fmt.Scanln(&code) 288 | return db.AuthCode(code) 289 | } 290 | 291 | // AuthCode gets the token associated with the given code. 292 | func (db *Dropbox) AuthCode(code string) error { 293 | t, err := db.config.Exchange(oauth2.NoContext, code) 294 | if err != nil { 295 | return err 296 | } 297 | 298 | db.token = t 299 | db.token.TokenType = "Bearer" 300 | return nil 301 | } 302 | 303 | // Error - all errors generated by HTTP transactions are of this type. 304 | // Other error may be passed on from library functions though. 305 | type Error struct { 306 | StatusCode int // HTTP status code 307 | Text string 308 | } 309 | 310 | // Error satisfy the error interface. 311 | func (e *Error) Error() string { 312 | return e.Text 313 | } 314 | 315 | // newError make a new error from a string. 316 | func newError(StatusCode int, Text string) *Error { 317 | return &Error{ 318 | StatusCode: StatusCode, 319 | Text: Text, 320 | } 321 | } 322 | 323 | // newErrorf makes a new error from sprintf parameters. 324 | func newErrorf(StatusCode int, Text string, Parameters ...interface{}) *Error { 325 | return newError(StatusCode, fmt.Sprintf(Text, Parameters...)) 326 | } 327 | 328 | func getResponse(r *http.Response) ([]byte, error) { 329 | var e requestError 330 | var b []byte 331 | var err error 332 | 333 | if b, err = ioutil.ReadAll(r.Body); err != nil { 334 | return nil, err 335 | } 336 | if r.StatusCode == http.StatusOK { 337 | return b, nil 338 | } 339 | if err = json.Unmarshal(b, &e); err == nil { 340 | switch v := e.Error.(type) { 341 | case string: 342 | return nil, newErrorf(r.StatusCode, "%s", v) 343 | case map[string]interface{}: 344 | for param, reason := range v { 345 | if reasonstr, ok := reason.(string); ok { 346 | return nil, newErrorf(r.StatusCode, "%s: %s", param, reasonstr) 347 | } 348 | } 349 | return nil, newErrorf(r.StatusCode, "wrong parameter") 350 | } 351 | } 352 | return nil, newErrorf(r.StatusCode, "unexpected HTTP status code %d", r.StatusCode) 353 | } 354 | 355 | // urlEncode encodes s for url 356 | func urlEncode(s string) string { 357 | // Would like to call url.escape(value, encodePath) here 358 | encoded := url.QueryEscape(s) 359 | encoded = strings.Replace(encoded, "+", "%20", -1) 360 | return encoded 361 | } 362 | 363 | // CommitChunkedUpload ends the chunked upload by giving a name to the UploadID. 364 | func (db *Dropbox) CommitChunkedUpload(uploadid, dst string, overwrite bool, parentRev string) (*Entry, error) { 365 | var err error 366 | var rawurl string 367 | var response *http.Response 368 | var params *url.Values 369 | var body []byte 370 | var rv Entry 371 | 372 | if dst[0] == '/' { 373 | dst = dst[1:] 374 | } 375 | 376 | params = &url.Values{ 377 | "locale": {db.Locale}, 378 | "upload_id": {uploadid}, 379 | "overwrite": {strconv.FormatBool(overwrite)}, 380 | } 381 | if len(parentRev) != 0 { 382 | params.Set("parent_rev", parentRev) 383 | } 384 | rawurl = fmt.Sprintf("%s/commit_chunked_upload/%s/%s?%s", db.APIContentURL, db.RootDirectory, urlEncode(dst), params.Encode()) 385 | 386 | if response, err = db.client().Post(rawurl, "", nil); err != nil { 387 | return nil, err 388 | } 389 | defer response.Body.Close() 390 | if body, err = getResponse(response); err != nil { 391 | return nil, err 392 | } 393 | err = json.Unmarshal(body, &rv) 394 | return &rv, err 395 | } 396 | 397 | // ChunkedUpload sends a chunk with a maximum size of chunksize, if there is no session a new one is created. 398 | func (db *Dropbox) ChunkedUpload(session *ChunkUploadResponse, input io.ReadCloser, chunksize int) (*ChunkUploadResponse, error) { 399 | var err error 400 | var rawurl string 401 | var cur ChunkUploadResponse 402 | var response *http.Response 403 | var body []byte 404 | var r *io.LimitedReader 405 | 406 | if chunksize <= 0 { 407 | chunksize = DefaultChunkSize 408 | } else if chunksize > MaxPutFileSize { 409 | chunksize = MaxPutFileSize 410 | } 411 | 412 | if session != nil { 413 | rawurl = fmt.Sprintf("%s/chunked_upload?upload_id=%s&offset=%d", db.APIContentURL, session.UploadID, session.Offset) 414 | } else { 415 | rawurl = fmt.Sprintf("%s/chunked_upload", db.APIContentURL) 416 | } 417 | r = &io.LimitedReader{R: input, N: int64(chunksize)} 418 | 419 | if response, err = db.client().Post(rawurl, "application/octet-stream", r); err != nil { 420 | return nil, err 421 | } 422 | defer response.Body.Close() 423 | if body, err = getResponse(response); err != nil { 424 | return nil, err 425 | } 426 | err = json.Unmarshal(body, &cur) 427 | if r.N != 0 { 428 | err = io.EOF 429 | } 430 | return &cur, err 431 | } 432 | 433 | // UploadByChunk uploads data from the input reader to the dst path on Dropbox by sending chunks of chunksize. 434 | func (db *Dropbox) UploadByChunk(input io.ReadCloser, chunksize int, dst string, overwrite bool, parentRev string) (*Entry, error) { 435 | var err error 436 | var cur *ChunkUploadResponse 437 | 438 | for err == nil { 439 | if cur, err = db.ChunkedUpload(cur, input, chunksize); err != nil && err != io.EOF { 440 | return nil, err 441 | } 442 | } 443 | return db.CommitChunkedUpload(cur.UploadID, dst, overwrite, parentRev) 444 | } 445 | 446 | // FilesPut uploads size bytes from the input reader to the dst path on Dropbox. 447 | func (db *Dropbox) FilesPut(input io.ReadCloser, size int64, dst string, overwrite bool, parentRev string) (*Entry, error) { 448 | var err error 449 | var rawurl string 450 | var rv Entry 451 | var request *http.Request 452 | var response *http.Response 453 | var params *url.Values 454 | var body []byte 455 | 456 | if size > MaxPutFileSize { 457 | return nil, fmt.Errorf("could not upload files bigger than 150MB using this method, use UploadByChunk instead") 458 | } 459 | if dst[0] == '/' { 460 | dst = dst[1:] 461 | } 462 | 463 | params = &url.Values{"overwrite": {strconv.FormatBool(overwrite)}, "locale": {db.Locale}} 464 | if len(parentRev) != 0 { 465 | params.Set("parent_rev", parentRev) 466 | } 467 | rawurl = fmt.Sprintf("%s/files_put/%s/%s?%s", db.APIContentURL, db.RootDirectory, urlEncode(dst), params.Encode()) 468 | 469 | if request, err = http.NewRequest("PUT", rawurl, input); err != nil { 470 | return nil, err 471 | } 472 | request.Header.Set("Content-Length", strconv.FormatInt(size, 10)) 473 | if response, err = db.client().Do(request); err != nil { 474 | return nil, err 475 | } 476 | defer response.Body.Close() 477 | if body, err = getResponse(response); err != nil { 478 | return nil, err 479 | } 480 | err = json.Unmarshal(body, &rv) 481 | return &rv, err 482 | } 483 | 484 | // UploadFile uploads the file located in the src path on the local disk to the dst path on Dropbox. 485 | func (db *Dropbox) UploadFile(src, dst string, overwrite bool, parentRev string) (*Entry, error) { 486 | var err error 487 | var fd *os.File 488 | var fsize int64 489 | 490 | if fd, err = os.Open(src); err != nil { 491 | return nil, err 492 | } 493 | defer fd.Close() 494 | 495 | if fi, err := fd.Stat(); err == nil { 496 | fsize = fi.Size() 497 | } else { 498 | return nil, err 499 | } 500 | return db.FilesPut(fd, fsize, dst, overwrite, parentRev) 501 | } 502 | 503 | // Thumbnails gets a thumbnail for an image. 504 | func (db *Dropbox) Thumbnails(src, format, size string) (io.ReadCloser, int64, *Entry, error) { 505 | var response *http.Response 506 | var rawurl string 507 | var err error 508 | var entry Entry 509 | 510 | switch format { 511 | case "": 512 | format = "jpeg" 513 | case "jpeg", "png": 514 | break 515 | default: 516 | return nil, 0, nil, fmt.Errorf("unsupported format '%s' must be jpeg or png", format) 517 | } 518 | switch size { 519 | case "": 520 | size = "s" 521 | case "xs", "s", "m", "l", "xl": 522 | break 523 | default: 524 | return nil, 0, nil, fmt.Errorf("unsupported size '%s' must be xs, s, m, l or xl", size) 525 | 526 | } 527 | if src[0] == '/' { 528 | src = src[1:] 529 | } 530 | rawurl = fmt.Sprintf("%s/thumbnails/%s/%s?format=%s&size=%s", db.APIContentURL, db.RootDirectory, urlEncode(src), urlEncode(format), urlEncode(size)) 531 | if response, err = db.client().Get(rawurl); err != nil { 532 | return nil, 0, nil, err 533 | } 534 | if response.StatusCode == http.StatusOK { 535 | json.Unmarshal([]byte(response.Header.Get("x-dropbox-metadata")), &entry) 536 | return response.Body, response.ContentLength, &entry, err 537 | } 538 | response.Body.Close() 539 | switch response.StatusCode { 540 | case http.StatusNotFound: 541 | return nil, 0, nil, os.ErrNotExist 542 | case http.StatusUnsupportedMediaType: 543 | return nil, 0, nil, newErrorf(response.StatusCode, "the image located at '%s' cannot be converted to a thumbnail", src) 544 | default: 545 | return nil, 0, nil, newErrorf(response.StatusCode, "unexpected HTTP status code %d", response.StatusCode) 546 | } 547 | } 548 | 549 | // ThumbnailsToFile downloads the file located in the src path on the Dropbox to the dst file on the local disk. 550 | func (db *Dropbox) ThumbnailsToFile(src, dst, format, size string) (*Entry, error) { 551 | var input io.ReadCloser 552 | var fd *os.File 553 | var err error 554 | var entry *Entry 555 | 556 | if fd, err = os.Create(dst); err != nil { 557 | return nil, err 558 | } 559 | defer fd.Close() 560 | 561 | if input, _, entry, err = db.Thumbnails(src, format, size); err != nil { 562 | os.Remove(dst) 563 | return nil, err 564 | } 565 | defer input.Close() 566 | if _, err = io.Copy(fd, input); err != nil { 567 | os.Remove(dst) 568 | } 569 | return entry, err 570 | } 571 | 572 | // Download requests the file located at src, the specific revision may be given. 573 | // offset is used in case the download was interrupted. 574 | // A io.ReadCloser and the file size is returned. 575 | func (db *Dropbox) Download(src, rev string, offset int) (io.ReadCloser, int64, error) { 576 | var request *http.Request 577 | var response *http.Response 578 | var rawurl string 579 | var err error 580 | 581 | if src[0] == '/' { 582 | src = src[1:] 583 | } 584 | 585 | rawurl = fmt.Sprintf("%s/files/%s/%s", db.APIContentURL, db.RootDirectory, urlEncode(src)) 586 | if len(rev) != 0 { 587 | rawurl += fmt.Sprintf("?rev=%s", rev) 588 | } 589 | if request, err = http.NewRequest("GET", rawurl, nil); err != nil { 590 | return nil, 0, err 591 | } 592 | if offset != 0 { 593 | request.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) 594 | } 595 | 596 | if response, err = db.client().Do(request); err != nil { 597 | return nil, 0, err 598 | } 599 | if response.StatusCode == http.StatusOK || response.StatusCode == http.StatusPartialContent { 600 | return response.Body, response.ContentLength, err 601 | } 602 | response.Body.Close() 603 | switch response.StatusCode { 604 | case http.StatusNotFound: 605 | return nil, 0, os.ErrNotExist 606 | default: 607 | return nil, 0, newErrorf(response.StatusCode, "unexpected HTTP status code %d", response.StatusCode) 608 | } 609 | } 610 | 611 | // DownloadToFileResume resumes the download of the file located in the src path on the Dropbox to the dst file on the local disk. 612 | func (db *Dropbox) DownloadToFileResume(src, dst, rev string) error { 613 | var input io.ReadCloser 614 | var fi os.FileInfo 615 | var fd *os.File 616 | var offset int 617 | var err error 618 | 619 | if fd, err = os.OpenFile(dst, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil { 620 | return err 621 | } 622 | defer fd.Close() 623 | if fi, err = fd.Stat(); err != nil { 624 | return err 625 | } 626 | offset = int(fi.Size()) 627 | 628 | if input, _, err = db.Download(src, rev, offset); err != nil { 629 | return err 630 | } 631 | defer input.Close() 632 | _, err = io.Copy(fd, input) 633 | return err 634 | } 635 | 636 | // DownloadToFile downloads the file located in the src path on the Dropbox to the dst file on the local disk. 637 | // If the destination file exists it will be truncated. 638 | func (db *Dropbox) DownloadToFile(src, dst, rev string) error { 639 | var input io.ReadCloser 640 | var fd *os.File 641 | var err error 642 | 643 | if fd, err = os.Create(dst); err != nil { 644 | return err 645 | } 646 | defer fd.Close() 647 | 648 | if input, _, err = db.Download(src, rev, 0); err != nil { 649 | os.Remove(dst) 650 | return err 651 | } 652 | defer input.Close() 653 | if _, err = io.Copy(fd, input); err != nil { 654 | os.Remove(dst) 655 | } 656 | return err 657 | } 658 | 659 | func (db *Dropbox) doRequest(method, path string, params *url.Values, receiver interface{}) error { 660 | var body []byte 661 | var rawurl string 662 | var response *http.Response 663 | var request *http.Request 664 | var err error 665 | 666 | if params == nil { 667 | params = &url.Values{"locale": {db.Locale}} 668 | } else { 669 | params.Set("locale", db.Locale) 670 | } 671 | rawurl = fmt.Sprintf("%s/%s?%s", db.APIURL, urlEncode(path), params.Encode()) 672 | if request, err = http.NewRequest(method, rawurl, nil); err != nil { 673 | return err 674 | } 675 | if response, err = db.client().Do(request); err != nil { 676 | return err 677 | } 678 | defer response.Body.Close() 679 | if body, err = getResponse(response); err != nil { 680 | return err 681 | } 682 | err = json.Unmarshal(body, receiver) 683 | return err 684 | } 685 | 686 | // GetAccountInfo gets account information for the user currently authenticated. 687 | func (db *Dropbox) GetAccountInfo() (*Account, error) { 688 | var rv Account 689 | 690 | err := db.doRequest("GET", "account/info", nil, &rv) 691 | return &rv, err 692 | } 693 | 694 | // Shares shares a file. 695 | func (db *Dropbox) Shares(path string, shortURL bool) (*Link, error) { 696 | var rv Link 697 | var params *url.Values 698 | 699 | params = &url.Values{"short_url": {strconv.FormatBool(shortURL)}} 700 | act := strings.Join([]string{"shares", db.RootDirectory, path}, "/") 701 | err := db.doRequest("POST", act, params, &rv) 702 | return &rv, err 703 | } 704 | 705 | // Media shares a file for streaming (direct access). 706 | func (db *Dropbox) Media(path string) (*Link, error) { 707 | var rv Link 708 | 709 | act := strings.Join([]string{"media", db.RootDirectory, path}, "/") 710 | err := db.doRequest("POST", act, nil, &rv) 711 | return &rv, err 712 | } 713 | 714 | // Search searches the entries matching all the words contained in query in the given path. 715 | // The maximum number of entries and whether to consider deleted file may be given. 716 | func (db *Dropbox) Search(path, query string, fileLimit int, includeDeleted bool) ([]Entry, error) { 717 | var rv []Entry 718 | var params *url.Values 719 | 720 | if fileLimit <= 0 || fileLimit > SearchLimitMax { 721 | fileLimit = SearchLimitDefault 722 | } 723 | params = &url.Values{ 724 | "query": {query}, 725 | "file_limit": {strconv.FormatInt(int64(fileLimit), 10)}, 726 | "include_deleted": {strconv.FormatBool(includeDeleted)}, 727 | } 728 | act := strings.Join([]string{"search", db.RootDirectory, path}, "/") 729 | err := db.doRequest("GET", act, params, &rv) 730 | return rv, err 731 | } 732 | 733 | // Delta gets modifications since the cursor. 734 | func (db *Dropbox) Delta(cursor, pathPrefix string) (*DeltaPage, error) { 735 | var rv DeltaPage 736 | var params *url.Values 737 | type deltaPageParser struct { 738 | Reset bool `json:"reset"` // if true the local state must be cleared. 739 | HasMore bool `json:"has_more"` // if true an other call to delta should be made. 740 | Cursor // Tag of the current state. 741 | Entries [][]json.RawMessage `json:"entries"` // List of changed entries. 742 | } 743 | var dpp deltaPageParser 744 | 745 | params = &url.Values{} 746 | if len(cursor) != 0 { 747 | params.Set("cursor", cursor) 748 | } 749 | if len(pathPrefix) != 0 { 750 | params.Set("path_prefix", pathPrefix) 751 | } 752 | err := db.doRequest("POST", "delta", params, &dpp) 753 | rv = DeltaPage{Reset: dpp.Reset, HasMore: dpp.HasMore, Cursor: dpp.Cursor} 754 | rv.Entries = make([]DeltaEntry, 0, len(dpp.Entries)) 755 | for _, jentry := range dpp.Entries { 756 | var path string 757 | var entry Entry 758 | 759 | if len(jentry) != 2 { 760 | return nil, fmt.Errorf("malformed reply") 761 | } 762 | 763 | if err = json.Unmarshal(jentry[0], &path); err != nil { 764 | return nil, err 765 | } 766 | if err = json.Unmarshal(jentry[1], &entry); err != nil { 767 | return nil, err 768 | } 769 | if entry.Path == "" { 770 | rv.Entries = append(rv.Entries, DeltaEntry{Path: path, Entry: nil}) 771 | } else { 772 | rv.Entries = append(rv.Entries, DeltaEntry{Path: path, Entry: &entry}) 773 | } 774 | } 775 | return &rv, err 776 | } 777 | 778 | // LongPollDelta waits for a notification to happen. 779 | func (db *Dropbox) LongPollDelta(cursor string, timeout int) (*DeltaPoll, error) { 780 | var rv DeltaPoll 781 | var params *url.Values 782 | var body []byte 783 | var rawurl string 784 | var response *http.Response 785 | var err error 786 | var client http.Client 787 | 788 | params = &url.Values{} 789 | if timeout != 0 { 790 | if timeout < PollMinTimeout || timeout > PollMaxTimeout { 791 | return nil, fmt.Errorf("timeout out of range [%d; %d]", PollMinTimeout, PollMaxTimeout) 792 | } 793 | params.Set("timeout", strconv.FormatInt(int64(timeout), 10)) 794 | } 795 | params.Set("cursor", cursor) 796 | rawurl = fmt.Sprintf("%s/longpoll_delta?%s", db.APINotifyURL, params.Encode()) 797 | if response, err = client.Get(rawurl); err != nil { 798 | return nil, err 799 | } 800 | defer response.Body.Close() 801 | if body, err = getResponse(response); err != nil { 802 | return nil, err 803 | } 804 | err = json.Unmarshal(body, &rv) 805 | return &rv, err 806 | } 807 | 808 | // Metadata gets the metadata for a file or a directory. 809 | // If list is true and src is a directory, immediate child will be sent in the Contents field. 810 | // If include_deleted is true, entries deleted will be sent. 811 | // hash is the hash of the contents of a directory, it is used to avoid sending data when directory did not change. 812 | // rev is the specific revision to get the metadata from. 813 | // limit is the maximum number of entries requested. 814 | func (db *Dropbox) Metadata(src string, list bool, includeDeleted bool, hash, rev string, limit int) (*Entry, error) { 815 | var rv Entry 816 | var params *url.Values 817 | 818 | if limit <= 0 { 819 | limit = MetadataLimitDefault 820 | } else if limit > MetadataLimitMax { 821 | limit = MetadataLimitMax 822 | } 823 | params = &url.Values{ 824 | "list": {strconv.FormatBool(list)}, 825 | "include_deleted": {strconv.FormatBool(includeDeleted)}, 826 | "file_limit": {strconv.FormatInt(int64(limit), 10)}, 827 | } 828 | if len(rev) != 0 { 829 | params.Set("rev", rev) 830 | } 831 | if len(hash) != 0 { 832 | params.Set("hash", hash) 833 | } 834 | 835 | act := strings.Join([]string{"metadata", db.RootDirectory, src}, "/") 836 | err := db.doRequest("GET", act, params, &rv) 837 | return &rv, err 838 | } 839 | 840 | // CopyRef gets a reference to a file. 841 | // This reference can be used to copy this file to another user's Dropbox by passing it to the Copy method. 842 | func (db *Dropbox) CopyRef(src string) (*CopyRef, error) { 843 | var rv CopyRef 844 | act := strings.Join([]string{"copy_ref", db.RootDirectory, src}, "/") 845 | err := db.doRequest("GET", act, nil, &rv) 846 | return &rv, err 847 | } 848 | 849 | // Revisions gets the list of revisions for a file. 850 | func (db *Dropbox) Revisions(src string, revLimit int) ([]Entry, error) { 851 | var rv []Entry 852 | if revLimit <= 0 { 853 | revLimit = RevisionsLimitDefault 854 | } else if revLimit > RevisionsLimitMax { 855 | revLimit = RevisionsLimitMax 856 | } 857 | act := strings.Join([]string{"revisions", db.RootDirectory, src}, "/") 858 | err := db.doRequest("GET", act, 859 | &url.Values{"rev_limit": {strconv.FormatInt(int64(revLimit), 10)}}, &rv) 860 | return rv, err 861 | } 862 | 863 | // Restore restores a deleted file at the corresponding revision. 864 | func (db *Dropbox) Restore(src string, rev string) (*Entry, error) { 865 | var rv Entry 866 | act := strings.Join([]string{"restore", db.RootDirectory, src}, "/") 867 | err := db.doRequest("POST", act, &url.Values{"rev": {rev}}, &rv) 868 | return &rv, err 869 | } 870 | 871 | // Copy copies a file. 872 | // If isRef is true src must be a reference from CopyRef instead of a path. 873 | func (db *Dropbox) Copy(src, dst string, isRef bool) (*Entry, error) { 874 | var rv Entry 875 | params := &url.Values{"root": {db.RootDirectory}, "to_path": {dst}} 876 | if isRef { 877 | params.Set("from_copy_ref", src) 878 | } else { 879 | params.Set("from_path", src) 880 | } 881 | err := db.doRequest("POST", "fileops/copy", params, &rv) 882 | return &rv, err 883 | } 884 | 885 | // CreateFolder creates a new directory. 886 | func (db *Dropbox) CreateFolder(path string) (*Entry, error) { 887 | var rv Entry 888 | err := db.doRequest("POST", "fileops/create_folder", 889 | &url.Values{"root": {db.RootDirectory}, "path": {path}}, &rv) 890 | return &rv, err 891 | } 892 | 893 | // Delete removes a file or directory (it is a recursive delete). 894 | func (db *Dropbox) Delete(path string) (*Entry, error) { 895 | var rv Entry 896 | err := db.doRequest("POST", "fileops/delete", 897 | &url.Values{"root": {db.RootDirectory}, "path": {path}}, &rv) 898 | return &rv, err 899 | } 900 | 901 | // Move moves a file or directory. 902 | func (db *Dropbox) Move(src, dst string) (*Entry, error) { 903 | var rv Entry 904 | err := db.doRequest("POST", "fileops/move", 905 | &url.Values{"root": {db.RootDirectory}, 906 | "from_path": {src}, 907 | "to_path": {dst}}, &rv) 908 | return &rv, err 909 | } 910 | 911 | // LatestCursor returns the latest cursor without fetching any data. 912 | func (db *Dropbox) LatestCursor(prefix string, mediaInfo bool) (*Cursor, error) { 913 | var ( 914 | params = &url.Values{} 915 | cur Cursor 916 | ) 917 | 918 | if prefix != "" { 919 | params.Set("path_prefix", prefix) 920 | } 921 | 922 | if mediaInfo { 923 | params.Set("include_media_info", "true") 924 | } 925 | 926 | err := db.doRequest("POST", "delta/latest_cursor", params, &cur) 927 | return &cur, err 928 | } 929 | 930 | // SharedFolders returns the list of allowed shared folders. 931 | func (db *Dropbox) SharedFolders(sharedFolderID string) ([]SharedFolder, error) { 932 | var sharedFolders []SharedFolder 933 | var err error 934 | 935 | if sharedFolderID != "" { 936 | sharedFolders = make([]SharedFolder, 1) 937 | err = db.doRequest("GET", "/shared_folders/"+sharedFolderID, nil, &sharedFolders[0]) 938 | } else { 939 | err = db.doRequest("GET", "/shared_folders/", nil, &sharedFolders) 940 | } 941 | return sharedFolders, err 942 | } 943 | --------------------------------------------------------------------------------