2014-07-09 04:59:30 +08:00
// Dropbox interface
package dropbox
/ *
Limitations of dropbox
2014-07-12 18:46:45 +08:00
File system is case insensitive
2014-07-10 07:17:40 +08:00
2014-07-12 00:21:23 +08:00
FIXME Getting this sometimes
Failed to copy : Upload failed : invalid character '<' looking for beginning of value
This is a JSON decode error - from Update / UploadByChunk
- Caused by 500 error from dropbox
- See https : //github.com/stacktic/dropbox/issues/1
2014-07-12 18:46:45 +08:00
- Possibly confusing dropbox with excess concurrency ?
2015-05-10 18:25:54 +08:00
FIXME implement timeouts - need to get "github.com/stacktic/dropbox"
and hence "golang.org/x/oauth2" which uses DefaultTransport unless it
is set in the context passed into . Client ( )
func ( db * Dropbox ) client ( ) * http . Client {
return db . config . Client ( oauth2 . NoContext , db . token )
}
// HTTPClient is the context key to use with golang.org/x/net/context's
// WithValue function to associate an *http.Client value with a context.
var HTTPClient ContextKey
So pass in a context with HTTPClient set ...
2014-07-09 04:59:30 +08:00
* /
import (
"crypto/md5"
"errors"
"fmt"
"io"
2015-08-17 06:24:34 +08:00
"io/ioutil"
2014-07-09 04:59:30 +08:00
"log"
2014-07-14 18:24:04 +08:00
"path"
2014-07-09 04:59:30 +08:00
"strings"
"time"
"github.com/ncw/rclone/fs"
"github.com/stacktic/dropbox"
)
// Constants
const (
2015-08-17 06:24:34 +08:00
rcloneAppKey = "5jcck7diasz0rqy"
rcloneAppSecret = "1n9m04y2zx7bf26"
uploadChunkSize = 64 * 1024 // chunk size for upload
metadataLimit = dropbox . MetadataLimitDefault // max items to fetch at once
2014-07-09 04:59:30 +08:00
)
// Register with Fs
func init ( ) {
fs . Register ( & fs . FsInfo {
Name : "dropbox" ,
NewFs : NewFs ,
2014-07-30 00:50:07 +08:00
Config : configHelper ,
2014-07-09 04:59:30 +08:00
Options : [ ] fs . Option { {
Name : "app_key" ,
Help : "Dropbox App Key - leave blank to use rclone's." ,
} , {
Name : "app_secret" ,
Help : "Dropbox App Secret - leave blank to use rclone's." ,
} } ,
} )
}
// Configuration helper - called after the user has put in the defaults
2014-07-30 00:50:07 +08:00
func configHelper ( name string ) {
2014-07-09 04:59:30 +08:00
// See if already have a token
token := fs . ConfigFile . MustValue ( name , "token" )
if token != "" {
fmt . Printf ( "Already have a dropbox token - refresh?\n" )
if ! fs . Confirm ( ) {
return
}
}
// Get a dropbox
db := newDropbox ( name )
// This method will ask the user to visit an URL and paste the generated code.
if err := db . Auth ( ) ; err != nil {
log . Fatalf ( "Failed to authorize: %v" , err )
}
// Get the token
token = db . AccessToken ( )
// Stuff it in the config file if it has changed
old := fs . ConfigFile . MustValue ( name , "token" )
if token != old {
fs . ConfigFile . SetValue ( name , "token" , token )
fs . SaveConfig ( )
}
}
// FsDropbox represents a remote dropbox server
type FsDropbox struct {
2015-08-17 06:24:34 +08:00
db * dropbox . Dropbox // the connection to the dropbox server
root string // the path we are working on
slashRoot string // root with "/" prefix, lowercase
slashRootSlash string // root with "/" prefix and postfix, lowercase
2014-07-09 04:59:30 +08:00
}
// FsObjectDropbox describes a dropbox object
type FsObjectDropbox struct {
2015-08-17 06:24:34 +08:00
dropbox * FsDropbox // what this object is part of
remote string // The remote path
bytes int64 // size of the object
modTime time . Time // time it was last modified
hasMetadata bool // metadata is valid
2014-07-09 04:59:30 +08:00
}
// ------------------------------------------------------------
// String converts this FsDropbox to a string
func ( f * FsDropbox ) String ( ) string {
return fmt . Sprintf ( "Dropbox root '%s'" , f . root )
}
// Makes a new dropbox from the config
func newDropbox ( name string ) * dropbox . Dropbox {
db := dropbox . NewDropbox ( )
appKey := fs . ConfigFile . MustValue ( name , "app_key" )
if appKey == "" {
appKey = rcloneAppKey
}
appSecret := fs . ConfigFile . MustValue ( name , "app_secret" )
if appSecret == "" {
appSecret = rcloneAppSecret
}
db . SetAppInfo ( appKey , appSecret )
return db
}
// NewFs contstructs an FsDropbox from the path, container:path
2014-07-14 18:24:04 +08:00
func NewFs ( name , root string ) ( fs . Fs , error ) {
2014-07-09 04:59:30 +08:00
db := newDropbox ( name )
f := & FsDropbox {
2014-07-14 18:24:04 +08:00
db : db ,
2014-07-09 04:59:30 +08:00
}
2014-07-14 18:24:04 +08:00
f . setRoot ( root )
2014-07-09 04:59:30 +08:00
// Read the token from the config file
token := fs . ConfigFile . MustValue ( name , "token" )
// Authorize the client
db . SetAccessToken ( token )
2014-07-14 18:24:04 +08:00
// See if the root is actually an object
entry , err := f . db . Metadata ( f . slashRoot , false , false , "" , "" , metadataLimit )
if err == nil && ! entry . IsDir {
remote := path . Base ( f . root )
newRoot := path . Dir ( f . root )
if newRoot == "." {
newRoot = ""
2014-07-12 19:38:30 +08:00
}
2014-07-14 18:24:04 +08:00
f . setRoot ( newRoot )
obj := f . NewFsObject ( remote )
// return a Fs Limited to this object
return fs . NewLimited ( f , obj ) , nil
}
2014-07-10 07:17:40 +08:00
2014-07-09 04:59:30 +08:00
return f , nil
}
2014-07-14 18:24:04 +08:00
// Sets root in f
func ( f * FsDropbox ) setRoot ( root string ) {
f . root = strings . Trim ( root , "/" )
2015-05-24 02:56:48 +08:00
lowerCaseRoot := strings . ToLower ( f . root )
f . slashRoot = "/" + lowerCaseRoot
2014-07-14 18:24:04 +08:00
f . slashRootSlash = f . slashRoot
2015-05-24 02:56:48 +08:00
if lowerCaseRoot != "" {
2014-07-14 18:24:04 +08:00
f . slashRootSlash += "/"
}
}
2014-07-09 04:59:30 +08:00
// Return an FsObject from a path
2014-07-30 00:50:07 +08:00
//
// May return nil if an error occurred
func ( f * FsDropbox ) newFsObjectWithInfo ( remote string , info * dropbox . Entry ) fs . Object {
2014-07-10 07:17:40 +08:00
o := & FsObjectDropbox {
2014-07-09 04:59:30 +08:00
dropbox : f ,
remote : remote ,
}
2014-07-12 18:46:45 +08:00
if info != nil {
2014-07-10 07:17:40 +08:00
o . setMetadataFromEntry ( info )
2014-07-09 04:59:30 +08:00
} else {
2014-07-10 07:17:40 +08:00
err := o . readEntryAndSetMetadata ( )
2014-07-09 04:59:30 +08:00
if err != nil {
// logged already fs.Debug("Failed to read info: %s", err)
2014-07-30 00:50:07 +08:00
return nil
2014-07-09 04:59:30 +08:00
}
}
2014-07-30 00:50:07 +08:00
return o
2014-07-09 04:59:30 +08:00
}
// Return an FsObject from a path
//
// May return nil if an error occurred
func ( f * FsDropbox ) NewFsObject ( remote string ) fs . Object {
2014-07-30 00:50:07 +08:00
return f . newFsObjectWithInfo ( remote , nil )
2014-07-09 04:59:30 +08:00
}
2015-05-24 02:56:48 +08:00
// Strips the root off path and returns it
func ( f * FsDropbox ) stripRoot ( path string ) * string {
lowercase := strings . ToLower ( path )
if ! strings . HasPrefix ( lowercase , f . slashRootSlash ) {
fs . Stats . Error ( )
2015-08-09 03:10:31 +08:00
fs . ErrorLog ( f , "Path '%s' is not under root '%s'" , path , f . slashRootSlash )
2015-05-24 02:56:48 +08:00
return nil
2014-07-09 04:59:30 +08:00
}
2015-05-24 02:56:48 +08:00
stripped := path [ len ( f . slashRootSlash ) : ]
return & stripped
2014-07-09 04:59:30 +08:00
}
2014-07-12 18:46:45 +08:00
// Walk the root returning a channel of FsObjects
func ( f * FsDropbox ) list ( out fs . ObjectsChan ) {
2015-05-24 02:56:48 +08:00
// Track path component case, it could be different for entries coming from DropBox API
// See https://www.dropboxforum.com/hc/communities/public/questions/201665409-Wrong-character-case-of-folder-name-when-calling-listFolder-using-Sync-API?locale=en-us
// and https://github.com/ncw/rclone/issues/53
nameTree := NewNameTree ( )
2014-07-12 18:46:45 +08:00
cursor := ""
for {
deltaPage , err := f . db . Delta ( cursor , f . slashRoot )
if err != nil {
fs . Stats . Error ( )
2015-08-09 03:10:31 +08:00
fs . ErrorLog ( f , "Couldn't list: %s" , err )
2014-07-12 18:46:45 +08:00
break
} else {
if deltaPage . Reset && cursor != "" {
2015-08-09 03:10:31 +08:00
fs . ErrorLog ( f , "Unexpected reset during listing - try again" )
2014-07-12 18:46:45 +08:00
fs . Stats . Error ( )
break
}
fs . Debug ( f , "%d delta entries received" , len ( deltaPage . Entries ) )
for i := range deltaPage . Entries {
deltaEntry := & deltaPage . Entries [ i ]
entry := deltaEntry . Entry
if entry == nil {
2014-07-13 17:53:53 +08:00
// This notifies of a deleted object
2014-07-12 18:46:45 +08:00
} else {
2015-05-24 02:56:48 +08:00
if len ( entry . Path ) <= 1 || entry . Path [ 0 ] != '/' {
fs . Stats . Error ( )
2015-08-09 03:10:31 +08:00
fs . ErrorLog ( f , "dropbox API inconsistency: a path should always start with a slash and be at least 2 characters: %s" , entry . Path )
2015-05-24 02:56:48 +08:00
continue
}
lastSlashIndex := strings . LastIndex ( entry . Path , "/" )
var parentPath string
if lastSlashIndex == 0 {
parentPath = ""
} else {
parentPath = entry . Path [ 1 : lastSlashIndex ]
}
lastComponent := entry . Path [ lastSlashIndex + 1 : ]
2014-07-13 17:53:53 +08:00
if entry . IsDir {
2015-05-24 02:56:48 +08:00
nameTree . PutCaseCorrectDirectoryName ( parentPath , lastComponent )
2014-07-13 17:53:53 +08:00
} else {
2015-05-24 02:56:48 +08:00
parentPathCorrectCase := nameTree . GetPathWithCorrectCase ( parentPath )
if parentPathCorrectCase != nil {
path := f . stripRoot ( * parentPathCorrectCase + "/" + lastComponent )
if path == nil {
// an error occurred and logged by stripRoot
continue
}
out <- f . newFsObjectWithInfo ( * path , entry )
} else {
nameTree . PutFile ( parentPath , lastComponent , entry )
}
2014-07-13 17:53:53 +08:00
}
2014-07-12 18:46:45 +08:00
}
}
if ! deltaPage . HasMore {
break
2014-07-09 04:59:30 +08:00
}
2014-12-23 20:40:53 +08:00
cursor = deltaPage . Cursor . Cursor
2014-07-09 04:59:30 +08:00
}
}
2015-05-24 02:56:48 +08:00
walkFunc := func ( caseCorrectFilePath string , entry * dropbox . Entry ) {
path := f . stripRoot ( "/" + caseCorrectFilePath )
if path == nil {
// an error occurred and logged by stripRoot
return
}
out <- f . newFsObjectWithInfo ( * path , entry )
}
nameTree . WalkFiles ( f . root , walkFunc )
2014-07-09 04:59:30 +08:00
}
// Walk the path returning a channel of FsObjects
func ( f * FsDropbox ) List ( ) fs . ObjectsChan {
out := make ( fs . ObjectsChan , fs . Config . Checkers )
go func ( ) {
defer close ( out )
2014-07-12 18:46:45 +08:00
f . list ( out )
2014-07-09 04:59:30 +08:00
} ( )
return out
}
// Walk the path returning a channel of FsObjects
func ( f * FsDropbox ) ListDir ( ) fs . DirChan {
out := make ( fs . DirChan , fs . Config . Checkers )
go func ( ) {
defer close ( out )
entry , err := f . db . Metadata ( f . root , true , false , "" , "" , metadataLimit )
if err != nil {
fs . Stats . Error ( )
2015-08-09 03:10:31 +08:00
fs . ErrorLog ( f , "Couldn't list directories in root: %s" , err )
2014-07-09 04:59:30 +08:00
} else {
for i := range entry . Contents {
entry := & entry . Contents [ i ]
if entry . IsDir {
2015-05-24 02:56:48 +08:00
name := f . stripRoot ( entry . Path )
if name == nil {
// an error occurred and logged by stripRoot
continue
}
2014-07-09 04:59:30 +08:00
out <- & fs . Dir {
2015-05-24 02:56:48 +08:00
Name : * name ,
2014-07-09 04:59:30 +08:00
When : time . Time ( entry . ClientMtime ) ,
2015-08-04 04:18:34 +08:00
Bytes : entry . Bytes ,
2014-07-09 04:59:30 +08:00
Count : - 1 ,
}
}
}
}
} ( )
return out
}
// A read closer which doesn't close the input
type readCloser struct {
in io . Reader
}
// Read bytes from the object - see io.Reader
func ( rc * readCloser ) Read ( p [ ] byte ) ( n int , err error ) {
return rc . in . Read ( p )
}
// Dummy close function
func ( rc * readCloser ) Close ( ) error {
return nil
}
// Put the object
//
// Copy the reader in to the new object which is returned
//
// The new object may have been created if an error is returned
func ( f * FsDropbox ) Put ( in io . Reader , remote string , modTime time . Time , size int64 ) ( fs . Object , error ) {
// Temporary FsObject under construction
o := & FsObjectDropbox { dropbox : f , remote : remote }
return o , o . Update ( in , modTime , size )
}
// Mkdir creates the container if it doesn't exist
func ( f * FsDropbox ) Mkdir ( ) error {
2014-07-13 17:51:47 +08:00
entry , err := f . db . Metadata ( f . slashRoot , false , false , "" , "" , metadataLimit )
if err == nil {
if entry . IsDir {
return nil
}
return fmt . Errorf ( "%q already exists as file" , f . root )
}
_ , err = f . db . CreateFolder ( f . slashRoot )
2014-07-09 04:59:30 +08:00
return err
}
// Rmdir deletes the container
//
// Returns an error if it isn't empty
func ( f * FsDropbox ) Rmdir ( ) error {
2014-07-12 18:46:45 +08:00
entry , err := f . db . Metadata ( f . slashRoot , true , false , "" , "" , 16 )
2014-07-09 04:59:30 +08:00
if err != nil {
return err
}
if len ( entry . Contents ) != 0 {
return errors . New ( "Directory not empty" )
}
return f . Purge ( )
}
// Return the precision
2015-08-17 06:24:34 +08:00
func ( f * FsDropbox ) Precision ( ) time . Duration {
return fs . ModTimeNotSupported
2014-07-09 04:59:30 +08:00
}
// Purge deletes all the files and the container
//
2014-07-13 17:53:53 +08:00
// Optional interface: Only implement this if you have a way of
// deleting all the files quicker than just running Remove() on the
// result of List()
2014-07-09 04:59:30 +08:00
func ( f * FsDropbox ) Purge ( ) error {
2014-07-13 17:53:53 +08:00
// Let dropbox delete the filesystem tree
2014-07-09 04:59:30 +08:00
_ , err := f . db . Delete ( f . slashRoot )
return err
}
// ------------------------------------------------------------
// Return the parent Fs
func ( o * FsObjectDropbox ) Fs ( ) fs . Fs {
return o . dropbox
}
// Return a string version
func ( o * FsObjectDropbox ) String ( ) string {
if o == nil {
return "<nil>"
}
return o . remote
}
// Return the remote path
func ( o * FsObjectDropbox ) Remote ( ) string {
return o . remote
}
// Md5sum returns the Md5sum of an object returning a lowercase hex string
func ( o * FsObjectDropbox ) Md5sum ( ) ( string , error ) {
2015-08-17 06:24:34 +08:00
return "" , nil
2014-07-09 04:59:30 +08:00
}
// Size returns the size of an object in bytes
func ( o * FsObjectDropbox ) Size ( ) int64 {
return o . bytes
}
2014-07-10 07:17:40 +08:00
// setMetadataFromEntry sets the fs data from a dropbox.Entry
//
// This isn't a complete set of metadata and has an inacurate date
func ( o * FsObjectDropbox ) setMetadataFromEntry ( info * dropbox . Entry ) {
2015-08-04 04:18:34 +08:00
o . bytes = info . Bytes
2014-07-09 04:59:30 +08:00
o . modTime = time . Time ( info . ClientMtime )
2015-08-17 06:24:34 +08:00
o . hasMetadata = true
2014-07-09 04:59:30 +08:00
}
2014-07-10 07:17:40 +08:00
// Reads the entry from dropbox
func ( o * FsObjectDropbox ) readEntry ( ) ( * dropbox . Entry , error ) {
entry , err := o . dropbox . db . Metadata ( o . remotePath ( ) , false , false , "" , "" , metadataLimit )
if err != nil {
fs . Debug ( o , "Error reading file: %s" , err )
return nil , fmt . Errorf ( "Error reading file: %s" , err )
}
return entry , nil
}
// Read entry if not set and set metadata from it
func ( o * FsObjectDropbox ) readEntryAndSetMetadata ( ) error {
// Last resort set time from client
if ! o . modTime . IsZero ( ) {
return nil
}
entry , err := o . readEntry ( )
if err != nil {
return err
}
o . setMetadataFromEntry ( entry )
return nil
}
2014-07-09 04:59:30 +08:00
// Returns the remote path for the object
func ( o * FsObjectDropbox ) remotePath ( ) string {
2014-07-12 18:46:45 +08:00
return o . dropbox . slashRootSlash + o . remote
2014-07-09 04:59:30 +08:00
}
2014-07-13 17:53:53 +08:00
// Returns the key for the metadata database for a given path
func metadataKey ( path string ) string {
// NB File system is case insensitive
path = strings . ToLower ( path )
2014-07-19 22:48:40 +08:00
hash := md5 . New ( )
2014-07-26 01:19:49 +08:00
_ , _ = hash . Write ( [ ] byte ( path ) )
2014-07-19 22:48:40 +08:00
return fmt . Sprintf ( "%x" , hash . Sum ( nil ) )
2014-07-13 17:53:53 +08:00
}
2014-07-10 07:17:40 +08:00
// Returns the key for the metadata database
func ( o * FsObjectDropbox ) metadataKey ( ) string {
2014-07-13 17:53:53 +08:00
return metadataKey ( o . remotePath ( ) )
2014-07-10 07:17:40 +08:00
}
2014-07-09 04:59:30 +08:00
// readMetaData gets the info if it hasn't already been fetched
func ( o * FsObjectDropbox ) readMetaData ( ) ( err error ) {
2015-08-17 06:24:34 +08:00
if o . hasMetadata {
2014-07-09 04:59:30 +08:00
return nil
}
2014-07-10 07:17:40 +08:00
// Last resort
2014-07-26 01:19:49 +08:00
return o . readEntryAndSetMetadata ( )
2014-07-09 04:59:30 +08:00
}
// ModTime returns the modification time of the object
//
// It attempts to read the objects mtime and if that isn't present the
// LastModified returned in the http headers
func ( o * FsObjectDropbox ) ModTime ( ) time . Time {
err := o . readMetaData ( )
if err != nil {
fs . Log ( o , "Failed to read metadata: %s" , err )
return time . Now ( )
}
return o . modTime
}
// Sets the modification time of the local fs object
2014-07-10 07:17:40 +08:00
//
// Commits the datastore
2014-07-09 04:59:30 +08:00
func ( o * FsObjectDropbox ) SetModTime ( modTime time . Time ) {
2015-08-17 06:24:34 +08:00
// FIXME not implemented
return
2014-07-09 04:59:30 +08:00
}
// Is this object storable
func ( o * FsObjectDropbox ) Storable ( ) bool {
return true
}
// Open an object for read
func ( o * FsObjectDropbox ) Open ( ) ( in io . ReadCloser , err error ) {
in , _ , err = o . dropbox . db . Download ( o . remotePath ( ) , "" , 0 )
return
}
// Update the already existing object
//
// Copy the reader into the object updating modTime and size
//
// The new object may have been created if an error is returned
func ( o * FsObjectDropbox ) Update ( in io . Reader , modTime time . Time , size int64 ) error {
2015-08-17 06:24:34 +08:00
entry , err := o . dropbox . db . UploadByChunk ( ioutil . NopCloser ( in ) , uploadChunkSize , o . remotePath ( ) , true , "" )
2014-07-09 04:59:30 +08:00
if err != nil {
return fmt . Errorf ( "Upload failed: %s" , err )
}
2014-07-10 07:17:40 +08:00
o . setMetadataFromEntry ( entry )
2015-08-17 06:24:34 +08:00
return nil
2014-07-09 04:59:30 +08:00
}
// Remove an object
func ( o * FsObjectDropbox ) Remove ( ) error {
_ , err := o . dropbox . db . Delete ( o . remotePath ( ) )
return err
}
// Check the interfaces are satisfied
var _ fs . Fs = & FsDropbox { }
var _ fs . Purger = & FsDropbox { }
var _ fs . Object = & FsObjectDropbox { }