Repository: rclone/rclone
Pull request #4466 (2020-07-29 02:32:47)

cmd/obscure: Allow obscure command to accept password on STDIN
#### What is the purpose of this change?

`rclone obscure` currently only accepts a command line argument of `password` to generate
an obfuscated password. This is an issue since generating obfuscated passwords programatically
requires sending the plain text password as a shell argument, which can cause problems if the
password contains shell characters, or if the password is from an untrusted source.

This patch opens up STDIN which will allow developers to open the STDIN source and print a password
directly to `rclone obscure`, which can increase safety and convenince.


#### Was the change discussed in an issue or in the forum before?

Unknown. A cursory glance did not turn up any forums posts.

#### Checklist

- [x] I have 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 :-)


dtibarra: I made the requested changes! I chose to make the combination of a hyphen as `arg[0]` AND the presence of data on `STDIN` be the trigger to accept input from `STDIN`. This way, we don't end up accidentally becoming unusable for someone who wants to obfuscate a literal hyphen. Let me know if this isn't acceptable and I can do it differently for you.

Patch:
diff --git a/backend/amazonclouddrive/amazonclouddrive.go b/backend/amazonclouddrive/amazonclouddrive.go
index ebe5c51d5..273f89535 100644
--- a/backend/amazonclouddrive/amazonclouddrive.go
+++ b/backend/amazonclouddrive/amazonclouddrive.go
@@ -937,8 +937,8 @@ func (f *Fs) Hashes() hash.Set {
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // ------------------------------------------------------------
diff --git a/backend/azureblob/azureblob.go b/backend/azureblob/azureblob.go
index 6ed2a1e3e..cb54f94fd 100644
--- a/backend/azureblob/azureblob.go
+++ b/backend/azureblob/azureblob.go
@@ -967,8 +967,7 @@ func (f *Fs) Hashes() hash.Set {
 }
 
 // Purge deletes all the files and directories including the old versions.
-func (f *Fs) Purge(ctx context.Context) error {
-	dir := "" // forward compat!
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	container, directory := f.split(dir)
 	if container == "" || directory != "" {
 		// Delegate to caller if not root of a container
diff --git a/backend/b2/b2.go b/backend/b2/b2.go
index 8b06ff8ed..e40adc224 100644
--- a/backend/b2/b2.go
+++ b/backend/b2/b2.go
@@ -1143,7 +1143,8 @@ func (f *Fs) deleteByID(ctx context.Context, ID, Name string) error {
 // if oldOnly is true then it deletes only non current files.
 //
 // Implemented here so we can make sure we delete old versions.
-func (f *Fs) purge(ctx context.Context, bucket, directory string, oldOnly bool) error {
+func (f *Fs) purge(ctx context.Context, dir string, oldOnly bool) error {
+	bucket, directory := f.split(dir)
 	if bucket == "" {
 		return errors.New("can't purge from root")
 	}
@@ -1218,19 +1219,19 @@ func (f *Fs) purge(ctx context.Context, bucket, directory string, oldOnly bool)
 	wg.Wait()
 
 	if !oldOnly {
-		checkErr(f.Rmdir(ctx, ""))
+		checkErr(f.Rmdir(ctx, dir))
 	}
 	return errReturn
 }
 
 // Purge deletes all the files and directories including the old versions.
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purge(ctx, f.rootBucket, f.rootDirectory, false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purge(ctx, dir, false)
 }
 
 // CleanUp deletes all the hidden files.
 func (f *Fs) CleanUp(ctx context.Context) error {
-	return f.purge(ctx, f.rootBucket, f.rootDirectory, true)
+	return f.purge(ctx, "", true)
 }
 
 // copy does a server side copy from dstObj <- srcObj
diff --git a/backend/box/box.go b/backend/box/box.go
index 25b8d3ba8..9903b4c30 100644
--- a/backend/box/box.go
+++ b/backend/box/box.go
@@ -862,8 +862,8 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // move a file or folder
diff --git a/backend/cache/cache.go b/backend/cache/cache.go
index d06e757ca..01140c358 100644
--- a/backend/cache/cache.go
+++ b/backend/cache/cache.go
@@ -1702,17 +1702,20 @@ func (f *Fs) Hashes() hash.Set {
 	return f.Fs.Hashes()
 }
 
-// Purge all files in the root and the root directory
-func (f *Fs) Purge(ctx context.Context) error {
-	fs.Infof(f, "purging cache")
-	f.cache.Purge()
+// Purge all files in the directory
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	if dir == "" {
+		// FIXME this isn't quite right as it should purge the dir prefix
+		fs.Infof(f, "purging cache")
+		f.cache.Purge()
+	}
 
 	do := f.Fs.Features().Purge
 	if do == nil {
-		return nil
+		return fs.ErrorCantPurge
 	}
 
-	err := do(ctx)
+	err := do(ctx, dir)
 	if err != nil {
 		return err
 	}
diff --git a/backend/chunker/chunker.go b/backend/chunker/chunker.go
index 42c9cf5b4..74bebacef 100644
--- a/backend/chunker/chunker.go
+++ b/backend/chunker/chunker.go
@@ -1333,7 +1333,7 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 	return f.base.Rmdir(ctx, dir)
 }
 
-// Purge all files in the root and the root directory
+// Purge all files in the directory
 //
 // Implement this if you have a way of deleting all the files
 // quicker than just running Remove() on the result of List()
@@ -1344,12 +1344,12 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 // As a result it removes not only composite chunker files with their
 // active chunks but also all hidden temporary chunks in the directory.
 //
-func (f *Fs) Purge(ctx context.Context) error {
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	do := f.base.Features().Purge
 	if do == nil {
 		return fs.ErrorCantPurge
 	}
-	return do(ctx)
+	return do(ctx, dir)
 }
 
 // Remove an object (chunks and metadata, if any)
diff --git a/backend/crypt/crypt.go b/backend/crypt/crypt.go
index 41c3a1925..831af4261 100644
--- a/backend/crypt/crypt.go
+++ b/backend/crypt/crypt.go
@@ -427,18 +427,18 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 	return f.Fs.Rmdir(ctx, f.cipher.EncryptDirName(dir))
 }
 
-// Purge all files in the root and the root directory
+// Purge all files in the directory specified
 //
 // Implement this if you have a way of deleting all the files
 // quicker than just running Remove() on the result of List()
 //
 // Return an error if it doesn't exist
-func (f *Fs) Purge(ctx context.Context) error {
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	do := f.Fs.Features().Purge
 	if do == nil {
 		return fs.ErrorCantPurge
 	}
-	return do(ctx)
+	return do(ctx, dir)
 }
 
 // Copy src to this remote using server side copy operations.
diff --git a/backend/drive/drive.go b/backend/drive/drive.go
index bcc2d8d18..2ef55f0fe 100755
--- a/backend/drive/drive.go
+++ b/backend/drive/drive.go
@@ -2208,10 +2208,9 @@ func (f *Fs) delete(ctx context.Context, id string, useTrash bool) error {
 	})
 }
 
-// Rmdir deletes a directory
-//
-// Returns an error if it isn't empty
-func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+// purgeCheck removes the dir directory, if check is set then it
+// refuses to do so if it has anything in
+func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
 	root := path.Join(f.root, dir)
 	dc := f.dirCache
 	directoryID, err := dc.FindDir(ctx, dir, false)
@@ -2224,20 +2223,22 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 		return f.delete(ctx, shortcutID, f.opt.UseTrash)
 	}
 	var trashedFiles = false
-	found, err := f.list(ctx, []string{directoryID}, "", false, false, true, func(item *drive.File) bool {
-		if !item.Trashed {
-			fs.Debugf(dir, "Rmdir: contains file: %q", item.Name)
-			return true
+	if check {
+		found, err := f.list(ctx, []string{directoryID}, "", false, false, true, func(item *drive.File) bool {
+			if !item.Trashed {
+				fs.Debugf(dir, "Rmdir: contains file: %q", item.Name)
+				return true
+			}
+			fs.Debugf(dir, "Rmdir: contains trashed file: %q", item.Name)
+			trashedFiles = true
+			return false
+		})
+		if err != nil {
+			return err
+		}
+		if found {
+			return errors.Errorf("directory not empty")
 		}
-		fs.Debugf(dir, "Rmdir: contains trashed file: %q", item.Name)
-		trashedFiles = true
-		return false
-	})
-	if err != nil {
-		return err
-	}
-	if found {
-		return errors.Errorf("directory not empty")
 	}
 	if root != "" {
 		// trash the directory if it had trashed files
@@ -2247,6 +2248,8 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 		if err != nil {
 			return err
 		}
+	} else if check {
+		return errors.New("can't purge root directory")
 	}
 	f.dirCache.FlushDir(dir)
 	if err != nil {
@@ -2255,6 +2258,13 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 	return nil
 }
 
+// Rmdir deletes a directory
+//
+// Returns an error if it isn't empty
+func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, true)
+}
+
 // Precision of the object storage system
 func (f *Fs) Precision() time.Duration {
 	return time.Millisecond
@@ -2348,23 +2358,11 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	if f.root == "" {
-		return errors.New("can't purge root directory")
-	}
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	if f.opt.TrashedOnly {
 		return errors.New("Can't purge with --drive-trashed-only. Use delete if you want to selectively delete files")
 	}
-	rootID, err := f.dirCache.RootID(ctx, false)
-	if err != nil {
-		return err
-	}
-	err = f.delete(ctx, shortcutID(rootID), f.opt.UseTrash)
-	f.dirCache.ResetRoot()
-	if err != nil {
-		return err
-	}
-	return nil
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // CleanUp empties the trash
diff --git a/backend/dropbox/dropbox.go b/backend/dropbox/dropbox.go
index 02c7af30c..ad740bf2f 100755
--- a/backend/dropbox/dropbox.go
+++ b/backend/dropbox/dropbox.go
@@ -611,10 +611,9 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
 	return err
 }
 
-// Rmdir deletes the container
-//
-// Returns an error if it isn't empty
-func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+// purgeCheck removes the root directory, if check is set then it
+// refuses to do so if it has anything in
+func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error) {
 	root := path.Join(f.slashRoot, dir)
 
 	// can't remove root
@@ -622,31 +621,33 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 		return errors.New("can't remove root directory")
 	}
 
