dropbox: Update dropbox to use the v2 API #349
This is feature complete with the old version but now supports modification times.
This commit is contained in:
parent
23acd3ce01
commit
8e214e838e
5 changed files with 379 additions and 667 deletions
|
@ -93,18 +93,14 @@ To copy a local directory to a dropbox directory called backup
|
||||||
|
|
||||||
### Modified time and MD5SUMs ###
|
### Modified time and MD5SUMs ###
|
||||||
|
|
||||||
Dropbox doesn't provide the ability to set modification times in the
|
Dropbox supports modified times, but the only way to set a
|
||||||
V1 public API, so rclone can't support modified time with Dropbox.
|
modification time is to re-upload the file.
|
||||||
|
|
||||||
This may change in the future - see these issues for details:
|
This means that if you uploaded your data with an older version of
|
||||||
|
rclone which didn't support the v2 API and modified times, rclone will
|
||||||
* [Dropbox V2 API](https://github.com/ncw/rclone/issues/349)
|
decide to upload all your old data to fix the modification times. If
|
||||||
* [Allow syncs for remotes that can't set modtime on existing objects](https://github.com/ncw/rclone/issues/348)
|
you don't want this to happen use `--size-only` or `--checksum` flag
|
||||||
|
to stop it.
|
||||||
Dropbox doesn't return any sort of checksum (MD5 or SHA1).
|
|
||||||
|
|
||||||
Together that means that syncs to dropbox will effectively have the
|
|
||||||
`--size-only` flag set.
|
|
||||||
|
|
||||||
### Specific options ###
|
### Specific options ###
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ Here is an overview of the major features of each cloud storage system.
|
||||||
| Google Drive | MD5 | Yes | No | Yes | R/W |
|
| Google Drive | MD5 | Yes | No | Yes | R/W |
|
||||||
| Amazon S3 | MD5 | Yes | No | No | R/W |
|
| Amazon S3 | MD5 | Yes | No | No | R/W |
|
||||||
| Openstack Swift | MD5 | Yes | No | No | R/W |
|
| Openstack Swift | MD5 | Yes | No | No | R/W |
|
||||||
| Dropbox | - | No | Yes | No | R |
|
| Dropbox | - | Yes | Yes | No | - |
|
||||||
| Google Cloud Storage | MD5 | Yes | No | No | R/W |
|
| Google Cloud Storage | MD5 | Yes | No | No | R/W |
|
||||||
| Amazon Drive | MD5 | No | Yes | No | R |
|
| Amazon Drive | MD5 | No | Yes | No | R |
|
||||||
| Microsoft OneDrive | SHA1 | Yes | Yes | No | R |
|
| Microsoft OneDrive | SHA1 | Yes | Yes | No | R |
|
||||||
|
|
|
@ -1,38 +1,61 @@
|
||||||
// Package dropbox provides an interface to Dropbox object storage
|
// Package dropbox provides an interface to Dropbox object storage
|
||||||
package dropbox
|
package dropbox
|
||||||
|
|
||||||
/*
|
// FIXME put low level retries in
|
||||||
Limitations of dropbox
|
// FIXME add dropbox style hashes
|
||||||
|
// FIXME dropbox for business would be quite easy to add
|
||||||
|
|
||||||
File system is case insensitive
|
/*
|
||||||
|
FIXME is case folding of PathDisplay going to cause a problem?
|
||||||
|
|
||||||
|
From the docs
|
||||||
|
|
||||||
|
path_display String. The cased path to be used for display purposes
|
||||||
|
only. In rare instances the casing will not correctly match the user's
|
||||||
|
filesystem, but this behavior will match the path provided in the Core
|
||||||
|
API v1, and at least the last path component will have the correct
|
||||||
|
casing. Changes to only the casing of paths won't be returned by
|
||||||
|
list_folder/continue. This field will be null if the file or folder is
|
||||||
|
not mounted. This field is optional.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/dropbox/dropbox-sdk-go-unofficial/dropbox"
|
||||||
|
"github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files"
|
||||||
"github.com/ncw/rclone/fs"
|
"github.com/ncw/rclone/fs"
|
||||||
"github.com/ncw/rclone/oauthutil"
|
"github.com/ncw/rclone/oauthutil"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stacktic/dropbox"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const (
|
const (
|
||||||
rcloneAppKey = "5jcck7diasz0rqy"
|
rcloneClientID = "5jcck7diasz0rqy"
|
||||||
rcloneEncryptedAppSecret = "fRS5vVLr2v6FbyXYnIgjwBuUAt0osq_QZTXAEcmZ7g"
|
rcloneEncryptedClientSecret = "fRS5vVLr2v6FbyXYnIgjwBuUAt0osq_QZTXAEcmZ7g"
|
||||||
metadataLimit = dropbox.MetadataLimitDefault // max items to fetch at once
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// Description of how to auth for this app
|
||||||
|
dropboxConfig = &oauth2.Config{
|
||||||
|
Scopes: []string{},
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "https://www.dropbox.com/1/oauth2/authorize",
|
||||||
|
TokenURL: "https://api.dropboxapi.com/1/oauth2/token",
|
||||||
|
}, // FIXME replace with this once vendored dropbox.OAuthEndpoint(""),
|
||||||
|
ClientID: rcloneClientID,
|
||||||
|
ClientSecret: fs.MustReveal(rcloneEncryptedClientSecret),
|
||||||
|
RedirectURL: oauthutil.RedirectLocalhostURL,
|
||||||
|
}
|
||||||
// A regexp matching path names for files Dropbox ignores
|
// A regexp matching path names for files Dropbox ignores
|
||||||
// See https://www.dropbox.com/en/help/145 - Ignored files
|
// See https://www.dropbox.com/en/help/145 - Ignored files
|
||||||
ignoredFiles = regexp.MustCompile(`(?i)(^|/)(desktop\.ini|thumbs\.db|\.ds_store|icon\r|\.dropbox|\.dropbox.attr)$`)
|
ignoredFiles = regexp.MustCompile(`(?i)(^|/)(desktop\.ini|thumbs\.db|\.ds_store|icon\r|\.dropbox|\.dropbox.attr)$`)
|
||||||
|
@ -48,7 +71,12 @@ func init() {
|
||||||
Name: "dropbox",
|
Name: "dropbox",
|
||||||
Description: "Dropbox",
|
Description: "Dropbox",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: configHelper,
|
Config: func(name string) {
|
||||||
|
err := oauthutil.Config("dropbox", name, dropboxConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to configure token: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
Name: "app_key",
|
Name: "app_key",
|
||||||
Help: "Dropbox App Key - leave blank normally.",
|
Help: "Dropbox App Key - leave blank normally.",
|
||||||
|
@ -60,45 +88,12 @@ func init() {
|
||||||
fs.VarP(&uploadChunkSize, "dropbox-chunk-size", "", fmt.Sprintf("Upload chunk size. Max %v.", maxUploadChunkSize))
|
fs.VarP(&uploadChunkSize, "dropbox-chunk-size", "", fmt.Sprintf("Upload chunk size. Max %v.", maxUploadChunkSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration helper - called after the user has put in the defaults
|
|
||||||
func configHelper(name string) {
|
|
||||||
// See if already have a token
|
|
||||||
token := fs.ConfigFileGet(name, "token")
|
|
||||||
if token != "" {
|
|
||||||
fmt.Printf("Already have a dropbox token - refresh?\n")
|
|
||||||
if !fs.Confirm() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a dropbox
|
|
||||||
db, err := newDropbox(name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create dropbox client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.ConfigFileGet(name, "token")
|
|
||||||
if token != old {
|
|
||||||
fs.ConfigFileSet(name, "token", token)
|
|
||||||
fs.SaveConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fs represents a remote dropbox server
|
// Fs represents a remote dropbox server
|
||||||
type Fs struct {
|
type Fs struct {
|
||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on
|
root string // the path we are working on
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
db *dropbox.Dropbox // the connection to the dropbox server
|
srv files.Client // the connection to the dropbox server
|
||||||
slashRoot string // root with "/" prefix, lowercase
|
slashRoot string // root with "/" prefix, lowercase
|
||||||
slashRootSlash string // root with "/" prefix and postfix, lowercase
|
slashRootSlash string // root with "/" prefix and postfix, lowercase
|
||||||
}
|
}
|
||||||
|
@ -110,7 +105,6 @@ type Object struct {
|
||||||
bytes int64 // size of the object
|
bytes int64 // size of the object
|
||||||
modTime time.Time // time it was last modified
|
modTime time.Time // time it was last modified
|
||||||
hasMetadata bool // metadata is valid
|
hasMetadata bool // metadata is valid
|
||||||
mimeType string // content type according to the server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
@ -135,51 +129,45 @@ func (f *Fs) Features() *fs.Features {
|
||||||
return f.features
|
return f.features
|
||||||
}
|
}
|
||||||
|
|
||||||
// Makes a new dropbox from the config
|
|
||||||
func newDropbox(name string) (*dropbox.Dropbox, error) {
|
|
||||||
db := dropbox.NewDropbox()
|
|
||||||
|
|
||||||
appKey := fs.ConfigFileGet(name, "app_key")
|
|
||||||
if appKey == "" {
|
|
||||||
appKey = rcloneAppKey
|
|
||||||
}
|
|
||||||
appSecret := fs.ConfigFileGet(name, "app_secret")
|
|
||||||
if appSecret == "" {
|
|
||||||
appSecret = fs.MustReveal(rcloneEncryptedAppSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.SetAppInfo(appKey, appSecret)
|
|
||||||
return db, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFs contstructs an Fs from the path, container:path
|
// NewFs contstructs an Fs from the path, container:path
|
||||||
func NewFs(name, root string) (fs.Fs, error) {
|
func NewFs(name, root string) (fs.Fs, error) {
|
||||||
if uploadChunkSize > maxUploadChunkSize {
|
if uploadChunkSize > maxUploadChunkSize {
|
||||||
return nil, errors.Errorf("chunk size too big, must be < %v", maxUploadChunkSize)
|
return nil, errors.Errorf("chunk size too big, must be < %v", maxUploadChunkSize)
|
||||||
}
|
}
|
||||||
db, err := newDropbox(name)
|
|
||||||
|
// Convert the old token if it exists. The old token was just
|
||||||
|
// just a string, the new one is a JSON blob
|
||||||
|
oldToken := strings.TrimSpace(fs.ConfigFileGet(name, fs.ConfigToken))
|
||||||
|
if oldToken != "" && oldToken[0] != '{' {
|
||||||
|
fs.Infof(name, "Converting token to new format")
|
||||||
|
newToken := fmt.Sprintf(`{"access_token":"%s","token_type":"bearer","expiry":"0001-01-01T00:00:00Z"}`, oldToken)
|
||||||
|
err := fs.ConfigSetValueAndSave(name, fs.ConfigToken, newToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "NewFS convert token")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oAuthClient, _, err := oauthutil.NewClient(name, dropboxConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to configure dropbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := dropbox.Config{
|
||||||
|
Verbose: false, // enables verbose logging in the SDK
|
||||||
|
Client: oAuthClient, // maybe???
|
||||||
|
}
|
||||||
|
srv := files.New(config)
|
||||||
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
db: db,
|
srv: srv,
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{CaseInsensitive: true, ReadMimeType: true}).Fill(f)
|
f.features = (&fs.Features{CaseInsensitive: true, ReadMimeType: true}).Fill(f)
|
||||||
f.setRoot(root)
|
f.setRoot(root)
|
||||||
|
|
||||||
// Read the token from the config file
|
|
||||||
token := fs.ConfigFileGet(name, "token")
|
|
||||||
|
|
||||||
// Set our custom context which enables our custom transport for timeouts etc
|
|
||||||
db.SetContext(oauthutil.Context())
|
|
||||||
|
|
||||||
// Authorize the client
|
|
||||||
db.SetAccessToken(token)
|
|
||||||
|
|
||||||
// See if the root is actually an object
|
// See if the root is actually an object
|
||||||
entry, err := f.db.Metadata(f.slashRoot, false, false, "", "", metadataLimit)
|
_, err = f.getFileMetadata(f.slashRoot)
|
||||||
if err == nil && !entry.IsDir {
|
if err == nil {
|
||||||
newRoot := path.Dir(f.root)
|
newRoot := path.Dir(f.root)
|
||||||
if newRoot == "." {
|
if newRoot == "." {
|
||||||
newRoot = ""
|
newRoot = ""
|
||||||
|
@ -188,7 +176,6 @@ func NewFs(name, root string) (fs.Fs, error) {
|
||||||
// return an error with an fs which points to the parent
|
// return an error with an fs which points to the parent
|
||||||
return f, fs.ErrorIsFile
|
return f, fs.ErrorIsFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,10 +191,58 @@ func (f *Fs) setRoot(root string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMetadata gets the metadata for a file or directory
|
||||||
|
func (f *Fs) getMetadata(objPath string) (entry files.IsMetadata, notFound bool, err error) {
|
||||||
|
entry, err = f.srv.GetMetadata(&files.GetMetadataArg{Path: objPath})
|
||||||
|
if err != nil {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case files.GetMetadataAPIError:
|
||||||
|
switch e.EndpointError.Path.Tag {
|
||||||
|
case files.LookupErrorNotFound:
|
||||||
|
notFound = true
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileMetadata gets the metadata for a file
|
||||||
|
func (f *Fs) getFileMetadata(filePath string) (fileInfo *files.FileMetadata, err error) {
|
||||||
|
entry, notFound, err := f.getMetadata(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if notFound {
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
|
}
|
||||||
|
fileInfo, ok := entry.(*files.FileMetadata)
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrorNotAFile
|
||||||
|
}
|
||||||
|
return fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDirMetadata gets the metadata for a directory
|
||||||
|
func (f *Fs) getDirMetadata(dirPath string) (dirInfo *files.FolderMetadata, err error) {
|
||||||
|
entry, notFound, err := f.getMetadata(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if notFound {
|
||||||
|
return nil, fs.ErrorDirNotFound
|
||||||
|
}
|
||||||
|
dirInfo, ok := entry.(*files.FolderMetadata)
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrorIsFile
|
||||||
|
}
|
||||||
|
return dirInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Return an Object from a path
|
// Return an Object from a path
|
||||||
//
|
//
|
||||||
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) newObjectWithInfo(remote string, info *dropbox.Entry) (fs.Object, error) {
|
func (f *Fs) newObjectWithInfo(remote string, info *files.FileMetadata) (fs.Object, error) {
|
||||||
o := &Object{
|
o := &Object{
|
||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
|
@ -254,56 +289,67 @@ func (f *Fs) stripRoot(path string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk the root returning a channel of Objects
|
// Walk the root returning a channel of Objects
|
||||||
func (f *Fs) list(out fs.ListOpts, dir string) {
|
func (f *Fs) list(out fs.ListOpts, dir string, recursive bool) {
|
||||||
// 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()
|
|
||||||
cursor := ""
|
|
||||||
root := f.slashRoot
|
root := f.slashRoot
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
root += "/" + dir
|
root += "/" + dir
|
||||||
// We assume that dir is entered in the correct case
|
|
||||||
// here which is likely since it probably came from a
|
|
||||||
// directory listing
|
|
||||||
nameTree.PutCaseCorrectPath(strings.Trim(root, "/"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
started := false
|
||||||
|
var res *files.ListFolderResult
|
||||||
|
var err error
|
||||||
for {
|
for {
|
||||||
deltaPage, err := f.db.Delta(cursor, root)
|
if !started {
|
||||||
|
arg := files.ListFolderArg{
|
||||||
|
Path: root,
|
||||||
|
Recursive: recursive,
|
||||||
|
}
|
||||||
|
if root == "/" {
|
||||||
|
arg.Path = "" // Specify root folder as empty string
|
||||||
|
}
|
||||||
|
res, err = f.srv.ListFolder(&arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.SetError(errors.Wrap(err, "couldn't list"))
|
switch e := err.(type) {
|
||||||
|
case files.ListFolderAPIError:
|
||||||
|
switch e.EndpointError.Path.Tag {
|
||||||
|
case files.LookupErrorNotFound:
|
||||||
|
err = fs.ErrorDirNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.SetError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if deltaPage.Reset && cursor != "" {
|
started = false
|
||||||
err = errors.New("unexpected reset during listing")
|
|
||||||
out.SetError(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fs.Debugf(f, "%d delta entries received", len(deltaPage.Entries))
|
|
||||||
for i := range deltaPage.Entries {
|
|
||||||
deltaEntry := &deltaPage.Entries[i]
|
|
||||||
entry := deltaEntry.Entry
|
|
||||||
if entry == nil {
|
|
||||||
// This notifies of a deleted object
|
|
||||||
} else {
|
} else {
|
||||||
if len(entry.Path) <= 1 || entry.Path[0] != '/' {
|
arg := files.ListFolderContinueArg{
|
||||||
fs.Debugf(f, "dropbox API inconsistency: a path should always start with a slash and be at least 2 characters: %s", entry.Path)
|
Cursor: res.Cursor,
|
||||||
|
}
|
||||||
|
res, err = f.srv.ListFolderContinue(&arg)
|
||||||
|
if err != nil {
|
||||||
|
out.SetError(errors.Wrap(err, "list continue"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, entry := range res.Entries {
|
||||||
|
var fileInfo *files.FileMetadata
|
||||||
|
var folderInfo *files.FolderMetadata
|
||||||
|
var metadata *files.Metadata
|
||||||
|
switch info := entry.(type) {
|
||||||
|
case *files.FolderMetadata:
|
||||||
|
folderInfo = info
|
||||||
|
metadata = &info.Metadata
|
||||||
|
case *files.FileMetadata:
|
||||||
|
fileInfo = info
|
||||||
|
metadata = &info.Metadata
|
||||||
|
default:
|
||||||
|
fs.Errorf(f, "Unknown type %T", entry)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSlashIndex := strings.LastIndex(entry.Path, "/")
|
entryPath := metadata.PathDisplay // FIXME PathLower
|
||||||
|
|
||||||
var parentPath string
|
if folderInfo != nil {
|
||||||
if lastSlashIndex == 0 {
|
name, err := f.stripRoot(entryPath + "/")
|
||||||
parentPath = ""
|
|
||||||
} else {
|
|
||||||
parentPath = entry.Path[1:lastSlashIndex]
|
|
||||||
}
|
|
||||||
lastComponent := entry.Path[lastSlashIndex+1:]
|
|
||||||
|
|
||||||
if entry.IsDir {
|
|
||||||
nameTree.PutCaseCorrectDirectoryName(parentPath, lastComponent)
|
|
||||||
name, err := f.stripRoot(entry.Path + "/")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.SetError(err)
|
out.SetError(err)
|
||||||
return
|
return
|
||||||
|
@ -312,23 +358,22 @@ func (f *Fs) list(out fs.ListOpts, dir string) {
|
||||||
if name != "" && name != dir {
|
if name != "" && name != dir {
|
||||||
dir := &fs.Dir{
|
dir := &fs.Dir{
|
||||||
Name: name,
|
Name: name,
|
||||||
When: time.Time(entry.ClientMtime),
|
When: time.Now(),
|
||||||
Bytes: entry.Bytes,
|
//When: folderInfo.ClientMtime,
|
||||||
Count: -1,
|
//Bytes: folderInfo.Bytes,
|
||||||
|
//Count: -1,
|
||||||
}
|
}
|
||||||
if out.AddDir(dir) {
|
if out.AddDir(dir) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if fileInfo != nil {
|
||||||
parentPathCorrectCase := nameTree.GetPathWithCorrectCase(parentPath)
|
path, err := f.stripRoot(entryPath)
|
||||||
if parentPathCorrectCase != nil {
|
|
||||||
path, err := f.stripRoot(*parentPathCorrectCase + "/" + lastComponent)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.SetError(err)
|
out.SetError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
o, err := f.newObjectWithInfo(path, entry)
|
o, err := f.newObjectWithInfo(path, fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.SetError(err)
|
out.SetError(err)
|
||||||
return
|
return
|
||||||
|
@ -336,74 +381,11 @@ func (f *Fs) list(out fs.ListOpts, dir string) {
|
||||||
if out.Add(o) {
|
if out.Add(o) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
nameTree.PutFile(parentPath, lastComponent, entry)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if !res.HasMore {
|
||||||
}
|
|
||||||
if !deltaPage.HasMore {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
cursor = deltaPage.Cursor.Cursor
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
walkFunc := func(caseCorrectFilePath string, entry *dropbox.Entry) error {
|
|
||||||
path, err := f.stripRoot("/" + caseCorrectFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o, err := f.newObjectWithInfo(path, entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if out.Add(o) {
|
|
||||||
return fs.ErrorListAborted
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := nameTree.WalkFiles(f.root, walkFunc)
|
|
||||||
if err != nil {
|
|
||||||
out.SetError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// listOneLevel walks the path one level deep
|
|
||||||
func (f *Fs) listOneLevel(out fs.ListOpts, dir string) {
|
|
||||||
root := f.root
|
|
||||||
if dir != "" {
|
|
||||||
root += "/" + dir
|
|
||||||
}
|
|
||||||
dirEntry, err := f.db.Metadata(root, true, false, "", "", metadataLimit)
|
|
||||||
if err != nil {
|
|
||||||
out.SetError(errors.Wrap(err, "couldn't list single level"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i := range dirEntry.Contents {
|
|
||||||
entry := &dirEntry.Contents[i]
|
|
||||||
// Normalise the path to the dir passed in
|
|
||||||
remote := path.Join(dir, path.Base(entry.Path))
|
|
||||||
if entry.IsDir {
|
|
||||||
dir := &fs.Dir{
|
|
||||||
Name: remote,
|
|
||||||
When: time.Time(entry.ClientMtime),
|
|
||||||
Bytes: entry.Bytes,
|
|
||||||
Count: -1,
|
|
||||||
}
|
|
||||||
if out.AddDir(dir) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
o, err := f.newObjectWithInfo(remote, entry)
|
|
||||||
if err != nil {
|
|
||||||
out.SetError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if out.Add(o) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,9 +395,9 @@ func (f *Fs) List(out fs.ListOpts, dir string) {
|
||||||
level := out.Level()
|
level := out.Level()
|
||||||
switch level {
|
switch level {
|
||||||
case 1:
|
case 1:
|
||||||
f.listOneLevel(out, dir)
|
f.list(out, dir, false)
|
||||||
case fs.MaxLevel:
|
case fs.MaxLevel:
|
||||||
f.list(out, dir)
|
f.list(out, dir, true)
|
||||||
default:
|
default:
|
||||||
out.SetError(fs.ErrorLevelNotSupported)
|
out.SetError(fs.ErrorLevelNotSupported)
|
||||||
}
|
}
|
||||||
|
@ -453,14 +435,25 @@ func (f *Fs) Put(in io.Reader, src fs.ObjectInfo) (fs.Object, error) {
|
||||||
// Mkdir creates the container if it doesn't exist
|
// Mkdir creates the container if it doesn't exist
|
||||||
func (f *Fs) Mkdir(dir string) error {
|
func (f *Fs) Mkdir(dir string) error {
|
||||||
root := path.Join(f.slashRoot, dir)
|
root := path.Join(f.slashRoot, dir)
|
||||||
entry, err := f.db.Metadata(root, false, false, "", "", metadataLimit)
|
|
||||||
if err == nil {
|
// can't create or run metadata on root
|
||||||
if entry.IsDir {
|
if root == "/" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.Errorf("%q already exists as file", f.root)
|
|
||||||
|
// check directory doesn't exist
|
||||||
|
_, err := f.getDirMetadata(root)
|
||||||
|
if err == nil {
|
||||||
|
return nil // directory exists already
|
||||||
|
} else if err != fs.ErrorDirNotFound {
|
||||||
|
return err // some other error
|
||||||
}
|
}
|
||||||
_, err = f.db.CreateFolder(root)
|
|
||||||
|
// create it
|
||||||
|
arg2 := files.CreateFolderArg{
|
||||||
|
Path: root,
|
||||||
|
}
|
||||||
|
_, err = f.srv.CreateFolder(&arg2)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,20 +462,42 @@ func (f *Fs) Mkdir(dir string) error {
|
||||||
// Returns an error if it isn't empty
|
// Returns an error if it isn't empty
|
||||||
func (f *Fs) Rmdir(dir string) error {
|
func (f *Fs) Rmdir(dir string) error {
|
||||||
root := path.Join(f.slashRoot, dir)
|
root := path.Join(f.slashRoot, dir)
|
||||||
entry, err := f.db.Metadata(root, true, false, "", "", 16)
|
|
||||||
if err != nil {
|
// can't remove root
|
||||||
return err
|
if root == "/" {
|
||||||
|
return errors.New("can't remove root directory")
|
||||||
}
|
}
|
||||||
if len(entry.Contents) != 0 {
|
|
||||||
|
// check directory exists
|
||||||
|
_, err := f.getDirMetadata(root)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Rmdir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check directory empty
|
||||||
|
arg := files.ListFolderArg{
|
||||||
|
Path: root,
|
||||||
|
Recursive: false,
|
||||||
|
}
|
||||||
|
if root == "/" {
|
||||||
|
arg.Path = "" // Specify root folder as empty string
|
||||||
|
}
|
||||||
|
res, err := f.srv.ListFolder(&arg)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Rmdir")
|
||||||
|
}
|
||||||
|
if len(res.Entries) != 0 {
|
||||||
return errors.New("directory not empty")
|
return errors.New("directory not empty")
|
||||||
}
|
}
|
||||||
_, err = f.db.Delete(root)
|
|
||||||
|
// remove it
|
||||||
|
_, err = f.srv.Delete(&files.DeleteArg{Path: root})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precision returns the precision
|
// Precision returns the precision
|
||||||
func (f *Fs) Precision() time.Duration {
|
func (f *Fs) Precision() time.Duration {
|
||||||
return fs.ModTimeNotSupported
|
return time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy src to this remote using server side copy operations.
|
// Copy src to this remote using server side copy operations.
|
||||||
|
@ -507,16 +522,25 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
|
||||||
remote: remote,
|
remote: remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
srcPath := srcObj.remotePath()
|
// Copy
|
||||||
dstPath := dstObj.remotePath()
|
arg := files.RelocationArg{}
|
||||||
entry, err := f.db.Copy(srcPath, dstPath, false)
|
arg.FromPath = srcObj.remotePath()
|
||||||
|
arg.ToPath = dstObj.remotePath()
|
||||||
|
entry, err := f.srv.Copy(&arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "copy failed")
|
return nil, errors.Wrap(err, "copy failed")
|
||||||
}
|
}
|
||||||
err = dstObj.setMetadataFromEntry(entry)
|
|
||||||
|
// Set the metadata
|
||||||
|
fileInfo, ok := entry.(*files.FileMetadata)
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrorNotAFile
|
||||||
|
}
|
||||||
|
err = dstObj.setMetadataFromEntry(fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "copy failed")
|
return nil, errors.Wrap(err, "copy failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return dstObj, nil
|
return dstObj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -527,7 +551,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
|
||||||
// result of List()
|
// result of List()
|
||||||
func (f *Fs) Purge() error {
|
func (f *Fs) Purge() error {
|
||||||
// Let dropbox delete the filesystem tree
|
// Let dropbox delete the filesystem tree
|
||||||
_, err := f.db.Delete(f.slashRoot)
|
_, err := f.srv.Delete(&files.DeleteArg{Path: f.slashRoot})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,13 +577,21 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
|
||||||
remote: remote,
|
remote: remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
srcPath := srcObj.remotePath()
|
// Do the move
|
||||||
dstPath := dstObj.remotePath()
|
arg := files.RelocationArg{}
|
||||||
entry, err := f.db.Move(srcPath, dstPath)
|
arg.FromPath = srcObj.remotePath()
|
||||||
|
arg.ToPath = dstObj.remotePath()
|
||||||
|
entry, err := f.srv.Move(&arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "move failed")
|
return nil, errors.Wrap(err, "move failed")
|
||||||
}
|
}
|
||||||
err = dstObj.setMetadataFromEntry(entry)
|
|
||||||
|
// Set the metadata
|
||||||
|
fileInfo, ok := entry.(*files.FileMetadata)
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrorNotAFile
|
||||||
|
}
|
||||||
|
err = dstObj.setMetadataFromEntry(fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "move failed")
|
return nil, errors.Wrap(err, "move failed")
|
||||||
}
|
}
|
||||||
|
@ -584,19 +616,25 @@ func (f *Fs) DirMove(src fs.Fs, srcRemote, dstRemote string) error {
|
||||||
dstPath := path.Join(f.slashRoot, dstRemote)
|
dstPath := path.Join(f.slashRoot, dstRemote)
|
||||||
|
|
||||||
// Check if destination exists
|
// Check if destination exists
|
||||||
entry, err := f.db.Metadata(f.slashRoot, false, false, "", "", metadataLimit)
|
_, err := f.getDirMetadata(f.slashRoot)
|
||||||
if err == nil && !entry.IsDeleted {
|
if err == nil {
|
||||||
return fs.ErrorDirExists
|
return fs.ErrorDirExists
|
||||||
|
} else if err != fs.ErrorDirNotFound {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the parent directory exists
|
// Make sure the parent directory exists
|
||||||
// ...apparently not necessary
|
// ...apparently not necessary
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
_, err = f.db.Move(srcPath, dstPath)
|
arg := files.RelocationArg{}
|
||||||
|
arg.FromPath = srcPath
|
||||||
|
arg.ToPath = dstPath
|
||||||
|
_, err = f.srv.Move(&arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "MoveDir failed")
|
return errors.Wrap(err, "MoveDir failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -635,32 +673,19 @@ func (o *Object) Size() int64 {
|
||||||
return o.bytes
|
return o.bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMetadataFromEntry sets the fs data from a dropbox.Entry
|
// setMetadataFromEntry sets the fs data from a files.FileMetadata
|
||||||
//
|
//
|
||||||
// This isn't a complete set of metadata and has an inacurate date
|
// This isn't a complete set of metadata and has an inacurate date
|
||||||
func (o *Object) setMetadataFromEntry(info *dropbox.Entry) error {
|
func (o *Object) setMetadataFromEntry(info *files.FileMetadata) error {
|
||||||
if info.IsDir {
|
o.bytes = int64(info.Size)
|
||||||
return errors.Wrapf(fs.ErrorNotAFile, "%q", o.remote)
|
o.modTime = info.ClientModified
|
||||||
}
|
|
||||||
o.bytes = info.Bytes
|
|
||||||
o.modTime = time.Time(info.ClientMtime)
|
|
||||||
o.mimeType = info.MimeType
|
|
||||||
o.hasMetadata = true
|
o.hasMetadata = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the entry from dropbox
|
// Reads the entry for a file from dropbox
|
||||||
func (o *Object) readEntry() (*dropbox.Entry, error) {
|
func (o *Object) readEntry() (*files.FileMetadata, error) {
|
||||||
entry, err := o.fs.db.Metadata(o.remotePath(), false, false, "", "", metadataLimit)
|
return o.fs.getFileMetadata(o.remotePath())
|
||||||
if err != nil {
|
|
||||||
if dropboxErr, ok := err.(*dropbox.Error); ok {
|
|
||||||
if dropboxErr.StatusCode == http.StatusNotFound {
|
|
||||||
return nil, fs.ErrorObjectNotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return entry, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read entry if not set and set metadata from it
|
// Read entry if not set and set metadata from it
|
||||||
|
@ -721,7 +746,9 @@ func (o *Object) ModTime() time.Time {
|
||||||
//
|
//
|
||||||
// Commits the datastore
|
// Commits the datastore
|
||||||
func (o *Object) SetModTime(modTime time.Time) error {
|
func (o *Object) SetModTime(modTime time.Time) error {
|
||||||
// FIXME not implemented
|
// Dropbox doesn't have a way of doing this so returning this
|
||||||
|
// error will cause the file to be re-uploaded to set the
|
||||||
|
// time.
|
||||||
return fs.ErrorCantSetModTime
|
return fs.ErrorCantSetModTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,30 +759,69 @@ func (o *Object) Storable() bool {
|
||||||
|
|
||||||
// Open an object for read
|
// Open an object for read
|
||||||
func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
||||||
// FIXME should send a patch for dropbox module which allow setting headers
|
headers := fs.OpenOptionHeaders(options)
|
||||||
var offset int64
|
arg := files.DownloadArg{Path: o.remotePath(), ExtraHeaders: headers}
|
||||||
for _, option := range options {
|
_, in, err = o.fs.srv.Download(&arg)
|
||||||
switch x := option.(type) {
|
|
||||||
case *fs.SeekOption:
|
|
||||||
offset = x.Offset
|
|
||||||
default:
|
|
||||||
if option.Mandatory() {
|
|
||||||
fs.Logf(o, "Unsupported mandatory option: %v", option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
in, _, err = o.fs.db.Download(o.remotePath(), "", offset)
|
switch e := err.(type) {
|
||||||
if dropboxErr, ok := err.(*dropbox.Error); ok {
|
case files.DownloadAPIError:
|
||||||
// Dropbox return 461 for copyright violation so don't
|
// Don't attempt to retry copyright violation errors
|
||||||
// attempt to retry this error
|
if e.EndpointError.Path.Tag == files.LookupErrorRestrictedContent {
|
||||||
if dropboxErr.StatusCode == 461 {
|
|
||||||
return nil, fs.NoRetryError(err)
|
return nil, fs.NoRetryError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uploadChunked uploads the object in parts
|
||||||
|
//
|
||||||
|
// Call only if size is >= uploadChunkSize
|
||||||
|
//
|
||||||
|
// FIXME rework for retries
|
||||||
|
func (o *Object) uploadChunked(in io.Reader, commitInfo *files.CommitInfo, size int64) (entry *files.FileMetadata, err error) {
|
||||||
|
chunkSize := int64(uploadChunkSize)
|
||||||
|
chunks := int(size/chunkSize) + 1
|
||||||
|
|
||||||
|
// write the first whole chunk
|
||||||
|
fs.Debugf(o, "Uploading chunk 1/%d", chunks)
|
||||||
|
res, err := o.fs.srv.UploadSessionStart(&files.UploadSessionStartArg{}, &io.LimitedReader{R: in, N: chunkSize})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := files.UploadSessionCursor{
|
||||||
|
SessionId: res.SessionId,
|
||||||
|
Offset: uint64(chunkSize),
|
||||||
|
}
|
||||||
|
appendArg := files.UploadSessionAppendArg{
|
||||||
|
Cursor: &cursor,
|
||||||
|
Close: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// write more whole chunks (if any)
|
||||||
|
for i := 2; i < chunks; i++ {
|
||||||
|
fs.Debugf(o, "Uploading chunk %d/%d", i, chunks)
|
||||||
|
err = o.fs.srv.UploadSessionAppendV2(&appendArg, &io.LimitedReader{R: in, N: chunkSize})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cursor.Offset += uint64(chunkSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the remains
|
||||||
|
args := &files.UploadSessionFinishArg{
|
||||||
|
Cursor: &cursor,
|
||||||
|
Commit: commitInfo,
|
||||||
|
}
|
||||||
|
fs.Debugf(o, "Uploading chunk %d/%d", chunks, chunks)
|
||||||
|
entry, err = o.fs.srv.UploadSessionFinish(args, in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Update the already existing object
|
// Update the already existing object
|
||||||
//
|
//
|
||||||
// Copy the reader into the object updating modTime and size
|
// Copy the reader into the object updating modTime and size
|
||||||
|
@ -767,7 +833,19 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo) error {
|
||||||
fs.Logf(o, "File name disallowed - not uploading")
|
fs.Logf(o, "File name disallowed - not uploading")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
entry, err := o.fs.db.UploadByChunk(ioutil.NopCloser(in), int(uploadChunkSize), remote, true, "")
|
commitInfo := files.NewCommitInfo(o.remotePath())
|
||||||
|
commitInfo.Mode.Tag = "overwrite"
|
||||||
|
// The Dropbox API only accepts timestamps in UTC with second precision.
|
||||||
|
commitInfo.ClientModified = src.ModTime().UTC().Round(time.Second)
|
||||||
|
|
||||||
|
size := src.Size()
|
||||||
|
var err error
|
||||||
|
var entry *files.FileMetadata
|
||||||
|
if size > int64(uploadChunkSize) {
|
||||||
|
entry, err = o.uploadChunked(in, commitInfo, size)
|
||||||
|
} else {
|
||||||
|
entry, err = o.fs.srv.Upload(commitInfo, in)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "upload failed")
|
return errors.Wrap(err, "upload failed")
|
||||||
}
|
}
|
||||||
|
@ -776,20 +854,10 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo) error {
|
||||||
|
|
||||||
// Remove an object
|
// Remove an object
|
||||||
func (o *Object) Remove() error {
|
func (o *Object) Remove() error {
|
||||||
_, err := o.fs.db.Delete(o.remotePath())
|
_, err := o.fs.srv.Delete(&files.DeleteArg{Path: o.remotePath()})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MimeType of an Object if known, "" otherwise
|
|
||||||
func (o *Object) MimeType() string {
|
|
||||||
err := o.readMetaData()
|
|
||||||
if err != nil {
|
|
||||||
fs.Logf(o, "Failed to read metadata: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return o.mimeType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
// Check the interfaces are satisfied
|
||||||
var (
|
var (
|
||||||
_ fs.Fs = (*Fs)(nil)
|
_ fs.Fs = (*Fs)(nil)
|
||||||
|
@ -798,5 +866,4 @@ var (
|
||||||
_ fs.Mover = (*Fs)(nil)
|
_ fs.Mover = (*Fs)(nil)
|
||||||
_ fs.DirMover = (*Fs)(nil)
|
_ fs.DirMover = (*Fs)(nil)
|
||||||
_ fs.Object = (*Object)(nil)
|
_ fs.Object = (*Object)(nil)
|
||||||
_ fs.MimeTyper = (*Object)(nil)
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,211 +0,0 @@
|
||||||
package dropbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ncw/rclone/fs"
|
|
||||||
"github.com/stacktic/dropbox"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FIXME Get rid of Stats.Error() counting and return errors
|
|
||||||
|
|
||||||
type nameTreeNode struct {
|
|
||||||
// Map from lowercase directory name to tree node
|
|
||||||
Directories map[string]*nameTreeNode
|
|
||||||
|
|
||||||
// Map from file name (case sensitive) to dropbox entry
|
|
||||||
Files map[string]*dropbox.Entry
|
|
||||||
|
|
||||||
// Empty string if exact case is unknown or root node
|
|
||||||
CaseCorrectName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
|
|
||||||
func newNameTreeNode(caseCorrectName string) *nameTreeNode {
|
|
||||||
return &nameTreeNode{
|
|
||||||
CaseCorrectName: caseCorrectName,
|
|
||||||
Directories: make(map[string]*nameTreeNode),
|
|
||||||
Files: make(map[string]*dropbox.Entry),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNameTree() *nameTreeNode {
|
|
||||||
return newNameTreeNode("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) String() string {
|
|
||||||
if len(tree.CaseCorrectName) == 0 {
|
|
||||||
return "nameTreeNode/<root>"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("nameTreeNode/%q", tree.CaseCorrectName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) getTreeNode(path string) *nameTreeNode {
|
|
||||||
if len(path) == 0 {
|
|
||||||
// no lookup required, just return root
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
current := tree
|
|
||||||
for _, component := range strings.Split(path, "/") {
|
|
||||||
if len(component) == 0 {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "getTreeNode: path component is empty (full path %q)", path)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lowercase := strings.ToLower(component)
|
|
||||||
|
|
||||||
lookup := current.Directories[lowercase]
|
|
||||||
if lookup == nil {
|
|
||||||
lookup = newNameTreeNode("")
|
|
||||||
current.Directories[lowercase] = lookup
|
|
||||||
}
|
|
||||||
|
|
||||||
current = lookup
|
|
||||||
}
|
|
||||||
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutCaseCorrectPath puts a known good path into the nameTree
|
|
||||||
func (tree *nameTreeNode) PutCaseCorrectPath(caseCorrectPath string) {
|
|
||||||
if len(caseCorrectPath) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
current := tree
|
|
||||||
for _, component := range strings.Split(caseCorrectPath, "/") {
|
|
||||||
if len(component) == 0 {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "PutCaseCorrectPath: path component is empty (full path %q)", caseCorrectPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lowercase := strings.ToLower(component)
|
|
||||||
lookup := current.Directories[lowercase]
|
|
||||||
if lookup == nil {
|
|
||||||
lookup = newNameTreeNode(component)
|
|
||||||
current.Directories[lowercase] = lookup
|
|
||||||
}
|
|
||||||
current = lookup
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) PutCaseCorrectDirectoryName(parentPath string, caseCorrectDirectoryName string) {
|
|
||||||
if len(caseCorrectDirectoryName) == 0 {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "PutCaseCorrectDirectoryName: empty caseCorrectDirectoryName is not allowed (parentPath: %q)", parentPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
node := tree.getTreeNode(parentPath)
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerCaseDirectoryName := strings.ToLower(caseCorrectDirectoryName)
|
|
||||||
directory := node.Directories[lowerCaseDirectoryName]
|
|
||||||
if directory == nil {
|
|
||||||
directory = newNameTreeNode(caseCorrectDirectoryName)
|
|
||||||
node.Directories[lowerCaseDirectoryName] = directory
|
|
||||||
} else {
|
|
||||||
if len(directory.CaseCorrectName) > 0 {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "PutCaseCorrectDirectoryName: directory %q is already exists under parent path %q", caseCorrectDirectoryName, parentPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
directory.CaseCorrectName = caseCorrectDirectoryName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) PutFile(parentPath string, caseCorrectFileName string, dropboxEntry *dropbox.Entry) {
|
|
||||||
node := tree.getTreeNode(parentPath)
|
|
||||||
if node == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Files[caseCorrectFileName] != nil {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "PutFile: file %q is already exists at %q", caseCorrectFileName, parentPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Files[caseCorrectFileName] = dropboxEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) GetPathWithCorrectCase(path string) *string {
|
|
||||||
if path == "" {
|
|
||||||
empty := ""
|
|
||||||
return &empty
|
|
||||||
}
|
|
||||||
|
|
||||||
var result bytes.Buffer
|
|
||||||
|
|
||||||
current := tree
|
|
||||||
for _, component := range strings.Split(path, "/") {
|
|
||||||
if component == "" {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "GetPathWithCorrectCase: path component is empty (full path %q)", path)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lowercase := strings.ToLower(component)
|
|
||||||
|
|
||||||
current = current.Directories[lowercase]
|
|
||||||
if current == nil || current.CaseCorrectName == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = result.WriteString("/")
|
|
||||||
_, _ = result.WriteString(current.CaseCorrectName)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultString := result.String()
|
|
||||||
return &resultString
|
|
||||||
}
|
|
||||||
|
|
||||||
type nameTreeFileWalkFunc func(caseCorrectFilePath string, entry *dropbox.Entry) error
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) walkFilesRec(currentPath string, walkFunc nameTreeFileWalkFunc) error {
|
|
||||||
var prefix string
|
|
||||||
if currentPath == "" {
|
|
||||||
prefix = ""
|
|
||||||
} else {
|
|
||||||
prefix = currentPath + "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, entry := range tree.Files {
|
|
||||||
err := walkFunc(prefix+name, entry)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for lowerCaseName, directory := range tree.Directories {
|
|
||||||
caseCorrectName := directory.CaseCorrectName
|
|
||||||
if caseCorrectName == "" {
|
|
||||||
fs.Stats.Error()
|
|
||||||
fs.Errorf(tree, "WalkFiles: exact name of the directory %q is unknown (parent path: %q)", lowerCaseName, currentPath)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := directory.walkFilesRec(prefix+caseCorrectName, walkFunc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *nameTreeNode) WalkFiles(rootPath string, walkFunc nameTreeFileWalkFunc) error {
|
|
||||||
node := tree.getTreeNode(rootPath)
|
|
||||||
if node == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.walkFilesRec(rootPath, walkFunc)
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
package dropbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ncw/rclone/fs"
|
|
||||||
dropboxapi "github.com/stacktic/dropbox"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutCaseCorrectDirectoryName(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutCaseCorrectDirectoryName("a/b", "C")
|
|
||||||
|
|
||||||
assert.Equal(t, "", tree.CaseCorrectName, "Root CaseCorrectName should be empty")
|
|
||||||
|
|
||||||
a := tree.Directories["a"]
|
|
||||||
assert.Equal(t, "", a.CaseCorrectName, "CaseCorrectName at 'a' should be empty")
|
|
||||||
|
|
||||||
b := a.Directories["b"]
|
|
||||||
assert.Equal(t, "", b.CaseCorrectName, "CaseCorrectName at 'a/b' should be empty")
|
|
||||||
|
|
||||||
c := b.Directories["c"]
|
|
||||||
assert.Equal(t, "C", c.CaseCorrectName, "CaseCorrectName at 'a/b/c' should be 'C'")
|
|
||||||
|
|
||||||
assert.Equal(t, errors, fs.Stats.GetErrors(), "No errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutCaseCorrectPath(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutCaseCorrectPath("A/b/C")
|
|
||||||
|
|
||||||
assert.Equal(t, "", tree.CaseCorrectName, "Root CaseCorrectName should be empty")
|
|
||||||
|
|
||||||
a := tree.Directories["a"]
|
|
||||||
assert.Equal(t, "A", a.CaseCorrectName, "CaseCorrectName at 'a' should be 'A'")
|
|
||||||
|
|
||||||
b := a.Directories["b"]
|
|
||||||
assert.Equal(t, "b", b.CaseCorrectName, "CaseCorrectName at 'a/b' should be 'b'")
|
|
||||||
|
|
||||||
c := b.Directories["c"]
|
|
||||||
assert.Equal(t, "C", c.CaseCorrectName, "CaseCorrectName at 'a/b/c' should be 'C'")
|
|
||||||
|
|
||||||
assert.Equal(t, errors, fs.Stats.GetErrors(), "No errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutCaseCorrectDirectoryNameEmptyComponent(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutCaseCorrectDirectoryName("/a", "C")
|
|
||||||
tree.PutCaseCorrectDirectoryName("b/", "C")
|
|
||||||
tree.PutCaseCorrectDirectoryName("a//b", "C")
|
|
||||||
|
|
||||||
assert.True(t, fs.Stats.GetErrors() == errors+3, "3 errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutCaseCorrectDirectoryNameEmptyParent(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutCaseCorrectDirectoryName("", "C")
|
|
||||||
|
|
||||||
c := tree.Directories["c"]
|
|
||||||
assert.True(t, c.CaseCorrectName == "C", "CaseCorrectName at 'c' should be 'C'")
|
|
||||||
|
|
||||||
assert.True(t, fs.Stats.GetErrors() == errors, "No errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPathWithCorrectCase(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutCaseCorrectDirectoryName("a", "C")
|
|
||||||
assert.True(t, tree.GetPathWithCorrectCase("a/c") == nil, "Path for 'a' should not be available")
|
|
||||||
|
|
||||||
tree.PutCaseCorrectDirectoryName("", "A")
|
|
||||||
assert.True(t, *tree.GetPathWithCorrectCase("a/c") == "/A/C", "Path for 'a/c' should be '/A/C'")
|
|
||||||
|
|
||||||
assert.True(t, fs.Stats.GetErrors() == errors, "No errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutAndWalk(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutFile("a", "F", &dropboxapi.Entry{Path: "xxx"})
|
|
||||||
tree.PutCaseCorrectDirectoryName("", "A")
|
|
||||||
|
|
||||||
numCalled := 0
|
|
||||||
walkFunc := func(caseCorrectFilePath string, entry *dropboxapi.Entry) error {
|
|
||||||
assert.True(t, caseCorrectFilePath == "A/F", "caseCorrectFilePath should be A/F, not "+caseCorrectFilePath)
|
|
||||||
assert.True(t, entry.Path == "xxx", "entry.Path should be xxx")
|
|
||||||
numCalled++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := tree.WalkFiles("", walkFunc)
|
|
||||||
assert.True(t, err == nil, "No error should be returned")
|
|
||||||
assert.True(t, numCalled == 1, "walk func should be called only once")
|
|
||||||
assert.True(t, fs.Stats.GetErrors() == errors, "No errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutAndWalkWithPrefix(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutFile("a", "F", &dropboxapi.Entry{Path: "xxx"})
|
|
||||||
tree.PutCaseCorrectDirectoryName("", "A")
|
|
||||||
|
|
||||||
numCalled := 0
|
|
||||||
walkFunc := func(caseCorrectFilePath string, entry *dropboxapi.Entry) error {
|
|
||||||
assert.True(t, caseCorrectFilePath == "A/F", "caseCorrectFilePath should be A/F, not "+caseCorrectFilePath)
|
|
||||||
assert.True(t, entry.Path == "xxx", "entry.Path should be xxx")
|
|
||||||
numCalled++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := tree.WalkFiles("A", walkFunc)
|
|
||||||
assert.True(t, err == nil, "No error should be returned")
|
|
||||||
assert.True(t, numCalled == 1, "walk func should be called only once")
|
|
||||||
assert.True(t, fs.Stats.GetErrors() == errors, "No errors should be reported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutAndWalkIncompleteTree(t *testing.T) {
|
|
||||||
errors := fs.Stats.GetErrors()
|
|
||||||
|
|
||||||
tree := newNameTree()
|
|
||||||
tree.PutFile("a", "F", &dropboxapi.Entry{Path: "xxx"})
|
|
||||||
|
|
||||||
walkFunc := func(caseCorrectFilePath string, entry *dropboxapi.Entry) error {
|
|
||||||
t.Fatal("Should not be called")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := tree.WalkFiles("", walkFunc)
|
|
||||||
assert.True(t, err == nil, "No error should be returned")
|
|
||||||
assert.True(t, fs.Stats.GetErrors() == errors+1, "One error should be reported")
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue