mirror of
https://github.com/rclone/rclone.git
synced 2025-12-20 10:13:20 +00:00
Use a vendor directory for repeatable builds - fixes #816
This is using godep to manage the vendor directory.
This commit is contained in:
7
vendor/github.com/stacktic/dropbox/.travis.yml
generated
vendored
Normal file
7
vendor/github.com/stacktic/dropbox/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1.2
|
||||
- 1.2.2
|
||||
- 1.3
|
||||
- tip
|
||||
23
vendor/github.com/stacktic/dropbox/LICENSE
generated
vendored
Normal file
23
vendor/github.com/stacktic/dropbox/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2014, Arnaud Ysmal
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
79
vendor/github.com/stacktic/dropbox/README.md
generated
vendored
Normal file
79
vendor/github.com/stacktic/dropbox/README.md
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
dropbox
|
||||
=======
|
||||
Go client library for the Dropbox core and Datastore API with support for uploading and downloading encrypted files.
|
||||
|
||||
Support of the Datastore API should be considered as a beta version.
|
||||
|
||||
Prerequisite
|
||||
------------
|
||||
To use this library, you must have a valid client ID (app key) and client secret (app secret) provided by Dropbox.<br>
|
||||
To register a new client application, please visit https://www.dropbox.com/developers/apps/create
|
||||
|
||||
Installation
|
||||
------------
|
||||
This library depends on the oauth2 package, it can be installed with the go get command:
|
||||
|
||||
$ go get golang.org/x/oauth2
|
||||
|
||||
This package can be installed with the go get command:
|
||||
|
||||
$ go get github.com/stacktic/dropbox
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
This simple 4-step example will show you how to create a folder:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"dropbox"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var db *dropbox.Dropbox
|
||||
|
||||
var clientid, clientsecret string
|
||||
var token string
|
||||
|
||||
clientid = "xxxxx"
|
||||
clientsecret = "yyyyy"
|
||||
token = "zzzz"
|
||||
|
||||
// 1. Create a new dropbox object.
|
||||
db = dropbox.NewDropbox()
|
||||
|
||||
// 2. Provide your clientid and clientsecret (see prerequisite).
|
||||
db.SetAppInfo(clientid, clientsecret)
|
||||
|
||||
// 3. Provide the user token.
|
||||
db.SetAccessToken(token)
|
||||
|
||||
// 4. Send your commands.
|
||||
// In this example, you will create a new folder named "demo".
|
||||
folder := "demo"
|
||||
if _, err = db.CreateFolder(folder); err != nil {
|
||||
fmt.Printf("Error creating folder %s: %s\n", folder, err)
|
||||
} else {
|
||||
fmt.Printf("Folder %s successfully created\n", folder)
|
||||
}
|
||||
}
|
||||
|
||||
If you do not know the user token, you can replace step 3 by a call to the Auth method:
|
||||
|
||||
// This method will ask the user to visit an URL and paste the generated code.
|
||||
if err = db.Auth(); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
// You can now retrieve the token if you want.
|
||||
token = db.AccessToken()
|
||||
|
||||
If you want a more complete example, please check the following project: https://github.com/stacktic/dbox.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
API documentation can be found here: http://godoc.org/github.com/stacktic/dropbox.
|
||||
228
vendor/github.com/stacktic/dropbox/crypto.go
generated
vendored
Normal file
228
vendor/github.com/stacktic/dropbox/crypto.go
generated
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
** SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package dropbox
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GenerateKey generates a key by reading length bytes from /dev/random
|
||||
func GenerateKey(length int) ([]byte, error) {
|
||||
var err error
|
||||
var fd io.Reader
|
||||
var rv []byte
|
||||
|
||||
if fd, err = os.Open("/dev/random"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rv = make([]byte, length)
|
||||
_, err = io.ReadFull(fd, rv)
|
||||
return rv, err
|
||||
}
|
||||
|
||||
func newCrypter(key []byte, in io.Reader, size int, newCipher func(key []byte) (cipher.Block, error)) (io.ReadCloser, int, error) {
|
||||
var block cipher.Block
|
||||
var err error
|
||||
|
||||
if block, err = newCipher(key); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
outsize := size - size%block.BlockSize() + 2*block.BlockSize()
|
||||
|
||||
rd, wr := io.Pipe()
|
||||
go encrypt(block, in, size, wr)
|
||||
return rd, outsize, nil
|
||||
}
|
||||
|
||||
func newDecrypter(key []byte, in io.Reader, size int, newCipher func(key []byte) (cipher.Block, error)) (io.ReadCloser, error) {
|
||||
var block cipher.Block
|
||||
var err error
|
||||
|
||||
if block, err = newCipher(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd, wr := io.Pipe()
|
||||
go decrypt(block, in, size, wr)
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
// NewAESDecrypterReader creates and returns a new io.ReadCloser to decrypt the given io.Reader containing size bytes with the given AES key.
|
||||
// The AES key should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
|
||||
func NewAESDecrypterReader(key []byte, input io.Reader, size int) (io.ReadCloser, error) {
|
||||
return newDecrypter(key, input, size, aes.NewCipher)
|
||||
}
|
||||
|
||||
// NewAESCrypterReader creates and returns a new io.ReadCloser to encrypt the given io.Reader containing size bytes with the given AES key.
|
||||
// The AES key should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
|
||||
func NewAESCrypterReader(key []byte, input io.Reader, size int) (io.ReadCloser, int, error) {
|
||||
return newCrypter(key, input, size, aes.NewCipher)
|
||||
}
|
||||
|
||||
func encrypt(block cipher.Block, in io.Reader, size int, out io.WriteCloser) error {
|
||||
var err error
|
||||
var rd int
|
||||
var buf []byte
|
||||
var last bool
|
||||
var encrypter cipher.BlockMode
|
||||
|
||||
defer out.Close()
|
||||
|
||||
buf = make([]byte, block.BlockSize())
|
||||
|
||||
if _, err = io.ReadFull(rand.Reader, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
encrypter = cipher.NewCBCEncrypter(block, buf)
|
||||
|
||||
if _, err = out.Write(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
for !last {
|
||||
if rd, err = io.ReadFull(in, buf); err != nil {
|
||||
if err == io.ErrUnexpectedEOF || err == io.EOF {
|
||||
buf = buf[:rd]
|
||||
buf = append(buf, 0x80)
|
||||
for len(buf) < block.BlockSize() {
|
||||
buf = append(buf, 0x00)
|
||||
}
|
||||
last = true
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
encrypter.CryptBlocks(buf, buf)
|
||||
if _, err = out.Write(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decrypt(block cipher.Block, in io.Reader, size int, out io.WriteCloser) error {
|
||||
var err error
|
||||
var buf []byte
|
||||
var count int
|
||||
var decrypter cipher.BlockMode
|
||||
|
||||
defer out.Close()
|
||||
|
||||
buf = make([]byte, block.BlockSize())
|
||||
if _, err = io.ReadFull(in, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
decrypter = cipher.NewCBCDecrypter(block, buf)
|
||||
|
||||
count = (size - block.BlockSize()) / block.BlockSize()
|
||||
for count > 0 && err == nil {
|
||||
if _, err = io.ReadFull(in, buf); err == nil {
|
||||
decrypter.CryptBlocks(buf, buf)
|
||||
if count == 1 {
|
||||
for count = block.BlockSize() - 1; buf[count] == 0x00; count-- {
|
||||
continue
|
||||
}
|
||||
if buf[count] == 0x80 {
|
||||
buf = buf[:count]
|
||||
}
|
||||
}
|
||||
_, err = out.Write(buf)
|
||||
}
|
||||
count--
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// FilesPutAES uploads and encrypts size bytes from the input reader to the dst path on Dropbox.
|
||||
func (db *Dropbox) FilesPutAES(key []byte, input io.ReadCloser, size int64, dst string, overwrite bool, parentRev string) (*Entry, error) {
|
||||
var encreader io.ReadCloser
|
||||
var outsize int
|
||||
var err error
|
||||
|
||||
if encreader, outsize, err = NewAESCrypterReader(key, input, int(size)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.FilesPut(encreader, int64(outsize), dst, overwrite, parentRev)
|
||||
}
|
||||
|
||||
// UploadFileAES uploads and encrypts the file located in the src path on the local disk to the dst path on Dropbox.
|
||||
func (db *Dropbox) UploadFileAES(key []byte, src, dst string, overwrite bool, parentRev string) (*Entry, error) {
|
||||
var err error
|
||||
var fd *os.File
|
||||
var fsize int64
|
||||
|
||||
if fd, err = os.Open(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if fi, err := fd.Stat(); err == nil {
|
||||
fsize = fi.Size()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return db.FilesPutAES(key, fd, fsize, dst, overwrite, parentRev)
|
||||
}
|
||||
|
||||
// DownloadAES downloads and decrypts the file located in the src path on Dropbox and returns a io.ReadCloser.
|
||||
func (db *Dropbox) DownloadAES(key []byte, src, rev string, offset int) (io.ReadCloser, error) {
|
||||
var in io.ReadCloser
|
||||
var size int64
|
||||
var err error
|
||||
|
||||
if in, size, err = db.Download(src, rev, int64(offset)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewAESDecrypterReader(key, in, int(size))
|
||||
}
|
||||
|
||||
// DownloadToFileAES downloads and decrypts the file located in the src path on Dropbox to the dst file on the local disk.
|
||||
func (db *Dropbox) DownloadToFileAES(key []byte, src, dst, rev string) error {
|
||||
var input io.ReadCloser
|
||||
var fd *os.File
|
||||
var err error
|
||||
|
||||
if fd, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if input, err = db.DownloadAES(key, src, rev, 0); err != nil {
|
||||
os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
defer input.Close()
|
||||
if _, err = io.Copy(fd, input); err != nil {
|
||||
os.Remove(dst)
|
||||
}
|
||||
return err
|
||||
}
|
||||
620
vendor/github.com/stacktic/dropbox/datastores.go
generated
vendored
Normal file
620
vendor/github.com/stacktic/dropbox/datastores.go
generated
vendored
Normal file
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
** SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package dropbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// List represents a value of type list.
|
||||
type List struct {
|
||||
record *Record
|
||||
field string
|
||||
values []interface{}
|
||||
}
|
||||
|
||||
// Fields represents a record.
|
||||
type Fields map[string]value
|
||||
|
||||
// Record represents an entry in a table.
|
||||
type Record struct {
|
||||
table *Table
|
||||
recordID string
|
||||
fields Fields
|
||||
isDeleted bool
|
||||
}
|
||||
|
||||
// Table represents a list of records.
|
||||
type Table struct {
|
||||
datastore *Datastore
|
||||
tableID string
|
||||
records map[string]*Record
|
||||
}
|
||||
|
||||
// DatastoreInfo represents the information about a datastore.
|
||||
type DatastoreInfo struct {
|
||||
ID string
|
||||
handle string
|
||||
revision int
|
||||
title string
|
||||
mtime time.Time
|
||||
}
|
||||
|
||||
type datastoreDelta struct {
|
||||
Revision int `json:"rev"`
|
||||
Changes listOfChanges `json:"changes"`
|
||||
Nonce *string `json:"nonce"`
|
||||
}
|
||||
|
||||
type listOfDelta []datastoreDelta
|
||||
|
||||
// Datastore represents a datastore.
|
||||
type Datastore struct {
|
||||
manager *DatastoreManager
|
||||
info DatastoreInfo
|
||||
changes listOfChanges
|
||||
tables map[string]*Table
|
||||
isDeleted bool
|
||||
autoCommit bool
|
||||
changesQueue chan changeWork
|
||||
}
|
||||
|
||||
// DatastoreManager represents all datastores linked to the current account.
|
||||
type DatastoreManager struct {
|
||||
dropbox *Dropbox
|
||||
datastores []*Datastore
|
||||
token string
|
||||
}
|
||||
|
||||
const (
|
||||
defaultDatastoreID = "default"
|
||||
maxGlobalIDLength = 63
|
||||
maxIDLength = 64
|
||||
|
||||
localIDPattern = `[a-z0-9_-]([a-z0-9._-]{0,62}[a-z0-9_-])?`
|
||||
globalIDPattern = `.[A-Za-z0-9_-]{1,63}`
|
||||
fieldsIDPattern = `[A-Za-z0-9._+/=-]{1,64}`
|
||||
fieldsSpecialIDPattern = `:[A-Za-z0-9._+/=-]{1,63}`
|
||||
)
|
||||
|
||||
var (
|
||||
localIDRegexp *regexp.Regexp
|
||||
globalIDRegexp *regexp.Regexp
|
||||
fieldsIDRegexp *regexp.Regexp
|
||||
fieldsSpecialIDRegexp *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
if localIDRegexp, err = regexp.Compile(localIDPattern); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
if globalIDRegexp, err = regexp.Compile(globalIDPattern); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
if fieldsIDRegexp, err = regexp.Compile(fieldsIDPattern); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
if fieldsSpecialIDRegexp, err = regexp.Compile(fieldsSpecialIDPattern); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func isValidDatastoreID(ID string) bool {
|
||||
if ID[0] == '.' {
|
||||
return globalIDRegexp.MatchString(ID)
|
||||
}
|
||||
return localIDRegexp.MatchString(ID)
|
||||
}
|
||||
|
||||
func isValidID(ID string) bool {
|
||||
if ID[0] == ':' {
|
||||
return fieldsSpecialIDRegexp.MatchString(ID)
|
||||
}
|
||||
return fieldsIDRegexp.MatchString(ID)
|
||||
}
|
||||
|
||||
const (
|
||||
// TypeBoolean is the returned type when the value is a bool
|
||||
TypeBoolean AtomType = iota
|
||||
// TypeInteger is the returned type when the value is an int
|
||||
TypeInteger
|
||||
// TypeDouble is the returned type when the value is a float
|
||||
TypeDouble
|
||||
// TypeString is the returned type when the value is a string
|
||||
TypeString
|
||||
// TypeBytes is the returned type when the value is a []byte
|
||||
TypeBytes
|
||||
// TypeDate is the returned type when the value is a Date
|
||||
TypeDate
|
||||
// TypeList is the returned type when the value is a List
|
||||
TypeList
|
||||
)
|
||||
|
||||
// AtomType represents the type of the value.
|
||||
type AtomType int
|
||||
|
||||
// NewDatastoreManager returns a new DatastoreManager linked to the current account.
|
||||
func (db *Dropbox) NewDatastoreManager() *DatastoreManager {
|
||||
return &DatastoreManager{
|
||||
dropbox: db,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenDatastore opens or creates a datastore.
|
||||
func (dmgr *DatastoreManager) OpenDatastore(dsID string) (*Datastore, error) {
|
||||
rev, handle, _, err := dmgr.dropbox.openOrCreateDatastore(dsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rv := &Datastore{
|
||||
manager: dmgr,
|
||||
info: DatastoreInfo{
|
||||
ID: dsID,
|
||||
handle: handle,
|
||||
revision: rev,
|
||||
},
|
||||
tables: make(map[string]*Table),
|
||||
changesQueue: make(chan changeWork),
|
||||
}
|
||||
if rev > 0 {
|
||||
err = rv.LoadSnapshot()
|
||||
}
|
||||
go rv.doHandleChange()
|
||||
return rv, err
|
||||
}
|
||||
|
||||
// OpenDefaultDatastore opens the default datastore.
|
||||
func (dmgr *DatastoreManager) OpenDefaultDatastore() (*Datastore, error) {
|
||||
return dmgr.OpenDatastore(defaultDatastoreID)
|
||||
}
|
||||
|
||||
// ListDatastores lists all datastores.
|
||||
func (dmgr *DatastoreManager) ListDatastores() ([]DatastoreInfo, error) {
|
||||
info, _, err := dmgr.dropbox.listDatastores()
|
||||
return info, err
|
||||
}
|
||||
|
||||
// DeleteDatastore deletes a datastore.
|
||||
func (dmgr *DatastoreManager) DeleteDatastore(dsID string) error {
|
||||
_, err := dmgr.dropbox.deleteDatastore(dsID)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateDatastore creates a global datastore with a unique ID, empty string for a random key.
|
||||
func (dmgr *DatastoreManager) CreateDatastore(dsID string) (*Datastore, error) {
|
||||
rev, handle, _, err := dmgr.dropbox.createDatastore(dsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Datastore{
|
||||
manager: dmgr,
|
||||
info: DatastoreInfo{
|
||||
ID: dsID,
|
||||
handle: handle,
|
||||
revision: rev,
|
||||
},
|
||||
tables: make(map[string]*Table),
|
||||
changesQueue: make(chan changeWork),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AwaitDeltas awaits for deltas and applies them.
|
||||
func (ds *Datastore) AwaitDeltas() error {
|
||||
if len(ds.changes) != 0 {
|
||||
return fmt.Errorf("changes already pending")
|
||||
}
|
||||
_, _, deltas, err := ds.manager.dropbox.await([]*Datastore{ds}, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changes, ok := deltas[ds.info.handle]
|
||||
if !ok || len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ds.applyDelta(changes)
|
||||
}
|
||||
|
||||
func (ds *Datastore) applyDelta(dds []datastoreDelta) error {
|
||||
if len(ds.changes) != 0 {
|
||||
return fmt.Errorf("changes already pending")
|
||||
}
|
||||
for _, d := range dds {
|
||||
if d.Revision < ds.info.revision {
|
||||
continue
|
||||
}
|
||||
for _, c := range d.Changes {
|
||||
ds.applyChange(c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the datastore.
|
||||
func (ds *Datastore) Close() {
|
||||
close(ds.changesQueue)
|
||||
}
|
||||
|
||||
// Delete deletes the datastore.
|
||||
func (ds *Datastore) Delete() error {
|
||||
return ds.manager.DeleteDatastore(ds.info.ID)
|
||||
}
|
||||
|
||||
// SetTitle sets the datastore title to the given string.
|
||||
func (ds *Datastore) SetTitle(t string) error {
|
||||
if len(ds.info.title) == 0 {
|
||||
return ds.insertRecord(":info", "info", Fields{
|
||||
"title": value{
|
||||
values: []interface{}{t},
|
||||
},
|
||||
})
|
||||
}
|
||||
return ds.updateField(":info", "info", "title", t)
|
||||
}
|
||||
|
||||
// SetMTime sets the datastore mtime to the given time.
|
||||
func (ds *Datastore) SetMTime(t time.Time) error {
|
||||
if time.Time(ds.info.mtime).IsZero() {
|
||||
return ds.insertRecord(":info", "info", Fields{
|
||||
"mtime": value{
|
||||
values: []interface{}{t},
|
||||
},
|
||||
})
|
||||
}
|
||||
return ds.updateField(":info", "info", "mtime", t)
|
||||
}
|
||||
|
||||
// Rollback reverts all local changes and discards them.
|
||||
func (ds *Datastore) Rollback() error {
|
||||
if len(ds.changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := len(ds.changes) - 1; i >= 0; i-- {
|
||||
ds.applyChange(ds.changes[i].Revert)
|
||||
}
|
||||
ds.changes = ds.changes[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTable returns the requested table.
|
||||
func (ds *Datastore) GetTable(tableID string) (*Table, error) {
|
||||
if !isValidID(tableID) {
|
||||
return nil, fmt.Errorf("invalid table ID %s", tableID)
|
||||
}
|
||||
t, ok := ds.tables[tableID]
|
||||
if ok {
|
||||
return t, nil
|
||||
}
|
||||
t = &Table{
|
||||
datastore: ds,
|
||||
tableID: tableID,
|
||||
records: make(map[string]*Record),
|
||||
}
|
||||
ds.tables[tableID] = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Commit commits the changes registered by sending them to the server.
|
||||
func (ds *Datastore) Commit() error {
|
||||
rev, err := ds.manager.dropbox.putDelta(ds.info.handle, ds.info.revision, ds.changes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ds.changes = ds.changes[:0]
|
||||
ds.info.revision = rev
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSnapshot updates the state of the datastore from the server.
|
||||
func (ds *Datastore) LoadSnapshot() error {
|
||||
if len(ds.changes) != 0 {
|
||||
return fmt.Errorf("could not load snapshot when there are pending changes")
|
||||
}
|
||||
rows, rev, err := ds.manager.dropbox.getSnapshot(ds.info.handle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.tables = make(map[string]*Table)
|
||||
for _, r := range rows {
|
||||
if _, ok := ds.tables[r.TID]; !ok {
|
||||
ds.tables[r.TID] = &Table{
|
||||
datastore: ds,
|
||||
tableID: r.TID,
|
||||
records: make(map[string]*Record),
|
||||
}
|
||||
}
|
||||
ds.tables[r.TID].records[r.RowID] = &Record{
|
||||
table: ds.tables[r.TID],
|
||||
recordID: r.RowID,
|
||||
fields: r.Data,
|
||||
}
|
||||
}
|
||||
ds.info.revision = rev
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatastore returns the datastore associated with this table.
|
||||
func (t *Table) GetDatastore() *Datastore {
|
||||
return t.datastore
|
||||
}
|
||||
|
||||
// GetID returns the ID of this table.
|
||||
func (t *Table) GetID() string {
|
||||
return t.tableID
|
||||
}
|
||||
|
||||
// Get returns the record with this ID.
|
||||
func (t *Table) Get(recordID string) (*Record, error) {
|
||||
if !isValidID(recordID) {
|
||||
return nil, fmt.Errorf("invalid record ID %s", recordID)
|
||||
}
|
||||
return t.records[recordID], nil
|
||||
}
|
||||
|
||||
// GetOrInsert gets the requested record.
|
||||
func (t *Table) GetOrInsert(recordID string) (*Record, error) {
|
||||
if !isValidID(recordID) {
|
||||
return nil, fmt.Errorf("invalid record ID %s", recordID)
|
||||
}
|
||||
return t.GetOrInsertWithFields(recordID, nil)
|
||||
}
|
||||
|
||||
// GetOrInsertWithFields gets the requested table.
|
||||
func (t *Table) GetOrInsertWithFields(recordID string, fields Fields) (*Record, error) {
|
||||
if !isValidID(recordID) {
|
||||
return nil, fmt.Errorf("invalid record ID %s", recordID)
|
||||
}
|
||||
if r, ok := t.records[recordID]; ok {
|
||||
return r, nil
|
||||
}
|
||||
if fields == nil {
|
||||
fields = make(Fields)
|
||||
}
|
||||
if err := t.datastore.insertRecord(t.tableID, recordID, fields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.records[recordID], nil
|
||||
}
|
||||
|
||||
// Query returns a list of records matching all the given fields.
|
||||
func (t *Table) Query(fields Fields) ([]*Record, error) {
|
||||
var records []*Record
|
||||
|
||||
next:
|
||||
for _, record := range t.records {
|
||||
for qf, qv := range fields {
|
||||
if rv, ok := record.fields[qf]; !ok || !reflect.DeepEqual(qv, rv) {
|
||||
continue next
|
||||
}
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// GetTable returns the table associated with this record.
|
||||
func (r *Record) GetTable() *Table {
|
||||
return r.table
|
||||
}
|
||||
|
||||
// GetID returns the ID of this record.
|
||||
func (r *Record) GetID() string {
|
||||
return r.recordID
|
||||
}
|
||||
|
||||
// IsDeleted returns whether this record was deleted.
|
||||
func (r *Record) IsDeleted() bool {
|
||||
return r.isDeleted
|
||||
}
|
||||
|
||||
// DeleteRecord deletes this record.
|
||||
func (r *Record) DeleteRecord() {
|
||||
r.table.datastore.deleteRecord(r.table.tableID, r.recordID)
|
||||
}
|
||||
|
||||
// HasField returns whether this field exists.
|
||||
func (r *Record) HasField(field string) (bool, error) {
|
||||
if !isValidID(field) {
|
||||
return false, fmt.Errorf("invalid field %s", field)
|
||||
}
|
||||
_, ok := r.fields[field]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// Get gets the current value of this field.
|
||||
func (r *Record) Get(field string) (interface{}, bool, error) {
|
||||
if !isValidID(field) {
|
||||
return nil, false, fmt.Errorf("invalid field %s", field)
|
||||
}
|
||||
v, ok := r.fields[field]
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
if v.isList {
|
||||
return &List{
|
||||
record: r,
|
||||
field: field,
|
||||
values: v.values,
|
||||
}, true, nil
|
||||
}
|
||||
return v.values[0], true, nil
|
||||
}
|
||||
|
||||
// GetOrCreateList gets the current value of this field.
|
||||
func (r *Record) GetOrCreateList(field string) (*List, error) {
|
||||
if !isValidID(field) {
|
||||
return nil, fmt.Errorf("invalid field %s", field)
|
||||
}
|
||||
v, ok := r.fields[field]
|
||||
if ok && !v.isList {
|
||||
return nil, fmt.Errorf("not a list")
|
||||
}
|
||||
if !ok {
|
||||
if err := r.table.datastore.listCreate(r.table.tableID, r.recordID, field); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v = r.fields[field]
|
||||
}
|
||||
return &List{
|
||||
record: r,
|
||||
field: field,
|
||||
values: v.values,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getType(i interface{}) (AtomType, error) {
|
||||
switch i.(type) {
|
||||
case bool:
|
||||
return TypeBoolean, nil
|
||||
case int, int32, int64:
|
||||
return TypeInteger, nil
|
||||
case float32, float64:
|
||||
return TypeDouble, nil
|
||||
case string:
|
||||
return TypeString, nil
|
||||
case []byte:
|
||||
return TypeBytes, nil
|
||||
case time.Time:
|
||||
return TypeDate, nil
|
||||
}
|
||||
return 0, fmt.Errorf("type %s not supported", reflect.TypeOf(i).Name())
|
||||
}
|
||||
|
||||
// GetFieldType returns the type of the given field.
|
||||
func (r *Record) GetFieldType(field string) (AtomType, error) {
|
||||
if !isValidID(field) {
|
||||
return 0, fmt.Errorf("invalid field %s", field)
|
||||
}
|
||||
v, ok := r.fields[field]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("no such field: %s", field)
|
||||
}
|
||||
if v.isList {
|
||||
return TypeList, nil
|
||||
}
|
||||
return getType(v.values[0])
|
||||
}
|
||||
|
||||
// Set sets the value of a field.
|
||||
func (r *Record) Set(field string, value interface{}) error {
|
||||
if !isValidID(field) {
|
||||
return fmt.Errorf("invalid field %s", field)
|
||||
}
|
||||
return r.table.datastore.updateField(r.table.tableID, r.recordID, field, value)
|
||||
}
|
||||
|
||||
// DeleteField deletes the given field from this record.
|
||||
func (r *Record) DeleteField(field string) error {
|
||||
if !isValidID(field) {
|
||||
return fmt.Errorf("invalid field %s", field)
|
||||
}
|
||||
return r.table.datastore.deleteField(r.table.tableID, r.recordID, field)
|
||||
}
|
||||
|
||||
// FieldNames returns a list of fields names.
|
||||
func (r *Record) FieldNames() []string {
|
||||
var rv []string
|
||||
|
||||
rv = make([]string, 0, len(r.fields))
|
||||
for k := range r.fields {
|
||||
rv = append(rv, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
// IsEmpty returns whether the list contains an element.
|
||||
func (l *List) IsEmpty() bool {
|
||||
return len(l.values) == 0
|
||||
}
|
||||
|
||||
// Size returns the number of elements in the list.
|
||||
func (l *List) Size() int {
|
||||
return len(l.values)
|
||||
}
|
||||
|
||||
// GetType gets the type of the n-th element in the list.
|
||||
func (l *List) GetType(n int) (AtomType, error) {
|
||||
if n >= len(l.values) {
|
||||
return 0, fmt.Errorf("out of bound index")
|
||||
}
|
||||
return getType(l.values[n])
|
||||
}
|
||||
|
||||
// Get gets the n-th element in the list.
|
||||
func (l *List) Get(n int) (interface{}, error) {
|
||||
if n >= len(l.values) {
|
||||
return 0, fmt.Errorf("out of bound index")
|
||||
}
|
||||
return l.values[n], nil
|
||||
}
|
||||
|
||||
// AddAtPos inserts the item at the n-th position in the list.
|
||||
func (l *List) AddAtPos(n int, i interface{}) error {
|
||||
if n > len(l.values) {
|
||||
return fmt.Errorf("out of bound index")
|
||||
}
|
||||
err := l.record.table.datastore.listInsert(l.record.table.tableID, l.record.recordID, l.field, n, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.values = l.record.fields[l.field].values
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add adds the item at the end of the list.
|
||||
func (l *List) Add(i interface{}) error {
|
||||
return l.AddAtPos(len(l.values), i)
|
||||
}
|
||||
|
||||
// Set sets the value of the n-th element of the list.
|
||||
func (l *List) Set(n int, i interface{}) error {
|
||||
if n >= len(l.values) {
|
||||
return fmt.Errorf("out of bound index")
|
||||
}
|
||||
return l.record.table.datastore.listPut(l.record.table.tableID, l.record.recordID, l.field, n, i)
|
||||
}
|
||||
|
||||
// Remove removes the n-th element of the list.
|
||||
func (l *List) Remove(n int) error {
|
||||
if n >= len(l.values) {
|
||||
return fmt.Errorf("out of bound index")
|
||||
}
|
||||
err := l.record.table.datastore.listDelete(l.record.table.tableID, l.record.recordID, l.field, n)
|
||||
l.values = l.record.fields[l.field].values
|
||||
return err
|
||||
}
|
||||
|
||||
// Move moves the element from the from-th position to the to-th.
|
||||
func (l *List) Move(from, to int) error {
|
||||
if from >= len(l.values) || to >= len(l.values) {
|
||||
return fmt.Errorf("out of bound index")
|
||||
}
|
||||
return l.record.table.datastore.listMove(l.record.table.tableID, l.record.recordID, l.field, from, to)
|
||||
}
|
||||
516
vendor/github.com/stacktic/dropbox/datastores_changes.go
generated
vendored
Normal file
516
vendor/github.com/stacktic/dropbox/datastores_changes.go
generated
vendored
Normal file
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
** SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package dropbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type value struct {
|
||||
values []interface{}
|
||||
isList bool
|
||||
}
|
||||
|
||||
type fieldOp struct {
|
||||
Op string
|
||||
Index int
|
||||
Index2 int
|
||||
Data value
|
||||
}
|
||||
|
||||
type opDict map[string]fieldOp
|
||||
|
||||
type change struct {
|
||||
Op string
|
||||
TID string
|
||||
RecordID string
|
||||
Ops opDict
|
||||
Data Fields
|
||||
Revert *change
|
||||
}
|
||||
type listOfChanges []*change
|
||||
|
||||
type changeWork struct {
|
||||
c *change
|
||||
out chan error
|
||||
}
|
||||
|
||||
const (
|
||||
recordDelete = "D"
|
||||
recordInsert = "I"
|
||||
recordUpdate = "U"
|
||||
fieldDelete = "D"
|
||||
fieldPut = "P"
|
||||
listCreate = "LC"
|
||||
listDelete = "LD"
|
||||
listInsert = "LI"
|
||||
listMove = "LM"
|
||||
listPut = "LP"
|
||||
)
|
||||
|
||||
func newValueFromInterface(i interface{}) *value {
|
||||
if a, ok := i.([]byte); ok {
|
||||
return &value{
|
||||
values: []interface{}{a},
|
||||
isList: false,
|
||||
}
|
||||
}
|
||||
if reflect.TypeOf(i).Kind() == reflect.Slice || reflect.TypeOf(i).Kind() == reflect.Array {
|
||||
val := reflect.ValueOf(i)
|
||||
v := &value{
|
||||
values: make([]interface{}, val.Len()),
|
||||
isList: true,
|
||||
}
|
||||
for i := range v.values {
|
||||
v.values[i] = val.Index(i).Interface()
|
||||
}
|
||||
return v
|
||||
}
|
||||
return &value{
|
||||
values: []interface{}{i},
|
||||
isList: false,
|
||||
}
|
||||
}
|
||||
|
||||
func newValue(v *value) *value {
|
||||
var nv *value
|
||||
|
||||
nv = &value{
|
||||
values: make([]interface{}, len(v.values)),
|
||||
isList: v.isList,
|
||||
}
|
||||
copy(nv.values, v.values)
|
||||
return nv
|
||||
}
|
||||
|
||||
func newFields(f Fields) Fields {
|
||||
var n Fields
|
||||
|
||||
n = make(Fields)
|
||||
for k, v := range f {
|
||||
n[k] = *newValue(&v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (ds *Datastore) deleteRecord(table, record string) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordDelete,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) insertRecord(table, record string, values Fields) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordInsert,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Data: newFields(values),
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) updateFields(table, record string, values map[string]interface{}) error {
|
||||
var dsval opDict
|
||||
|
||||
dsval = make(opDict)
|
||||
for k, v := range values {
|
||||
dsval[k] = fieldOp{
|
||||
Op: fieldPut,
|
||||
Data: *newValueFromInterface(v),
|
||||
}
|
||||
}
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: dsval,
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) updateField(table, record, field string, i interface{}) error {
|
||||
return ds.updateFields(table, record, map[string]interface{}{field: i})
|
||||
}
|
||||
|
||||
func (ds *Datastore) deleteField(table, record, field string) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: opDict{
|
||||
field: fieldOp{
|
||||
Op: fieldDelete,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) listCreate(table, record, field string) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: opDict{
|
||||
field: fieldOp{
|
||||
Op: listCreate,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) listDelete(table, record, field string, pos int) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: opDict{
|
||||
field: fieldOp{
|
||||
Op: listDelete,
|
||||
Index: pos,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) listInsert(table, record, field string, pos int, i interface{}) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: opDict{
|
||||
field: fieldOp{
|
||||
Op: listInsert,
|
||||
Index: pos,
|
||||
Data: *newValueFromInterface(i),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) listMove(table, record, field string, from, to int) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: opDict{
|
||||
field: fieldOp{
|
||||
Op: listMove,
|
||||
Index: from,
|
||||
Index2: to,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) listPut(table, record, field string, pos int, i interface{}) error {
|
||||
return ds.handleChange(&change{
|
||||
Op: recordUpdate,
|
||||
TID: table,
|
||||
RecordID: record,
|
||||
Ops: opDict{
|
||||
field: fieldOp{
|
||||
Op: listPut,
|
||||
Index: pos,
|
||||
Data: *newValueFromInterface(i),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) handleChange(c *change) error {
|
||||
var out chan error
|
||||
|
||||
if ds.changesQueue == nil {
|
||||
return fmt.Errorf("datastore is closed")
|
||||
}
|
||||
out = make(chan error)
|
||||
ds.changesQueue <- changeWork{
|
||||
c: c,
|
||||
out: out,
|
||||
}
|
||||
return <-out
|
||||
}
|
||||
|
||||
func (ds *Datastore) doHandleChange() {
|
||||
var err error
|
||||
var c *change
|
||||
|
||||
q := ds.changesQueue
|
||||
for cw := range q {
|
||||
c = cw.c
|
||||
|
||||
if err = ds.validateChange(c); err != nil {
|
||||
cw.out <- err
|
||||
continue
|
||||
}
|
||||
if c.Revert, err = ds.inverseChange(c); err != nil {
|
||||
cw.out <- err
|
||||
continue
|
||||
}
|
||||
|
||||
if err = ds.applyChange(c); err != nil {
|
||||
cw.out <- err
|
||||
continue
|
||||
}
|
||||
|
||||
ds.changes = append(ds.changes, c)
|
||||
|
||||
if ds.autoCommit {
|
||||
if err = ds.Commit(); err != nil {
|
||||
cw.out <- err
|
||||
}
|
||||
}
|
||||
close(cw.out)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *Datastore) validateChange(c *change) error {
|
||||
var t *Table
|
||||
var r *Record
|
||||
var ok bool
|
||||
|
||||
if t, ok = ds.tables[c.TID]; !ok {
|
||||
t = &Table{
|
||||
datastore: ds,
|
||||
tableID: c.TID,
|
||||
records: make(map[string]*Record),
|
||||
}
|
||||
}
|
||||
|
||||
r = t.records[c.RecordID]
|
||||
|
||||
switch c.Op {
|
||||
case recordInsert, recordDelete:
|
||||
return nil
|
||||
case recordUpdate:
|
||||
if r == nil {
|
||||
return fmt.Errorf("no such record: %s", c.RecordID)
|
||||
}
|
||||
for field, op := range c.Ops {
|
||||
if op.Op == fieldPut || op.Op == fieldDelete {
|
||||
continue
|
||||
}
|
||||
v, ok := r.fields[field]
|
||||
if op.Op == listCreate {
|
||||
if ok {
|
||||
return fmt.Errorf("field %s already exists", field)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("no such field: %s", field)
|
||||
}
|
||||
if !v.isList {
|
||||
return fmt.Errorf("field %s is not a list", field)
|
||||
}
|
||||
maxIndex := len(v.values) - 1
|
||||
if op.Op == listInsert {
|
||||
maxIndex++
|
||||
}
|
||||
if op.Index > maxIndex {
|
||||
return fmt.Errorf("out of bound access index %d on [0:%d]", op.Index, maxIndex)
|
||||
}
|
||||
if op.Index2 > maxIndex {
|
||||
return fmt.Errorf("out of bound access index %d on [0:%d]", op.Index, maxIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) applyChange(c *change) error {
|
||||
var t *Table
|
||||
var r *Record
|
||||
var ok bool
|
||||
|
||||
if t, ok = ds.tables[c.TID]; !ok {
|
||||
t = &Table{
|
||||
datastore: ds,
|
||||
tableID: c.TID,
|
||||
records: make(map[string]*Record),
|
||||
}
|
||||
ds.tables[c.TID] = t
|
||||
}
|
||||
|
||||
r = t.records[c.RecordID]
|
||||
|
||||
switch c.Op {
|
||||
case recordInsert:
|
||||
t.records[c.RecordID] = &Record{
|
||||
table: t,
|
||||
recordID: c.RecordID,
|
||||
fields: newFields(c.Data),
|
||||
}
|
||||
case recordDelete:
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.isDeleted = true
|
||||
delete(t.records, c.RecordID)
|
||||
case recordUpdate:
|
||||
for field, op := range c.Ops {
|
||||
v, ok := r.fields[field]
|
||||
switch op.Op {
|
||||
case fieldPut:
|
||||
r.fields[field] = *newValue(&op.Data)
|
||||
case fieldDelete:
|
||||
if ok {
|
||||
delete(r.fields, field)
|
||||
}
|
||||
case listCreate:
|
||||
if !ok {
|
||||
r.fields[field] = value{isList: true}
|
||||
}
|
||||
case listDelete:
|
||||
copy(v.values[op.Index:], v.values[op.Index+1:])
|
||||
v.values = v.values[:len(v.values)-1]
|
||||
r.fields[field] = v
|
||||
case listInsert:
|
||||
v.values = append(v.values, op.Data)
|
||||
copy(v.values[op.Index+1:], v.values[op.Index:len(v.values)-1])
|
||||
v.values[op.Index] = op.Data.values[0]
|
||||
r.fields[field] = v
|
||||
case listMove:
|
||||
val := v.values[op.Index]
|
||||
if op.Index < op.Index2 {
|
||||
copy(v.values[op.Index:op.Index2], v.values[op.Index+1:op.Index2+1])
|
||||
} else {
|
||||
copy(v.values[op.Index2+1:op.Index+1], v.values[op.Index2:op.Index])
|
||||
}
|
||||
v.values[op.Index2] = val
|
||||
r.fields[field] = v
|
||||
case listPut:
|
||||
r.fields[field].values[op.Index] = op.Data.values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) inverseChange(c *change) (*change, error) {
|
||||
var t *Table
|
||||
var r *Record
|
||||
var ok bool
|
||||
var rev *change
|
||||
|
||||
if t, ok = ds.tables[c.TID]; !ok {
|
||||
t = &Table{
|
||||
datastore: ds,
|
||||
tableID: c.TID,
|
||||
records: make(map[string]*Record),
|
||||
}
|
||||
ds.tables[c.TID] = t
|
||||
}
|
||||
|
||||
r = t.records[c.RecordID]
|
||||
|
||||
switch c.Op {
|
||||
case recordInsert:
|
||||
return &change{
|
||||
Op: recordDelete,
|
||||
TID: c.TID,
|
||||
RecordID: c.RecordID,
|
||||
}, nil
|
||||
case recordDelete:
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &change{
|
||||
Op: recordInsert,
|
||||
TID: c.TID,
|
||||
RecordID: c.RecordID,
|
||||
Data: newFields(r.fields),
|
||||
}, nil
|
||||
case recordUpdate:
|
||||
rev = &change{
|
||||
Op: recordUpdate,
|
||||
TID: c.TID,
|
||||
RecordID: c.RecordID,
|
||||
Ops: make(opDict),
|
||||
}
|
||||
for field, op := range c.Ops {
|
||||
switch op.Op {
|
||||
case fieldPut:
|
||||
if v, ok := r.fields[field]; ok {
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: fieldPut,
|
||||
Data: *newValue(&v),
|
||||
}
|
||||
} else {
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: fieldDelete,
|
||||
}
|
||||
}
|
||||
case fieldDelete:
|
||||
if v, ok := r.fields[field]; ok {
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: fieldPut,
|
||||
Data: *newValue(&v),
|
||||
}
|
||||
}
|
||||
case listCreate:
|
||||
if _, ok := r.fields[field]; !ok {
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: fieldDelete,
|
||||
}
|
||||
}
|
||||
case listDelete:
|
||||
v := r.fields[field]
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: listInsert,
|
||||
Index: op.Index,
|
||||
Data: value{
|
||||
values: []interface{}{v.values[op.Index]},
|
||||
isList: false,
|
||||
},
|
||||
}
|
||||
case listInsert:
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: listDelete,
|
||||
Index: op.Index,
|
||||
}
|
||||
case listMove:
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: listMove,
|
||||
Index: op.Index2,
|
||||
Index2: op.Index,
|
||||
}
|
||||
case listPut:
|
||||
v := r.fields[field]
|
||||
rev.Ops[field] = fieldOp{
|
||||
Op: listPut,
|
||||
Index: op.Index,
|
||||
Data: value{
|
||||
values: []interface{}{v.values[op.Index]},
|
||||
isList: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rev, nil
|
||||
}
|
||||
303
vendor/github.com/stacktic/dropbox/datastores_parser.go
generated
vendored
Normal file
303
vendor/github.com/stacktic/dropbox/datastores_parser.go
generated
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
** SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package dropbox
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type atom struct {
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func encodeDBase64(b []byte) string {
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
||||
}
|
||||
|
||||
func decodeDBase64(s string) ([]byte, error) {
|
||||
pad := 4 - len(s)%4
|
||||
if pad != 4 {
|
||||
s += strings.Repeat("=", pad)
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of a.
|
||||
func (a atom) MarshalJSON() ([]byte, error) {
|
||||
switch v := a.Value.(type) {
|
||||
case bool, string:
|
||||
return json.Marshal(v)
|
||||
case float64:
|
||||
if math.IsNaN(v) {
|
||||
return []byte(`{"N": "nan"}`), nil
|
||||
} else if math.IsInf(v, 1) {
|
||||
return []byte(`{"N": "+inf"}`), nil
|
||||
} else if math.IsInf(v, -1) {
|
||||
return []byte(`{"N": "-inf"}`), nil
|
||||
}
|
||||
return json.Marshal(v)
|
||||
case time.Time:
|
||||
return []byte(fmt.Sprintf(`{"T": "%d"}`, v.UnixNano()/int64(time.Millisecond))), nil
|
||||
case int, int32, int64:
|
||||
return []byte(fmt.Sprintf(`{"I": "%d"}`, v)), nil
|
||||
case []byte:
|
||||
return []byte(fmt.Sprintf(`{"B": "%s"}`, encodeDBase64(v))), nil
|
||||
}
|
||||
return nil, fmt.Errorf("wrong format")
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by a.
|
||||
func (a *atom) UnmarshalJSON(data []byte) error {
|
||||
var i interface{}
|
||||
var err error
|
||||
|
||||
if err = json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
switch v := i.(type) {
|
||||
case bool, int, int32, int64, float32, float64, string:
|
||||
a.Value = v
|
||||
return nil
|
||||
case map[string]interface{}:
|
||||
for key, rval := range v {
|
||||
val, ok := rval.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not parse atom")
|
||||
}
|
||||
switch key {
|
||||
case "I":
|
||||
a.Value, err = strconv.ParseInt(val, 10, 64)
|
||||
return nil
|
||||
case "N":
|
||||
switch val {
|
||||
case "nan":
|
||||
a.Value = math.NaN()
|
||||
return nil
|
||||
case "+inf":
|
||||
a.Value = math.Inf(1)
|
||||
return nil
|
||||
case "-inf":
|
||||
a.Value = math.Inf(-1)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown special type %s", val)
|
||||
}
|
||||
case "T":
|
||||
t, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse atom")
|
||||
}
|
||||
a.Value = time.Unix(t/1000, (t%1000)*int64(time.Millisecond))
|
||||
return nil
|
||||
|
||||
case "B":
|
||||
a.Value, err = decodeDBase64(val)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("could not parse atom")
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of v.
|
||||
func (v value) MarshalJSON() ([]byte, error) {
|
||||
if v.isList {
|
||||
var a []atom
|
||||
|
||||
a = make([]atom, len(v.values))
|
||||
for i := range v.values {
|
||||
a[i].Value = v.values[i]
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
return json.Marshal(atom{Value: v.values[0]})
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by v.
|
||||
func (v *value) UnmarshalJSON(data []byte) error {
|
||||
var isArray bool
|
||||
var err error
|
||||
var a atom
|
||||
var as []atom
|
||||
|
||||
for _, d := range data {
|
||||
if d == ' ' {
|
||||
continue
|
||||
}
|
||||
if d == '[' {
|
||||
isArray = true
|
||||
}
|
||||
break
|
||||
}
|
||||
if isArray {
|
||||
if err = json.Unmarshal(data, &as); err != nil {
|
||||
return err
|
||||
}
|
||||
v.values = make([]interface{}, len(as))
|
||||
for i, at := range as {
|
||||
v.values[i] = at.Value
|
||||
}
|
||||
v.isList = true
|
||||
return nil
|
||||
}
|
||||
if err = json.Unmarshal(data, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
v.values = make([]interface{}, 1)
|
||||
v.values[0] = a.Value
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by f.
|
||||
func (f *fieldOp) UnmarshalJSON(data []byte) error {
|
||||
var i []json.RawMessage
|
||||
var err error
|
||||
|
||||
if err = json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(i[0], &f.Op); err != nil {
|
||||
return err
|
||||
}
|
||||
switch f.Op {
|
||||
case fieldPut:
|
||||
if len(i) != 2 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
return json.Unmarshal(i[1], &f.Data)
|
||||
case fieldDelete, listCreate:
|
||||
if len(i) != 1 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
case listInsert, listPut:
|
||||
if len(i) != 3 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
if err = json.Unmarshal(i[1], &f.Index); err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(i[2], &f.Data)
|
||||
case listDelete:
|
||||
if len(i) != 2 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
return json.Unmarshal(i[1], &f.Index)
|
||||
case listMove:
|
||||
if len(i) != 3 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
if err = json.Unmarshal(i[1], &f.Index); err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(i[2], &f.Index2)
|
||||
default:
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of f.
|
||||
func (f fieldOp) MarshalJSON() ([]byte, error) {
|
||||
switch f.Op {
|
||||
case fieldPut:
|
||||
return json.Marshal([]interface{}{f.Op, f.Data})
|
||||
case fieldDelete, listCreate:
|
||||
return json.Marshal([]interface{}{f.Op})
|
||||
case listInsert, listPut:
|
||||
return json.Marshal([]interface{}{f.Op, f.Index, f.Data})
|
||||
case listDelete:
|
||||
return json.Marshal([]interface{}{f.Op, f.Index})
|
||||
case listMove:
|
||||
return json.Marshal([]interface{}{f.Op, f.Index, f.Index2})
|
||||
}
|
||||
return nil, fmt.Errorf("could not marshal Change type")
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the JSON-encoded data and stores the result in the value pointed to by c.
|
||||
func (c *change) UnmarshalJSON(data []byte) error {
|
||||
var i []json.RawMessage
|
||||
var err error
|
||||
|
||||
if err = json.Unmarshal(data, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(i) < 3 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(i[0], &c.Op); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal(i[1], &c.TID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal(i[2], &c.RecordID); err != nil {
|
||||
return err
|
||||
}
|
||||
switch c.Op {
|
||||
case recordInsert:
|
||||
if len(i) != 4 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
if err = json.Unmarshal(i[3], &c.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
case recordUpdate:
|
||||
if len(i) != 4 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
if err = json.Unmarshal(i[3], &c.Ops); err != nil {
|
||||
return err
|
||||
}
|
||||
case recordDelete:
|
||||
if len(i) != 3 {
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("wrong format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of c.
|
||||
func (c change) MarshalJSON() ([]byte, error) {
|
||||
switch c.Op {
|
||||
case recordInsert:
|
||||
return json.Marshal([]interface{}{recordInsert, c.TID, c.RecordID, c.Data})
|
||||
case recordUpdate:
|
||||
return json.Marshal([]interface{}{recordUpdate, c.TID, c.RecordID, c.Ops})
|
||||
case recordDelete:
|
||||
return json.Marshal([]interface{}{recordDelete, c.TID, c.RecordID})
|
||||
}
|
||||
return nil, fmt.Errorf("could not marshal Change type")
|
||||
}
|
||||
305
vendor/github.com/stacktic/dropbox/datastores_requests.go
generated
vendored
Normal file
305
vendor/github.com/stacktic/dropbox/datastores_requests.go
generated
vendored
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
** SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package dropbox
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type row struct {
|
||||
TID string `json:"tid"`
|
||||
RowID string `json:"rowid"`
|
||||
Data Fields `json:"data"`
|
||||
}
|
||||
|
||||
type infoDict struct {
|
||||
Title string `json:"title"`
|
||||
MTime struct {
|
||||
Time DBTime `json:"T"`
|
||||
} `json:"mtime"`
|
||||
}
|
||||
|
||||
type datastoreInfo struct {
|
||||
ID string `json:"dsid"`
|
||||
Handle string `json:"handle"`
|
||||
Revision int `json:"rev"`
|
||||
Info infoDict `json:"info"`
|
||||
}
|
||||
|
||||
func (db *Dropbox) openOrCreateDatastore(dsID string) (int, string, bool, error) {
|
||||
var r struct {
|
||||
Revision int `json:"rev"`
|
||||
Handle string `json:"handle"`
|
||||
Created bool `json:"created"`
|
||||
}
|
||||
|
||||
err := db.doRequest("POST", "datastores/get_or_create_datastore", &url.Values{"dsid": {dsID}}, &r)
|
||||
return r.Revision, r.Handle, r.Created, err
|
||||
}
|
||||
|
||||
func (db *Dropbox) listDatastores() ([]DatastoreInfo, string, error) {
|
||||
var rv []DatastoreInfo
|
||||
|
||||
var dl struct {
|
||||
Info []datastoreInfo `json:"datastores"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
if err := db.doRequest("GET", "datastores/list_datastores", nil, &dl); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
rv = make([]DatastoreInfo, len(dl.Info))
|
||||
for i, di := range dl.Info {
|
||||
rv[i] = DatastoreInfo{
|
||||
ID: di.ID,
|
||||
handle: di.Handle,
|
||||
revision: di.Revision,
|
||||
title: di.Info.Title,
|
||||
mtime: time.Time(di.Info.MTime.Time),
|
||||
}
|
||||
}
|
||||
return rv, dl.Token, nil
|
||||
}
|
||||
|
||||
func (db *Dropbox) deleteDatastore(handle string) (*string, error) {
|
||||
var r struct {
|
||||
NotFound string `json:"notfound"`
|
||||
OK string `json:"ok"`
|
||||
}
|
||||
|
||||
if err := db.doRequest("POST", "datastores/delete_datastore", &url.Values{"handle": {handle}}, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(r.NotFound) != 0 {
|
||||
return nil, fmt.Errorf(r.NotFound)
|
||||
}
|
||||
return &r.OK, nil
|
||||
}
|
||||
|
||||
func generateDatastoreID() (string, error) {
|
||||
var b []byte
|
||||
var blen int
|
||||
|
||||
b = make([]byte, 1)
|
||||
_, err := io.ReadFull(rand.Reader, b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
blen = (int(b[0]) % maxGlobalIDLength) + 1
|
||||
b = make([]byte, blen)
|
||||
_, err = io.ReadFull(rand.Reader, b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return encodeDBase64(b), nil
|
||||
}
|
||||
|
||||
func (db *Dropbox) createDatastore(key string) (int, string, bool, error) {
|
||||
var r struct {
|
||||
Revision int `json:"rev"`
|
||||
Handle string `json:"handle"`
|
||||
Created bool `json:"created"`
|
||||
NotFound string `json:"notfound"`
|
||||
}
|
||||
var b64key string
|
||||
var err error
|
||||
|
||||
if len(key) != 0 {
|
||||
b64key = encodeDBase64([]byte(key))
|
||||
} else {
|
||||
b64key, err = generateDatastoreID()
|
||||
if err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
}
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(b64key))
|
||||
rhash := hash.Sum(nil)
|
||||
dsID := "." + encodeDBase64(rhash[:])
|
||||
|
||||
params := &url.Values{
|
||||
"key": {b64key},
|
||||
"dsid": {dsID},
|
||||
}
|
||||
if err := db.doRequest("POST", "datastores/create_datastore", params, &r); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
if len(r.NotFound) != 0 {
|
||||
return 0, "", false, fmt.Errorf("%s", r.NotFound)
|
||||
}
|
||||
return r.Revision, r.Handle, r.Created, nil
|
||||
}
|
||||
|
||||
func (db *Dropbox) putDelta(handle string, rev int, changes listOfChanges) (int, error) {
|
||||
var r struct {
|
||||
Revision int `json:"rev"`
|
||||
NotFound string `json:"notfound"`
|
||||
Conflict string `json:"conflict"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
var js []byte
|
||||
var err error
|
||||
|
||||
if len(changes) == 0 {
|
||||
return rev, nil
|
||||
}
|
||||
|
||||
if js, err = json.Marshal(changes); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
params := &url.Values{
|
||||
"handle": {handle},
|
||||
"rev": {strconv.FormatInt(int64(rev), 10)},
|
||||
"changes": {string(js)},
|
||||
}
|
||||
|
||||
if err = db.doRequest("POST", "datastores/put_delta", params, &r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(r.NotFound) != 0 {
|
||||
return 0, fmt.Errorf("%s", r.NotFound)
|
||||
}
|
||||
if len(r.Conflict) != 0 {
|
||||
return 0, fmt.Errorf("%s", r.Conflict)
|
||||
}
|
||||
if len(r.Error) != 0 {
|
||||
return 0, fmt.Errorf("%s", r.Error)
|
||||
}
|
||||
return r.Revision, nil
|
||||
}
|
||||
|
||||
func (db *Dropbox) getDelta(handle string, rev int) ([]datastoreDelta, error) {
|
||||
var rv struct {
|
||||
Deltas []datastoreDelta `json:"deltas"`
|
||||
NotFound string `json:"notfound"`
|
||||
}
|
||||
err := db.doRequest("GET", "datastores/get_deltas",
|
||||
&url.Values{
|
||||
"handle": {handle},
|
||||
"rev": {strconv.FormatInt(int64(rev), 10)},
|
||||
}, &rv)
|
||||
|
||||
if len(rv.NotFound) != 0 {
|
||||
return nil, fmt.Errorf("%s", rv.NotFound)
|
||||
}
|
||||
return rv.Deltas, err
|
||||
}
|
||||
|
||||
func (db *Dropbox) getSnapshot(handle string) ([]row, int, error) {
|
||||
var r struct {
|
||||
Rows []row `json:"rows"`
|
||||
Revision int `json:"rev"`
|
||||
NotFound string `json:"notfound"`
|
||||
}
|
||||
|
||||
if err := db.doRequest("GET", "datastores/get_snapshot",
|
||||
&url.Values{"handle": {handle}}, &r); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(r.NotFound) != 0 {
|
||||
return nil, 0, fmt.Errorf("%s", r.NotFound)
|
||||
}
|
||||
return r.Rows, r.Revision, nil
|
||||
}
|
||||
|
||||
func (db *Dropbox) await(cursors []*Datastore, token string) (string, []DatastoreInfo, map[string][]datastoreDelta, error) {
|
||||
var params *url.Values
|
||||
var dis []DatastoreInfo
|
||||
var dd map[string][]datastoreDelta
|
||||
|
||||
type awaitResult struct {
|
||||
Deltas struct {
|
||||
Results map[string]struct {
|
||||
Deltas []datastoreDelta `json:"deltas"`
|
||||
NotFound string `json:"notfound"`
|
||||
} `json:"deltas"`
|
||||
} `json:"get_deltas"`
|
||||
Datastores struct {
|
||||
Info []datastoreInfo `json:"datastores"`
|
||||
Token string `json:"token"`
|
||||
} `json:"list_datastores"`
|
||||
}
|
||||
var r awaitResult
|
||||
if len(token) == 0 && len(cursors) == 0 {
|
||||
return "", nil, nil, fmt.Errorf("at least one parameter required")
|
||||
}
|
||||
params = &url.Values{}
|
||||
if len(token) != 0 {
|
||||
js, err := json.Marshal(map[string]string{"token": token})
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
params.Set("list_datastores", string(js))
|
||||
}
|
||||
if len(cursors) != 0 {
|
||||
m := make(map[string]int)
|
||||
for _, ds := range cursors {
|
||||
m[ds.info.handle] = ds.info.revision
|
||||
}
|
||||
js, err := json.Marshal(map[string]map[string]int{"cursors": m})
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
params.Set("get_deltas", string(js))
|
||||
}
|
||||
if err := db.doRequest("GET", "datastores/await", params, &r); err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if len(r.Deltas.Results) == 0 && len(r.Datastores.Info) == 0 {
|
||||
return token, nil, nil, fmt.Errorf("await timed out")
|
||||
}
|
||||
if len(r.Datastores.Token) != 0 {
|
||||
token = r.Datastores.Token
|
||||
}
|
||||
if len(r.Deltas.Results) != 0 {
|
||||
dd = make(map[string][]datastoreDelta)
|
||||
for k, v := range r.Deltas.Results {
|
||||
dd[k] = v.Deltas
|
||||
}
|
||||
}
|
||||
if len(r.Datastores.Info) != 0 {
|
||||
dis = make([]DatastoreInfo, len(r.Datastores.Info))
|
||||
for i, di := range r.Datastores.Info {
|
||||
dis[i] = DatastoreInfo{
|
||||
ID: di.ID,
|
||||
handle: di.Handle,
|
||||
revision: di.Revision,
|
||||
title: di.Info.Title,
|
||||
mtime: time.Time(di.Info.MTime.Time),
|
||||
}
|
||||
}
|
||||
}
|
||||
return token, dis, dd, nil
|
||||
}
|
||||
943
vendor/github.com/stacktic/dropbox/dropbox.go
generated
vendored
Normal file
943
vendor/github.com/stacktic/dropbox/dropbox.go
generated
vendored
Normal file
@@ -0,0 +1,943 @@
|
||||
/*
|
||||
** Copyright (c) 2014 Arnaud Ysmal. All Rights Reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
** OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
** DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
** SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
// Package dropbox implements the Dropbox core and datastore API.
|
||||
package dropbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// ErrNotAuth is the error returned when the OAuth token is not provided
|
||||
var ErrNotAuth = errors.New("authentication required")
|
||||
|
||||
// Account represents information about the user account.
|
||||
type Account struct {
|
||||
ReferralLink string `json:"referral_link,omitempty"` // URL for referral.
|
||||
DisplayName string `json:"display_name,omitempty"` // User name.
|
||||
UID int `json:"uid,omitempty"` // User account ID.
|
||||
Country string `json:"country,omitempty"` // Country ISO code.
|
||||
QuotaInfo struct {
|
||||
Shared int64 `json:"shared,omitempty"` // Quota for shared files.
|
||||
Quota int64 `json:"quota,omitempty"` // Quota in bytes.
|
||||
Normal int64 `json:"normal,omitempty"` // Quota for non-shared files.
|
||||
} `json:"quota_info"`
|
||||
}
|
||||
|
||||
// CopyRef represents the reply of CopyRef.
|
||||
type CopyRef struct {
|
||||
CopyRef string `json:"copy_ref"` // Reference to use on fileops/copy.
|
||||
Expires string `json:"expires"` // Expiration date.
|
||||
}
|
||||
|
||||
// DeltaPage represents the reply of delta.
|
||||
type DeltaPage struct {
|
||||
Reset bool // if true the local state must be cleared.
|
||||
HasMore bool // if true an other call to delta should be made.
|
||||
Cursor // Tag of the current state.
|
||||
Entries []DeltaEntry // List of changed entries.
|
||||
}
|
||||
|
||||
// DeltaEntry represents the list of changes for a given path.
|
||||
type DeltaEntry struct {
|
||||
Path string // Path of this entry in lowercase.
|
||||
Entry *Entry // nil when this entry does not exists.
|
||||
}
|
||||
|
||||
// DeltaPoll represents the reply of longpoll_delta.
|
||||
type DeltaPoll struct {
|
||||
Changes bool `json:"changes"` // true if the polled path has changed.
|
||||
Backoff int `json:"backoff"` // time in second before calling poll again.
|
||||
}
|
||||
|
||||
// ChunkUploadResponse represents the reply of chunked_upload.
|
||||
type ChunkUploadResponse struct {
|
||||
UploadID string `json:"upload_id"` // Unique ID of this upload.
|
||||
Offset int64 `json:"offset"` // Size in bytes of already sent data.
|
||||
Expires DBTime `json:"expires"` // Expiration time of this upload.
|
||||
}
|
||||
|
||||
// Cursor represents the tag of a server state at a given moment.
|
||||
type Cursor struct {
|
||||
Cursor string `json:"cursor"`
|
||||
}
|
||||
|
||||
// Format of reply when http error code is not 200.
|
||||
// Format may be:
|
||||
// {"error": "reason"}
|
||||
// {"error": {"param": "reason"}}
|
||||
type requestError struct {
|
||||
Error interface{} `json:"error"` // Description of this error.
|
||||
}
|
||||
|
||||
const (
|
||||
// PollMinTimeout is the minimum timeout for longpoll.
|
||||
PollMinTimeout = 30
|
||||
// PollMaxTimeout is the maximum timeout for longpoll.
|
||||
PollMaxTimeout = 480
|
||||
// DefaultChunkSize is the maximum size of a file sendable using files_put.
|
||||
DefaultChunkSize = 4 * 1024 * 1024
|
||||
// MaxPutFileSize is the maximum size of a file sendable using files_put.
|
||||
MaxPutFileSize = 150 * 1024 * 1024
|
||||
// MetadataLimitMax is the maximum number of entries returned by metadata.
|
||||
MetadataLimitMax = 25000
|
||||
// MetadataLimitDefault is the default number of entries returned by metadata.
|
||||
MetadataLimitDefault = 10000
|
||||
// RevisionsLimitMax is the maximum number of revisions returned by revisions.
|
||||
RevisionsLimitMax = 1000
|
||||
// RevisionsLimitDefault is the default number of revisions returned by revisions.
|
||||
RevisionsLimitDefault = 10
|
||||
// SearchLimitMax is the maximum number of entries returned by search.
|
||||
SearchLimitMax = 1000
|
||||
// SearchLimitDefault is the default number of entries returned by search.
|
||||
SearchLimitDefault = 1000
|
||||
// DateFormat is the format to use when decoding a time.
|
||||
DateFormat = time.RFC1123Z
|
||||
)
|
||||
|
||||
// DBTime allow marshalling and unmarshalling of time.
|
||||
type DBTime time.Time
|
||||
|
||||
// UnmarshalJSON unmarshals a time according to the Dropbox format.
|
||||
func (dbt *DBTime) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
var err error
|
||||
var t time.Time
|
||||
|
||||
if err = json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if t, err = time.ParseInLocation(DateFormat, s, time.UTC); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.IsZero() {
|
||||
*dbt = DBTime(time.Time{})
|
||||
} else {
|
||||
*dbt = DBTime(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals a time according to the Dropbox format.
|
||||
func (dbt DBTime) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Time(dbt).Format(DateFormat))
|
||||
}
|
||||
|
||||
// Modifier represents the user who made a change on a particular file
|
||||
type Modifier struct {
|
||||
UID int64 `json:"uid"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
// Entry represents the metadata of a file or folder.
|
||||
type Entry struct {
|
||||
Bytes int64 `json:"bytes,omitempty"` // Size of the file in bytes.
|
||||
ClientMtime DBTime `json:"client_mtime,omitempty"` // Modification time set by the client when added.
|
||||
Contents []Entry `json:"contents,omitempty"` // List of children for a directory.
|
||||
Hash string `json:"hash,omitempty"` // Hash of this entry.
|
||||
Icon string `json:"icon,omitempty"` // Name of the icon displayed for this entry.
|
||||
IsDeleted bool `json:"is_deleted,omitempty"` // true if this entry was deleted.
|
||||
IsDir bool `json:"is_dir,omitempty"` // true if this entry is a directory.
|
||||
MimeType string `json:"mime_type,omitempty"` // MimeType of this entry.
|
||||
Modified DBTime `json:"modified,omitempty"` // Date of last modification.
|
||||
Path string `json:"path,omitempty"` // Absolute path of this entry.
|
||||
Revision string `json:"rev,omitempty"` // Unique ID for this file revision.
|
||||
Root string `json:"root,omitempty"` // dropbox or sandbox.
|
||||
Size string `json:"size,omitempty"` // Size of the file humanized/localized.
|
||||
ThumbExists bool `json:"thumb_exists,omitempty"` // true if a thumbnail is available for this entry.
|
||||
Modifier *Modifier `json:"modifier"` // last user to edit the file if in a shared folder
|
||||
ParentSharedFolderID string `json:"parent_shared_folder_id,omitempty"`
|
||||
}
|
||||
|
||||
// Link for sharing a file.
|
||||
type Link struct {
|
||||
Expires DBTime `json:"expires"` // Expiration date of this link.
|
||||
URL string `json:"url"` // URL to share.
|
||||
}
|
||||
|
||||
// User represents a Dropbox user.
|
||||
type User struct {
|
||||
UID int64 `json:"uid"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
// SharedFolderMember represents access right associated with a Dropbox user.
|
||||
type SharedFolderMember struct {
|
||||
User User `json:"user"`
|
||||
Active bool `json:"active"`
|
||||
AccessType string `json:"access_type"`
|
||||
}
|
||||
|
||||
// SharedFolder reprensents a directory with a specific sharing policy.
|
||||
type SharedFolder struct {
|
||||
SharedFolderID string `json:"shared_folder_id"`
|
||||
SharedFolderName string `json:"shared_folder_name"`
|
||||
Path string `json:"path"`
|
||||
AccessType string `json:"access_type"`
|
||||
SharedLinkPolicy string `json:"shared_link_policy"`
|
||||
Owner User `json:"owner"`
|
||||
Membership []SharedFolderMember `json:"membership"`
|
||||
}
|
||||
|
||||
// Dropbox client.
|
||||
type Dropbox struct {
|
||||
RootDirectory string // dropbox or sandbox.
|
||||
Locale string // Locale sent to the API to translate/format messages.
|
||||
APIURL string // Normal API URL.
|
||||
APIContentURL string // URL for transferring files.
|
||||
APINotifyURL string // URL for realtime notification.
|
||||
config *oauth2.Config
|
||||
token *oauth2.Token
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewDropbox returns a new Dropbox configured.
|
||||
func NewDropbox() *Dropbox {
|
||||
db := &Dropbox{
|
||||
RootDirectory: "auto", // auto (recommended), dropbox or sandbox.
|
||||
Locale: "en",
|
||||
APIURL: "https://api.dropbox.com/1",
|
||||
APIContentURL: "https://api-content.dropbox.com/1",
|
||||
APINotifyURL: "https://api-notify.dropbox.com/1",
|
||||
ctx: oauth2.NoContext,
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// SetAppInfo sets the clientid (app_key) and clientsecret (app_secret).
|
||||
// You have to register an application on https://www.dropbox.com/developers/apps.
|
||||
func (db *Dropbox) SetAppInfo(clientid, clientsecret string) error {
|
||||
|
||||
db.config = &oauth2.Config{
|
||||
ClientID: clientid,
|
||||
ClientSecret: clientsecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://www.dropbox.com/1/oauth2/authorize",
|
||||
TokenURL: "https://api.dropbox.com/1/oauth2/token",
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAccessToken sets access token to avoid calling Auth method.
|
||||
func (db *Dropbox) SetAccessToken(accesstoken string) {
|
||||
db.token = &oauth2.Token{AccessToken: accesstoken}
|
||||
}
|
||||
|
||||
// SetContext allow to set a custom context.
|
||||
func (db *Dropbox) SetContext(ctx context.Context) {
|
||||
db.ctx = ctx
|
||||
}
|
||||
|
||||
// AccessToken returns the OAuth access token.
|
||||
func (db *Dropbox) AccessToken() string {
|
||||
return db.token.AccessToken
|
||||
}
|
||||
|
||||
// SetRedirectURL updates the configuration with the given redirection URL.
|
||||
func (db *Dropbox) SetRedirectURL(url string) {
|
||||
db.config.RedirectURL = url
|
||||
}
|
||||
|
||||
func (db *Dropbox) client() *http.Client {
|
||||
return db.config.Client(db.ctx, db.token)
|
||||
}
|
||||
|
||||
// Auth displays the URL to authorize this application to connect to your account.
|
||||
func (db *Dropbox) Auth() error {
|
||||
var code string
|
||||
|
||||
fmt.Printf("Please visit:\n%s\nEnter the code: ",
|
||||
db.config.AuthCodeURL(""))
|
||||
fmt.Scanln(&code)
|
||||
return db.AuthCode(code)
|
||||
}
|
||||
|
||||
// AuthCode gets the token associated with the given code.
|
||||
func (db *Dropbox) AuthCode(code string) error {
|
||||
t, err := db.config.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db.token = t
|
||||
db.token.TokenType = "Bearer"
|
||||
return nil
|
||||
}
|
||||
|
||||
// Error - all errors generated by HTTP transactions are of this type.
|
||||
// Other error may be passed on from library functions though.
|
||||
type Error struct {
|
||||
StatusCode int // HTTP status code
|
||||
Text string
|
||||
}
|
||||
|
||||
// Error satisfy the error interface.
|
||||
func (e *Error) Error() string {
|
||||
return e.Text
|
||||
}
|
||||
|
||||
// newError make a new error from a string.
|
||||
func newError(StatusCode int, Text string) *Error {
|
||||
return &Error{
|
||||
StatusCode: StatusCode,
|
||||
Text: Text,
|
||||
}
|
||||
}
|
||||
|
||||
// newErrorf makes a new error from sprintf parameters.
|
||||
func newErrorf(StatusCode int, Text string, Parameters ...interface{}) *Error {
|
||||
return newError(StatusCode, fmt.Sprintf(Text, Parameters...))
|
||||
}
|
||||
|
||||
func getResponse(r *http.Response) ([]byte, error) {
|
||||
var e requestError
|
||||
var b []byte
|
||||
var err error
|
||||
|
||||
if b, err = ioutil.ReadAll(r.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.StatusCode == http.StatusOK {
|
||||
return b, nil
|
||||
}
|
||||
if err = json.Unmarshal(b, &e); err == nil {
|
||||
switch v := e.Error.(type) {
|
||||
case string:
|
||||
return nil, newErrorf(r.StatusCode, "%s", v)
|
||||
case map[string]interface{}:
|
||||
for param, reason := range v {
|
||||
if reasonstr, ok := reason.(string); ok {
|
||||
return nil, newErrorf(r.StatusCode, "%s: %s", param, reasonstr)
|
||||
}
|
||||
}
|
||||
return nil, newErrorf(r.StatusCode, "wrong parameter")
|
||||
}
|
||||
}
|
||||
return nil, newErrorf(r.StatusCode, "unexpected HTTP status code %d", r.StatusCode)
|
||||
}
|
||||
|
||||
// urlEncode encodes s for url
|
||||
func urlEncode(s string) string {
|
||||
// Would like to call url.escape(value, encodePath) here
|
||||
encoded := url.QueryEscape(s)
|
||||
encoded = strings.Replace(encoded, "+", "%20", -1)
|
||||
return encoded
|
||||
}
|
||||
|
||||
// CommitChunkedUpload ends the chunked upload by giving a name to the UploadID.
|
||||
func (db *Dropbox) CommitChunkedUpload(uploadid, dst string, overwrite bool, parentRev string) (*Entry, error) {
|
||||
var err error
|
||||
var rawurl string
|
||||
var response *http.Response
|
||||
var params *url.Values
|
||||
var body []byte
|
||||
var rv Entry
|
||||
|
||||
if dst[0] == '/' {
|
||||
dst = dst[1:]
|
||||
}
|
||||
|
||||
params = &url.Values{
|
||||
"locale": {db.Locale},
|
||||
"upload_id": {uploadid},
|
||||
"overwrite": {strconv.FormatBool(overwrite)},
|
||||
}
|
||||
if len(parentRev) != 0 {
|
||||
params.Set("parent_rev", parentRev)
|
||||
}
|
||||
rawurl = fmt.Sprintf("%s/commit_chunked_upload/%s/%s?%s", db.APIContentURL, db.RootDirectory, urlEncode(dst), params.Encode())
|
||||
|
||||
if response, err = db.client().Post(rawurl, "", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if body, err = getResponse(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// ChunkedUpload sends a chunk with a maximum size of chunksize, if there is no session a new one is created.
|
||||
func (db *Dropbox) ChunkedUpload(session *ChunkUploadResponse, input io.ReadCloser, chunksize int) (*ChunkUploadResponse, error) {
|
||||
var err error
|
||||
var rawurl string
|
||||
var cur ChunkUploadResponse
|
||||
var response *http.Response
|
||||
var body []byte
|
||||
var r *io.LimitedReader
|
||||
|
||||
if chunksize <= 0 {
|
||||
chunksize = DefaultChunkSize
|
||||
} else if chunksize > MaxPutFileSize {
|
||||
chunksize = MaxPutFileSize
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
rawurl = fmt.Sprintf("%s/chunked_upload?upload_id=%s&offset=%d", db.APIContentURL, session.UploadID, session.Offset)
|
||||
} else {
|
||||
rawurl = fmt.Sprintf("%s/chunked_upload", db.APIContentURL)
|
||||
}
|
||||
r = &io.LimitedReader{R: input, N: int64(chunksize)}
|
||||
|
||||
if response, err = db.client().Post(rawurl, "application/octet-stream", r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if body, err = getResponse(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &cur)
|
||||
if r.N != 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return &cur, err
|
||||
}
|
||||
|
||||
// UploadByChunk uploads data from the input reader to the dst path on Dropbox by sending chunks of chunksize.
|
||||
func (db *Dropbox) UploadByChunk(input io.ReadCloser, chunksize int, dst string, overwrite bool, parentRev string) (*Entry, error) {
|
||||
var err error
|
||||
var cur *ChunkUploadResponse
|
||||
|
||||
for err == nil {
|
||||
if cur, err = db.ChunkedUpload(cur, input, chunksize); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return db.CommitChunkedUpload(cur.UploadID, dst, overwrite, parentRev)
|
||||
}
|
||||
|
||||
// FilesPut uploads size bytes from the input reader to the dst path on Dropbox.
|
||||
func (db *Dropbox) FilesPut(input io.ReadCloser, size int64, dst string, overwrite bool, parentRev string) (*Entry, error) {
|
||||
var err error
|
||||
var rawurl string
|
||||
var rv Entry
|
||||
var request *http.Request
|
||||
var response *http.Response
|
||||
var params *url.Values
|
||||
var body []byte
|
||||
|
||||
if size > MaxPutFileSize {
|
||||
return nil, fmt.Errorf("could not upload files bigger than 150MB using this method, use UploadByChunk instead")
|
||||
}
|
||||
if dst[0] == '/' {
|
||||
dst = dst[1:]
|
||||
}
|
||||
|
||||
params = &url.Values{"overwrite": {strconv.FormatBool(overwrite)}, "locale": {db.Locale}}
|
||||
if len(parentRev) != 0 {
|
||||
params.Set("parent_rev", parentRev)
|
||||
}
|
||||
rawurl = fmt.Sprintf("%s/files_put/%s/%s?%s", db.APIContentURL, db.RootDirectory, urlEncode(dst), params.Encode())
|
||||
|
||||
if request, err = http.NewRequest("PUT", rawurl, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
if response, err = db.client().Do(request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if body, err = getResponse(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// UploadFile uploads the file located in the src path on the local disk to the dst path on Dropbox.
|
||||
func (db *Dropbox) UploadFile(src, dst string, overwrite bool, parentRev string) (*Entry, error) {
|
||||
var err error
|
||||
var fd *os.File
|
||||
var fsize int64
|
||||
|
||||
if fd, err = os.Open(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if fi, err := fd.Stat(); err == nil {
|
||||
fsize = fi.Size()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return db.FilesPut(fd, fsize, dst, overwrite, parentRev)
|
||||
}
|
||||
|
||||
// Thumbnails gets a thumbnail for an image.
|
||||
func (db *Dropbox) Thumbnails(src, format, size string) (io.ReadCloser, int64, *Entry, error) {
|
||||
var response *http.Response
|
||||
var rawurl string
|
||||
var err error
|
||||
var entry Entry
|
||||
|
||||
switch format {
|
||||
case "":
|
||||
format = "jpeg"
|
||||
case "jpeg", "png":
|
||||
break
|
||||
default:
|
||||
return nil, 0, nil, fmt.Errorf("unsupported format '%s' must be jpeg or png", format)
|
||||
}
|
||||
switch size {
|
||||
case "":
|
||||
size = "s"
|
||||
case "xs", "s", "m", "l", "xl":
|
||||
break
|
||||
default:
|
||||
return nil, 0, nil, fmt.Errorf("unsupported size '%s' must be xs, s, m, l or xl", size)
|
||||
|
||||
}
|
||||
if src[0] == '/' {
|
||||
src = src[1:]
|
||||
}
|
||||
rawurl = fmt.Sprintf("%s/thumbnails/%s/%s?format=%s&size=%s", db.APIContentURL, db.RootDirectory, urlEncode(src), urlEncode(format), urlEncode(size))
|
||||
if response, err = db.client().Get(rawurl); err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
json.Unmarshal([]byte(response.Header.Get("x-dropbox-metadata")), &entry)
|
||||
return response.Body, response.ContentLength, &entry, err
|
||||
}
|
||||
response.Body.Close()
|
||||
switch response.StatusCode {
|
||||
case http.StatusNotFound:
|
||||
return nil, 0, nil, os.ErrNotExist
|
||||
case http.StatusUnsupportedMediaType:
|
||||
return nil, 0, nil, newErrorf(response.StatusCode, "the image located at '%s' cannot be converted to a thumbnail", src)
|
||||
default:
|
||||
return nil, 0, nil, newErrorf(response.StatusCode, "unexpected HTTP status code %d", response.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// ThumbnailsToFile downloads the file located in the src path on the Dropbox to the dst file on the local disk.
|
||||
func (db *Dropbox) ThumbnailsToFile(src, dst, format, size string) (*Entry, error) {
|
||||
var input io.ReadCloser
|
||||
var fd *os.File
|
||||
var err error
|
||||
var entry *Entry
|
||||
|
||||
if fd, err = os.Create(dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if input, _, entry, err = db.Thumbnails(src, format, size); err != nil {
|
||||
os.Remove(dst)
|
||||
return nil, err
|
||||
}
|
||||
defer input.Close()
|
||||
if _, err = io.Copy(fd, input); err != nil {
|
||||
os.Remove(dst)
|
||||
}
|
||||
return entry, err
|
||||
}
|
||||
|
||||
// Download requests the file located at src, the specific revision may be given.
|
||||
// offset is used in case the download was interrupted.
|
||||
// A io.ReadCloser and the file size is returned.
|
||||
func (db *Dropbox) Download(src, rev string, offset int64) (io.ReadCloser, int64, error) {
|
||||
var request *http.Request
|
||||
var response *http.Response
|
||||
var rawurl string
|
||||
var err error
|
||||
|
||||
if src[0] == '/' {
|
||||
src = src[1:]
|
||||
}
|
||||
|
||||
rawurl = fmt.Sprintf("%s/files/%s/%s", db.APIContentURL, db.RootDirectory, urlEncode(src))
|
||||
if len(rev) != 0 {
|
||||
rawurl += fmt.Sprintf("?rev=%s", rev)
|
||||
}
|
||||
if request, err = http.NewRequest("GET", rawurl, nil); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if offset != 0 {
|
||||
request.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
|
||||
}
|
||||
|
||||
if response, err = db.client().Do(request); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK || response.StatusCode == http.StatusPartialContent {
|
||||
return response.Body, response.ContentLength, err
|
||||
}
|
||||
response.Body.Close()
|
||||
switch response.StatusCode {
|
||||
case http.StatusNotFound:
|
||||
return nil, 0, os.ErrNotExist
|
||||
default:
|
||||
return nil, 0, newErrorf(response.StatusCode, "unexpected HTTP status code %d", response.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadToFileResume resumes the download of the file located in the src path on the Dropbox to the dst file on the local disk.
|
||||
func (db *Dropbox) DownloadToFileResume(src, dst, rev string) error {
|
||||
var input io.ReadCloser
|
||||
var fi os.FileInfo
|
||||
var fd *os.File
|
||||
var offset int64
|
||||
var err error
|
||||
|
||||
if fd, err = os.OpenFile(dst, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
if fi, err = fd.Stat(); err != nil {
|
||||
return err
|
||||
}
|
||||
offset = fi.Size()
|
||||
|
||||
if input, _, err = db.Download(src, rev, offset); err != nil {
|
||||
return err
|
||||
}
|
||||
defer input.Close()
|
||||
_, err = io.Copy(fd, input)
|
||||
return err
|
||||
}
|
||||
|
||||
// DownloadToFile downloads the file located in the src path on the Dropbox to the dst file on the local disk.
|
||||
// If the destination file exists it will be truncated.
|
||||
func (db *Dropbox) DownloadToFile(src, dst, rev string) error {
|
||||
var input io.ReadCloser
|
||||
var fd *os.File
|
||||
var err error
|
||||
|
||||
if fd, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if input, _, err = db.Download(src, rev, 0); err != nil {
|
||||
os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
defer input.Close()
|
||||
if _, err = io.Copy(fd, input); err != nil {
|
||||
os.Remove(dst)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Dropbox) doRequest(method, path string, params *url.Values, receiver interface{}) error {
|
||||
var body []byte
|
||||
var rawurl string
|
||||
var response *http.Response
|
||||
var request *http.Request
|
||||
var err error
|
||||
|
||||
if params == nil {
|
||||
params = &url.Values{"locale": {db.Locale}}
|
||||
} else {
|
||||
params.Set("locale", db.Locale)
|
||||
}
|
||||
rawurl = fmt.Sprintf("%s/%s?%s", db.APIURL, urlEncode(path), params.Encode())
|
||||
if request, err = http.NewRequest(method, rawurl, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if response, err = db.client().Do(request); err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if body, err = getResponse(response); err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(body, receiver)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAccountInfo gets account information for the user currently authenticated.
|
||||
func (db *Dropbox) GetAccountInfo() (*Account, error) {
|
||||
var rv Account
|
||||
|
||||
err := db.doRequest("GET", "account/info", nil, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Shares shares a file.
|
||||
func (db *Dropbox) Shares(path string, shortURL bool) (*Link, error) {
|
||||
var rv Link
|
||||
var params *url.Values
|
||||
|
||||
params = &url.Values{"short_url": {strconv.FormatBool(shortURL)}}
|
||||
act := strings.Join([]string{"shares", db.RootDirectory, path}, "/")
|
||||
err := db.doRequest("POST", act, params, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Media shares a file for streaming (direct access).
|
||||
func (db *Dropbox) Media(path string) (*Link, error) {
|
||||
var rv Link
|
||||
|
||||
act := strings.Join([]string{"media", db.RootDirectory, path}, "/")
|
||||
err := db.doRequest("POST", act, nil, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Search searches the entries matching all the words contained in query in the given path.
|
||||
// The maximum number of entries and whether to consider deleted file may be given.
|
||||
func (db *Dropbox) Search(path, query string, fileLimit int, includeDeleted bool) ([]Entry, error) {
|
||||
var rv []Entry
|
||||
var params *url.Values
|
||||
|
||||
if fileLimit <= 0 || fileLimit > SearchLimitMax {
|
||||
fileLimit = SearchLimitDefault
|
||||
}
|
||||
params = &url.Values{
|
||||
"query": {query},
|
||||
"file_limit": {strconv.FormatInt(int64(fileLimit), 10)},
|
||||
"include_deleted": {strconv.FormatBool(includeDeleted)},
|
||||
}
|
||||
act := strings.Join([]string{"search", db.RootDirectory, path}, "/")
|
||||
err := db.doRequest("GET", act, params, &rv)
|
||||
return rv, err
|
||||
}
|
||||
|
||||
// Delta gets modifications since the cursor.
|
||||
func (db *Dropbox) Delta(cursor, pathPrefix string) (*DeltaPage, error) {
|
||||
var rv DeltaPage
|
||||
var params *url.Values
|
||||
type deltaPageParser struct {
|
||||
Reset bool `json:"reset"` // if true the local state must be cleared.
|
||||
HasMore bool `json:"has_more"` // if true an other call to delta should be made.
|
||||
Cursor // Tag of the current state.
|
||||
Entries [][]json.RawMessage `json:"entries"` // List of changed entries.
|
||||
}
|
||||
var dpp deltaPageParser
|
||||
|
||||
params = &url.Values{}
|
||||
if len(cursor) != 0 {
|
||||
params.Set("cursor", cursor)
|
||||
}
|
||||
if len(pathPrefix) != 0 {
|
||||
params.Set("path_prefix", pathPrefix)
|
||||
}
|
||||
err := db.doRequest("POST", "delta", params, &dpp)
|
||||
rv = DeltaPage{Reset: dpp.Reset, HasMore: dpp.HasMore, Cursor: dpp.Cursor}
|
||||
rv.Entries = make([]DeltaEntry, 0, len(dpp.Entries))
|
||||
for _, jentry := range dpp.Entries {
|
||||
var path string
|
||||
var entry Entry
|
||||
|
||||
if len(jentry) != 2 {
|
||||
return nil, fmt.Errorf("malformed reply")
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(jentry[0], &path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = json.Unmarshal(jentry[1], &entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry.Path == "" {
|
||||
rv.Entries = append(rv.Entries, DeltaEntry{Path: path, Entry: nil})
|
||||
} else {
|
||||
rv.Entries = append(rv.Entries, DeltaEntry{Path: path, Entry: &entry})
|
||||
}
|
||||
}
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// LongPollDelta waits for a notification to happen.
|
||||
func (db *Dropbox) LongPollDelta(cursor string, timeout int) (*DeltaPoll, error) {
|
||||
var rv DeltaPoll
|
||||
var params *url.Values
|
||||
var body []byte
|
||||
var rawurl string
|
||||
var response *http.Response
|
||||
var err error
|
||||
var client http.Client
|
||||
|
||||
params = &url.Values{}
|
||||
if timeout != 0 {
|
||||
if timeout < PollMinTimeout || timeout > PollMaxTimeout {
|
||||
return nil, fmt.Errorf("timeout out of range [%d; %d]", PollMinTimeout, PollMaxTimeout)
|
||||
}
|
||||
params.Set("timeout", strconv.FormatInt(int64(timeout), 10))
|
||||
}
|
||||
params.Set("cursor", cursor)
|
||||
rawurl = fmt.Sprintf("%s/longpoll_delta?%s", db.APINotifyURL, params.Encode())
|
||||
if response, err = client.Get(rawurl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if body, err = getResponse(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Metadata gets the metadata for a file or a directory.
|
||||
// If list is true and src is a directory, immediate child will be sent in the Contents field.
|
||||
// If include_deleted is true, entries deleted will be sent.
|
||||
// hash is the hash of the contents of a directory, it is used to avoid sending data when directory did not change.
|
||||
// rev is the specific revision to get the metadata from.
|
||||
// limit is the maximum number of entries requested.
|
||||
func (db *Dropbox) Metadata(src string, list bool, includeDeleted bool, hash, rev string, limit int) (*Entry, error) {
|
||||
var rv Entry
|
||||
var params *url.Values
|
||||
|
||||
if limit <= 0 {
|
||||
limit = MetadataLimitDefault
|
||||
} else if limit > MetadataLimitMax {
|
||||
limit = MetadataLimitMax
|
||||
}
|
||||
params = &url.Values{
|
||||
"list": {strconv.FormatBool(list)},
|
||||
"include_deleted": {strconv.FormatBool(includeDeleted)},
|
||||
"file_limit": {strconv.FormatInt(int64(limit), 10)},
|
||||
}
|
||||
if len(rev) != 0 {
|
||||
params.Set("rev", rev)
|
||||
}
|
||||
if len(hash) != 0 {
|
||||
params.Set("hash", hash)
|
||||
}
|
||||
|
||||
src = strings.Trim(src, "/")
|
||||
act := strings.Join([]string{"metadata", db.RootDirectory, src}, "/")
|
||||
err := db.doRequest("GET", act, params, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// CopyRef gets a reference to a file.
|
||||
// This reference can be used to copy this file to another user's Dropbox by passing it to the Copy method.
|
||||
func (db *Dropbox) CopyRef(src string) (*CopyRef, error) {
|
||||
var rv CopyRef
|
||||
act := strings.Join([]string{"copy_ref", db.RootDirectory, src}, "/")
|
||||
err := db.doRequest("GET", act, nil, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Revisions gets the list of revisions for a file.
|
||||
func (db *Dropbox) Revisions(src string, revLimit int) ([]Entry, error) {
|
||||
var rv []Entry
|
||||
if revLimit <= 0 {
|
||||
revLimit = RevisionsLimitDefault
|
||||
} else if revLimit > RevisionsLimitMax {
|
||||
revLimit = RevisionsLimitMax
|
||||
}
|
||||
act := strings.Join([]string{"revisions", db.RootDirectory, src}, "/")
|
||||
err := db.doRequest("GET", act,
|
||||
&url.Values{"rev_limit": {strconv.FormatInt(int64(revLimit), 10)}}, &rv)
|
||||
return rv, err
|
||||
}
|
||||
|
||||
// Restore restores a deleted file at the corresponding revision.
|
||||
func (db *Dropbox) Restore(src string, rev string) (*Entry, error) {
|
||||
var rv Entry
|
||||
act := strings.Join([]string{"restore", db.RootDirectory, src}, "/")
|
||||
err := db.doRequest("POST", act, &url.Values{"rev": {rev}}, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Copy copies a file.
|
||||
// If isRef is true src must be a reference from CopyRef instead of a path.
|
||||
func (db *Dropbox) Copy(src, dst string, isRef bool) (*Entry, error) {
|
||||
var rv Entry
|
||||
params := &url.Values{"root": {db.RootDirectory}, "to_path": {dst}}
|
||||
if isRef {
|
||||
params.Set("from_copy_ref", src)
|
||||
} else {
|
||||
params.Set("from_path", src)
|
||||
}
|
||||
err := db.doRequest("POST", "fileops/copy", params, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// CreateFolder creates a new directory.
|
||||
func (db *Dropbox) CreateFolder(path string) (*Entry, error) {
|
||||
var rv Entry
|
||||
err := db.doRequest("POST", "fileops/create_folder",
|
||||
&url.Values{"root": {db.RootDirectory}, "path": {path}}, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Delete removes a file or directory (it is a recursive delete).
|
||||
func (db *Dropbox) Delete(path string) (*Entry, error) {
|
||||
var rv Entry
|
||||
err := db.doRequest("POST", "fileops/delete",
|
||||
&url.Values{"root": {db.RootDirectory}, "path": {path}}, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// Move moves a file or directory.
|
||||
func (db *Dropbox) Move(src, dst string) (*Entry, error) {
|
||||
var rv Entry
|
||||
err := db.doRequest("POST", "fileops/move",
|
||||
&url.Values{"root": {db.RootDirectory},
|
||||
"from_path": {src},
|
||||
"to_path": {dst}}, &rv)
|
||||
return &rv, err
|
||||
}
|
||||
|
||||
// LatestCursor returns the latest cursor without fetching any data.
|
||||
func (db *Dropbox) LatestCursor(prefix string, mediaInfo bool) (*Cursor, error) {
|
||||
var (
|
||||
params = &url.Values{}
|
||||
cur Cursor
|
||||
)
|
||||
|
||||
if prefix != "" {
|
||||
params.Set("path_prefix", prefix)
|
||||
}
|
||||
|
||||
if mediaInfo {
|
||||
params.Set("include_media_info", "true")
|
||||
}
|
||||
|
||||
err := db.doRequest("POST", "delta/latest_cursor", params, &cur)
|
||||
return &cur, err
|
||||
}
|
||||
|
||||
// SharedFolders returns the list of allowed shared folders.
|
||||
func (db *Dropbox) SharedFolders(sharedFolderID string) ([]SharedFolder, error) {
|
||||
var sharedFolders []SharedFolder
|
||||
var err error
|
||||
|
||||
if sharedFolderID != "" {
|
||||
sharedFolders = make([]SharedFolder, 1)
|
||||
err = db.doRequest("GET", "/shared_folders/"+sharedFolderID, nil, &sharedFolders[0])
|
||||
} else {
|
||||
err = db.doRequest("GET", "/shared_folders/", nil, &sharedFolders)
|
||||
}
|
||||
return sharedFolders, err
|
||||
}
|
||||
Reference in New Issue
Block a user