-	// check directory exists
-	_, err := f.getDirMetadata(root)
-	if err != nil {
-		return errors.Wrap(err, "Rmdir")
-	}
+	if check {
+		// check directory exists
+		_, err = f.getDirMetadata(root)
+		if err != nil {
+			return errors.Wrap(err, "Rmdir")
+		}
 
-	root = f.opt.Enc.FromStandardPath(root)
-	// check directory empty
-	arg := files.ListFolderArg{
-		Path:      root,
-		Recursive: false,
-	}
-	if root == "/" {
-		arg.Path = "" // Specify root folder as empty string
-	}
-	var res *files.ListFolderResult
-	err = f.pacer.Call(func() (bool, error) {
-		res, err = f.srv.ListFolder(&arg)
-		return shouldRetry(err)
-	})
-	if err != nil {
-		return errors.Wrap(err, "Rmdir")
-	}
-	if len(res.Entries) != 0 {
-		return errors.New("directory not empty")
+		root = f.opt.Enc.FromStandardPath(root)
+		// check directory empty
+		arg := files.ListFolderArg{
+			Path:      root,
+			Recursive: false,
+		}
+		if root == "/" {
+			arg.Path = "" // Specify root folder as empty string
+		}
+		var res *files.ListFolderResult
+		err = f.pacer.Call(func() (bool, error) {
+			res, err = f.srv.ListFolder(&arg)
+			return shouldRetry(err)
+		})
+		if err != nil {
+			return errors.Wrap(err, "Rmdir")
+		}
+		if len(res.Entries) != 0 {
+			return errors.New("directory not empty")
+		}
 	}
 
 	// remove it
@@ -657,6 +658,13 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 	return err
 }
 
