Repository: rclone/rclone
Pull request #4357 (2020-06-15 21:50:33)

Contributing squashing
#### What is the purpose of this change?

Add how-to-squash. A few months ago, I couldn't get my head around git. Why not just have it there?

#### Was the change discussed in an issue or in the forum before?

No.

#### Checklist

- [x] I have totally not read the [contribution guidelines](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#submitting-a-pull-request).
- [ ] ~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/rclone/rclone/blob/master/CONTRIBUTING.md#commit-messages).
- [x] I'm done, this Pull Request is ready for review :-)



Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index df0fa8074..85fbce96a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -79,8 +79,15 @@ request](https://help.github.com/articles/creating-a-pull-request/).
 
 You patch will get reviewed and you might get asked to fix some stuff.
 
-If so, then make the changes in the same branch, squash the commits,
-rebase it to master then push it to GitHub with `--force`.
+If so, then make the changes in the same branch, squash the commits (make multiple commits one commit) by running:
+```
+git log # See how many commits you want to squash
+git reset --soft HEAD~2 # This squashes the 2 latest commits together.
+git status # Check what will happen, if you made a mistake resetting, you can run git reset 'HEAD@{1}' to undo.
+git commit # Add a new commit message.
+git push --force # Push the squashed commit to your GitHub repo.
+# For more, see Stack Overflow, Git docs, or generally Duck around the web. jtagcat also reccommends wizardzines.com
+```
 
 ## CI for your fork ##
 
diff --git a/backend/ftp/ftp.go b/backend/ftp/ftp.go
index 8119b19ca..2850229a7 100644
--- a/backend/ftp/ftp.go
+++ b/backend/ftp/ftp.go
@@ -50,8 +50,19 @@ func init() {
 			IsPassword: true,
 			Required:   true,
 		}, {
-			Name:    "tls",
-			Help:    "Use FTP over TLS (Implicit)",
+			Name: "tls",
+			Help: `Use FTPS over TLS (Implicit)
+When using implicit FTP over TLS the client will connect using TLS
+right from the start, which in turn breaks the compatibility with
+non-TLS-aware servers. This is usually served over port 990 rather
+than port 21. Cannot be used in combination with explicit FTP.`,
+			Default: false,
+		}, {
+			Name: "explicit_tls",
+			Help: `Use FTP over TLS (Explicit)
+When using explicit FTP over TLS the client explicitly request
+security from the server in order to upgrade a plain text connection
+to an encrypted one. Cannot be used in combination with implicit FTP.`,
 			Default: false,
 		}, {
 			Name:     "concurrency",
@@ -90,6 +101,7 @@ type Options struct {
 	Pass              string               `config:"pass"`
 	Port              string               `config:"port"`
 	TLS               bool                 `config:"tls"`
+	ExplicitTLS       bool                 `config:"explicit_tls"`
 	Concurrency       int                  `config:"concurrency"`
 	SkipVerifyTLSCert bool                 `config:"no_check_certificate"`
 	DisableEPSV       bool                 `config:"disable_epsv"`
@@ -152,12 +164,21 @@ func (f *Fs) Features() *fs.Features {
 func (f *Fs) ftpConnection() (*ftp.ServerConn, error) {
 	fs.Debugf(f, "Connecting to FTP server")
 	ftpConfig := []ftp.DialOption{ftp.DialWithTimeout(fs.Config.ConnectTimeout)}
-	if f.opt.TLS {
+	if f.opt.TLS && f.opt.ExplicitTLS {
+		fs.Errorf(f, "Implicit TLS and explicit TLS are mutually incompatible. Please revise your config")
+		return nil, errors.New("Implicit TLS and explicit TLS are mutually incompatible. Please revise your config")
+	} else if f.opt.TLS {
 		tlsConfig := &tls.Config{
 			ServerName:         f.opt.Host,
 			InsecureSkipVerify: f.opt.SkipVerifyTLSCert,
 		}
 		ftpConfig = append(ftpConfig, ftp.DialWithTLS(tlsConfig))
+	} else if f.opt.ExplicitTLS {
+		tlsConfig := &tls.Config{
+			ServerName:         f.opt.Host,
+			InsecureSkipVerify: f.opt.SkipVerifyTLSCert,
+		}
+		ftpConfig = append(ftpConfig, ftp.DialWithExplicitTLS(tlsConfig))
 	}
 	if f.opt.DisableEPSV {
 		ftpConfig = append(ftpConfig, ftp.DialWithDisabledEPSV(true))
diff --git a/backend/jottacloud/jottacloud.go b/backend/jottacloud/jottacloud.go
index bd97a9996..9bd7b783a 100644
--- a/backend/jottacloud/jottacloud.go
+++ b/backend/jottacloud/jottacloud.go
@@ -46,7 +46,7 @@ const (
 	decayConstant      = 2 // bigger for slower decay, exponential
 	defaultDevice      = "Jotta"
 	defaultMountpoint  = "Archive"
-	rootURL            = "https://www.jottacloud.com/jfs/"
+	rootURL            = "https://jfs.jottacloud.com/jfs/"
 	apiURL             = "https://api.jottacloud.com/"
 	baseURL            = "https://www.jottacloud.com/"
 	defaultTokenURL    = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
@@ -1088,7 +1088,8 @@ func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *ap
 	var resp *http.Response
 	err = f.pacer.Call(func() (bool, error) {
 		resp, err = f.srv.CallXML(ctx, &opts, nil, &info)
-		return shouldRetry(resp, err)
+		retry, _ := shouldRetry(resp, err)
+		return (retry && resp.StatusCode != 500), err
 	})
 	if err != nil {
 		return nil, err
@@ -1192,6 +1193,18 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
 
 	_, err = f.copyOrMove(ctx, "mvDir", path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(srcPath))+"/", dstRemote)
 
+	// surprise! jottacloud fucked up dirmove - the api spits out an error but
+	// dir gets moved regardless
+	if apiErr, ok := err.(*api.Error); ok {
+		if apiErr.StatusCode == 500 {
+			_, err := f.NewObject(ctx, dstRemote)
+			if err == fs.ErrorNotAFile {
+				log.Printf("FIXME: ignoring DirMove error - move succeeded anyway\n")
+				return nil
+			}
+			return err
+		}
+	}
 	if err != nil {
 		return errors.Wrap(err, "couldn't move directory")
 	}
diff --git a/cmd/serve/dlna/cds.go b/cmd/serve/dlna/cds.go
index 14bbb49e0..0b5f4b198 100644
--- a/cmd/serve/dlna/cds.go
+++ b/cmd/serve/dlna/cds.go
@@ -77,10 +77,12 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi
 	}
 
 	if fileInfo.IsDir() {
+		defaultChildCount := 1
 		obj.Class = "object.container.storageFolder"
 		obj.Title = fileInfo.Name()
 		return upnpav.Container{
-			Object: obj,
+			Object:     obj,
+			ChildCount: &defaultChildCount,
 		}, nil
 	}
 
diff --git a/docs/content/authors.md b/docs/content/authors.md
index 71c24c353..c8c8c9bf1 100644
--- a/docs/content/authors.md
+++ b/docs/content/authors.md
@@ -384,3 +384,4 @@ put them back in again.` >}}
   * Kamil Trzciński <ayufan@ayufan.eu>
   * Zac Rubin <z-0@users.noreply.github.com>
   * Vincent Feltz <psycho@feltzv.fr>
+  * Heiko Bornholdt <bornholdt@informatik.uni-hamburg.de>
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 1fdd6025b..aa6d154d4 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -203,6 +203,11 @@ To copy files and directories in `https://example.com/path/to/dir` to `/tmp/dir`
 To copy files and directories from `example.com` in the relative
 directory `path/to/dir` to `/tmp/dir` using sftp.
 
+### Valid remote names
+
+ - Remote names may only contain 0-9, A-Z ,a-z ,_ , - and space.
+ - Remote names may not start with -.
+
 Quoting and the shell
 ---------------------
 
diff --git a/docs/content/ftp.md b/docs/content/ftp.md
index 0e5b23a60..0714e2bb2 100644
--- a/docs/content/ftp.md
+++ b/docs/content/ftp.md
@@ -61,6 +61,9 @@ password:
 Use FTP over TLS (Implicit)
 Enter a boolean value (true or false). Press Enter for the default ("false").
 tls> 
+Use FTP over TLS (Explicit)
+Enter a boolean value (true or false). Press Enter for the default ("false").
+explicit_tls> 
 Remote config
 --------------------
 [remote]
@@ -181,11 +184,29 @@ FTP password
 
 Use FTP over TLS (Implicit)
 
+When using implicit FTP over TLS the client will connect using TLS
+right from the start, which in turn breaks the compatibility with
+non-TLS-aware servers. This is usually served over port 990 rather
+than port 21. Cannot be used in combination with explicit FTP.
+
 - Config:      tls
 - Env Var:     RCLONE_FTP_TLS
 - Type:        bool
 - Default:     false
 
+#### --ftp-explicit-tls
+
+Use FTP over TLS (Explicit)
+
+When using explicit FTP over TLS the client explicitly request
+security from the server in order to upgrade a plain text connection
+to an encrypted one. Cannot be used in combination with implicit FTP.
+
+- Config:      explicit_tls
+- Env Var:     RCLONE_FTP_TLS
+- Type:        bool
+- Default:     false
+
 ### Advanced Options
 
 Here are the advanced options specific to ftp (FTP Connection).
@@ -243,6 +264,3 @@ FTP could support server side move but doesn't yet.
 
 Note that the ftp backend does not support the `ftp_proxy` environment
 variable yet.
-
-Note that while implicit FTP over TLS is supported,
-explicit FTP over TLS is not.
diff --git a/fs/fspath/path.go b/fs/fspath/path.go
index fd6034181..adf19a6ce 100644
--- a/fs/fspath/path.go
+++ b/fs/fspath/path.go
@@ -19,6 +19,7 @@ const (
 var (
 	errInvalidCharacters = errors.New("config name contains invalid characters - may only contain 0-9, A-Z ,a-z ,_ , - and space ")
 	errCantBeEmpty       = errors.New("can't use empty string as a path")
+	errCantStartWithDash = errors.New("config name starts with -")
 
 	// urlMatcher is a pattern to match an rclone URL
 	// note that this matches invalid remoteNames
@@ -36,6 +37,10 @@ func CheckConfigName(configName string) error {
 	if !configNameMatcher.MatchString(configName) {
 		return errInvalidCharacters
 	}
+	// Reject configName, if it starts with -, complicates usage. (#4261)
+	if strings.HasPrefix(configName, "-") {
+		return errCantStartWithDash
+	}
 	return nil
 }
 
diff --git a/fs/operations/dedupe.go b/fs/operations/dedupe.go
index 963155376..799a4fac9 100644
--- a/fs/operations/dedupe.go
+++ b/fs/operations/dedupe.go
@@ -316,7 +316,7 @@ func Deduplicate(ctx context.Context, f fs.Fs, mode DeduplicateMode) error {
 
 	for remote, objs := range files {
 		if len(objs) > 1 {
-			fs.Logf(remote, "Found %d duplicates - deleting identical copies", len(objs))
+			fs.Logf(remote, "Found %d files with duplicate names", len(objs))
 			objs = dedupeDeleteIdentical(ctx, ht, remote, objs)
 			if len(objs) <= 1 {
 				fs.Logf(remote, "All duplicates removed")
@@ -342,7 +342,7 @@ func Deduplicate(ctx context.Context, f fs.Fs, mode DeduplicateMode) error {
 				sortSmallestFirst(objs)
 				dedupeDeleteAllButOne(ctx, 0, remote, objs)
 			case DeduplicateSkip:
-				// skip
+				fs.Logf(remote, "Skipping %d files with duplicate names", len(objs))
 			default:
 				//skip
 			}
diff --git a/go.mod b/go.mod
index a4afb68ca..f43a0d673 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect
 	github.com/hanwen/go-fuse/v2 v2.0.3-0.20191108143333-152e6ac32d54
-	github.com/jlaffaye/ftp v0.0.0-20191218041957-e1b8fdd0dcc3
+	github.com/jlaffaye/ftp v0.0.0-20200422224957-b9f3ade29122
 	github.com/jzelinskie/whirlpool v0.0.0-20170603002051-c19460b8caa6
 	github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
 	github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
diff --git a/go.sum b/go.sum
index 6903361c2..306b80e3b 100644
--- a/go.sum
+++ b/go.sum
@@ -208,6 +208,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M
 github.com/jlaffaye/ftp v0.0.0-20190624084859-c1312a7102bf/go.mod h1:lli8NYPQOFy3O++YmYbqVgOcQ1JPCwdOy+5zSjKJ9qY=
 github.com/jlaffaye/ftp v0.0.0-20191218041957-e1b8fdd0dcc3 h1:QyB6CQGLB65Al72mAIbqrkGRk56JdGMHgBziM3F0FCw=
 github.com/jlaffaye/ftp v0.0.0-20191218041957-e1b8fdd0dcc3/go.mod h1:PwUeyujmhaGohgOf0kJKxPfk3HcRv8QD/wAUN44go4k=
+github.com/jlaffaye/ftp v0.0.0-20200422224957-b9f3ade29122 h1:dzYWuozdWNaY7mTQh5ZdmoJt2BUMavwhiux0AfGwg90=
+github.com/jlaffaye/ftp v0.0.0-20200422224957-b9f3ade29122/go.mod h1:PwUeyujmhaGohgOf0kJKxPfk3HcRv8QD/wAUN44go4k=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
diff --git a/vendor/github.com/jlaffaye/ftp/README.md b/vendor/github.com/jlaffaye/ftp/README.md
index c9df01e33..8488deb9c 100644
--- a/vendor/github.com/jlaffaye/ftp/README.md
+++ b/vendor/github.com/jlaffaye/ftp/README.md
@@ -50,6 +50,7 @@ r, err := c.Retr("test-file.txt")
 if err != nil {
 	panic(err)
 }
+defer r.Close()
 
 buf, err := ioutil.ReadAll(r)
 println(string(buf))
diff --git a/vendor/github.com/jlaffaye/ftp/ftp.go b/vendor/github.com/jlaffaye/ftp/ftp.go
index 376ff5e4f..341e58d2f 100644
--- a/vendor/github.com/jlaffaye/ftp/ftp.go
+++ b/vendor/github.com/jlaffaye/ftp/ftp.go
@@ -50,6 +50,7 @@ type dialOptions struct {
 	context     context.Context
 	dialer      net.Dialer
 	tlsConfig   *tls.Config
+	explicitTLS bool
 	conn        net.Conn
 	disableEPSV bool
 	location    *time.Location
@@ -90,7 +91,7 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
 
 		if do.dialFunc != nil {
 			tconn, err = do.dialFunc("tcp", addr)
-		} else if do.tlsConfig != nil {
+		} else if do.tlsConfig != nil && !do.explicitTLS {
 			tconn, err = tls.DialWithDialer(&do.dialer, "tcp", addr, do.tlsConfig)
 		} else {
 			ctx := do.context
@@ -111,15 +112,10 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
 	// If we use the domain name, we might not resolve to the same IP.
 	remoteAddr := tconn.RemoteAddr().(*net.TCPAddr)
 
-	var sourceConn io.ReadWriteCloser = tconn
-	if do.debugOutput != nil {
-		sourceConn = newDebugWrapper(tconn, do.debugOutput)
-	}
-
 	c := &ServerConn{
 		options:  do,
 		features: make(map[string]string),
-		conn:     textproto.NewConn(sourceConn),
+		conn:     textproto.NewConn(do.wrapConn(tconn)),
 		host:     remoteAddr.IP.String(),
 	}
 
@@ -129,6 +125,15 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
 		return nil, err
 	}
 
+	if do.explicitTLS {
+		if err := c.authTLS(); err != nil {
+			_ = c.Quit()
+			return nil, err
+		}
+		tconn = tls.Client(tconn, do.tlsConfig)
+		c.conn = textproto.NewConn(do.wrapConn(tconn))
+	}
+
 	err = c.feat()
 	if err != nil {
 		c.Quit()
@@ -198,6 +203,15 @@ func DialWithTLS(tlsConfig *tls.Config) DialOption {
 	}}
 }
 
+// DialWithExplicitTLS returns a DialOption that configures the ServerConn to be upgraded to TLS
+// See DialWithTLS for general TLS documentation
+func DialWithExplicitTLS(tlsConfig *tls.Config) DialOption {
+	return DialOption{func(do *dialOptions) {
+		do.explicitTLS = true
+		do.tlsConfig = tlsConfig
+	}}
+}
+
 // DialWithDebugOutput returns a DialOption that configures the ServerConn to write to the Writer
 // everything it reads from the server
 func DialWithDebugOutput(w io.Writer) DialOption {
@@ -218,6 +232,14 @@ func DialWithDialFunc(f func(network, address string) (net.Conn, error)) DialOpt
 	}}
 }
 
+func (o *dialOptions) wrapConn(netConn net.Conn) io.ReadWriteCloser {
+	if o.debugOutput == nil {
+		return netConn
+	}
+
+	return newDebugWrapper(netConn, o.debugOutput)
+}
+
 // Connect is an alias to Dial, for backward compatibility
 func Connect(addr string) (*ServerConn, error) {
 	return Dial(addr)
@@ -269,6 +291,12 @@ func (c *ServerConn) Login(user, password string) error {
 	return err
 }
 
+// authTLS upgrades the connection to use TLS
+func (c *ServerConn) authTLS() error {
+	_, _, err := c.cmd(StatusAuthOK, "AUTH TLS")
+	return err
+}
+
 // feat issues a FEAT FTP command to list the additional commands supported by
 // the remote FTP server.
 // FEAT is described in RFC 2389
@@ -629,6 +657,27 @@ func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
 	return err
 }
 
+// Append issues a APPE FTP command to store a file to the remote FTP server.
+// If a file already exists with the given path, then the content of the
+// io.Reader is appended. Otherwise, a new file is created with that content.
+//
+// Hint: io.Pipe() can be used if an io.Writer is required.
+func (c *ServerConn) Append(path string, r io.Reader) error {
+	conn, err := c.cmdDataConnFrom(0, "APPE %s", path)
+	if err != nil {
+		return err
+	}
+
+	_, err = io.Copy(conn, r)
+	conn.Close()
+	if err != nil {
+		return err
+	}
+
+	_, _, err = c.conn.ReadResponse(StatusClosingDataConnection)
+	return err
+}
+
 // Rename renames a file on the remote FTP server.
 func (c *ServerConn) Rename(from, to string) error {
 	_, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from)
@@ -701,6 +750,20 @@ func (c *ServerConn) RemoveDir(path string) error {
 	return err
 }
 
+//Walk prepares the internal walk function so that the caller can begin traversing the directory
+func (c *ServerConn) Walk(root string) *Walker {
+	w := new(Walker)
+	w.serverConn = c
+
+	if !strings.HasSuffix(root, "/") {
+		root += "/"
+	}
+
+	w.root = root
+
+	return w
+}
+
 // NoOp issues a NOOP FTP command.
 // NOOP has no effects and is usually used to prevent the remote FTP server to
 // close the otherwise idle connection.
diff --git a/vendor/github.com/jlaffaye/ftp/status.go b/vendor/github.com/jlaffaye/ftp/status.go
index 7d281b8b8..c8ea026c9 100644
--- a/vendor/github.com/jlaffaye/ftp/status.go
+++ b/vendor/github.com/jlaffaye/ftp/status.go
@@ -25,6 +25,7 @@ const (
 	StatusLoggedIn              = 230
 	StatusLoggedOut             = 231
 	StatusLogoutAck             = 232
+	StatusAuthOK                = 234
 	StatusRequestedFileActionOK = 250
 	StatusPathCreated           = 257
 
@@ -73,6 +74,7 @@ var statusText = map[int]string{
 	StatusLoggedIn:              "User logged in, proceed.",
 	StatusLoggedOut:             "User logged out; service terminated.",
 	StatusLogoutAck:             "Logout command noted, will complete when transfer done.",
+	StatusAuthOK:                "AUTH command OK",
 	StatusRequestedFileActionOK: "Requested file action okay, completed.",
 	StatusPathCreated:           "Path created.",
 
diff --git a/vendor/github.com/jlaffaye/ftp/walker.go b/vendor/github.com/jlaffaye/ftp/walker.go
new file mode 100644
index 000000000..dacd0d8d0
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/walker.go
@@ -0,0 +1,84 @@
+package ftp
+
+import (
+	pa "path"
+	"strings"
+)
+
+//Walker traverses the directory tree of a remote FTP server
+type Walker struct {
+	serverConn *ServerConn
+	root       string
+	cur        item
+	stack      []item
+	descend    bool
+}
+
+type item struct {
+	path  string
+	entry Entry
+	err   error
+}
+
+// Next advances the Walker to the next file or directory,
+// which will then be available through the Path, Stat, and Err methods.
+// It returns false when the walk stops at the end of the tree.
+func (w *Walker) Next() bool {
+	if w.descend && w.cur.err == nil && w.cur.entry.Type == EntryTypeFolder {
+		list, err := w.serverConn.List(w.cur.path)
+		if err != nil {
+			w.cur.err = err
+			w.stack = append(w.stack, w.cur)
+		} else {
+			for i := len(list) - 1; i >= 0; i-- {
+				if !strings.HasSuffix(w.cur.path, "/") {
+					w.cur.path += "/"
+				}
+
+				var path string
+				if list[i].Type == EntryTypeFolder {
+					path = pa.Join(w.cur.path, list[i].Name)
+				} else {
+					path = w.cur.path
+				}
+
+				w.stack = append(w.stack, item{path, *list[i], nil})
+			}
+		}
+	}
+
+	if len(w.stack) == 0 {
+		return false
+	}
+	i := len(w.stack) - 1
+	w.cur = w.stack[i]
+	w.stack = w.stack[:i]
+	w.descend = true
+	return true
+}
+
+//SkipDir tells the Next function to skip the currently processed directory
+func (w *Walker) SkipDir() {
+	w.descend = false
+}
+
+//Err returns the error, if any, for the most recent attempt by Next to
+//visit a file or a directory. If a directory has an error, the walker
+//will not descend in that directory
+func (w *Walker) Err() error {
+	return w.cur.err
+}
+
+// Stat returns info for the most recent file or directory
+// visited by a call to Step.
+func (w *Walker) Stat() Entry {
+	return w.cur.entry
+}
+
+// Path returns the path to the most recent file or directory
+// visited by a call to Next. It contains the argument to Walk
+// as a prefix; that is, if Walk is called with "dir", which is
+// a directory containing the file "a", Path will return "dir/a".
+func (w *Walker) Path() string {
+	return w.cur.path
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 5c26e8782..8ff60655d 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -141,7 +141,7 @@ github.com/hanwen/go-fuse/v2/internal/utimens
 github.com/hanwen/go-fuse/v2/splice
 # github.com/inconshreveable/mousetrap v1.0.0
 github.com/inconshreveable/mousetrap
-# github.com/jlaffaye/ftp v0.0.0-20191218041957-e1b8fdd0dcc3
+# github.com/jlaffaye/ftp v0.0.0-20200422224957-b9f3ade29122
 ## explicit
 github.com/jlaffaye/ftp
 # github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af


Test patch:
diff --git a/fs/fspath/path_test.go b/fs/fspath/path_test.go
index 5e8cba2e5..135708ec8 100644
--- a/fs/fspath/path_test.go
+++ b/fs/fspath/path_test.go
@@ -23,6 +23,9 @@ func TestCheckConfigName(t *testing.T) {
 		{"rem\\ote", errInvalidCharacters},
 		{"[remote", errInvalidCharacters},
 		{"*", errInvalidCharacters},
+		{"-remote", errCantStartWithDash},
+		{"r-emote-", nil},
+		{"_rem_ote_", nil},
 	} {
 		got := CheckConfigName(test.in)
 		assert.Equal(t, test.want, got, test.in)