Repository: ncw/rclone
Pull request #3350 (2019-07-15 13:01:04)

Koofr backend: support for setting modification times
<!--
Thank you very much for contributing code or documentation to rclone! Please
fill out the following questions to make it easier for us to review your
changes.

You do not need to check all the boxes below all at once, feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box.
-->

#### What is the purpose of this change?

Support for setting modification times on Koofr backend.

#### Was the change discussed in an issue or in the forum before?

No.

#### Checklist

- [X] I have read the [contribution guidelines](https://github.com/ncw/rclone/blob/master/CONTRIBUTING.md#submitting-a-pull-request).
- [X] I have added tests for all changes in this PR if appropriate.
- [X] I have added documentation for the changes if appropriate.
- [X] All commit messages are in [house style](https://github.com/ncw/rclone/blob/master/CONTRIBUTING.md#commit-messages).
- [X] I'm done, this Pull Request is ready for review :-)


buengese: Could you explain a bit more how this works? What if the user answers the setmtime option wrong? Could this be detected automatically?
jkmcnk: > Could you explain a bit more how this works? What if the user answers the setmtime option wrong? Could this be detected automatically?

for this to be relevant, the user must meddle with the advanced options in the first place, to select a location that goes to a dropbox or amazon drive backed storage, and not to a Koofr-hosted one, so I don't find this such a problem. tbh, it could be detected automatically, but would require some network communication with the server just to return from the Precision() method: thus, I opted to avoid this.


buengese: I see looks fine to be but I think we're gonna wait for ncw to return from vacation before merging this.
jkmcnk: > I see looks fine to me but I think we're gonna wait for ncw to return from vacation before merging this.

ofc. we're in no hurry, but hopefully we get this merged in prior to next release.

Patch:
diff --git a/Makefile b/Makefile
index 833de26b6..871d94f8d 100644
--- a/Makefile
+++ b/Makefile
@@ -151,7 +151,7 @@ log_since_last_release:
 	git log $(LAST_TAG)..
 
 compile_all:
-	go run bin/cross-compile.go -parallel 8 -compile-only $(BUILDTAGS) $(TAG)
+	go run bin/cross-compile.go -compile-only $(BUILDTAGS) $(TAG)
 
 appveyor_upload:
 	rclone --config bin/travis.rclone.conf -v copy --exclude '*beta-latest*' build/ $(BETA_UPLOAD)
@@ -172,7 +172,7 @@ ifeq ($(TRAVIS_OS_NAME),linux)
 	go run bin/get-github-release.go -extract nfpm goreleaser/nfpm 'nfpm_.*\.tar.gz'
 endif
 	git log $(LAST_TAG).. > /tmp/git-log.txt
-	go run bin/cross-compile.go -release beta-latest -git-log /tmp/git-log.txt $(BUILD_FLAGS) -parallel 8 $(BUILDTAGS) $(TAG)
+	go run bin/cross-compile.go -release beta-latest -git-log /tmp/git-log.txt $(BUILD_FLAGS) $(BUILDTAGS) $(TAG)
 	rclone --config bin/travis.rclone.conf -v copy --exclude '*beta-latest*' build/ $(BETA_UPLOAD)
 ifndef BRANCH_PATH
 	rclone --config bin/travis.rclone.conf -v copy --include '*beta-latest*' --include version.txt build/ $(BETA_UPLOAD_ROOT)
diff --git a/backend/fichier/api.go b/backend/fichier/api.go
index 0f46b5b5c..b7514b192 100644
--- a/backend/fichier/api.go
+++ b/backend/fichier/api.go
@@ -88,7 +88,7 @@ func (f *Fs) listSharedFiles(ctx context.Context, id string) (entries fs.DirEntr
 	return entries, nil
 }
 
-func (f *Fs) listFiles(directoryID string) (filesList *FilesList, err error) {
+func (f *Fs) listFiles(directoryID int) (filesList *FilesList, err error) {
 	// fs.Debugf(f, "Requesting files for dir `%s`", directoryID)
 	request := ListFilesRequest{
 		FolderID: directoryID,
@@ -111,7 +111,7 @@ func (f *Fs) listFiles(directoryID string) (filesList *FilesList, err error) {
 	return filesList, nil
 }
 
-func (f *Fs) listFolders(directoryID string) (foldersList *FoldersList, err error) {
+func (f *Fs) listFolders(directoryID int) (foldersList *FoldersList, err error) {
 	// fs.Debugf(f, "Requesting folders for id `%s`", directoryID)
 
 	request := ListFolderRequest{
@@ -148,12 +148,17 @@ func (f *Fs) listDir(ctx context.Context, dir string) (entries fs.DirEntries, er
 		return nil, err
 	}
 
-	files, err := f.listFiles(directoryID)
+	folderID, err := strconv.Atoi(directoryID)
 	if err != nil {
 		return nil, err
 	}
 
-	folders, err := f.listFolders(directoryID)
+	files, err := f.listFiles(folderID)
+	if err != nil {
+		return nil, err
+	}
+
+	folders, err := f.listFolders(folderID)
 	if err != nil {
 		return nil, err
 	}
@@ -199,12 +204,12 @@ func getRemote(dir, fileName string) string {
 	return dir + "/" + fileName
 }
 
-func (f *Fs) makeFolder(leaf, directoryID string) (response *MakeFolderResponse, err error) {
+func (f *Fs) makeFolder(leaf string, folderID int) (response *MakeFolderResponse, err error) {
 	name := replaceReservedChars(leaf)
 	// fs.Debugf(f, "Creating folder `%s` in id `%s`", name, directoryID)
 
 	request := MakeFolderRequest{
-		FolderID: directoryID,
+		FolderID: folderID,
 		Name:     name,
 	}
 
@@ -227,11 +232,11 @@ func (f *Fs) makeFolder(leaf, directoryID string) (response *MakeFolderResponse,
 	return response, err
 }
 
-func (f *Fs) removeFolder(name, directoryID string) (response *GenericOKResponse, err error) {
+func (f *Fs) removeFolder(name string, folderID int) (response *GenericOKResponse, err error) {
 	// fs.Debugf(f, "Removing folder with id `%s`", directoryID)
 
 	request := &RemoveFolderRequest{
-		FolderID: directoryID,
+		FolderID: folderID,
 	}
 
 	opts := rest.Opts{
diff --git a/backend/fichier/fichier.go b/backend/fichier/fichier.go
index b474bcea1..0a18b3210 100644
--- a/backend/fichier/fichier.go
+++ b/backend/fichier/fichier.go
@@ -70,7 +70,11 @@ type Fs struct {
 
 // FindLeaf finds a directory of name leaf in the folder with ID pathID
 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
-	folders, err := f.listFolders(pathID)
+	folderID, err := strconv.Atoi(pathID)
+	if err != nil {
+		return "", false, err
+	}
+	folders, err := f.listFolders(folderID)
 	if err != nil {
 		return "", false, err
 	}
@@ -87,7 +91,11 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
 
 // CreateDir makes a directory with pathID as parent and name leaf
 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
-	resp, err := f.makeFolder(leaf, pathID)
+	folderID, err := strconv.Atoi(pathID)
+	if err != nil {
+		return "", err
+	}
+	resp, err := f.makeFolder(leaf, folderID)
 	if err != nil {
 		return "", err
 	}
@@ -239,7 +247,11 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
 		return nil, err
 	}
 
-	files, err := f.listFiles(directoryID)
+	folderID, err := strconv.Atoi(directoryID)
+	if err != nil {
+		return nil, err
+	}
+	files, err := f.listFiles(folderID)
 	if err != nil {
 		return nil, err
 	}
@@ -371,12 +383,17 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 		return err
 	}
 
-	did, err := f.dirCache.FindDir(ctx, dir, false)
+	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
+	if err != nil {
+		return err
+	}
+
+	folderID, err := strconv.Atoi(directoryID)
 	if err != nil {
 		return err
 	}
 
-	_, err = f.removeFolder(dir, did)
+	_, err = f.removeFolder(dir, folderID)
 	if err != nil {
 		return err
 	}
diff --git a/backend/fichier/structs.go b/backend/fichier/structs.go
index ef08b906c..b5487b59f 100644
--- a/backend/fichier/structs.go
+++ b/backend/fichier/structs.go
@@ -2,12 +2,12 @@ package fichier
 
 // ListFolderRequest is the request structure of the corresponding request
 type ListFolderRequest struct {
-	FolderID string `json:"folder_id"`
+	FolderID int `json:"folder_id"`
 }
 
 // ListFilesRequest is the request structure of the corresponding request
 type ListFilesRequest struct {
-	FolderID string `json:"folder_id"`
+	FolderID int `json:"folder_id"`
 }
 
 // DownloadRequest is the request structure of the corresponding request
@@ -18,7 +18,7 @@ type DownloadRequest struct {
 
 // RemoveFolderRequest is the request structure of the corresponding request
 type RemoveFolderRequest struct {
-	FolderID string `json:"folder_id"`
+	FolderID int `json:"folder_id"`
 }
 
 // RemoveFileRequest is the request structure of the corresponding request
@@ -40,7 +40,7 @@ type GenericOKResponse struct {
 // MakeFolderRequest is the request structure of the corresponding request
 type MakeFolderRequest struct {
 	Name     string `json:"name"`
-	FolderID string `json:"folder_id"`
+	FolderID int    `json:"folder_id"`
 }
 
 // MakeFolderResponse is the response structure of the corresponding request
@@ -108,12 +108,12 @@ type Folder struct {
 	CreateDate string `json:"create_date"`
 	ID         int    `json:"id"`
 	Name       string `json:"name"`
-	Pass       string `json:"pass"`
+	Pass       int    `json:"pass"`
 }
 
 // FoldersList is the structure how 1Fichier returns a list of Folders
 type FoldersList struct {
-	FolderID   string   `json:"folder_id"`
+	FolderID   int      `json:"folder_id"`
 	Name       string   `json:"name"`
 	Status     string   `json:"Status"`
 	SubFolders []Folder `json:"sub_folders"`
diff --git a/backend/koofr/koofr.go b/backend/koofr/koofr.go
index 10488b426..83c3c748b 100644
--- a/backend/koofr/koofr.go
+++ b/backend/koofr/koofr.go
@@ -40,6 +40,12 @@ func init() {
 				Required: false,
 				Default:  "",
 				Advanced: true,
+			}, {
+				Name:     "setmtime",
+				Help:     "Does the backend support setting modification time. Set this to false if you use a mount ID that points to a Dropbox or Amazon Drive backend.",
+				Default:  true,
+				Required: true,
+				Advanced: true,
 			}, {
 				Name:     "user",
 				Help:     "Your Koofr user name",
@@ -60,6 +66,7 @@ type Options struct {
 	MountID  string `config:"mountid"`
 	User     string `config:"user"`
 	Password string `config:"password"`
+	SetMTime bool   `config:"setmtime"`
 }
 
 // A Fs is a representation of a remote Koofr Fs
@@ -140,7 +147,7 @@ func (o *Object) Storable() bool {
 
 // SetModTime is not supported
 func (o *Object) SetModTime(ctx context.Context, mtime time.Time) error {
-	return nil
+	return fs.ErrorCantSetModTimeWithoutDelete
 }
 
 // Open opens the Object for reading
@@ -179,10 +186,12 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadClo
 
 // Update updates the Object contents
 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
-	putopts := &koofrclient.PutFilter{
-		ForceOverwrite:    true,
-		NoRename:          true,
-		IgnoreNonExisting: true,
+	mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000
+	putopts := &koofrclient.PutOptions{
+		ForceOverwrite:             true,
+		NoRename:                   true,
+		OverwriteIgnoreNonExisting: true,
+		SetModified:                &mtime,
 	}
 	fullPath := o.fullPath()
 	dirPath := dir(fullPath)
@@ -191,7 +200,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 	if err != nil {
 		return err
 	}
-	info, err := o.fs.client.FilesPutOptions(o.fs.mountID, dirPath, name, in, putopts)
+	info, err := o.fs.client.FilesPutWithOptions(o.fs.mountID, dirPath, name, in, putopts)
 	if err != nil {
 		return err
 	}
@@ -226,7 +235,10 @@ func (f *Fs) Features() *fs.Features {
 
 // Precision denotes that setting modification times is not supported
 func (f *Fs) Precision() time.Duration {
-	return fs.ModTimeNotSupported
+	if !f.opt.SetMTime {
+		return fs.ModTimeNotSupported
+	}
+	return time.Millisecond
 }
 
 // Hashes returns a set of hashes are Provided by the Fs
@@ -336,10 +348,12 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (obj fs.Object, err e
 
 // Put updates a remote Object
 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (obj fs.Object, err error) {
-	putopts := &koofrclient.PutFilter{
-		ForceOverwrite:    true,
-		NoRename:          true,
-		IgnoreNonExisting: true,
+	mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000
+	putopts := &koofrclient.PutOptions{
+		ForceOverwrite:             true,
+		NoRename:                   true,
+		OverwriteIgnoreNonExisting: true,
+		SetModified:                &mtime,
 	}
 	fullPath := f.fullPath(src.Remote())
 	dirPath := dir(fullPath)
@@ -348,7 +362,7 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
 	if err != nil {
 		return nil, err
 	}
-	info, err := f.client.FilesPutOptions(f.mountID, dirPath, name, in, putopts)
+	info, err := f.client.FilesPutWithOptions(f.mountID, dirPath, name, in, putopts)
 	if err != nil {
 		return nil, translateErrorsObject(err)
 	}
@@ -466,9 +480,10 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	if err != nil {
 		return nil, fs.ErrorCantCopy
 	}
+	mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000
 	err = f.client.FilesCopy((src.(*Object)).fs.mountID,
 		(src.(*Object)).fs.fullPath((src.(*Object)).remote),
-		f.mountID, dstFullPath)
+		f.mountID, dstFullPath, koofrclient.CopyOptions{SetModified: &mtime})
 	if err != nil {
 		return nil, fs.ErrorCantCopy
 	}
diff --git a/backend/opendrive/opendrive.go b/backend/opendrive/opendrive.go
index 6240ecf95..ddeab9546 100644
--- a/backend/opendrive/opendrive.go
+++ b/backend/opendrive/opendrive.go
@@ -1,11 +1,9 @@
 package opendrive
 
 import (
-	"bytes"
 	"context"
 	"fmt"
 	"io"
-	"mime/multipart"
 	"net/http"
 	"net/url"
 	"path"
@@ -947,84 +945,35 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 		fs.Debugf(o, "Uploading chunk %d, size=%d, remain=%d", chunkCounter, currentChunkSize, remainingBytes)
 
 		chunk := readers.NewRepeatableLimitReaderBuffer(in, buf, currentChunkSize)
+		var reply uploadFileChunkReply
 		err = o.fs.pacer.Call(func() (bool, error) {
 			// seek to the start in case this is a retry
 			if _, err = chunk.Seek(0, io.SeekStart); err != nil {
 				return false, err
 			}
-			var formBody bytes.Buffer
-			w := multipart.NewWriter(&formBody)
-			fw, err := w.CreateFormFile("file_data", o.remote)
-			if err != nil {
-				return false, err
-			}
-			if _, err = io.Copy(fw, chunk); err != nil {
-				return false, err
-			}
-			// Add session_id
-			if fw, err = w.CreateFormField("session_id"); err != nil {
-				return false, err
-			}
-			if _, err = fw.Write([]byte(o.fs.session.SessionID)); err != nil {
-				return false, err
-			}
-			// Add session_id
-			if fw, err = w.CreateFormField("session_id"); err != nil {
-				return false, err
-			}
-			if _, err = fw.Write([]byte(o.fs.session.SessionID)); err != nil {
-				return false, err
-			}
-			// Add file_id
-			if fw, err = w.CreateFormField("file_id"); err != nil {
-				return false, err
-			}
-			if _, err = fw.Write([]byte(o.id)); err != nil {
-				return false, err
-			}
-			// Add temp_location
-			if fw, err = w.CreateFormField("temp_location"); err != nil {
-				return false, err
-			}
-			if _, err = fw.Write([]byte(openResponse.TempLocation)); err != nil {
-				return false, err
-			}
-			// Add chunk_offset
-			if fw, err = w.CreateFormField("chunk_offset"); err != nil {
-				return false, err
-			}
-			if _, err = fw.Write([]byte(strconv.FormatInt(chunkOffset, 10))); err != nil {
-				return false, err
-			}
-			// Add chunk_size
-			if fw, err = w.CreateFormField("chunk_size"); err != nil {
-				return false, err
-			}
-			if _, err = fw.Write([]byte(strconv.FormatInt(currentChunkSize, 10))); err != nil {
-				return false, err
-			}
-			// Don't forget to close the multipart writer.
-			// If you don't close it, your request will be missing the terminating boundary.
-			err = w.Close()
-			if err != nil {
-				return false, err
-			}
-
 			opts := rest.Opts{
-				Method:       "POST",
-				Path:         "/upload/upload_file_chunk.json",
-				Body:         &formBody,
-				ExtraHeaders: map[string]string{"Content-Type": w.FormDataContentType()},
+				Method: "POST",
+				Path:   "/upload/upload_file_chunk.json",
+				Body:   chunk,
+				MultipartParams: url.Values{
+					"session_id":    []string{o.fs.session.SessionID},
+					"file_id":       []string{o.id},
+					"temp_location": []string{openResponse.TempLocation},
+					"chunk_offset":  []string{strconv.FormatInt(chunkOffset, 10)},
+					"chunk_size":    []string{strconv.FormatInt(currentChunkSize, 10)},
+				},
+				MultipartContentName: "file_data", // ..name of the parameter which is the attached file
+				MultipartFileName:    o.remote,    // ..name of the file for the attached file
+
 			}
-			resp, err = o.fs.srv.Call(&opts)
+			resp, err = o.fs.srv.CallJSON(&opts, nil, &reply)
 			return o.fs.shouldRetry(resp, err)
 		})
 		if err != nil {
 			return errors.Wrap(err, "failed to create file")
 		}
-		err = resp.Body.Close()
-		if err != nil {
-			return errors.Wrap(err, "close failed on create file")
+		if reply.TotalWritten != currentChunkSize {
+			return errors.Errorf("failed to create file: incomplete write of %d/%d bytes", reply.TotalWritten, currentChunkSize)
 		}
 
 		chunkCounter++
diff --git a/backend/opendrive/types.go b/backend/opendrive/types.go
index 2cbcf92c1..92aaf29ad 100644
--- a/backend/opendrive/types.go
+++ b/backend/opendrive/types.go
@@ -212,3 +212,7 @@ type permissions struct {
 	FileID       string `json:"file_id"`
 	FileIsPublic int64  `json:"file_ispublic"`
 }
+
+type uploadFileChunkReply struct {
+	TotalWritten int64 `json:"TotalWritten"`
+}
diff --git a/backend/webdav/webdav.go b/backend/webdav/webdav.go
index 6517d06fe..b20151143 100644
--- a/backend/webdav/webdav.go
+++ b/backend/webdav/webdav.go
@@ -15,6 +15,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"os/exec"
 	"path"
 	"strings"
 	"time"
@@ -80,17 +81,22 @@ func init() {
 		}, {
 			Name: "bearer_token",
 			Help: "Bearer token instead of user/pass (eg a Macaroon)",
+		}, {
+			Name:     "bearer_token_command",
+			Help:     "Command to run to get a bearer token",
+			Advanced: true,
 		}},
 	})
 }
 
 // Options defines the configuration for this backend
 type Options struct {
-	URL         string `config:"url"`
-	Vendor      string `config:"vendor"`
-	User        string `config:"user"`
-	Pass        string `config:"pass"`
-	BearerToken string `config:"bearer_token"`
+	URL                string `config:"url"`
+	Vendor             string `config:"vendor"`
+	User               string `config:"user"`
+	Pass               string `config:"pass"`
+	BearerToken        string `config:"bearer_token"`
+	BearerTokenCommand string `config:"bearer_token_command"`
 }
 
 // Fs represents a remote webdav
@@ -158,7 +164,16 @@ var retryErrorCodes = []int{
 
 // shouldRetry returns a boolean as to whether this resp and err
 // deserve to be retried.  It returns the err as a convenience
-func shouldRetry(resp *http.Response, err error) (bool, error) {
+func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
+	// If we have a bearer token command and it has expired then refresh it
+	if f.opt.BearerTokenCommand != "" && resp != nil && resp.StatusCode == 401 {
+		fs.Debugf(f, "Bearer token expired: %v", err)
+		authErr := f.fetchAndSetBearerToken()
+		if authErr != nil {
+			err = authErr
+		}
+		return true, err
+	}
 	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
 }
 
@@ -207,7 +222,7 @@ func (f *Fs) readMetaDataForPath(path string, depth string) (info *api.Prop, err
 	var resp *http.Response
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.CallXML(&opts, nil, &result)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if apiErr, ok := err.(*api.Error); ok {
 		// does not exist
@@ -330,7 +345,12 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
 	if opt.User != "" || opt.Pass != "" {
 		f.srv.SetUserPass(opt.User, opt.Pass)
 	} else if opt.BearerToken != "" {
-		f.srv.SetHeader("Authorization", "BEARER "+opt.BearerToken)
+		f.setBearerToken(opt.BearerToken)
+	} else if f.opt.BearerTokenCommand != "" {
+		err = f.fetchAndSetBearerToken()
+		if err != nil {
+			return nil, err
+		}
 	}
 	f.srv.SetErrorHandler(errorHandler)
 	err = f.setQuirks(opt.Vendor)
@@ -360,6 +380,49 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
 	return f, nil
 }
 
+// sets the BearerToken up
+func (f *Fs) setBearerToken(token string) {
+	f.opt.BearerToken = token
+	f.srv.SetHeader("Authorization", "BEARER "+token)
+}
+
+// fetch the bearer token using the command
+func (f *Fs) fetchBearerToken(cmd string) (string, error) {
+	var (
+		args   = strings.Split(cmd, " ")
+		stdout bytes.Buffer
+		stderr bytes.Buffer
+		c      = exec.Command(args[0], args[1:]...)
+	)
+	c.Stdout = &stdout
+	c.Stderr = &stderr
+	var (
+		err          = c.Run()
+		stdoutString = strings.TrimSpace(stdout.String())
+		stderrString = strings.TrimSpace(stderr.String())
+	)
+	if err != nil {
+		if stderrString == "" {
+			stderrString = stdoutString
+		}
+		return "", errors.Wrapf(err, "failed to get bearer token using %q: %s", f.opt.BearerTokenCommand, stderrString)
+	}
+	return stdoutString, nil
+}
+
+// fetch the bearer token and set it if successful
+func (f *Fs) fetchAndSetBearerToken() error {
+	if f.opt.BearerTokenCommand == "" {
+		return nil
+	}
+	token, err := f.fetchBearerToken(f.opt.BearerTokenCommand)
+	if err != nil {
+		return err
+	}
+	f.setBearerToken(token)
+	return nil
+}
+
 // setQuirks adjusts the Fs for the vendor passed in
 func (f *Fs) setQuirks(vendor string) error {
 	switch vendor {
@@ -480,7 +543,7 @@ func (f *Fs) listAll(dir string, directoriesOnly bool, filesOnly bool, depth str
 	var resp *http.Response
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.CallXML(&opts, nil, &result)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if err != nil {
 		if apiErr, ok := err.(*api.Error); ok {
@@ -649,7 +712,7 @@ func (f *Fs) _mkdir(dirPath string) error {
 	}
 	err := f.pacer.Call(func() (bool, error) {
 		resp, err := f.srv.Call(&opts)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if apiErr, ok := err.(*api.Error); ok {
 		// already exists
@@ -713,7 +776,7 @@ func (f *Fs) purgeCheck(dir string, check bool) error {
 	var err error
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.CallXML(&opts, nil, nil)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if err != nil {
 		return errors.Wrap(err, "rmdir failed")
@@ -776,7 +839,7 @@ func (f *Fs) copyOrMove(ctx context.Context, src fs.Object, remote string, metho
 	}
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.Call(&opts)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if err != nil {
 		return nil, errors.Wrap(err, "Copy call failed")
@@ -872,7 +935,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
 	}
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.Call(&opts)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if err != nil {
 		return errors.Wrap(err, "DirMove MOVE call failed")
@@ -913,7 +976,7 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
 	var err error
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.CallXML(&opts, nil, &q)
-		return shouldRetry(resp, err)
+		return f.shouldRetry(resp, err)
 	})
 	if err != nil {
 		return nil, errors.Wrap(err, "about call failed")
@@ -1033,7 +1096,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
 	}
 	err = o.fs.pacer.Call(func() (bool, error) {
 		resp, err = o.fs.srv.Call(&opts)
-		return shouldRetry(resp, err)
+		return o.fs.shouldRetry(resp, err)
 	})
 	if err != nil {
 		return nil, err
@@ -1081,7 +1144,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 	}
 	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
 		resp, err = o.fs.srv.Call(&opts)
-		return shouldRetry(resp, err)
+		return o.fs.shouldRetry(resp, err)
 	})
 	if err != nil {
 		// Give the WebDAV server a chance to get its internal state in order after the
@@ -1108,7 +1171,7 @@ func (o *Object) Remove(ctx context.Context) error {
 	}
 	return o.fs.pacer.Call(func() (bool, error) {
 		resp, err := o.fs.srv.Call(&opts)
-		return shouldRetry(resp, err)
+		return o.fs.shouldRetry(resp, err)
 	})
 }
 
diff --git a/docs/content/authors.md b/docs/content/authors.md
index 1567fd3f9..b58dc01cd 100644
--- a/docs/content/authors.md
+++ b/docs/content/authors.md
@@ -271,3 +271,5 @@ Contributors
   * AbelThar <abela.tharen@gmail.com>
   * Matti Niemenmaa <matti.niemenmaa+git@iki.fi>
   * Russell Davis <russelldavis@users.noreply.github.com>
+  * Yi FU <yi.fu@tink.se>
+  * Paul Millar <paul.millar@desy.de>
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 278c2b894..5d14b1647 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -328,6 +328,8 @@ If running rclone from a script you might want to use today's date as
 the directory name passed to `--backup-dir` to store the old files, or
 you might want to pass `--suffix` with today's date.
 
+See `--compare-dest` and `--copy-dest`.
+
 ### --bind string ###
 
 Local address to bind to for outgoing connections.  This can be an
@@ -447,6 +449,18 @@ quicker than without the `--checksum` flag.
 When using this flag, rclone won't update mtimes of remote files if
 they are incorrect as it would normally.
 
+### --compare-dest=DIR ###
+
+When using `sync`, `copy` or `move` DIR is checked in addition to the 
+destination for files. If a file identical to the source is found that 
+file is NOT copied from source. This is useful to copy just files that 
+have changed since the last backup.
+
+You must use the same remote as the destination of the sync.  The 
+compare directory must not overlap the destination directory.
+
+See `--copy-dest` and `--backup-dir`.
+
 ### --config=CONFIG_FILE ###
 
 Specify the location of the rclone config file.
@@ -475,6 +489,19 @@ The connection timeout is the amount of time rclone will wait for a
 connection to go through to a remote object storage system.  It is
 `1m` by default.
 
+### --copy-dest=DIR ###
+
+When using `sync`, `copy` or `move` DIR is checked in addition to the 
+destination for files. If a file identical to the source is found that 
+file is server side copied from DIR to the destination. This is useful 
+for incremental backup.
+
+The remote in use must support server side copy and you must
+use the same remote as the destination of the sync.  The compare
+directory must not overlap the destination directory.
+
+See `--compare-dest` and `--backup-dir`.
+
 ### --dedupe-mode MODE ###
 
 Mode to run dedupe command in.  One of `interactive`, `skip`, `first`, `newest`, `oldest`, `rename`.  The default is `interactive`.  See the dedupe command for more information as to what these options mean.
diff --git a/docs/content/webdav.md b/docs/content/webdav.md
index e8a7f277b..d9e389273 100644
--- a/docs/content/webdav.md
+++ b/docs/content/webdav.md
@@ -63,7 +63,7 @@ password:
 Confirm the password:
 password:
 Bearer token instead of user/pass (eg a Macaroon)
-bearer_token> 
+bearer_token>
 Remote config
 --------------------
 [remote]
@@ -72,7 +72,7 @@ url = https://example.com/remote.php/webdav/
 vendor = nextcloud
 user = user
 pass = *** ENCRYPTED ***
-bearer_token = 
+bearer_token =
 --------------------
 y) Yes this is OK
 e) Edit this remote
@@ -257,8 +257,13 @@ pass = encryptedpassword
 
 ### dCache ###
 
-[dCache](https://www.dcache.org/) is a storage system with WebDAV doors that support, beside basic and x509, 
-authentication with [Macaroons](https://www.dcache.org/manuals/workshop-2017-05-29-Umea/000-Final/anupam_macaroons_v02.pdf) (bearer tokens).
+dCache is a storage system that supports many protocols and
+authentication/authorisation schemes.  For WebDAV clients, it allows
+users to authenticate with username and password (BASIC), X.509,
+Kerberos, and various bearer tokens, including
+[Macaroons](https://www.dcache.org/manuals/workshop-2017-05-29-Umea/000-Final/anupam_macaroons_v02.pdf)
+and [OpenID-Connect](https://en.wikipedia.org/wiki/OpenID_Connect)
+access tokens.
 
 Configure as normal using the `other` type.  Don't enter a username or
 password, instead enter your Macaroon as the `bearer_token`.
@@ -277,3 +282,52 @@ bearer_token = your-macaroon
 
 There is a [script](https://github.com/sara-nl/GridScripts/blob/master/get-macaroon) that
 obtains a Macaroon from a dCache WebDAV endpoint, and creates an rclone config file.
+
+Macaroons may also be obtained from the dCacheView
+web-browser/JavaScript client that comes with dCache.
+
+### OpenID-Connect ###
+
+dCache also supports authenticating with OpenID-Connect access tokens.
+OpenID-Connect is a protocol (based on OAuth 2.0) that allows services
+to identify users who have authenticated with some central service.
+
+Support for OpenID-Connect in rclone is currently achieved using
+another software package called
+[oidc-agent](https://github.com/indigo-dc/oidc-agent).  This is a
+command-line tool that facilitates obtaining an access token.  Once
+installed and configured, an access token is obtained by running the
+`oidc-token` command.  The following example shows a (shortened)
+access token obtained from the *XDC* OIDC Provider.
+
+```
+paul@celebrimbor:~$ oidc-token XDC
+eyJraWQ[...]QFXDt0
+paul@celebrimbor:~$
+```
+
+**Note** Before the `oidc-token` command will work, the refresh token
+must be loaded into the oidc agent.  This is done with the `oidc-add`
+command (e.g., `oidc-add XDC`).  This is typically done once per login
+session.  Full details on this and how to register oidc-agent with
+your OIDC Provider are provided in the [oidc-agent
+documentation](https://indigo-dc.gitbooks.io/oidc-agent/).
+
+The rclone `bearer_token_command` configuration option is used to
+fetch the access token from oidc-agent.
+
+Configure as a normal WebDAV endpoint, using the 'other' vendor,
+leaving the username and password empty.  When prompted, choose to
+edit the advanced config and enter the command to get a bearer token
+(e.g., `oidc-agent XDC`).
+
+The following example config shows a WebDAV endpoint that uses
+oidc-agent to supply an access token from the *XDC* OIDC Provider.
+
+```
+[dcache]
+type = webdav
+url = https://dcache.example.org/
+vendor = other
+bearer_token_command = oidc-token XDC
+```
diff --git a/fs/config.go b/fs/config.go
index 6b77fc700..6817df55a 100644
--- a/fs/config.go
+++ b/fs/config.go
@@ -66,6 +66,8 @@ type ConfigInfo struct {
 	NoTraverse             bool
 	NoUpdateModTime        bool
 	DataRateUnit           string
+	CompareDest            string
+	CopyDest               string
 	BackupDir              string
 	Suffix                 string
 	SuffixKeepExtension    bool
diff --git a/fs/config/config.go b/fs/config/config.go
index df18f66a5..cd8497986 100644
--- a/fs/config/config.go
+++ b/fs/config/config.go
@@ -1085,7 +1085,9 @@ func editOptions(ri *fs.RegInfo, name string, isNew bool) {
 						continue
 					}
 				}
-				FileSet(name, option.Name, ChooseOption(&option, name))
+				if option.Hide&fs.OptionHideConfigurator == 0 {
+					FileSet(name, option.Name, ChooseOption(&option, name))
+				}
 			}
 		}
 	}
diff --git a/fs/config/configflags/configflags.go b/fs/config/configflags/configflags.go
index 97e5cbe11..99c9fead7 100644
--- a/fs/config/configflags/configflags.go
+++ b/fs/config/configflags/configflags.go
@@ -67,6 +67,8 @@ func AddFlags(flagSet *pflag.FlagSet) {
 	flags.BoolVarP(flagSet, &fs.Config.IgnoreCaseSync, "ignore-case-sync", "", fs.Config.IgnoreCaseSync, "Ignore case when synchronizing")
 	flags.BoolVarP(flagSet, &fs.Config.NoTraverse, "no-traverse", "", fs.Config.NoTraverse, "Don't traverse destination file system on copy.")
 	flags.BoolVarP(flagSet, &fs.Config.NoUpdateModTime, "no-update-modtime", "", fs.Config.NoUpdateModTime, "Don't update destination mod-time if files identical.")
+	flags.StringVarP(flagSet, &fs.Config.CompareDest, "compare-dest", "", fs.Config.CompareDest, "use DIR to server side copy flies from.")
+	flags.StringVarP(flagSet, &fs.Config.CopyDest, "copy-dest", "", fs.Config.CopyDest, "Compare dest to DIR also.")
 	flags.StringVarP(flagSet, &fs.Config.BackupDir, "backup-dir", "", fs.Config.BackupDir, "Make backups into hierarchy based in DIR.")
 	flags.StringVarP(flagSet, &fs.Config.Suffix, "suffix", "", fs.Config.Suffix, "Suffix to add to changed files.")
 	flags.BoolVarP(flagSet, &fs.Config.SuffixKeepExtension, "suffix-keep-extension", "", fs.Config.SuffixKeepExtension, "Preserve the extension when using --suffix.")
@@ -152,6 +154,10 @@ func SetFlags() {
 		log.Fatalf(`Can't use --size-only and --ignore-size together.`)
 	}
 
+	if fs.Config.CompareDest != "" && fs.Config.CopyDest != "" {
+		log.Fatalf(`Can't use --compare-dest with --copy-dest.`)
+	}
+
 	switch {
 	case len(fs.Config.StatsOneLineDateFormat) > 0:
 		fs.Config.StatsOneLineDate = true
diff --git a/fs/fs.go b/fs/fs.go
index 4166c349c..81bd80001 100644
--- a/fs/fs.go
+++ b/fs/fs.go
@@ -68,6 +68,7 @@ var (
 	ErrorDirectoryNotEmpty           = errors.New("directory not empty")
 	ErrorImmutableModified           = errors.New("immutable file modified")
 	ErrorPermissionDenied            = errors.New("permission denied")
+	ErrorCantShareDirectories        = errors.New("this backend can't share directories with link")
 )
 
 // RegInfo provides information about a filesystem
diff --git a/fs/operations/operations.go b/fs/operations/operations.go
index b629c715c..3529a847a 100644
--- a/fs/operations/operations.go
+++ b/fs/operations/operations.go
@@ -96,7 +96,7 @@ func CheckHashes(ctx context.Context, src fs.ObjectInfo, dst fs.Object) (equal b
 // Otherwise the file is considered to be not equal including if there
 // were errors reading info.
 func Equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object) bool {
-	return equal(ctx, src, dst, fs.Config.SizeOnly, fs.Config.CheckSum)
+	return equal(ctx, src, dst, fs.Config.SizeOnly, fs.Config.CheckSum, !fs.Config.NoUpdateModTime)
 }
 
 // sizeDiffers compare the size of src and dst taking into account the
@@ -110,7 +110,7 @@ func sizeDiffers(src, dst fs.ObjectInfo) bool {
 
 var checksumWarning sync.Once
 
-func equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object, sizeOnly, checkSum bool) bool {
+func equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object, sizeOnly, checkSum, UpdateModTime bool) bool {
 	if sizeDiffers(src, dst) {
 		fs.Debugf(src, "Sizes differ (src %d vs dst %d)", src.Size(), dst.Size())
 		return false
@@ -169,7 +169,7 @@ func equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object, sizeOnly, chec
 	}
 
 	// mod time differs but hash is the same to reset mod time if required
-	if !fs.Config.NoUpdateModTime {
+	if UpdateModTime {
 		if fs.Config.DryRun {
 			fs.Logf(src, "Not updating modification time as --dry-run")
 		} else {
@@ -1360,6 +1360,115 @@ func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error {
 	return nil
 }
 
+// GetCompareDest sets up --compare-dest
+func GetCompareDest() (CompareDest fs.Fs, err error) {
+	CompareDest, err = cache.Get(fs.Config.CompareDest)
+	if err != nil {
+		return nil, fserrors.FatalError(errors.Errorf("Failed to make fs for --compare-dest %q: %v", fs.Config.CompareDest, err))
+	}
+	return CompareDest, nil
+}
+
+// compareDest checks --compare-dest to see if src needs to
+// be copied
+//
+// Returns True if src is in --compare-dest
+func compareDest(ctx context.Context, dst, src fs.Object, CompareDest fs.Fs) (NoNeedTransfer bool, err error) {
+	var remote string
+	if dst == nil {
+		remote = src.Remote()
+	} else {
+		remote = dst.Remote()
+	}
+	CompareDestFile, err := CompareDest.NewObject(ctx, remote)
+	switch err {
+	case fs.ErrorObjectNotFound:
+		return false, nil
+	case nil:
+		break
+	default:
+		return false, err
+	}
+	if Equal(ctx, src, CompareDestFile) {
+		fs.Debugf(src, "Destination found in --compare-dest, skipping")
+		return true, nil
+	}
+	return false, nil
+}
+
+// GetCopyDest sets up --copy-dest
+func GetCopyDest(fdst fs.Fs) (CopyDest fs.Fs, err error) {
+	CopyDest, err = cache.Get(fs.Config.CopyDest)
+	if err != nil {
+		return nil, fserrors.FatalError(errors.Errorf("Failed to make fs for --copy-dest %q: %v", fs.Config.CopyDest, err))
+	}
+	if !SameConfig(fdst, CopyDest) {
+		return nil, fserrors.FatalError(errors.New("parameter to --copy-dest has to be on the same remote as destination"))
+	}
+	if CopyDest.Features().Copy == nil {
+		return nil, fserrors.FatalError(errors.New("can't use --copy-dest on a remote which doesn't support server side copy"))
+	}
+	return CopyDest, nil
+}
+
+// copyDest checks --copy-dest to see if src needs to
+// be copied
+//
+// Returns True if src was copied from --copy-dest
+func copyDest(ctx context.Context, fdst fs.Fs, dst, src fs.Object, CopyDest, backupDir fs.Fs) (NoNeedTransfer bool, err error) {
+	var remote string
+	if dst == nil {
+		remote = src.Remote()
+	} else {
+		remote = dst.Remote()
+	}
+	CopyDestFile, err := CopyDest.NewObject(ctx, remote)
+	switch err {
+	case fs.ErrorObjectNotFound:
+		return false, nil
+	case nil:
+		break
+	default:
+		return false, err
+	}
+	if equal(ctx, src, CopyDestFile, fs.Config.SizeOnly, fs.Config.CheckSum, false) {
+		if dst == nil || !Equal(ctx, src, dst) {
+			if dst != nil && backupDir != nil {
+				err = MoveBackupDir(ctx, backupDir, dst)
+				if err != nil {
+					return false, errors.Wrap(err, "moving to --backup-dir failed")
+				}
+				// If successful zero out the dstObj as it is no longer there
+				dst = nil
+			}
+			_, err := Copy(ctx, fdst, dst, remote, CopyDestFile)
+			if err != nil {
+				fs.Errorf(src, "Destination found in --copy-dest, error copying")
+				return false, nil
+			}
+			fs.Debugf(src, "Destination found in --copy-dest, using server side copy")
+			return true, nil
+		}
+		fs.Debugf(src, "Unchanged skipping")
+		return true, nil
+	}
+	fs.Debugf(src, "Destination not found in --copy-dest")
+	return false, nil
+}
+
+// CompareOrCopyDest checks --compare-dest and --copy-dest to see if src
+// does not need to be copied
+//
+// Returns True if src does not need to be copied
+func CompareOrCopyDest(ctx context.Context, fdst fs.Fs, dst, src fs.Object, CompareOrCopyDest, backupDir fs.Fs) (NoNeedTransfer bool, err error) {
+	if fs.Config.CompareDest != "" {
+		return compareDest(ctx, dst, src, CompareOrCopyDest)
+	} else if fs.Config.CopyDest != "" {
+		return copyDest(ctx, fdst, dst, src, CompareOrCopyDest, backupDir)
+	}
+	return false, nil
+}
+
 // NeedTransfer checks to see if src needs to be copied to dst using
 // the current config.
 //
@@ -1577,13 +1686,31 @@ func moveOrCopyFile(ctx context.Context, fdst fs.Fs, fsrc fs.Fs, dstFileName str
 		return err
 	}
 
-	if NeedTransfer(ctx, dstObj, srcObj) {
+	var backupDir, copyDestDir fs.Fs
+	if fs.Config.BackupDir != "" || fs.Config.Suffix != "" {
+		backupDir, err = BackupDir(fdst, fsrc, srcFileName)
+		if err != nil {
+			return errors.Wrap(err, "creating Fs for --backup-dir failed")
+		}
+	}
+	if fs.Config.CompareDest != "" {
+		copyDestDir, err = GetCompareDest()
+		if err != nil {
+			return err
+		}
+	} else if fs.Config.CopyDest != "" {
+		copyDestDir, err = GetCopyDest(fdst)
+		if err != nil {
+			return err
+		}
+	}
+	NoNeedTransfer, err := CompareOrCopyDest(ctx, fdst, dstObj, srcObj, copyDestDir, backupDir)
+	if err != nil {
+		return err
+	}
+	if !NoNeedTransfer && NeedTransfer(ctx, dstObj, srcObj) {
 		// If destination already exists, then we must move it into --backup-dir if required
-		if dstObj != nil && (fs.Config.BackupDir != "" || fs.Config.Suffix != "") {
-			backupDir, err := BackupDir(fdst, fsrc, srcFileName)
-			if err != nil {
-				return errors.Wrap(err, "creating Fs for --backup-dir failed")
-			}
+		if dstObj != nil && backupDir != nil {
 			err = MoveBackupDir(ctx, backupDir, dstObj)
 			if err != nil {
 				return errors.Wrap(err, "moving to --backup-dir failed")
diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go
index c51a1fc75..1a33be81a 100644
--- a/fs/rc/rcserver/rcserver.go
+++ b/fs/rc/rcserver/rcserver.go
@@ -182,6 +182,8 @@ func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string)
 		return
 	}
 
+	delete(in, "_async") // remove the async parameter after parsing so vfs operations don't get confused
+
 	fs.Debugf(nil, "rc: %q: with parameters %+v", path, in)
 	var out rc.Params
 	if isAsync {
diff --git a/fs/sync/sync.go b/fs/sync/sync.go
index 903998847..f96bdcd5b 100644
--- a/fs/sync/sync.go
+++ b/fs/sync/sync.go
@@ -28,39 +28,40 @@ type syncCopyMove struct {
 	deleteEmptySrcDirs bool
 	dir                string
 	// internal state
-	ctx            context.Context        // internal context for controlling go-routines
-	cancel         func()                 // cancel the context
-	noTraverse     bool                   // if set don't traverse the dst
-	deletersWg     sync.WaitGroup         // for delete before go routine
-	deleteFilesCh  chan fs.Object         // channel to receive deletes if delete before
-	trackRenames   bool                   // set if we should do server side renames
-	dstFilesMu     sync.Mutex             // protect dstFiles
-	dstFiles       map[string]fs.Object   // dst files, always filled
-	srcFiles       map[string]fs.Object   // src files, only used if deleteBefore
-	srcFilesChan   chan fs.Object         // passes src objects
-	srcFilesResult chan error             // error result of src listing
-	dstFilesResult chan error             // error result of dst listing
-	dstEmptyDirsMu sync.Mutex             // protect dstEmptyDirs
-	dstEmptyDirs   map[string]fs.DirEntry // potentially empty directories
-	srcEmptyDirsMu sync.Mutex             // protect srcEmptyDirs
-	srcEmptyDirs   map[string]fs.DirEntry // potentially empty directories
-	checkerWg      sync.WaitGroup         // wait for checkers
-	toBeChecked    *pipe                  // checkers channel
-	transfersWg    sync.WaitGroup         // wait for transfers
-	toBeUploaded   *pipe                  // copiers channel
-	errorMu        sync.Mutex             // Mutex covering the errors variables
-	err            error                  // normal error from copy process
-	noRetryErr     error                  // error with NoRetry set
-	fatalErr       error                  // fatal error
-	commonHash     hash.Type              // common hash type between src and dst
-	renameMapMu    sync.Mutex             // mutex to protect the below
-	renameMap      map[string][]fs.Object // dst files by hash - only used by trackRenames
-	renamerWg      sync.WaitGroup         // wait for renamers
-	toBeRenamed    *pipe                  // renamers channel
-	trackRenamesWg sync.WaitGroup         // wg for background track renames
-	trackRenamesCh chan fs.Object         // objects are pumped in here
-	renameCheck    []fs.Object            // accumulate files to check for rename here
-	backupDir      fs.Fs                  // place to store overwrites/deletes
+	ctx             context.Context        // internal context for controlling go-routines
+	cancel          func()                 // cancel the context
+	noTraverse      bool                   // if set don't traverse the dst
+	deletersWg      sync.WaitGroup         // for delete before go routine
+	deleteFilesCh   chan fs.Object         // channel to receive deletes if delete before
+	trackRenames    bool                   // set if we should do server side renames
+	dstFilesMu      sync.Mutex             // protect dstFiles
+	dstFiles        map[string]fs.Object   // dst files, always filled
+	srcFiles        map[string]fs.Object   // src files, only used if deleteBefore
+	srcFilesChan    chan fs.Object         // passes src objects
+	srcFilesResult  chan error             // error result of src listing
+	dstFilesResult  chan error             // error result of dst listing
+	dstEmptyDirsMu  sync.Mutex             // protect dstEmptyDirs
+	dstEmptyDirs    map[string]fs.DirEntry // potentially empty directories
+	srcEmptyDirsMu  sync.Mutex             // protect srcEmptyDirs
+	srcEmptyDirs    map[string]fs.DirEntry // potentially empty directories
+	checkerWg       sync.WaitGroup         // wait for checkers
+	toBeChecked     *pipe                  // checkers channel
+	transfersWg     sync.WaitGroup         // wait for transfers
+	toBeUploaded    *pipe                  // copiers channel
+	errorMu         sync.Mutex             // Mutex covering the errors variables
+	err             error                  // normal error from copy process
+	noRetryErr      error                  // error with NoRetry set
+	fatalErr        error                  // fatal error
+	commonHash      hash.Type              // common hash type between src and dst
+	renameMapMu     sync.Mutex             // mutex to protect the below
+	renameMap       map[string][]fs.Object // dst files by hash - only used by trackRenames
+	renamerWg       sync.WaitGroup         // wait for renamers
+	toBeRenamed     *pipe                  // renamers channel
+	trackRenamesWg  sync.WaitGroup         // wg for background track renames
+	trackRenamesCh  chan fs.Object         // objects are pumped in here
+	renameCheck     []fs.Object            // accumulate files to check for rename here
+	compareCopyDest fs.Fs                  // place to check for files to server side copy
+	backupDir       fs.Fs                  // place to store overwrites/deletes
 }
 
 func newSyncCopyMove(ctx context.Context, fdst, fsrc fs.Fs, deleteMode fs.DeleteMode, DoMove bool, deleteEmptySrcDirs bool, copyEmptySrcDirs bool) (*syncCopyMove, error) {
@@ -127,6 +128,19 @@ func newSyncCopyMove(ctx context.Context, fdst, fsrc fs.Fs, deleteMode fs.Delete
 			return nil, err
 		}
 	}
+	if fs.Config.CompareDest != "" {
+		var err error
+		s.compareCopyDest, err = operations.GetCompareDest()
+		if err != nil {
+			return nil, err
+		}
+	} else if fs.Config.CopyDest != "" {
+		var err error
+		s.compareCopyDest, err = operations.GetCopyDest(fdst)
+		if err != nil {
+			return nil, err
+		}
+	}
 	return s, nil
 }
 
@@ -204,7 +218,11 @@ func (s *syncCopyMove) pairChecker(in *pipe, out *pipe, wg *sync.WaitGroup) {
 		accounting.Stats.Checking(src.Remote())
 		// Check to see if can store this
 		if src.Storable() {
-			if operations.NeedTransfer(s.ctx, pair.Dst, pair.Src) {
+			NoNeedTransfer, err := operations.CompareOrCopyDest(s.ctx, s.fdst, pair.Dst, pair.Src, s.compareCopyDest, s.backupDir)
+			if err != nil {
+				s.processError(err)
+			}
+			if !NoNeedTransfer && operations.NeedTransfer(s.ctx, pair.Dst, pair.Src) {
 				// If files are treated as immutable, fail if destination exists and does not match
 				if fs.Config.Immutable && pair.Dst != nil {
 					fs.Errorf(pair.Dst, "Source and destination exist but do not match: immutable file modified")
@@ -764,10 +782,17 @@ func (s *syncCopyMove) SrcOnly(src fs.DirEntry) (recurse bool) {
 			case s.trackRenamesCh <- x:
 			}
 		} else {
-			// No need to check since doesn't exist
-			ok := s.toBeUploaded.Put(s.ctx, fs.ObjectPair{Src: x, Dst: nil})
-			if !ok {
-				return
+			// Check CompareDest && CopyDest
+			NoNeedTransfer, err := operations.CompareOrCopyDest(s.ctx, s.fdst, nil, x, s.compareCopyDest, s.backupDir)
+			if err != nil {
+				s.processError(err)
+			}
+			if !NoNeedTransfer {
+				// No need to check since doesn't exist
+				ok := s.toBeUploaded.Put(s.ctx, fs.ObjectPair{Src: x, Dst: nil})
+				if !ok {
+					return
+				}
 			}
 		}
 	case fs.Directory:
diff --git a/go.mod b/go.mod
index 14660c0d1..cfeb0650d 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ require (
 	github.com/jzelinskie/whirlpool v0.0.0-20170603002051-c19460b8caa6
 	github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
 	github.com/koofr/go-httpclient v0.0.0-20180104120329-03786175608a
-	github.com/koofr/go-koofrclient v0.0.0-20190131164641-7f327592caff
+	github.com/koofr/go-koofrclient v0.0.0-20190715104037-2c5813f3c29c
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/kr/pretty v0.1.0 // indirect
 	github.com/mattn/go-runewidth v0.0.4
diff --git a/go.sum b/go.sum
index 6f4203e01..d1f273d50 100644
--- a/go.sum
+++ b/go.sum
@@ -108,6 +108,8 @@ github.com/koofr/go-httpclient v0.0.0-20180104120329-03786175608a h1:W+gnfphB7Wp
 github.com/koofr/go-httpclient v0.0.0-20180104120329-03786175608a/go.mod h1:3xszwh+rNrYk1r9SStc4iJ326gne1OaBcrdB1ACsbzI=
 github.com/koofr/go-koofrclient v0.0.0-20190131164641-7f327592caff h1:GlfzG8bgyoJYz+5sMvGpYnHrg4veNVNnDGuE9hTEMHk=
 github.com/koofr/go-koofrclient v0.0.0-20190131164641-7f327592caff/go.mod h1:MRAz4Gsxd+OzrZ0owwrUHc0zLESL+1Y5syqK/sJxK2A=
+github.com/koofr/go-koofrclient v0.0.0-20190715104037-2c5813f3c29c h1:MaVra5jrE7MxT4k0v0JD2Gh6h94BFu2DgDYq+zMGBW0=
+github.com/koofr/go-koofrclient v0.0.0-20190715104037-2c5813f3c29c/go.mod h1:MRAz4Gsxd+OzrZ0owwrUHc0zLESL+1Y5syqK/sJxK2A=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
diff --git a/lib/rest/rest.go b/lib/rest/rest.go
index 50903444a..5f04b5e69 100644
--- a/lib/rest/rest.go
+++ b/lib/rest/rest.go
@@ -286,12 +286,12 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) {
 // MultipartUpload creates an io.Reader which produces an encoded a
 // multipart form upload from the params passed in and the  passed in
 //
-// in - the body of the file
+// in - the body of the file (may be nil)
 // params - the form parameters
 // fileName - is the name of the attached file
 // contentName - the name of the parameter for the file
 //
-// the *int64 returned is the overhead in addition to the file contents, in case Content-Length is required
+// the int64 returned is the overhead in addition to the file contents, in case Content-Length is required
 //
 // NB This doesn't allow setting the content type of the attachment
 func MultipartUpload(in io.Reader, params url.Values, contentName, fileName string) (io.ReadCloser, string, int64, error) {
@@ -315,9 +315,11 @@ func MultipartUpload(in io.Reader, params url.Values, contentName, fileName stri
 			}
 		}
 	}
-	_, err = dummyMultipartWriter.CreateFormFile(contentName, fileName)
-	if err != nil {
-		return nil, "", 0, err
+	if in != nil {
+		_, err = dummyMultipartWriter.CreateFormFile(contentName, fileName)
+		if err != nil {
+			return nil, "", 0, err
+		}
 	}
 
 	err = dummyMultipartWriter.Close()
@@ -341,16 +343,18 @@ func MultipartUpload(in io.Reader, params url.Values, contentName, fileName stri
 			}
 		}
 
-		part, err := writer.CreateFormFile(contentName, fileName)
-		if err != nil {
-			_ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to create form file"))
-			return
-		}
+		if in != nil {
+			part, err := writer.CreateFormFile(contentName, fileName)
+			if err != nil {
+				_ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to create form file"))
+				return
+			}
 
-		_, err = io.Copy(part, in)
-		if err != nil {
-			_ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to copy data"))
-			return
+			_, err = io.Copy(part, in)
+			if err != nil {
+				_ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to copy data"))
+				return
+			}
 		}
 
 		err = writer.Close()
@@ -423,8 +427,7 @@ func (api *Client) callCodec(opts *Opts, request interface{}, response interface
 			opts.Body = bytes.NewBuffer(requestBody)
 		}
 	}
-	isMultipart := (opts.MultipartParams != nil || opts.MultipartContentName != "") && opts.Body != nil
-	if isMultipart {
+	if opts.MultipartParams != nil || opts.MultipartContentName != "" {
 		params := opts.MultipartParams
 		if params == nil {
 			params = url.Values{}
diff --git a/vendor/github.com/koofr/go-koofrclient/api_scheme.go b/vendor/github.com/koofr/go-koofrclient/api_scheme.go
index 78e75b050..6c2df0e63 100644
--- a/vendor/github.com/koofr/go-koofrclient/api_scheme.go
+++ b/vendor/github.com/koofr/go-koofrclient/api_scheme.go
@@ -102,6 +102,7 @@ type FolderCreate struct {
 type FileCopy struct {
 	ToMountId string `json:"toMountId"`
 	TPath     string `json:"toPath"`
+	Modified  *int64 `json:"modified,omitempty"`
 }
 
 type FileMove struct {
@@ -118,20 +119,25 @@ type FileUpload struct {
 	Name string `json:"name"`
 }
 
-type PutFilter struct {
-	Modified          *int64
-	Size              *int64
-	Hash              *string
-	IgnoreNonExisting bool
-	NoRename          bool
-	ForceOverwrite    bool
+type PutOptions struct {
+	OverwriteIfModified        *int64
+	OverwriteIfSize            *int64
+	OverwriteIfHash            *string
+	OverwriteIgnoreNonExisting bool
+	NoRename                   bool
+	ForceOverwrite             bool
+	SetModified                *int64
 }
 
-type DeleteFilter struct {
-	Modified *int64
-	Size     *int64
-	Hash     *string
-	IfEmpty  bool
+type CopyOptions struct {
+	SetModified *int64
+}
+
+type DeleteOptions struct {
+	RemoveIfModified *int64
+	RemoveIfSize     *int64
+	RemoveIfHash     *string
+	RemoveIfEmpty    bool
 }
 
 type FileInfo struct {
diff --git a/vendor/github.com/koofr/go-koofrclient/client_files.go b/vendor/github.com/koofr/go-koofrclient/client_files.go
index 2e73fef74..5ef1ecb9d 100644
--- a/vendor/github.com/koofr/go-koofrclient/client_files.go
+++ b/vendor/github.com/koofr/go-koofrclient/client_files.go
@@ -83,25 +83,25 @@ func (c *KoofrClient) FilesDelete(mountId string, path string) (err error) {
 	return c.filesDelete(mountId, path, nil)
 }
 
-func (c *KoofrClient) FilesDeleteIf(mountId string, path string, deleteFilter *DeleteFilter) (err error) {
-	return c.filesDelete(mountId, path, deleteFilter)
+func (c *KoofrClient) FilesDeleteWithOptions(mountId string, path string, deleteOptions *DeleteOptions) (err error) {
+	return c.filesDelete(mountId, path, deleteOptions)
 }
 
-func (c *KoofrClient) filesDelete(mountId string, path string, deleteFilter *DeleteFilter) (err error) {
+func (c *KoofrClient) filesDelete(mountId string, path string, deleteOptions *DeleteOptions) (err error) {
 	params := url.Values{}
 	params.Set("path", path)
 
-	if deleteFilter != nil {
-		if deleteFilter.Size != nil {
-			params.Set("removeIfSize", fmt.Sprintf("%d", *deleteFilter.Size))
+	if deleteOptions != nil {
+		if deleteOptions.RemoveIfSize != nil {
+			params.Set("removeIfSize", fmt.Sprintf("%d", *deleteOptions.RemoveIfSize))
 		}
-		if deleteFilter.Modified != nil {
-			params.Set("removeIfModified", fmt.Sprintf("%d", *deleteFilter.Modified))
+		if deleteOptions.RemoveIfModified != nil {
+			params.Set("removeIfModified", fmt.Sprintf("%d", *deleteOptions.RemoveIfModified))
 		}
-		if deleteFilter.Hash != nil {
-			params.Set("removeIfHash", fmt.Sprintf("%s", *deleteFilter.Hash))
+		if deleteOptions.RemoveIfHash != nil {
+			params.Set("removeIfHash", fmt.Sprintf("%s", *deleteOptions.RemoveIfHash))
 		}
-		if deleteFilter.IfEmpty {
+		if deleteOptions.RemoveIfEmpty {
 			params.Set("removeIfEmpty", "")
 		}
 	}
@@ -152,8 +152,8 @@ func (c *KoofrClient) FilesNewFolder(mountId string, path string, name string) (
 	return
 }
 
-func (c *KoofrClient) FilesCopy(mountId string, path string, toMountId string, toPath string) (err error) {
-	reqData := FileCopy{toMountId, toPath}
+func (c *KoofrClient) FilesCopy(mountId string, path string, toMountId string, toPath string, options CopyOptions) (err error) {
+	reqData := FileCopy{toMountId, toPath, options.SetModified}
 
 	params := url.Values{}
 	params.Set("path", path)
@@ -230,35 +230,41 @@ func (c *KoofrClient) FilesGet(mountId string, path string) (reader io.ReadClose
 }
 
 func (c *KoofrClient) FilesPut(mountId string, path string, name string, reader io.Reader) (newName string, err error) {
-	info, err := c.FilesPutOptions(mountId, path, name, reader, nil)
+	info, err := c.FilesPutWithOptions(mountId, path, name, reader, nil)
 	return info.Name, err
 }
 
-func (c *KoofrClient) FilesPutOptions(mountId string, path string, name string, reader io.Reader, putFilter *PutFilter) (fileInfo *FileInfo, err error) {
+func (c *KoofrClient) FilesPutWithOptions(mountId string, path string, name string, reader io.Reader, putOptions *PutOptions) (fileInfo *FileInfo, err error) {
 	params := url.Values{}
 	params.Set("path", path)
 	params.Set("filename", name)
 	params.Set("info", "true")
 
-	if putFilter != nil {
-		if putFilter.Size != nil {
-			params.Set("overwriteIfSize", fmt.Sprintf("%d", *putFilter.Size))
+	if putOptions != nil {
+		if putOptions.OverwriteIfSize != nil {
+			params.Set("overwriteIfSize", fmt.Sprintf("%d", *putOptions.OverwriteIfSize))
 		}
-		if putFilter.Modified != nil {
-			params.Set("overwriteIfModified", fmt.Sprintf("%d", *putFilter.Modified))
+		if putOptions.OverwriteIfModified != nil {
+			params.Set("overwriteIfModified", fmt.Sprintf("%d", *putOptions.OverwriteIfModified))
 		}
-		if putFilter.Hash != nil {
-			params.Set("overwriteIfHash", fmt.Sprintf("%s", *putFilter.Hash))
+		if putOptions.OverwriteIfHash != nil {
+			params.Set("overwriteIfHash", fmt.Sprintf("%s", *putOptions.OverwriteIfHash))
 		}
-		if putFilter.IgnoreNonExisting {
+		if putOptions.OverwriteIfHash != nil {
+			params.Set("overwriteIfHash", fmt.Sprintf("%s", *putOptions.OverwriteIfHash))
+		}
+		if putOptions.OverwriteIgnoreNonExisting {
 			params.Set("overwriteIgnoreNonexisting", "")
 		}
-		if putFilter.NoRename {
+		if putOptions.NoRename {
 			params.Set("autorename", "false")
 		}
-		if putFilter.ForceOverwrite {
+		if putOptions.ForceOverwrite {
 			params.Set("overwrite", "true")
 		}
+		if putOptions.SetModified != nil {
+			params.Set("modified", fmt.Sprintf("%d", *putOptions.SetModified))
+		}
 	}
 
 	request := httpclient.RequestData{
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 3fb9d4694..c511221a8 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -111,7 +111,7 @@ github.com/jzelinskie/whirlpool
 github.com/kardianos/osext
 # github.com/koofr/go-httpclient v0.0.0-20180104120329-03786175608a
 github.com/koofr/go-httpclient
-# github.com/koofr/go-koofrclient v0.0.0-20190131164641-7f327592caff
+# github.com/koofr/go-koofrclient v0.0.0-20190715104037-2c5813f3c29c
 github.com/koofr/go-koofrclient
 # github.com/kr/fs v0.1.0
 github.com/kr/fs


Test patch:
diff --git a/fs/operations/operations_test.go b/fs/operations/operations_test.go
index 44dd9cbf9..75f84373a 100644
--- a/fs/operations/operations_test.go
+++ b/fs/operations/operations_test.go
@@ -866,6 +866,183 @@ func TestCopyFileBackupDir(t *testing.T) {
 	fstest.CheckItems(t, r.Fremote, file1old, file1)
 }
 
+// Test with CompareDest set
+func TestCopyFileCompareDest(t *testing.T) {
+	r := fstest.NewRun(t)
+	defer r.Finalise()
+
+	fs.Config.CompareDest = r.FremoteName + "/CompareDest"
+	defer func() {
+		fs.Config.CompareDest = ""
+	}()
+	fdst, err := fs.NewFs(r.FremoteName + "/dst")
+	require.NoError(t, err)
+
+	// check empty dest, empty compare
+	file1 := r.WriteFile("one", "one", t1)
+	fstest.CheckItems(t, r.Flocal, file1)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file1.Path, file1.Path)
+	require.NoError(t, err)
+
+	file1dst := file1
+	file1dst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1dst)
+
+	// check old dest, empty compare
+	file1b := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file1dst)
+	fstest.CheckItems(t, r.Flocal, file1b)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file1b.Path, file1b.Path)
+	require.NoError(t, err)
+
+	file1bdst := file1b
+	file1bdst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1bdst)
+
+	// check old dest, new compare
+	file3 := r.WriteObject(context.Background(), "dst/one", "one", t1)
+	file2 := r.WriteObject(context.Background(), "CompareDest/one", "onet2", t2)
+	file1c := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file3)
+	fstest.CheckItems(t, r.Flocal, file1c)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file1c.Path, file1c.Path)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file3)
+
+	// check empty dest, new compare
+	file4 := r.WriteObject(context.Background(), "CompareDest/two", "two", t2)
+	file5 := r.WriteFile("two", "two", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+	fstest.CheckItems(t, r.Flocal, file1c, file5)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file5.Path, file5.Path)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+
+	// check new dest, new compare
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file5.Path, file5.Path)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+
+	// check empty dest, old compare
+	file5b := r.WriteFile("two", "twot3", t3)
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+	fstest.CheckItems(t, r.Flocal, file1c, file5b)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file5b.Path, file5b.Path)
+	require.NoError(t, err)
+
+	file5bdst := file5b
+	file5bdst.Path = "dst/two"
+
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4, file5bdst)
+}
+
+// Test with CopyDest set
+func TestCopyFileCopyDest(t *testing.T) {
+	r := fstest.NewRun(t)
+	defer r.Finalise()
+
+	if r.Fremote.Features().Copy == nil {
+		t.Skip("Skipping test as remote does not support server side copy")
+	}
+
+	fs.Config.CopyDest = r.FremoteName + "/CopyDest"
+	defer func() {
+		fs.Config.CopyDest = ""
+	}()
+
+	fdst, err := fs.NewFs(r.FremoteName + "/dst")
+	require.NoError(t, err)
+
+	// check empty dest, empty copy
+	file1 := r.WriteFile("one", "one", t1)
+	fstest.CheckItems(t, r.Flocal, file1)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file1.Path, file1.Path)
+	require.NoError(t, err)
+
+	file1dst := file1
+	file1dst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1dst)
+
+	// check old dest, empty copy
+	file1b := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file1dst)
+	fstest.CheckItems(t, r.Flocal, file1b)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file1b.Path, file1b.Path)
+	require.NoError(t, err)
+
+	file1bdst := file1b
+	file1bdst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1bdst)
+
+	// check old dest, new copy, backup-dir
+
+	fs.Config.BackupDir = r.FremoteName + "/BackupDir"
+
+	file3 := r.WriteObject(context.Background(), "dst/one", "one", t1)
+	file2 := r.WriteObject(context.Background(), "CopyDest/one", "onet2", t2)
+	file1c := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file3)
+	fstest.CheckItems(t, r.Flocal, file1c)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file1c.Path, file1c.Path)
+	require.NoError(t, err)
+
+	file2dst := file2
+	file2dst.Path = "dst/one"
+	file3.Path = "BackupDir/one"
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3)
+	fs.Config.BackupDir = ""
+
+	// check empty dest, new copy
+	file4 := r.WriteObject(context.Background(), "CopyDest/two", "two", t2)
+	file5 := r.WriteFile("two", "two", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4)
+	fstest.CheckItems(t, r.Flocal, file1c, file5)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file5.Path, file5.Path)
+	require.NoError(t, err)
+
+	file4dst := file4
+	file4dst.Path = "dst/two"
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst)
+
+	// check new dest, new copy
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file5.Path, file5.Path)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst)
+
+	// check empty dest, old copy
+	file6 := r.WriteObject(context.Background(), "CopyDest/three", "three", t2)
+	file7 := r.WriteFile("three", "threet3", t3)
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst, file6)
+	fstest.CheckItems(t, r.Flocal, file1c, file5, file7)
+
+	err = operations.CopyFile(context.Background(), fdst, r.Flocal, file7.Path, file7.Path)
+	require.NoError(t, err)
+
+	file7dst := file7
+	file7dst.Path = "dst/three"
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst, file6, file7dst)
+}
+
 // testFsInfo is for unit testing fs.Info
 type testFsInfo struct {
 	name      string
diff --git a/fs/sync/sync_test.go b/fs/sync/sync_test.go
index 6c9929b81..d721601ed 100644
--- a/fs/sync/sync_test.go
+++ b/fs/sync/sync_test.go
@@ -1216,6 +1216,196 @@ func TestSyncOverlap(t *testing.T) {
 	checkErr(Sync(context.Background(), FremoteSync, FremoteSync, false))
 }
 
+// Test with CompareDest set
+func TestSyncCompareDest(t *testing.T) {
+	r := fstest.NewRun(t)
+	defer r.Finalise()
+
+	fs.Config.CompareDest = r.FremoteName + "/CompareDest"
+	defer func() {
+		fs.Config.CompareDest = ""
+	}()
+
+	fdst, err := fs.NewFs(r.FremoteName + "/dst")
+	require.NoError(t, err)
+
+	// check empty dest, empty compare
+	file1 := r.WriteFile("one", "one", t1)
+	fstest.CheckItems(t, r.Flocal, file1)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file1dst := file1
+	file1dst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1dst)
+
+	// check old dest, empty compare
+	file1b := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file1dst)
+	fstest.CheckItems(t, r.Flocal, file1b)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file1bdst := file1b
+	file1bdst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1bdst)
+
+	// check old dest, new compare
+	file3 := r.WriteObject(context.Background(), "dst/one", "one", t1)
+	file2 := r.WriteObject(context.Background(), "CompareDest/one", "onet2", t2)
+	file1c := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file3)
+	fstest.CheckItems(t, r.Flocal, file1c)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file3)
+
+	// check empty dest, new compare
+	file4 := r.WriteObject(context.Background(), "CompareDest/two", "two", t2)
+	file5 := r.WriteFile("two", "two", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+	fstest.CheckItems(t, r.Flocal, file1c, file5)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+
+	// check new dest, new compare
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+
+	// check empty dest, old compare
+	file5b := r.WriteFile("two", "twot3", t3)
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4)
+	fstest.CheckItems(t, r.Flocal, file1c, file5b)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file5bdst := file5b
+	file5bdst.Path = "dst/two"
+
+	fstest.CheckItems(t, r.Fremote, file2, file3, file4, file5bdst)
+}
+
+// Test with CopyDest set
+func TestSyncCopyDest(t *testing.T) {
+	r := fstest.NewRun(t)
+	defer r.Finalise()
+
+	if r.Fremote.Features().Copy == nil {
+		t.Skip("Skipping test as remote does not support server side copy")
+	}
+
+	fs.Config.CopyDest = r.FremoteName + "/CopyDest"
+	defer func() {
+		fs.Config.CopyDest = ""
+	}()
+
+	fdst, err := fs.NewFs(r.FremoteName + "/dst")
+	require.NoError(t, err)
+
+	// check empty dest, empty copy
+	file1 := r.WriteFile("one", "one", t1)
+	fstest.CheckItems(t, r.Flocal, file1)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file1dst := file1
+	file1dst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1dst)
+
+	// check old dest, empty copy
+	file1b := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file1dst)
+	fstest.CheckItems(t, r.Flocal, file1b)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file1bdst := file1b
+	file1bdst.Path = "dst/one"
+
+	fstest.CheckItems(t, r.Fremote, file1bdst)
+
+	// check old dest, new copy, backup-dir
+
+	fs.Config.BackupDir = r.FremoteName + "/BackupDir"
+
+	file3 := r.WriteObject(context.Background(), "dst/one", "one", t1)
+	file2 := r.WriteObject(context.Background(), "CopyDest/one", "onet2", t2)
+	file1c := r.WriteFile("one", "onet2", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file3)
+	fstest.CheckItems(t, r.Flocal, file1c)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file2dst := file2
+	file2dst.Path = "dst/one"
+	file3.Path = "BackupDir/one"
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3)
+	fs.Config.BackupDir = ""
+
+	// check empty dest, new copy
+	file4 := r.WriteObject(context.Background(), "CopyDest/two", "two", t2)
+	file5 := r.WriteFile("two", "two", t2)
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4)
+	fstest.CheckItems(t, r.Flocal, file1c, file5)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file4dst := file4
+	file4dst.Path = "dst/two"
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst)
+
+	// check new dest, new copy
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst)
+
+	// check empty dest, old copy
+	file6 := r.WriteObject(context.Background(), "CopyDest/three", "three", t2)
+	file7 := r.WriteFile("three", "threet3", t3)
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst, file6)
+	fstest.CheckItems(t, r.Flocal, file1c, file5, file7)
+
+	accounting.Stats.ResetCounters()
+	err = Sync(context.Background(), fdst, r.Flocal, false)
+	require.NoError(t, err)
+
+	file7dst := file7
+	file7dst.Path = "dst/three"
+
+	fstest.CheckItems(t, r.Fremote, file2, file2dst, file3, file4, file4dst, file6, file7dst)
+}
+
 // Test with BackupDir set
 func testSyncBackupDir(t *testing.T, suffix string, suffixKeepExtension bool) {
 	r := fstest.NewRun(t)
diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go
index c144b7319..d127f2eb6 100644
--- a/fstest/fstests/fstests.go
+++ b/fstest/fstests/fstests.go
@@ -1410,27 +1410,31 @@ func Run(t *testing.T, opt *Opt) {
 				// sharing directory for the first time
 				path := path.Dir(file2.Path)
 				link3, err := doPublicLink(context.Background(), path)
-				require.NoError(t, err)
-				require.NotEqual(t, "", link3, "Link should not be empty")
+				if err != nil && errors.Cause(err) == fs.ErrorCantShareDirectories {
+					t.Log("skipping directory tests as not supported on this backend")
+				} else {
+					require.NoError(t, err)
+					require.NotEqual(t, "", link3, "Link should not be empty")
 
-				// sharing directory for the second time
-				link3, err = doPublicLink(context.Background(), path)
-				require.NoError(t, err)
-				require.NotEqual(t, "", link3, "Link should not be empty")
+					// sharing directory for the second time
+					link3, err = doPublicLink(context.Background(), path)
+					require.NoError(t, err)
+					require.NotEqual(t, "", link3, "Link should not be empty")
 
-				// sharing the "root" directory in a subremote
-				subRemote, _, removeSubRemote, err := fstest.RandomRemote(remoteName, false)
-				require.NoError(t, err)
-				defer removeSubRemote()
-				// ensure sub remote isn't empty
-				buf := bytes.NewBufferString("somecontent")
-				obji := object.NewStaticObjectInfo("somefile", time.Now(), int64(buf.Len()), true, nil, nil)
-				_, err = subRemote.Put(context.Background(), buf, obji)
-				require.NoError(t, err)
+					// sharing the "root" directory in a subremote
+					subRemote, _, removeSubRemote, err := fstest.RandomRemote(remoteName, false)
+					require.NoError(t, err)
+					defer removeSubRemote()
+					// ensure sub remote isn't empty
+					buf := bytes.NewBufferString("somecontent")
+					obji := object.NewStaticObjectInfo("somefile", time.Now(), int64(buf.Len()), true, nil, nil)
+					_, err = subRemote.Put(context.Background(), buf, obji)
+					require.NoError(t, err)
 
-				link4, err := subRemote.Features().PublicLink(context.Background(), "")
-				require.NoError(t, err, "Sharing root in a sub-remote should work")
-				require.NotEqual(t, "", link4, "Link should not be empty")
+					link4, err := subRemote.Features().PublicLink(context.Background(), "")
+					require.NoError(t, err, "Sharing root in a sub-remote should work")
+					require.NotEqual(t, "", link4, "Link should not be empty")
+				}
 			})
 
 			// TestSetTier tests SetTier and GetTier functionality