+// Rmdir deletes the container
+//
+// Returns an error if it isn't empty
+func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, true)
+}
+
 // Precision returns the precision
 func (f *Fs) Precision() time.Duration {
 	return time.Second
@@ -719,15 +727,8 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) (err error) {
-	// Let dropbox delete the filesystem tree
-	err = f.pacer.Call(func() (bool, error) {
-		_, err = f.srv.DeleteV2(&files.DeleteArg{
-			Path: f.opt.Enc.FromStandardPath(f.slashRoot),
-		})
-		return shouldRetry(err)
-	})
-	return err
+func (f *Fs) Purge(ctx context.Context, dir string) (err error) {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // Move src to this remote using server side move operations.
diff --git a/backend/jottacloud/jottacloud.go b/backend/jottacloud/jottacloud.go
index 9c7958858..10eb955e4 100644
--- a/backend/jottacloud/jottacloud.go
+++ b/backend/jottacloud/jottacloud.go
@@ -1070,8 +1070,8 @@ func (f *Fs) Precision() time.Duration {
 }
 
 // Purge deletes all the files and the container
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // copyOrMoves copies or moves directories or files depending on the method parameter
diff --git a/backend/local/local.go b/backend/local/local.go
index 85210ddbb..90aad3c96 100644
--- a/backend/local/local.go
+++ b/backend/local/local.go
@@ -616,20 +616,21 @@ func (f *Fs) readPrecision() (precision time.Duration) {
 	return
 }
 
-// Purge deletes all the files and directories
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	fi, err := f.lstat(f.root)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	dir = f.localPath(dir)
+	fi, err := f.lstat(dir)
 	if err != nil {
 		return err
 	}
 	if !fi.Mode().IsDir() {
-		return errors.Errorf("can't purge non directory: %q", f.root)
+		return errors.Errorf("can't purge non directory: %q", dir)
 	}
-	return os.RemoveAll(f.root)
+	return os.RemoveAll(dir)
 }
 
 // Move src to this remote using server side move operations.
diff --git a/backend/mailru/mailru.go b/backend/mailru/mailru.go
index b1a61b76e..316e39558 100644
--- a/backend/mailru/mailru.go
+++ b/backend/mailru/mailru.go
@@ -1162,12 +1162,12 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 	return f.purgeWithCheck(ctx, dir, true, "rmdir")
 }
 
-// Purge deletes all the files and the root directory
+// Purge deletes all the files in the directory
 // Optional interface: Only implement this if you have a way of deleting
 // all the files quicker than just running Remove() on the result of List()
-func (f *Fs) Purge(ctx context.Context) error {
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	// fs.Debugf(f, ">>> Purge")
-	return f.purgeWithCheck(ctx, "", false, "purge")
+	return f.purgeWithCheck(ctx, dir, false, "purge")
 }
 
 // purgeWithCheck() removes the root directory.
diff --git a/backend/mega/mega.go b/backend/mega/mega.go
index efb23ff9d..bcd007aa2 100644
--- a/backend/mega/mega.go
+++ b/backend/mega/mega.go
@@ -669,13 +669,13 @@ func (f *Fs) Precision() time.Duration {
 	return fs.ModTimeNotSupported
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck("", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(dir, false)
 }
 
 // move a file or folder (srcFs, srcRemote, info) to (f, dstRemote)
diff --git a/backend/onedrive/onedrive.go b/backend/onedrive/onedrive.go
index a41766bfe..ea4007b79 100755
--- a/backend/onedrive/onedrive.go
+++ b/backend/onedrive/onedrive.go
@@ -1073,13 +1073,13 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	return dstObj, nil
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // Move src to this remote using server side move operations.
diff --git a/backend/opendrive/opendrive.go b/backend/opendrive/opendrive.go
index 35f01b8be..7ba80a00f 100644
--- a/backend/opendrive/opendrive.go
+++ b/backend/opendrive/opendrive.go
@@ -506,13 +506,13 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
 	return nil
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // Return an Object from a path
diff --git a/backend/pcloud/pcloud.go b/backend/pcloud/pcloud.go
index 465b35ea6..b020c1144 100644
--- a/backend/pcloud/pcloud.go
+++ b/backend/pcloud/pcloud.go
@@ -671,13 +671,13 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	return dstObj, nil
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // CleanUp empties the trash
diff --git a/backend/premiumizeme/premiumizeme.go b/backend/premiumizeme/premiumizeme.go
index 1d9b580e2..3fb0270cd 100644
--- a/backend/premiumizeme/premiumizeme.go
+++ b/backend/premiumizeme/premiumizeme.go
@@ -609,13 +609,13 @@ func (f *Fs) Precision() time.Duration {
 	return fs.ModTimeNotSupported
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // move a file or folder
diff --git a/backend/putio/fs.go b/backend/putio/fs.go
index 42b751ccc..c6f3c0e9e 100644
--- a/backend/putio/fs.go
+++ b/backend/putio/fs.go
@@ -458,10 +458,9 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
 	return err
 }
 
-// Rmdir deletes the container
-//
-// Returns an error if it isn't empty
-func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
+// purgeCheck removes the root directory, if check is set then it
+// refuses to do so if it has anything in
+func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error) {
 	// defer log.Trace(f, "dir=%v", dir)("err=%v", &err)
 
 	root := strings.Trim(path.Join(f.root, dir), "/")
@@ -478,18 +477,20 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
 	}
 	dirID := atoi(directoryID)
 
-	// check directory empty
-	var children []putio.File
-	err = f.pacer.Call(func() (bool, error) {
-		// fs.Debugf(f, "listing files: %d", dirID)
-		children, _, err = f.client.Files.List(ctx, dirID)
-		return shouldRetry(err)
-	})
-	if err != nil {
-		return errors.Wrap(err, "Rmdir")
-	}
-	if len(children) != 0 {
-		return errors.New("directory not empty")
+	if check {
+		// check directory empty
+		var children []putio.File
+		err = f.pacer.Call(func() (bool, error) {
+			// fs.Debugf(f, "listing files: %d", dirID)
+			children, _, err = f.client.Files.List(ctx, dirID)
+			return shouldRetry(err)
+		})
+		if err != nil {
+			return errors.Wrap(err, "Rmdir")
+		}
+		if len(children) != 0 {
+			return errors.New("directory not empty")
+		}
 	}
 
 	// remove it
@@ -502,35 +503,26 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
 	return err
 }
 
+// Rmdir deletes the container
+//
+// Returns an error if it isn't empty
+func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
+	return f.purgeCheck(ctx, dir, true)
+}
+
 // Precision returns the precision
 func (f *Fs) Precision() time.Duration {
 	return time.Second
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) (err error) {
+func (f *Fs) Purge(ctx context.Context, dir string) (err error) {
 	// defer log.Trace(f, "")("err=%v", &err)
-
-	if f.root == "" {
-		return errors.New("can't purge root directory")
-	}
-	rootIDs, err := f.dirCache.RootID(ctx, false)
-	if err != nil {
-		return err
-	}
-	rootID := atoi(rootIDs)
-	// Let putio delete the filesystem tree
-	err = f.pacer.Call(func() (bool, error) {
-		// fs.Debugf(f, "deleting file: %d", rootID)
-		err = f.client.Files.Delete(ctx, rootID)
-		return shouldRetry(err)
-	})
-	f.dirCache.ResetRoot()
-	return err
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // Copy src to this remote using server side copy operations.
diff --git a/backend/seafile/seafile.go b/backend/seafile/seafile.go
index 544e72a4e..7eb36080d 100644
--- a/backend/seafile/seafile.go
+++ b/backend/seafile/seafile.go
@@ -584,29 +584,38 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
 	return nil
 }
 
-// Rmdir removes the directory or library if empty
-//
-// Return an error if it doesn't exist or isn't empty
-func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+// purgeCheck removes the root directory, if check is set then it
+// refuses to do so if it has anything in
+func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
 	libraryName, dirPath := f.splitPath(dir)
 	libraryID, err := f.getLibraryID(ctx, libraryName)
 	if err != nil {
 		return err
 	}
 
-	directoryEntries, err := f.getDirectoryEntries(ctx, libraryID, dirPath, false)
-	if err != nil {
-		return err
-	}
-	if len(directoryEntries) > 0 {
-		return fs.ErrorDirectoryNotEmpty
+	if check {
+		directoryEntries, err := f.getDirectoryEntries(ctx, libraryID, dirPath, false)
+		if err != nil {
+			return err
+		}
+		if len(directoryEntries) > 0 {
+			return fs.ErrorDirectoryNotEmpty
+		}
 	}
+
 	if dirPath == "" || dirPath == "/" {
 		return f.deleteLibrary(ctx, libraryID)
 	}
 	return f.deleteDir(ctx, libraryID, dirPath)
 }
 
+// Rmdir removes the directory or library if empty
+//
+// Return an error if it doesn't exist or isn't empty
+func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, true)
+}
+
 // ==================== Optional Interface fs.ListRer ====================
 
 // ListR lists the objects and directories of the Fs starting
@@ -893,33 +902,14 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
 
 // ==================== Optional Interface fs.Purger ====================
 
-// Purge all files in the root and the root directory
+// Purge all files in the directory
 //
 // Implement this if you have a way of deleting all the files
 // quicker than just running Remove() on the result of List()
 //
 // Return an error if it doesn't exist
-func (f *Fs) Purge(ctx context.Context) error {
-	if f.libraryName == "" {
-		return errors.New("Cannot delete from the root of the server. Please select a library")
-	}
-	libraryID, err := f.getLibraryID(ctx, f.libraryName)
-	if err != nil {
-		return err
-	}
-	if f.rootDirectory == "" {
-		// Delete library
-		err = f.deleteLibrary(ctx, libraryID)
-		if err != nil {
-			return err
-		}
-		return nil
-	}
-	err = f.deleteDir(ctx, libraryID, f.rootDirectory)
-	if err != nil {
-		return err
-	}
-	return nil
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // ==================== Optional Interface fs.CleanUpper ====================
diff --git a/backend/sharefile/sharefile.go b/backend/sharefile/sharefile.go
index 7741f34eb..ee392f490 100644
--- a/backend/sharefile/sharefile.go
+++ b/backend/sharefile/sharefile.go
@@ -853,8 +853,8 @@ func (f *Fs) Precision() time.Duration {
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // updateItem patches a file or folder
diff --git a/backend/sugarsync/sugarsync.go b/backend/sugarsync/sugarsync.go
index 235c381c1..b4a71553e 100644
--- a/backend/sugarsync/sugarsync.go
+++ b/backend/sugarsync/sugarsync.go
@@ -895,12 +895,12 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	return dstObj, nil
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	// Caution: Deleting a folder may orphan objects. It's important
 	// to remove the contents of the folder before you delete the
 	// folder. That's because removing a folder using DELETE does not
@@ -920,7 +920,7 @@ func (f *Fs) Purge(ctx context.Context) error {
 	if f.opt.HardDelete {
 		return fs.ErrorCantPurge
 	}
-	return f.purgeCheck(ctx, "", false)
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // moveFile moves a file server side
diff --git a/backend/swift/swift.go b/backend/swift/swift.go
index 127ea0402..761b8f9c8 100644
--- a/backend/swift/swift.go
+++ b/backend/swift/swift.go
@@ -840,17 +840,21 @@ func (f *Fs) Precision() time.Duration {
 	return time.Nanosecond
 }
 
-// Purge deletes all the files and directories
+// Purge deletes all the files in the directory
 //
 // Implemented here so we can make sure we delete directory markers
-func (f *Fs) Purge(ctx context.Context) error {
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	container, directory := f.split(dir)
+	if container == "" {
+		return fs.ErrorListBucketRequired
+	}
 	// Delete all the files including the directory markers
 	toBeDeleted := make(chan fs.Object, fs.Config.Transfers)
 	delErr := make(chan error, 1)
 	go func() {
 		delErr <- operations.DeleteFiles(ctx, toBeDeleted)
 	}()
-	err := f.list(f.rootContainer, f.rootDirectory, f.rootDirectory, f.rootContainer == "", true, true, func(entry fs.DirEntry) error {
+	err := f.list(container, directory, f.rootDirectory, false, true, true, func(entry fs.DirEntry) error {
 		if o, ok := entry.(*Object); ok {
 			toBeDeleted <- o
 		}
@@ -864,7 +868,7 @@ func (f *Fs) Purge(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	return f.Rmdir(ctx, "")
+	return f.Rmdir(ctx, dir)
 }
 
 // Copy src to this remote using server side copy operations.
diff --git a/backend/union/union.go b/backend/union/union.go
index 3ce07d756..a803a7e51 100644
--- a/backend/union/union.go
+++ b/backend/union/union.go
@@ -162,13 +162,13 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
 	return errs.Err()
 }
 
-// Purge all files in the root and the root directory
+// Purge all files in the directory
 //
 // Implement this if you have a way of deleting all the files
 // quicker than just running Remove() on the result of List()
 //
 // Return an error if it doesn't exist
-func (f *Fs) Purge(ctx context.Context) error {
+func (f *Fs) Purge(ctx context.Context, dir string) error {
 	for _, r := range f.upstreams {
 		if r.Features().Purge == nil {
 			return fs.ErrorCantPurge
@@ -180,7 +180,7 @@ func (f *Fs) Purge(ctx context.Context) error {
 	}
 	errs := Errors(make([]error, len(upstreams)))
 	multithread(len(upstreams), func(i int) {
-		err := upstreams[i].Features().Purge(ctx)
+		err := upstreams[i].Features().Purge(ctx, dir)
 		errs[i] = errors.Wrap(err, upstreams[i].Name())
 	})
 	return errs.Err()
diff --git a/backend/webdav/webdav.go b/backend/webdav/webdav.go
index 8ac1a47fb..ebbf2befe 100644
--- a/backend/webdav/webdav.go
+++ b/backend/webdav/webdav.go
@@ -899,13 +899,13 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	return f.copyOrMove(ctx, src, remote, "COPY")
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // Move src to this remote using server side move operations.
diff --git a/backend/yandex/yandex.go b/backend/yandex/yandex.go
index 570f3891d..04d97f975 100644
--- a/backend/yandex/yandex.go
+++ b/backend/yandex/yandex.go
@@ -637,13 +637,13 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
 	return f.purgeCheck(ctx, dir, true)
 }
 
-// Purge deletes all the files and the container
+// Purge deletes all the files in the directory
 //
 // Optional interface: Only implement this if you have a way of
 // deleting all the files quicker than just running Remove() on the
 // result of List()
-func (f *Fs) Purge(ctx context.Context) error {
-	return f.purgeCheck(ctx, "", false)
+func (f *Fs) Purge(ctx context.Context, dir string) error {
+	return f.purgeCheck(ctx, dir, false)
 }
 
 // copyOrMoves copies or moves directories or files depending on the method parameter
diff --git a/cmd/obscure/obscure.go b/cmd/obscure/obscure.go
index 38f70c14a..84ae266b8 100644
--- a/cmd/obscure/obscure.go
+++ b/cmd/obscure/obscure.go
@@ -3,6 +3,9 @@ package obscure
 import (
 	"fmt"
 
+	"io/ioutil"
+	"os"
+
 	"github.com/rclone/rclone/cmd"
 	"github.com/rclone/rclone/fs/config/obscure"
 	"github.com/spf13/cobra"
@@ -26,13 +29,29 @@ Many equally important things (like access tokens) are not obscured in
 the config file. However it is very hard to shoulder surf a 64
 character hex token.
 
+This command can also accept a password through STDIN instead of an
+argument by passing a hyphen as an argument. Example:
+
+echo "secretpassword" | rclone obscure -
+
+If there is no data on STDIN to read, rclone obscure will default to
+obfuscating the hyphen itself.
+
 If you want to encrypt the config file then please use config file
 encryption - see [rclone config](/commands/rclone_config/) for more
 info.`,
 	Run: func(command *cobra.Command, args []string) {
 		cmd.CheckArgs(1, 1, command, args)
+		var password string
+		fi, _ := os.Stdin.Stat()
+		if args[0] == "-" && (fi.Mode()&os.ModeCharDevice) == 0 {
+			bytes, _ := ioutil.ReadAll(os.Stdin)
+			password = string(bytes)
+		} else {
+			password = args[0]
+		}
 		cmd.Run(false, false, command, func() error {
-			obscured := obscure.MustObscure(args[0])
+			obscured := obscure.MustObscure(password)
 			fmt.Println(obscured)
 			return nil
 		})
diff --git a/fs/fs.go b/fs/fs.go
index 0348f1296..c564ec809 100644
--- a/fs/fs.go
+++ b/fs/fs.go
@@ -518,13 +518,13 @@ type Features struct {
 	SlowModTime             bool // if calling ModTime() generally takes an extra transaction
 	SlowHash                bool // if calling Hash() generally takes an extra transaction
 
-	// Purge all files in the root and the root directory
+	// Purge all files in the directory specified
 	//
 	// Implement this if you have a way of deleting all the files
 	// quicker than just running Remove() on the result of List()
 	//
 	// Return an error if it doesn't exist
-	Purge func(ctx context.Context) error
+	Purge func(ctx context.Context, dir string) error
 
 	// Copy src to this remote using server side copy operations.
 	//
@@ -883,13 +883,13 @@ func (ft *Features) WrapsFs(f Fs, w Fs) *Features {
 
 // Purger is an optional interfaces for Fs
 type Purger interface {
-	// Purge all files in the root and the root directory
+	// Purge all files in the directory specified
 	//
 	// Implement this if you have a way of deleting all the files
 	// quicker than just running Remove() on the result of List()
 	//
 	// Return an error if it doesn't exist
-	Purge(ctx context.Context) error
+	Purge(ctx context.Context, dir string) error
 }
 
 // Copier is an optional interface for Fs
diff --git a/fs/operations/operations.go b/fs/operations/operations.go
index 2d3a65108..915a15c4f 100644
--- a/fs/operations/operations.go
+++ b/fs/operations/operations.go
@@ -921,20 +921,16 @@ func Rmdir(ctx context.Context, f fs.Fs, dir string) error {
 }
 
 // Purge removes a directory and all of its contents
-func Purge(ctx context.Context, f fs.Fs, dir string) error {
+func Purge(ctx context.Context, f fs.Fs, dir string) (err error) {
 	doFallbackPurge := true
-	var err error
-	if dir == "" {
-		// FIXME change the Purge interface so it takes a dir - see #1891
-		if doPurge := f.Features().Purge; doPurge != nil {
-			doFallbackPurge = false
-			if SkipDestructive(ctx, fs.LogDirName(f, dir), "purge directory") {
-				return nil
-			}
-			err = doPurge(ctx)
-			if err == fs.ErrorCantPurge {
-				doFallbackPurge = true
-			}
+	if doPurge := f.Features().Purge; doPurge != nil {
+		doFallbackPurge = false
+		if SkipDestructive(ctx, fs.LogDirName(f, dir), "purge directory") {
+			return nil
+		}
+		err = doPurge(ctx, dir)
+		if err == fs.ErrorCantPurge {
+			doFallbackPurge = true
 		}
 	}
 	if doFallbackPurge {


Test patch:
diff --git a/backend/cache/cache_internal_test.go b/backend/cache/cache_internal_test.go
index be2cbfcba..becbfd92b 100644
--- a/backend/cache/cache_internal_test.go
+++ b/backend/cache/cache_internal_test.go
@@ -946,7 +946,7 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
 	}
 
 	if purge {
-		_ = f.Features().Purge(context.Background())
+		_ = f.Features().Purge(context.Background(), "")
 		require.NoError(t, err)
 	}
 	err = f.Mkdir(context.Background(), "")
@@ -955,7 +955,7 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
 }
 
 func (r *run) cleanupFs(t *testing.T, f fs.Fs, b *cache.Persistent) {
-	err := f.Features().Purge(context.Background())
+	err := f.Features().Purge(context.Background(), "")
 	require.NoError(t, err)
 	cfs, err := r.getCacheFs(f)
 	require.NoError(t, err)
diff --git a/fs/fs_test.go b/fs/fs_test.go
index d9e2788e6..981b74d0c 100644
--- a/fs/fs_test.go
+++ b/fs/fs_test.go
@@ -49,7 +49,7 @@ func TestFeaturesList(t *testing.T) {
 func TestFeaturesEnabled(t *testing.T) {
 	ft := new(Features)
 	ft.CaseInsensitive = true
-	ft.Purge = func(ctx context.Context) error { return nil }
+	ft.Purge = func(ctx context.Context, dir string) error { return nil }
 	enabled := ft.Enabled()
 
 	flag, ok := enabled["CaseInsensitive"]
diff --git a/fstest/fstest.go b/fstest/fstest.go
index b253abefe..8b30c3c96 100644
--- a/fstest/fstest.go
+++ b/fstest/fstest.go
@@ -482,7 +482,7 @@ func Purge(f fs.Fs) {
 	if doPurge := f.Features().Purge; doPurge != nil {
 		doFallbackPurge = false
 		fs.Debugf(f, "Purge remote")
-		err = doPurge(ctx)
+		err = doPurge(ctx, "")
 		if err == fs.ErrorCantPurge {
 			doFallbackPurge = true
 		}
diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go
index 5e2de4eca..857686853 100644
--- a/fstest/fstests/fstests.go
+++ b/fstest/fstests/fstests.go
@@ -974,6 +974,43 @@ func Run(t *testing.T, opt *Opt) {
 				assert.NotNil(t, err)
 			})
 
+			// TestFsPurge tests Purge
+			t.Run("FsPurge", func(t *testing.T) {
+				skipIfNotOk(t)
+
+				// Check have Purge
+				doPurge := remote.Features().Purge
+				if doPurge == nil {
+					t.Skip("FS has no Purge interface")
+				}
+
+				// put up a file to purge
+				fileToPurge := fstest.Item{
+					ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
+					Path:    "dirToPurge/fileToPurge.txt",
+				}
+				_, _ = testPut(ctx, t, remote, &fileToPurge)
+
+				fstest.CheckListingWithPrecision(t, remote, []fstest.Item{file1, file2, fileToPurge}, []string{
+					"dirToPurge",
+					"hello? sausage",
+					"hello? sausage/êé",
+					"hello? sausage/êé/Hello, 世界",
+					"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
+				}, fs.GetModifyWindow(remote))
+
+				// Now purge it
+				err = operations.Purge(ctx, remote, "dirToPurge")
+				require.NoError(t, err)
+
+				fstest.CheckListingWithPrecision(t, remote, []fstest.Item{file1, file2}, []string{
+					"hello? sausage",
+					"hello? sausage/êé",
+					"hello? sausage/êé/Hello, 世界",
+					"hello? sausage/êé/Hello, 世界/ \" ' @ < > & ? + ≠",
+				}, fs.GetModifyWindow(remote))
+			})
+
 			// TestFsCopy tests Copy
 			t.Run("FsCopy", func(t *testing.T) {
 				skipIfNotOk(t)