Repository: rclone/rclone
Pull request #5080 (2021-03-01 10:39:00)

cmd: rclone selfupdate
#### What is the purpose of this change?

Implements `rclone self-update` command (ticket #548)

Flags and features:
- verifies sha256 hash of release archive
- verifies access to replaced file and directory, optionally asks user to run with elevated privileges (in Scotland - elavated)
- mimics permission bits of original file, if any
- `--output` - downloads new executable to a custom location
- `--beta` - self-explanatory
- `--check` - just an alias of `rclone version --check`

Yet unimplemented, left for future patches:
- `--stable` (`--backports`)
- `--version`

**UPDATE 2021-03-08**:
Will be implemented:
- `--stable` (mutually exclusive with `--beta`)
- `--version VER` (micro-release autocompletion if stable)
- `--package deb` (linux only)
- `--package rpm` (linux only)
- PGP signature validation (pending public key)

#### Was the change discussed in an issue or in the forum before?

Yes:
- https://github.com/rclone/rclone/issues/548
- https://forum.rclone.org/t/previously-working-rclone-backup-now-giving-error-unknown-command-rcat-for-rclone/22507/6
- https://github.com/rclone/rclone/issues/5076

#### Checklist

- [x] I have read the [contribution guidelines](https://github.com/rclone/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/rclone/rclone/blob/master/CONTRIBUTING.md#commit-messages).
- [x] I'm done, this Pull Request is ready for review :-)


ivandeex: > Should we also add a note in the Docs that the manpages will no longer be accurate after the binary is updated using this?

I will add this warning in the success message
ivandeex: As I expected there is a Windows quirk. Windows never allows to rewrite currently running executable, even if user has write permission. The command will fail with exit code 1 and print the following message
```
Failed to replace old file, new version 1.54.xxx saved as C:\bin\rclone.exe.new
```
The workaround for scheduled update tasks is to use the `--output` flag:
```
C:\bin\rclone.exe self-update --output C:\bin\rclone.new.exe
del C:\bin\rclone.exe
rename C:\bin\rclone.new.exe C:\bin\rclone.exe
```
darthShadow: This seems like a reasonable alternate way of accomplishing it: https://stackoverflow.com/a/55247459/3476387
albertony: The rename trick works great, and it is also possible to tell Windows to automatically delete it on next startup, simply by adding the path into registry value HKLM\System\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations. Not that it is very important in this case, but just an idea..
ivandeex: I would like to have the first released version of the command only the required features, have it out as fast as possible and leave optional features for later enhancements.
I think we can postpone registry tricks or leave it for chocolatey installer. If someone installed a choco package, I'd recommend to update it using choco too, `cinst -y rclone` without mixing package with self-update strategy.
@albertony 
ivandeex: I have just answered a question https://github.com/rclone/rclone/issues/3545#issuecomment-789415244 and noticed that rclone for linux has two distinct flavors.
The one distributed in `zip` is a static binary that doesn't support `cmount`, the other in `deb` or `rpm` does have it. By the way, I think the `version` command should be extended to print current flavor.

Current draft of `selfupdate` supports only `zip`, consequently it will replace `libfuse`-aware binary and probably break tooling.

This problem manifests iself only on Linux. Windows and OSX distributions are always count-aware. Plan9 and other exotics never have it.

I will now add a detection code that will _refuse to update_ if rclone runs on Linux has libfuse/cmount support and updates its own running executable. I admit, it's a limitation, but we can solve it later.

Another solution is to extract single dynamic binary from distribution `.deb`. I believe that `.rpm` contains the same single dynamic binary, no `.so` files. AFAIK deb packages contain a plain old `ar` combined with _gnu-tar_ and _gzip_. This can be handled with golang.

Other solutions will take time, for example: make CI/CD produce a separate zip compiled with `cmount` tag (this needs a decision from @ncw), detect linux distribution (it's fragile and needs a lit of testing), run apt/rpm/yum/whatever as root, and so long.

@ncw Sorry for bothering, I would like to have your opinion
albertony: > I think we can postpone registry tricks

Sure. BTW: It's exposed as an Windows API function, [MoveFileEx](https://docs.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-movefileexa) with flag `MOVEFILE_DELAY_UNTIL_REBOOT`, so no direct registry access necessary - if it is easy to call the api from golang (or there is a library wrapping that function, could not find any with a quick search).

> I would like to have the first released version of the command have only the required features, release it as fast as possible and leave optional features for later enhancements.

I agree. Have not really decided myself what I would want (delete rclone.exe.old on boot or not), but just wanted to note it in case it could turn up useful..

Great work btw!
ivandeex: > Have not really decided myself what I would want (delete rclone.exe.old on boot or not), but just wanted to note...

A feature request ticket is something you can close when unsure or spam with `@youlazybastard love to see this` when...
ivandeex: Based on the discussion in https://forum.rclone.org/t/rclone-cmount-on-linux/22602 the official build of rclone for Linux does not support `cmount` and runtime detection of such features will need tweaks in the build process, so it's out of scope for this PR. I will address it in a later patch.
ivandeex: ### Implementation details:
- The command was named `selfupdate` (no dash) to be in line with other rclone commands, but added the `self-update` alias (with a dash) to aid restic users
- Included all features from https://github.com/rclone/rclone/pull/5080#issue-581944497: hash validation, proactive permission checks, `--output` flag for custom executable locations, `--check` flag as an alias for `rclone version --check`
- Included `--stable` flag (it is implied so in fact does nothing) from https://github.com/rclone/rclone/issues/5076#issuecomment-787441976
- Included `--beta` flag (mutually exclusive with `--stable`) from https://github.com/rclone/rclone/issues/5076#issuecomment-787441976
- Implemented `--version` flag proposed by @darthShadow  in https://github.com/rclone/rclone/issues/5076#issuecomment-787441976
- Implemented Windows trick that keeps old executable renamed with `.old` suffix (recommended by @darthShadow in https://github.com/rclone/rclone/pull/5080#issuecomment-788016526)
- Excluded _pending delete after boot_ feature proposed by @albertony in https://github.com/rclone/rclone/pull/5080#issuecomment-788902774 and https://github.com/rclone/rclone/pull/5080#issuecomment-789514033
- Added reasonable unit tests for Unix and Windows behavior (yet disabled it for `go test -short` to reduce load on rclone.org somewhat)
- Postponed the `dynamic` vs `static` check until later as explained in https://github.com/rclone/rclone/pull/5080#issuecomment-789592959
- Added the message `You could use 'rclone self-update' to get latest features.` in case of unknown subcommand (proposed by https://github.com/rclone/rclone/issues/5074 and [on the forum](https://forum.rclone.org/t/previously-working-rclone-backup-now-giving-error-unknown-command-rcat-for-rclone/22507/14))
- Postponed updating github issue templates until the next release. Unlike the item above they would become effective right after merge into master
- Included quick fix from https://github.com/rclone/rclone/pull/5093 as otherwise PR tests fail

All unit tests pass for me

@ncw 
Please review

@albertony @darthShadow 
Please take a look and give it a try
albertony: `.\rclone.exe selfupdate --stable --version 1.53`
Fails:
> Error: failed to update rclone: failed with 404 Not Found downloading https://downloads.rclone.org/1.53/rclone-1.53-windows-amd64.zip

`.\rclone.exe selfupdate --stable --version 1.53.0`
Fails:
>  Error: failed to update rclone: failed with 404 Not Found downloading https://downloads.rclone.org/1.53.0/rclone-1.53.0-windows-amd64.zip

`.\rclone.exe selfupdate --stable --version v1.53`
Fails:
> Error: failed to update rclone: failed with 404 Not Found downloading https://downloads.rclone.org/v1.53/rclone-v1.53-windows-amd64.zip

`.\rclone.exe selfupdate --stable --version v1.53.0`
Works!
albertony: Finished my testing for now. OS: windows/amd64. Self-compiled from your code, with go1.16.
Normal usage works fine! :clap:
Seems the `--version` option needs a fix as commented above, and I have one comment/question regarding the code for removal of rclone.exe.old.

ivandeex: > Seems the `--version` option needs a fix as commented above
Do you mean automatically prepend `v` as a convenience?
Ok, will do 👍 
I thought that `--version` should be literal and explicit by design.
albertony: > I thought that `--version` should be literal and explicit by design.

Ah, ok. I assumed it was suppose to work as in the following examples: https://github.com/rclone/rclone/issues/5076#issuecomment-787441976

Still think it would be nice to be a bit "lenient" on the version strings, although main point of this feature is for people to update to latest version so the `--version` option kind of defeats the purpose anyway.. :)

> Will prepend v to 1.53.0 if not found at start.

Good.

> But will not append .0 to e.g. 1.53 as beta versions have different structure.

That's ok.
ivandeex: @darthShadow 
Implementing proposed conversions from your table
`--stable --version 1.53` --> `v1.53.4`
`--stable --version 1.54` --> `v1.54.0`
requires some `index.html` parsing
(or trying micro versions sequentially creating extra load on download site)
It's more convenient for user but more fragile.
Do you think we should go for it?
I can do it but need a confirmation as I'm in doubt
albertony: > --stable --version 1.53 --> v1.53.4
> --stable --version 1.54 --> v1.54.0

An easy approach would be just appending ".0" when fewer than 3 part version numbers. That is not something that will surprise users, I think. Official releases seems always use 3 version numbers, and beta versions adds a suffix, so that should be safe. Resulting:

`--stable --version 1.53 --> v1.53.0`
`--stable --version 1.54 --> v1.54.0`
albertony: Just wanted to add, that instead of index.html parsing one could use the GitHub API to get the release information as json: https://api.github.com/repos/rclone/rclone/releases. Not saying its something that must be considered now.
ivandeex: > instead of index.html parsing one could use the GitHub API

json parsing vs html parsing - no problem (unless it requires access tokens)
accessing two sites (github.com) to achieve single download - less robust. 
i'd stick with index.html
ivandeex: I finally see that `index.html` from https://downloads.rclone.org can be parsed by a really simple regex. Will do what drShadow ordered.
ivandeex: @ncw
> The solution here would be to make the save file `rclone.RANDOMNUMBER.old.exe`.
> This might be a good idea for the `.new` file also - this will help with races in the very unlikely case that the user runs `self-update` twice.
> You can use `random.String()` to make random suffixes.

I'd like to enforce some `output results stability` if possible. It will greatly simplify shell/cron wrappers around `selfupdate`.
I will be default use random suffix with `.new` files because they are never retained, neither on success, nor on failure.
I will try `.old` without random suffix first, and add random suffix **only** if I detect race.
ivandeex: @ncw 
Note: as soon as your PGP key gets released in the wild embedded in the next rclone release, it's not secret anymore.
ncw: > I finally see that `index.html` from https://downloads.rclone.org can be parsed by a really simple regex. Will do what drShadow ordered.

It can actually be parsed by the http backend too...

> I'd like to enforce some `output results stability` if possible. It will greatly simplify shell/cron wrappers around `selfupdate`.
> I will by default use random suffix with `.new` files because they are never retained, neither on success, nor on failure.
> I will try `.old` without random suffix first, and add random suffix **only** if I detect race.

OK sounds perfect

> Note: as soon as your PGP key gets released in the wild embedded in the next rclone release, it's not secret anymore.

Only the public part needs to be in the binary, eg [restic verify.go](https://github.com/restic/restic/blob/master/internal/selfupdate/verify.go)





ivandeex: ### Summary of new changes after 1st review:

- using `fshttp.Client` now [ncw]
- replace GOOS `darwin` with `osx` in archive names [ncw]
- use `fstest/testy.SkipUnreliable` to run in integration suite only [ncw]
- removed unneeded `os.Remove(savedFile)` [albertony]
- prepend `v` to incomplete semantic `--version` [darthShadow]
- extract last micro release from `index.html` to complete version number [darthShadow]
- random suffix for `.new` files
- `.old` file by default is `rclone.old.exe`
- random old suffix `rclone.xxxx.old.exe` if a race was detected
- install `.deb`/`.rpm` by flag `--package deb|rpm` (default: zip) [ncw]
- preliminary pgp check (still waiting for a modern public pgp key) [ncw]

@ncw please have it reviewed
ivandeex: TODO will create an RFC issue for the [MoveFileEx](https://docs.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-movefileexa) aka _MOVEFILE_DELAY_UNTIL_REBOOT_ feature requested in https://github.com/rclone/rclone/pull/5080#issuecomment-789514033 when we merge this pull.
ncw: > ### Summary of new changes after 1st review:
[snip]

Great changes :-) I'll run through a code review now.

> Could you provide a valid ([#5080 (comment)](https://github.com/rclone/rclone/pull/5080#discussion_r589033247)) public key ([#5080 (comment)](https://github.com/rclone/rclone/pull/5080#discussion_r589226236)) ?

I think this should do it

```
$ gpg --armor --export-options export-minimal --export nick@craig-wood.com
```

Which produced

```
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGiBDuy3V0RBADVQOAF5aFiCxD3t2h6iAF2WMiaMlgZ6kX2i/u7addNkzX71VU9
7NpI0SnsP5YWt+gEedST6OmFbtLfZWCR4KWn5XnNdjCMNhxaH6WccVqNm4ALPIqT
59uVjkgf8RISmmoNJ1d+2wMWjQTUfwOEmoIgH6n+2MYNUKuctBrwAACflwCg1I1Q
O/prv/5hczdpQCs+fL87DxsD/Rt7pIXvsIOZyQWbIhSvNpGalJuMkW5Jx92UjsE9
1Ipo3Xr6SGRPgW9+NxAZAsiZfCX/19knAyNrN9blwL0rcPDnkhdGwK69kfjF+wq+
QbogRGodbKhqY4v+cMNkKiemBuTQiWPkpKjifwNsD1fNjNKfDP3pJ64Yz7a4fuzV
X1YwBACpKVuEen34lmcX6ziY4jq8rKibKBs4JjQCRO24kYoHDULVe+RS9krQWY5b
e0foDhru4dsKccefK099G+WEzKVCKxupstWkTT/iJwajR8mIqd4AhD0wO9W3MCfV
Ov8ykMDZ7qBWk1DHc87Ep3W1o8t8wq74ifV+HjhhWg8QAylXg7QlTmljayBDcmFp
Zy1Xb29kIDxuaWNrQGNyYWlnLXdvb2QuY29tPohxBBMRCAAxBQsHCgMEAxUDAgMW
AgECF4AWIQT79zfs6firGGBL0qyTk14C/ztU+gUCXjg2UgIZAQAKCRCTk14C/ztU
+lmmAJ4jH5FyULzStjisuTvHLTVz6G44eQCfaR5QGZFPseenE5ic2WeQcBcmtoG5
Ag0EO7LdgRAIAI6QdFBg3/xa1gFKPYy1ihV9eSdGqwWZGJvokWsfCvHy5180tj/v
UNOLAJrdqglMSvevNTXe8bT65D6423AAsLhch9wq/aNqrHolTYABzxRigjcS1//T
yln5naGUzlVQXDVfrDk3Md/NrkdOFj7r/YyMF0+iWwpFz2qAjL95i5wfVZ1kWGrT
2AmivE1wD1sWT/Ja3FDI0NRkU0Nbz/a0TKe4ml8iLVtZXpTRbxxCCPdkHXXgSyu1
eZ4NrF/wTJuvwGn12TJ1EF95aVkHxAUw0+KmLGdcyBG+IKuHamrsjWIAXGXV///K
AxPgUthccQ03HMjltFsrdmen5Q034YM3eOsAAwUH/jAKiIAA8LpZmZPnt9GZ4+Ol
Zp22VAfyfDOFl4Ol+cWjkLAgjAFsm5gnOKcRSE/9XPxnQqkhw7+ZygYuUMgTDJ99
/5IM1UQL3ooS+oFrDaE99S8bLeOe17skcdXcA/K83VqD9m93rQRnbtD+75zqKkZn
9WNFyKCXg5P6PFPdNYRtlQKOcwFR9mHRLUmapQSAM8Y2pCgALZ7GViKQca8/TT1T
gZk9fJMZYGez+IlOPxTJxjn80+vywk4/wdIWSiQj+8u5RzT9sjmm77wbMVNGRqYd
W/EemW9Zz9vi0CIvJGgbPMqcuxw8e/5lnuQ6Mi3uDR0P2RNIAhFrdZpVSME8xQaI
RgQYEQIABgUCO7LdgQAKCRCTk14C/ztU+mLBAKC2cdFy7eLaQAvyzcE2VK6HVIjn
JACguA00bxLQuJ4+RCJrLFZP8ZlN2sc=
=TtR5
-----END PGP PUBLIC KEY BLOCK-----
```

I've attached a zip with the ascii version and the binary version too just in case you want the binary version.

[ncw-pgp-keys.zip](https://github.com/rclone/rclone/files/6108210/ncw-pgp-keys.zip)


ivandeex: PGP signature validation implemented. Waiting for replies to https://github.com/rclone/rclone/pull/5080#discussion_r590341930
Proceeding to `rclone bisync`.

Patch:
diff --git a/.gitignore b/.gitignore
index 9a877432c..b20174667 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 *~
 _junk/
 rclone
+rclone.exe
 build
 docs/public
 rclone.iml
diff --git a/Makefile b/Makefile
index 0a7b70c50..0aa937c62 100644
--- a/Makefile
+++ b/Makefile
@@ -119,7 +119,7 @@ doc:	rclone.1 MANUAL.html MANUAL.txt rcdocs commanddocs
 rclone.1:	MANUAL.md
 	pandoc -s --from markdown-smart --to man MANUAL.md -o rclone.1
 
-MANUAL.md:	bin/make_manual.py docs/content/*.md commanddocs backenddocs
+MANUAL.md:	bin/make_manual.py docs/content/*.md commanddocs backenddocs rcdocs
 	./bin/make_manual.py
 
 MANUAL.html:	MANUAL.md
diff --git a/backend/drive/drive.go b/backend/drive/drive.go
index db30d38a1..f1d925d20 100755
--- a/backend/drive/drive.go
+++ b/backend/drive/drive.go
@@ -590,13 +590,13 @@ type Fs struct {
 }
 
 type baseObject struct {
-	fs           *Fs    // what this object is part of
-	remote       string // The remote path
-	id           string // Drive Id of this object
-	modifiedDate string // RFC3339 time it was last modified
-	mimeType     string // The object MIME type
-	bytes        int64  // size of the object
-	parents      int    // number of parents
+	fs           *Fs      // what this object is part of
+	remote       string   // The remote path
+	id           string   // Drive Id of this object
+	modifiedDate string   // RFC3339 time it was last modified
+	mimeType     string   // The object MIME type
+	bytes        int64    // size of the object
+	parents      []string // IDs of the parent directories
 }
 type documentObject struct {
 	baseObject
@@ -1236,7 +1236,7 @@ func (f *Fs) newBaseObject(remote string, info *drive.File) baseObject {
 		modifiedDate: modifiedDate,
 		mimeType:     info.MimeType,
 		bytes:        size,
-		parents:      len(info.Parents),
+		parents:      info.Parents,
 	}
 }
 
@@ -2019,6 +2019,9 @@ func (f *Fs) itemToDirEntry(remote string, item *drive.File) (entry fs.DirEntry,
 		f.dirCache.Put(remote, item.Id)
 		when, _ := time.Parse(timeFormatIn, item.ModifiedTime)
 		d := fs.NewDir(remote, when).SetID(item.Id)
+		if len(item.Parents) > 0 {
+			d.SetParentID(item.Parents[0])
+		}
 		return d, nil
 	case f.opt.AuthOwnerOnly && !isAuthOwned(item):
 		// ignore object
@@ -3722,7 +3725,7 @@ func (o *linkObject) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo
 
 // Remove an object
 func (o *baseObject) Remove(ctx context.Context) error {
-	if o.parents > 1 {
+	if len(o.parents) > 1 {
 		return errors.New("can't delete safely - has multiple parents")
 	}
 	return o.fs.delete(ctx, shortcutID(o.id), o.fs.opt.UseTrash)
@@ -3738,6 +3741,14 @@ func (o *baseObject) ID() string {
 	return o.id
 }
 
+// ParentID returns the ID of the Object parent if known, or "" if not
+func (o *baseObject) ParentID() string {
+	if len(o.parents) > 0 {
+		return o.parents[0]
+	}
+	return ""
+}
+
 func (o *documentObject) ext() string {
 	return o.baseObject.remote[len(o.baseObject.remote)-o.extLen:]
 }
@@ -3798,10 +3809,13 @@ var (
 	_ fs.Object          = (*Object)(nil)
 	_ fs.MimeTyper       = (*Object)(nil)
 	_ fs.IDer            = (*Object)(nil)
+	_ fs.ParentIDer      = (*Object)(nil)
 	_ fs.Object          = (*documentObject)(nil)
 	_ fs.MimeTyper       = (*documentObject)(nil)
 	_ fs.IDer            = (*documentObject)(nil)
+	_ fs.ParentIDer      = (*documentObject)(nil)
 	_ fs.Object          = (*linkObject)(nil)
 	_ fs.MimeTyper       = (*linkObject)(nil)
 	_ fs.IDer            = (*linkObject)(nil)
+	_ fs.ParentIDer      = (*linkObject)(nil)
 )
diff --git a/backend/fichier/fichier.go b/backend/fichier/fichier.go
index 7b722b34a..4dcef4eee 100644
--- a/backend/fichier/fichier.go
+++ b/backend/fichier/fichier.go
@@ -483,11 +483,21 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	return dstObj, nil
 }
 
+// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
+func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
+	o, err := f.NewObject(ctx, remote)
+	if err != nil {
+		return "", err
+	}
+	return o.(*Object).file.URL, nil
+}
+
 // Check the interfaces are satisfied
 var (
 	_ fs.Fs              = (*Fs)(nil)
 	_ fs.Mover           = (*Fs)(nil)
 	_ fs.Copier          = (*Fs)(nil)
+	_ fs.PublicLinker    = (*Fs)(nil)
 	_ fs.PutUncheckeder  = (*Fs)(nil)
 	_ dircache.DirCacher = (*Fs)(nil)
 )
diff --git a/backend/opendrive/opendrive.go b/backend/opendrive/opendrive.go
index 43352cde0..611cb005c 100644
--- a/backend/opendrive/opendrive.go
+++ b/backend/opendrive/opendrive.go
@@ -119,6 +119,7 @@ type Object struct {
 	fs      *Fs       // what this object is part of
 	remote  string    // The remote path
 	id      string    // ID of the file
+	parent  string    // ID of the parent directory
 	modTime time.Time // The modified time of the object if known
 	md5     string    // MD5 hash if known
 	size    int64     // Size of the object
@@ -233,7 +234,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 			// No root so return old f
 			return f, nil
 		}
-		_, err := tempF.newObjectWithInfo(ctx, remote, nil)
+		_, err := tempF.newObjectWithInfo(ctx, remote, nil, "")
 		if err != nil {
 			if err == fs.ErrorObjectNotFound {
 				// File doesn't exist so return old f
@@ -517,7 +518,7 @@ func (f *Fs) Purge(ctx context.Context, dir string) error {
 // Return an Object from a path
 //
 // If it can't be found it returns the error fs.ErrorObjectNotFound.
-func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File) (fs.Object, error) {
+func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File, parent string) (fs.Object, error) {
 	// fs.Debugf(nil, "newObjectWithInfo(%s, %v)", remote, file)
 
 	var o *Object
@@ -526,6 +527,7 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File) (
 			fs:      f,
 			remote:  remote,
 			id:      file.FileID,
+			parent:  parent,
 			modTime: time.Unix(file.DateModified, 0),
 			size:    file.Size,
 			md5:     file.FileHash,
@@ -548,7 +550,7 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, file *File) (
 // it returns the error fs.ErrorObjectNotFound.
 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
 	// fs.Debugf(nil, "NewObject(\"%s\")", remote)
-	return f.newObjectWithInfo(ctx, remote, nil)
+	return f.newObjectWithInfo(ctx, remote, nil, "")
 }
 
 // Creates from the parameters passed in a half finished Object which
@@ -768,6 +770,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
 		f.dirCache.Put(remote, folder.FolderID)
 		d := fs.NewDir(remote, time.Unix(folder.DateModified, 0)).SetID(folder.FolderID)
 		d.SetItems(int64(folder.ChildFolders))
+		d.SetParentID(directoryID)
 		entries = append(entries, d)
 	}
 
@@ -775,7 +778,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
 		file.Name = f.opt.Enc.ToStandardName(file.Name)
 		// fs.Debugf(nil, "File: %s (%s)", file.Name, file.FileID)
 		remote := path.Join(dir, file.Name)
-		o, err := f.newObjectWithInfo(ctx, remote, &file)
+		o, err := f.newObjectWithInfo(ctx, remote, &file, directoryID)
 		if err != nil {
 			return nil, err
 		}
@@ -1053,6 +1056,11 @@ func (o *Object) ID() string {
 	return o.id
 }
 
+// ParentID returns the ID of the Object parent directory if known, or "" if not
+func (o *Object) ParentID() string {
+	return o.parent
+}
+
 // Check the interfaces are satisfied
 var (
 	_ fs.Fs              = (*Fs)(nil)
@@ -1063,4 +1071,5 @@ var (
 	_ fs.DirCacheFlusher = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.IDer            = (*Object)(nil)
+	_ fs.ParentIDer      = (*Object)(nil)
 )
diff --git a/cmd/all/all.go b/cmd/all/all.go
index 3c899fa56..3d3f09b3f 100644
--- a/cmd/all/all.go
+++ b/cmd/all/all.go
@@ -47,6 +47,7 @@ import (
 	_ "github.com/rclone/rclone/cmd/reveal"
 	_ "github.com/rclone/rclone/cmd/rmdir"
 	_ "github.com/rclone/rclone/cmd/rmdirs"
+	_ "github.com/rclone/rclone/cmd/selfupdate"
 	_ "github.com/rclone/rclone/cmd/serve"
 	_ "github.com/rclone/rclone/cmd/settier"
 	_ "github.com/rclone/rclone/cmd/sha1sum"
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 9ce1acd15..3192086d6 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -25,6 +25,7 @@ import (
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/accounting"
 	"github.com/rclone/rclone/fs/cache"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/config/configflags"
 	"github.com/rclone/rclone/fs/config/flags"
 	"github.com/rclone/rclone/fs/filter"
@@ -382,6 +383,12 @@ func initConfig() {
 	// Finish parsing any command line flags
 	configflags.SetFlags(ci)
 
+	// Load the config
+	configfile.LoadConfig(ctx)
+
+	// Start accounting
+	accounting.Start(ctx)
+
 	// Hide console window
 	if ci.NoConsole {
 		terminal.HideConsole()
@@ -541,6 +548,9 @@ func Main() {
 	setupRootCommand(Root)
 	AddBackendFlags()
 	if err := Root.Execute(); err != nil {
+		if strings.HasPrefix(err.Error(), "unknown command") {
+			Root.PrintErrf("You could use '%s selfupdate' to get latest features.\n\n", Root.CommandPath())
+		}
 		log.Fatalf("Fatal error: %v", err)
 	}
 }
diff --git a/cmd/listremotes/listremotes.go b/cmd/listremotes/listremotes.go
index e41bce10f..40b4ea12d 100644
--- a/cmd/listremotes/listremotes.go
+++ b/cmd/listremotes/listremotes.go
@@ -41,7 +41,7 @@ When uses with the -l flag it lists the types too.
 		}
 		for _, remote := range remotes {
 			if listLong {
-				remoteType := config.FileGet(remote, "type", "UNKNOWN")
+				remoteType := config.FileGet(remote, "type")
 				fmt.Printf("%-*s %s\n", maxlen+1, remote+":", remoteType)
 			} else {
 				fmt.Printf("%s:\n", remote)
diff --git a/cmd/rcd/rcd.go b/cmd/rcd/rcd.go
index c12efd9e9..cc4be57f9 100644
--- a/cmd/rcd/rcd.go
+++ b/cmd/rcd/rcd.go
@@ -3,10 +3,13 @@ package rcd
 import (
 	"context"
 	"log"
+	"sync"
 
+	sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify"
 	"github.com/rclone/rclone/cmd"
 	"github.com/rclone/rclone/fs/rc/rcflags"
 	"github.com/rclone/rclone/fs/rc/rcserver"
+	"github.com/rclone/rclone/lib/atexit"
 	"github.com/spf13/cobra"
 )
 
@@ -48,6 +51,22 @@ See the [rc documentation](/rc/) for more info on the rc flags.
 			log.Fatal("rc server not configured")
 		}
 
+		// Notify stopping on exit
+		var finaliseOnce sync.Once
+		finalise := func() {
+			finaliseOnce.Do(func() {
+				_ = sysdnotify.Stopping()
+			})
+		}
+		fnHandle := atexit.Register(finalise)
+		defer atexit.Unregister(fnHandle)
+
+		// Notify ready to systemd
+		if err := sysdnotify.Ready(); err != nil {
+			log.Fatalf("failed to notify ready to systemd: %v", err)
+		}
+
 		s.Wait()
+		finalise()
 	},
 }
diff --git a/cmd/selfupdate/help.go b/cmd/selfupdate/help.go
new file mode 100644
index 000000000..0ec55cbf7
--- /dev/null
+++ b/cmd/selfupdate/help.go
@@ -0,0 +1,22 @@
+package selfupdate
+
+// Note: "|" will be replaced by backticks in the help string below
+var selfUpdateHelp string = `
+This command downloads the latest release of rclone and replaces
+the currently running binary. The download is verified with a hashsum
+and cryptographically signed signature.
+
+The |--version VER| flag, if given, will update to a concrete version
+instead of the latest one. If you omit micro version from |VER| (for
+example |1.53|), the latest matching micro version will be used.
+
+If you previously installed rclone via a package manager, the package may
+include local documentation or configure services. You may wish to update
+with the flag |--package deb| or |--package rpm| (whichever is correct for
+your OS) to update these too. This command with the default |--package zip|
+will update only the rclone executable so the local manual may become
+inaccurate after it.
+
+Note: Windows forbids deletion of a currently running executable so this
+command will rename the old executable to 'rclone.old.exe' upon success.
+`
diff --git a/cmd/selfupdate/selfupdate.go b/cmd/selfupdate/selfupdate.go
new file mode 100644
index 000000000..5ecadad4c
--- /dev/null
+++ b/cmd/selfupdate/selfupdate.go
@@ -0,0 +1,474 @@
+package selfupdate
+
+import (
+	"archive/zip"
+	"bufio"
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strings"
+
+	"github.com/pkg/errors"
+	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config/flags"
+	"github.com/rclone/rclone/fs/fshttp"
+	"github.com/rclone/rclone/lib/random"
+	"github.com/spf13/cobra"
+
+	versionCmd "github.com/rclone/rclone/cmd/version"
+)
+
+// Options contains options for the self-update command
+type Options struct {
+	Check   bool
+	Output  string // output path
+	Beta    bool   // mutually exclusive with Stable (false means "stable")
+	Stable  bool   // mutually exclusive with Beta
+	Version string
+	Package string // package format: zip, deb, rpm (empty string means "zip")
+}
+
+// Opt is options set via command line
+var Opt = Options{}
+
+func init() {
+	cmd.Root.AddCommand(cmdSelfUpdate)
+	cmdFlags := cmdSelfUpdate.Flags()
+	flags.BoolVarP(cmdFlags, &Opt.Check, "check", "", Opt.Check, "Check for latest release, do not download.")
+	flags.StringVarP(cmdFlags, &Opt.Output, "output", "", Opt.Output, "Save the downloaded binary at a given path (default: replace running binary)")
+	flags.BoolVarP(cmdFlags, &Opt.Stable, "stable", "", Opt.Stable, "Install stable release (this is the default)")
+	flags.BoolVarP(cmdFlags, &Opt.Beta, "beta", "", Opt.Beta, "Install beta release.")
+	flags.StringVarP(cmdFlags, &Opt.Version, "version", "", Opt.Version, "Install the given rclone version (default: latest)")
+	flags.StringVarP(cmdFlags, &Opt.Package, "package", "", Opt.Package, "Package format: zip|deb|rpm (default: zip)")
+}
+
+var cmdSelfUpdate = &cobra.Command{
+	Use:     "selfupdate",
+	Aliases: []string{"self-update"},
+	Short:   `Update the rclone binary.`,
+	Long:    strings.ReplaceAll(selfUpdateHelp, "|", "`"),
+	Run: func(command *cobra.Command, args []string) {
+		cmd.CheckArgs(0, 0, command, args)
+		if Opt.Package == "" {
+			Opt.Package = "zip"
+		}
+		gotActionFlags := Opt.Stable || Opt.Beta || Opt.Output != "" || Opt.Version != "" || Opt.Package != "zip"
+		if Opt.Check && !gotActionFlags {
+			versionCmd.CheckVersion()
+			return
+		}
+		if Opt.Package != "zip" {
+			if Opt.Package != "deb" && Opt.Package != "rpm" {
+				log.Fatalf("--package should be one of zip|deb|rpm")
+			}
+			if runtime.GOOS != "linux" {
+				log.Fatalf(".deb and .rpm packages are supported only on Linux")
+			} else if os.Geteuid() != 0 && !Opt.Check {
+				log.Fatalf(".deb and .rpm must be installed by root")
+			}
+			if Opt.Output != "" && !Opt.Check {
+				fmt.Println("Warning: --output is ignored with --package deb|rpm")
+			}
+		}
+		if err := InstallUpdate(context.Background(), &Opt); err != nil {
+			log.Fatalf("Error: %v", err)
+		}
+	},
+}
+
+// GetVersion can get the latest release number from the download site
+// or massage a stable release number - prepend semantic "v" prefix
+// or find the latest micro release for a given major.minor release.
+// Note: this will not be applied to beta releases.
+func GetVersion(ctx context.Context, beta bool, version string) (newVersion, siteURL string, err error) {
+	siteURL = "https://downloads.rclone.org"
+	if beta {
+		siteURL = "https://beta.rclone.org"
+	}
+
+	if version == "" {
+		// Request the latest release number from the download site
+		_, newVersion, _, err = versionCmd.GetVersion(siteURL + "/version.txt")
+		return
+	}
+
+	newVersion = version
+	if version[0] != 'v' {
+		newVersion = "v" + version
+	}
+	if beta {
+		return
+	}
+
+	if valid, _ := regexp.MatchString(`^v\d+\.\d+(\.\d+)?$`, newVersion); !valid {
+		return "", siteURL, errors.New("invalid semantic version")
+	}
+
+	// Find the latest stable micro release
+	if strings.Count(newVersion, ".") == 1 {
+		html, err := downloadFile(ctx, siteURL)
+		if err != nil {
+			return "", siteURL, errors.Wrap(err, "failed to get list of releases")
+		}
+		reSubver := fmt.Sprintf(`href="\./%s\.\d+/"`, regexp.QuoteMeta(newVersion))
+		allSubvers := regexp.MustCompile(reSubver).FindAllString(string(html), -1)
+		if allSubvers == nil {
+			return "", siteURL, errors.New("could not find the minor release")
+		}
+		// Use the fact that releases in the index are sorted by date
+		lastSubver := allSubvers[len(allSubvers)-1]
+		newVersion = lastSubver[8 : len(lastSubver)-2]
+	}
+	return
+}
+
+// InstallUpdate performs rclone self-update
+func InstallUpdate(ctx context.Context, opt *Options) error {
+	// Find the latest release number
+	if opt.Stable && opt.Beta {
+		return errors.New("--stable and --beta are mutually exclusive")
+	}
+
+	newVersion, siteURL, err := GetVersion(ctx, opt.Beta, opt.Version)
+	if err != nil {
+		return errors.Wrap(err, "unable to detect new version")
+	}
+
+	if newVersion == "" {
+		var err error
+		_, newVersion, _, err = versionCmd.GetVersion(siteURL + "/version.txt")
+		if err != nil {
+			return errors.Wrap(err, "unable to detect new version")
+		}
+	}
+
+	if newVersion == fs.Version {
+		fmt.Println("rclone is up to date")
+		return nil
+	}
+
+	// Install .deb/.rpm package if requested by user
+	if opt.Package == "deb" || opt.Package == "rpm" {
+		if opt.Check {
+			fmt.Println("Warning: --package flag is ignored in --check mode")
+		} else {
+			err := installPackage(ctx, opt.Beta, newVersion, siteURL, opt.Package)
+			if err == nil {
+				fmt.Printf("Successfully updated rclone package to version %s\n", newVersion)
+			}
+			return err
+		}
+	}
+
+	// Get the current executable path
+	executable, err := os.Executable()
+	if err != nil {
+		return errors.Wrap(err, "unable to find executable")
+	}
+
+	targetFile := opt.Output
+	if targetFile == "" {
+		targetFile = executable
+	}
+
+	if opt.Check {
+		fmt.Printf("Without --check this would install rclone version %s at %s\n", newVersion, targetFile)
+		return nil
+	}
+
+	// Make temporary file names and check for possible access errors in advance
+	var newFile string
+	if newFile, err = makeRandomExeName(targetFile, "new"); err != nil {
+		return err
+	}
+	savedFile := ""
+	if runtime.GOOS == "windows" {
+		savedFile = targetFile
+		if strings.HasSuffix(savedFile, ".exe") {
+			savedFile = savedFile[:len(savedFile)-4]
+		}
+		savedFile += ".old.exe"
+	}
+
+	if savedFile == executable || newFile == executable {
+		return fmt.Errorf("%s: a temporary file would overwrite the executable, specify a different --output path", targetFile)
+	}
+
+	if err := verifyAccess(targetFile); err != nil {
+		return err
+	}
+
+	// Download the update as a temporary file
+	err = downloadUpdate(ctx, opt.Beta, newVersion, siteURL, newFile, "zip")
+	if err != nil {
+		return errors.Wrap(err, "failed to update rclone")
+	}
+
+	err = replaceExecutable(targetFile, newFile, savedFile)
+	if err == nil {
+		fmt.Printf("Successfully updated rclone to version %s\n", newVersion)
+	}
+	return err
+}
+
+func installPackage(ctx context.Context, beta bool, version, siteURL, packageFormat string) error {
+	tempFile, err := ioutil.TempFile("", "rclone.*."+packageFormat)
+	if err != nil {
+		return errors.Wrap(err, "unable to write temporary package")
+	}
+	packageFile := tempFile.Name()
+	_ = tempFile.Close()
+	defer func() {
+		if rmErr := os.Remove(packageFile); rmErr != nil {
+			fs.Errorf(nil, "%s: could not remove temporary package: %v", packageFile, rmErr)
+		}
+	}()
+	if err := downloadUpdate(ctx, beta, version, siteURL, packageFile, packageFormat); err != nil {
+		return err
+	}
+
+	packageCommand := "dpkg"
+	if packageFormat == "rpm" {
+		packageCommand = "rpm"
+	}
+	cmd := exec.Command(packageCommand, "-i", packageFile)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to run %s: %v", packageCommand, err)
+	}
+	return nil
+}
+
+func replaceExecutable(targetFile, newFile, savedFile string) error {
+	// Copy permission bits from the old executable
+	// (it was extracted with mode 0755)
+	fileInfo, err := os.Lstat(targetFile)
+	if err == nil {
+		if err = os.Chmod(newFile, fileInfo.Mode()); err != nil {
+			return errors.Wrap(err, "failed to set permission")
+		}
+	}
+
+	if err = os.Remove(targetFile); os.IsNotExist(err) {
+		err = nil
+	}
+
+	if err != nil && savedFile != "" {
+		// Windows forbids removal of a running executable so we rename it.
+		// For starters, rename download as the original file with ".old.exe" appended.
+		var saveErr error
+		if saveErr = os.Remove(savedFile); os.IsNotExist(saveErr) {
+			saveErr = nil
+		}
+		if saveErr == nil {
+			saveErr = os.Rename(targetFile, savedFile)
+		}
+		if saveErr != nil {
+			// The ".old" file cannot be removed or cannot be renamed to.
+			// This usually means that the running executable has a name with ".old".
+			// This can happen in very rare cases, but we ought to handle it.
+			// Try inserting a randomness in the name to mitigate it.
+			fs.Debugf(nil, "%s: cannot replace old file, randomizing name", savedFile)
+
+			savedFile, saveErr = makeRandomExeName(targetFile, "old")
+			if saveErr == nil {
+				if saveErr = os.Remove(savedFile); os.IsNotExist(saveErr) {
+					saveErr = nil
+				}
+			}
+			if saveErr == nil {
+				saveErr = os.Rename(targetFile, savedFile)
+			}
+		}
+		if saveErr == nil {
+			fmt.Printf("The old executable was saved as %s\n", savedFile)
+			err = nil
+		}
+	}
+
+	if err == nil {
+		err = os.Rename(newFile, targetFile)
+	}
+	if err != nil {
+		if rmErr := os.Remove(newFile); rmErr != nil {
+			fs.Errorf(nil, "%s: could not remove temporary file: %v", newFile, rmErr)
+		}
+		return err
+	}
+	return nil
+}
+
+func makeRandomExeName(baseName, extension string) (string, error) {
+	const maxAttempts = 5
+
+	if runtime.GOOS == "windows" {
+		if strings.HasSuffix(baseName, ".exe") {
+			baseName = baseName[:len(baseName)-4]
+		}
+		extension += ".exe"
+	}
+
+	for attempt := 0; attempt < maxAttempts; attempt++ {
+		filename := fmt.Sprintf("%s.%s.%s", baseName, random.String(4), extension)
+		if _, err := os.Stat(filename); os.IsNotExist(err) {
+			return filename, nil
+		}
+	}
+
+	return "", fmt.Errorf("cannot find a file name like %s.xxxx.%s", baseName, extension)
+}
+
+func downloadUpdate(ctx context.Context, beta bool, version, siteURL, newFile, packageFormat string) error {
+	osName := runtime.GOOS
+	arch := runtime.GOARCH
+	if arch == "darwin" {
+		arch = "osx"
+	}
+
+	archiveFilename := fmt.Sprintf("rclone-%s-%s-%s.%s", version, osName, arch, packageFormat)
+	archiveURL := fmt.Sprintf("%s/%s/%s", siteURL, version, archiveFilename)
+	archiveBuf, err := downloadFile(ctx, archiveURL)
+	if err != nil {
+		return err
+	}
+	gotHash := sha256.Sum256(archiveBuf)
+	strHash := hex.EncodeToString(gotHash[:])
+	fs.Debugf(nil, "downloaded release archive with hashsum %s from %s", strHash, archiveURL)
+
+	// CI/CD does not provide hashsums for beta releases
+	if !beta {
+		if err := verifyHashsum(ctx, siteURL, version, archiveFilename, gotHash[:]); err != nil {
+			return err
+		}
+	}
+
+	if packageFormat == "deb" || packageFormat == "rpm" {
+		if err := ioutil.WriteFile(newFile, archiveBuf, 0644); err != nil {
+			return errors.Wrap(err, "cannot write temporary ."+packageFormat)
+		}
+		return nil
+	}
+
+	entryName := fmt.Sprintf("rclone-%s-%s-%s/rclone", version, osName, arch)
+	if runtime.GOOS == "windows" {
+		entryName += ".exe"
+	}
+
+	// Extract executable to a temporary file, then replace it by an instant rename
+	err = extractZipToFile(archiveBuf, entryName, newFile)
+	if err != nil {
+		return err
+	}
+	fs.Debugf(nil, "extracted %s to %s", entryName, newFile)
+	return nil
+}
+
+func verifyAccess(file string) error {
+	admin := "root"
+	if runtime.GOOS == "windows" {
+		admin = "Administrator"
+	}
+
+	fileInfo, fileErr := os.Lstat(file)
+
+	if fileErr != nil {
+		dir := filepath.Dir(file)
+		dirInfo, dirErr := os.Lstat(dir)
+		if dirErr != nil {
+			return dirErr
+		}
+		if !dirInfo.Mode().IsDir() {
+			return fmt.Errorf("%s: parent path is not a directory, specify a different path using --output", dir)
+		}
+		if !writable(dir) {
+			return fmt.Errorf("%s: directory is not writable, please run self-update as %s", dir, admin)
+		}
+	}
+
+	if fileErr == nil && !fileInfo.Mode().IsRegular() {
+		return fmt.Errorf("%s: path is not a normal file, specify a different path using --output", file)
+	}
+
+	if fileErr == nil && !writable(file) {
+		return fmt.Errorf("%s: file is not writable, run self-update as %s", file, admin)
+	}
+
+	return nil
+}
+
+func findFileHash(buf []byte, filename string) (hash []byte, err error) {
+	lines := bufio.NewScanner(bytes.NewReader(buf))
+	for lines.Scan() {
+		tokens := strings.Split(lines.Text(), "  ")
+		if len(tokens) == 2 && tokens[1] == filename {
+			if hash, err := hex.DecodeString(tokens[0]); err == nil {
+				return hash, nil
+			}
+		}
+	}
+	return nil, fmt.Errorf("%s: unable to find hash", filename)
+}
+
+func extractZipToFile(buf []byte, entryName, newFile string) error {
+	zipReader, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
+	if err != nil {
+		return err
+	}
+
+	var reader io.ReadCloser
+	for _, entry := range zipReader.File {
+		if entry.Name == entryName {
+			reader, err = entry.Open()
+			break
+		}
+	}
+	if reader == nil || err != nil {
+		return fmt.Errorf("%s: file not found in archive", entryName)
+	}
+	defer func() {
+		_ = reader.Close()
+	}()
+
+	err = os.Remove(newFile)
+	if err != nil && !os.IsNotExist(err) {
+		return fmt.Errorf("%s: unable to create new file: %v", newFile, err)
+	}
+	writer, err := os.OpenFile(newFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(0755))
+	if err != nil {
+		return err
+	}
+
+	_, err = io.Copy(writer, reader)
+	_ = writer.Close()
+	if err != nil {
+		if rmErr := os.Remove(newFile); rmErr != nil {
+			fs.Errorf(nil, "%s: could not remove temporary file: %v", newFile, rmErr)
+		}
+	}
+	return err
+}
+
+func downloadFile(ctx context.Context, url string) ([]byte, error) {
+	resp, err := fshttp.NewClient(ctx).Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer fs.CheckClose(resp.Body, &err)
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("failed with %s downloading %s", resp.Status, url)
+	}
+	return ioutil.ReadAll(resp.Body)
+}
diff --git a/cmd/selfupdate/verify.go b/cmd/selfupdate/verify.go
new file mode 100644
index 000000000..115119fed
--- /dev/null
+++ b/cmd/selfupdate/verify.go
@@ -0,0 +1,72 @@
+package selfupdate
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/pkg/errors"
+	"github.com/rclone/rclone/fs"
+	"golang.org/x/crypto/openpgp"
+	"golang.org/x/crypto/openpgp/clearsign"
+)
+
+var ncwPublicKeyPGP = `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBDuy3V0RBADVQOAF5aFiCxD3t2h6iAF2WMiaMlgZ6kX2i/u7addNkzX71VU9
+7NpI0SnsP5YWt+gEedST6OmFbtLfZWCR4KWn5XnNdjCMNhxaH6WccVqNm4ALPIqT
+59uVjkgf8RISmmoNJ1d+2wMWjQTUfwOEmoIgH6n+2MYNUKuctBrwAACflwCg1I1Q
+O/prv/5hczdpQCs+fL87DxsD/Rt7pIXvsIOZyQWbIhSvNpGalJuMkW5Jx92UjsE9
+1Ipo3Xr6SGRPgW9+NxAZAsiZfCX/19knAyNrN9blwL0rcPDnkhdGwK69kfjF+wq+
+QbogRGodbKhqY4v+cMNkKiemBuTQiWPkpKjifwNsD1fNjNKfDP3pJ64Yz7a4fuzV
+X1YwBACpKVuEen34lmcX6ziY4jq8rKibKBs4JjQCRO24kYoHDULVe+RS9krQWY5b
+e0foDhru4dsKccefK099G+WEzKVCKxupstWkTT/iJwajR8mIqd4AhD0wO9W3MCfV
+Ov8ykMDZ7qBWk1DHc87Ep3W1o8t8wq74ifV+HjhhWg8QAylXg7QlTmljayBDcmFp
+Zy1Xb29kIDxuaWNrQGNyYWlnLXdvb2QuY29tPohxBBMRCAAxBQsHCgMEAxUDAgMW
+AgECF4AWIQT79zfs6firGGBL0qyTk14C/ztU+gUCXjg2UgIZAQAKCRCTk14C/ztU
++lmmAJ4jH5FyULzStjisuTvHLTVz6G44eQCfaR5QGZFPseenE5ic2WeQcBcmtoG5
+Ag0EO7LdgRAIAI6QdFBg3/xa1gFKPYy1ihV9eSdGqwWZGJvokWsfCvHy5180tj/v
+UNOLAJrdqglMSvevNTXe8bT65D6423AAsLhch9wq/aNqrHolTYABzxRigjcS1//T
+yln5naGUzlVQXDVfrDk3Md/NrkdOFj7r/YyMF0+iWwpFz2qAjL95i5wfVZ1kWGrT
+2AmivE1wD1sWT/Ja3FDI0NRkU0Nbz/a0TKe4ml8iLVtZXpTRbxxCCPdkHXXgSyu1
+eZ4NrF/wTJuvwGn12TJ1EF95aVkHxAUw0+KmLGdcyBG+IKuHamrsjWIAXGXV///K
+AxPgUthccQ03HMjltFsrdmen5Q034YM3eOsAAwUH/jAKiIAA8LpZmZPnt9GZ4+Ol
+Zp22VAfyfDOFl4Ol+cWjkLAgjAFsm5gnOKcRSE/9XPxnQqkhw7+ZygYuUMgTDJ99
+/5IM1UQL3ooS+oFrDaE99S8bLeOe17skcdXcA/K83VqD9m93rQRnbtD+75zqKkZn
+9WNFyKCXg5P6PFPdNYRtlQKOcwFR9mHRLUmapQSAM8Y2pCgALZ7GViKQca8/TT1T
+gZk9fJMZYGez+IlOPxTJxjn80+vywk4/wdIWSiQj+8u5RzT9sjmm77wbMVNGRqYd
+W/EemW9Zz9vi0CIvJGgbPMqcuxw8e/5lnuQ6Mi3uDR0P2RNIAhFrdZpVSME8xQaI
+RgQYEQIABgUCO7LdgQAKCRCTk14C/ztU+mLBAKC2cdFy7eLaQAvyzcE2VK6HVIjn
+JACguA00bxLQuJ4+RCJrLFZP8ZlN2sc=
+=TtR5
+-----END PGP PUBLIC KEY BLOCK-----`
+
+func verifyHashsum(ctx context.Context, siteURL, version, archive string, hash []byte) error {
+	sumsURL := fmt.Sprintf("%s/%s/SHA256SUMS", siteURL, version)
+	sumsBuf, err := downloadFile(ctx, sumsURL)
+	if err != nil {
+		return err
+	}
+	fs.Debugf(nil, "downloaded hashsum list: %s", sumsURL)
+
+	keyRing, err := openpgp.ReadArmoredKeyRing(strings.NewReader(ncwPublicKeyPGP))
+	if err != nil {
+		return errors.New("unsupported signing key")
+	}
+	block, rest := clearsign.Decode(sumsBuf)
+	// block.Bytes = block.Bytes[1:] // uncomment to test invalid signature
+	_, err = openpgp.CheckDetachedSignature(keyRing, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body)
+	if err != nil || len(rest) > 0 {
+		return errors.New("invalid hashsum signature")
+	}
+
+	wantHash, err := findFileHash(sumsBuf, archive)
+	if err != nil {
+		return err
+	}
+	if !bytes.Equal(hash, wantHash) {
+		return fmt.Errorf("archive hash mismatch: want %02x vs got %02x", wantHash, hash)
+	}
+	return nil
+}
diff --git a/cmd/selfupdate/writable_unix.go b/cmd/selfupdate/writable_unix.go
new file mode 100644
index 000000000..f7c8a59fb
--- /dev/null
+++ b/cmd/selfupdate/writable_unix.go
@@ -0,0 +1,11 @@
+// +build !windows,!plan9,!js
+
+package selfupdate
+
+import (
+	"golang.org/x/sys/unix"
+)
+
+func writable(path string) bool {
+	return unix.Access(path, unix.W_OK) == nil
+}
diff --git a/cmd/selfupdate/writable_unsupported.go b/cmd/selfupdate/writable_unsupported.go
new file mode 100644
index 000000000..40f1d2429
--- /dev/null
+++ b/cmd/selfupdate/writable_unsupported.go
@@ -0,0 +1,7 @@
+// +build plan9 js
+
+package selfupdate
+
+func writable(path string) bool {
+	return true
+}
diff --git a/cmd/selfupdate/writable_windows.go b/cmd/selfupdate/writable_windows.go
new file mode 100644
index 000000000..8270d3936
--- /dev/null
+++ b/cmd/selfupdate/writable_windows.go
@@ -0,0 +1,16 @@
+// +build windows
+
+package selfupdate
+
+import (
+	"os"
+)
+
+func writable(path string) bool {
+	info, err := os.Stat(path)
+	const UserWritableBit = 128
+	if err == nil {
+		return info.Mode().Perm()&UserWritableBit != 0
+	}
+	return false
+}
diff --git a/cmd/version/version.go b/cmd/version/version.go
index 04f157e7f..85a049b37 100644
--- a/cmd/version/version.go
+++ b/cmd/version/version.go
@@ -59,7 +59,7 @@ Or
 	Run: func(command *cobra.Command, args []string) {
 		cmd.CheckArgs(0, 0, command, args)
 		if check {
-			checkVersion()
+			CheckVersion()
 		} else {
 			cmd.ShowVersion()
 		}
@@ -74,8 +74,8 @@ func stripV(s string) string {
 	return s
 }
 
-// getVersion gets the version by checking the download repository passed in
-func getVersion(url string) (v *semver.Version, vs string, date time.Time, err error) {
+// GetVersion gets the version available for download
+func GetVersion(url string) (v *semver.Version, vs string, date time.Time, err error) {
 	resp, err := http.Get(url)
 	if err != nil {
 		return v, vs, date, err
@@ -101,9 +101,8 @@ func getVersion(url string) (v *semver.Version, vs string, date time.Time, err e
 	return v, vs, date, err
 }
 
-// check the current version against available versions
-func checkVersion() {
-	// Get Current version
+// CheckVersion checks the installed version against available downloads
+func CheckVersion() {
 	vCurrent, err := semver.NewVersion(stripV(fs.Version))
 	if err != nil {
 		fs.Errorf(nil, "Failed to parse version: %v", err)
@@ -111,7 +110,7 @@ func checkVersion() {
 	const timeFormat = "2006-01-02"
 
 	printVersion := func(what, url string) {
-		v, vs, t, err := getVersion(url + "version.txt")
+		v, vs, t, err := GetVersion(url + "version.txt")
 		if err != nil {
 			fs.Errorf(nil, "Failed to get rclone %s version: %v", what, err)
 			return
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 22d9320eb..d29c02eff 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -161,8 +161,9 @@ The syntax of the paths passed to the rclone command are as follows.
 
 This refers to the local file system.
 
-On Windows only `\` may be used instead of `/` in local paths
-**only**, non local paths must use `/`.
+On Windows `\` may be used instead of `/` in local paths **only**,
+non local paths must use `/`. See [local filesystem](https://rclone.org/local/#windows-paths)
+documentation for more about Windows-specific paths.
 
 These paths needn't start with a leading `/` - if they don't then they
 will be relative to the current directory.
diff --git a/docs/content/local.md b/docs/content/local.md
index 47b6a66f2..5b3579c4b 100644
--- a/docs/content/local.md
+++ b/docs/content/local.md
@@ -10,15 +10,17 @@ Local paths are specified as normal filesystem paths, e.g. `/path/to/wherever`,
 
     rclone sync -i /home/source /tmp/destination
 
-Will sync `/home/source` to `/tmp/destination`
+Will sync `/home/source` to `/tmp/destination`.
 
-These can be configured into the config file for consistencies sake,
-but it is probably easier not to.
+For consistencies sake one can also configure a remote of type
+`local` in the config file, and access the local filesystem using
+rclone remote paths, e.g. `remote:path/to/wherever`, but it is probably
+easier not to.
 
 ### Modified time ###
 
 Rclone reads and writes the modified time using an accuracy determined by
-the OS.  Typically this is 1ns on Linux, 10 ns on Windows and 1 Second
+the OS. Typically this is 1ns on Linux, 10 ns on Windows and 1 Second
 on OS X.
 
 ### Filenames ###
@@ -109,15 +111,35 @@ These only get replaced if they are the last character in the name:
 Invalid UTF-8 bytes will also be [replaced](/overview/#invalid-utf8),
 as they can't be converted to UTF-16.
 
-### Long paths on Windows ###
+### Paths on Windows ###
 
-Rclone handles long paths automatically, by converting all paths to long
-[UNC paths](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath)
-which allows paths up to 32,767 characters.
+On Windows there are many ways of specifying a path to a file system resource.
+Both absolute paths like `C:\path\to\wherever`, and relative paths like
+`..\wherever` can be used, and path separator can be either
+`\` (as in `C:\path\to\wherever`) or `/` (as in `C:/path/to/wherever`).
+Length of these paths are limited to 259 characters for files and 247
+characters for directories, but there is an alternative extended-length
+path format increasing the limit to (approximately) 32,767 characters.
+This format requires absolute paths and the use of prefix `\\?\`,
+e.g. `\\?\D:\some\very\long\path`. For convenience rclone will automatically
+convert regular paths into the corresponding extended-length paths,
+so in most cases you do not have to worry about this (read more [below](#long-paths)).
 
-This is why you will see that your paths, for instance `c:\files` is
-converted to the UNC path `\\?\c:\files` in the output,
-and `\\server\share` is converted to `\\?\UNC\server\share`.
+Note that Windows supports using the same prefix `\\?\` to
+specify path to volumes identified by their GUID, e.g.
+`\\?\Volume{b75e2c83-0000-0000-0000-602f00000000}\some\path`.
+This is *not* supported in rclone, due to an [issue](https://github.com/golang/go/issues/39785)
+in go.
+
+#### Long paths ####
+
+Rclone handles long paths automatically, by converting all paths to
+[extended-length path format](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation), which allows paths up to 32,767 characters.
+
+This conversion will ensure paths are absolute and prefix them with
+the `\\?\`. This is why you will see that your paths, for instance
+`.\files` is shown as path `\\?\C:\files` in the output, and `\\server\share`
+as `\\?\UNC\server\share`.
 
 However, in rare cases this may cause problems with buggy file
 system drivers like [EncFS](https://github.com/rclone/rclone/issues/261).
@@ -141,7 +163,7 @@ And use rclone like this:
 
 This will use UNC paths on `c:\src` but not on `z:\dst`.
 Of course this will cause problems if the absolute path length of a
-file exceeds 258 characters on z, so only use this option if you have to.
+file exceeds 259 characters on z, so only use this option if you have to.
 
 ### Symlinks / Junction points
 
diff --git a/docs/content/overview.md b/docs/content/overview.md
index 23da7e105..21b0a13c8 100644
--- a/docs/content/overview.md
+++ b/docs/content/overview.md
@@ -330,7 +330,7 @@ upon backend specific capabilities.
 
 | Name                         | Purge | Copy | Move | DirMove | CleanUp | ListR | StreamUpload | LinkSharing | About | EmptyDir |
 | ---------------------------- |:-----:|:----:|:----:|:-------:|:-------:|:-----:|:------------:|:------------:|:-----:| :------: |
-| 1Fichier                     | No    | Yes   | Yes   | No      | No      | No    | No           | No           |   No  |  Yes |
+| 1Fichier                     | No    | Yes   | Yes   | No      | No      | No    | No           | Yes           |   No  |  Yes |
 | Amazon Drive                 | Yes   | No   | Yes  | Yes     | No [#575](https://github.com/rclone/rclone/issues/575) | No  | No  | No [#2178](https://github.com/rclone/rclone/issues/2178) | No  | Yes |
 | Amazon S3                    | No    | Yes  | No   | No      | Yes     | Yes   | Yes          | No [#2178](https://github.com/rclone/rclone/issues/2178) | No  | No |
 | Backblaze B2                 | No    | Yes  | No   | No      | Yes     | Yes   | Yes          | Yes          | No  | No |
diff --git a/fs/accounting/accounting.go b/fs/accounting/accounting.go
index 2ffa37d49..65bbb82bb 100644
--- a/fs/accounting/accounting.go
+++ b/fs/accounting/accounting.go
@@ -29,6 +29,18 @@ var ErrorMaxTransferLimitReachedFatal = fserrors.FatalError(ErrorMaxTransferLimi
 // transfer limit is reached and a graceful stop is required.
 var ErrorMaxTransferLimitReachedGraceful = fserrors.NoRetryError(ErrorMaxTransferLimitReached)
 
+// Start sets up the accounting, in particular the bandwidth limiting
+func Start(ctx context.Context) {
+	// Start the token bucket limiter
+	TokenBucket.StartTokenBucket(ctx)
+
+	// Start the bandwidth update ticker
+	TokenBucket.StartTokenTicker(ctx)
+
+	// Start the transactions per second limiter
+	StartLimitTPS(ctx)
+}
+
 // Account limits and accounts for one transfer
 type Account struct {
 	stats *StatsInfo
diff --git a/fs/config/authorize.go b/fs/config/authorize.go
new file mode 100644
index 000000000..4f44df4ee
--- /dev/null
+++ b/fs/config/authorize.go
@@ -0,0 +1,47 @@
+package config
+
+import (
+	"context"
+	"log"
+
+	"github.com/rclone/rclone/fs"
+)
+
+// Authorize is for remote authorization of headless machines.
+//
+// It expects 1 or 3 arguments
+//
+//   rclone authorize "fs name"
+//   rclone authorize "fs name" "client id" "client secret"
+func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
+	ctx = suppressConfirm(ctx)
+	switch len(args) {
+	case 1, 3:
+	default:
+		log.Fatalf("Invalid number of arguments: %d", len(args))
+	}
+	newType := args[0]
+	f := fs.MustFind(newType)
+	if f.Config == nil {
+		log.Fatalf("Can't authorize fs %q", newType)
+	}
+	// Name used for temporary fs
+	name := "**temp-fs**"
+
+	// Make sure we delete it
+	defer DeleteRemote(name)
+
+	// Indicate that we are running rclone authorize
+	Data.SetValue(name, ConfigAuthorize, "true")
+	if noAutoBrowser {
+		Data.SetValue(name, ConfigAuthNoBrowser, "true")
+	}
+
+	if len(args) == 3 {
+		Data.SetValue(name, ConfigClientID, args[1])
+		Data.SetValue(name, ConfigClientSecret, args[2])
+	}
+
+	m := fs.ConfigMap(f, name)
+	f.Config(ctx, name, m)
+}
diff --git a/fs/config/config.go b/fs/config/config.go
index 5eb02f6c0..b42be2e9d 100644
--- a/fs/config/config.go
+++ b/fs/config/config.go
@@ -2,44 +2,26 @@
 package config
 
 import (
-	"bufio"
-	"bytes"
 	"context"
-	"crypto/rand"
-	"crypto/sha256"
-	"encoding/base64"
 	"encoding/json"
 	"fmt"
-	"io"
-	"io/ioutil"
 	"log"
 	mathrand "math/rand"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"regexp"
 	"runtime"
-	"sort"
-	"strconv"
 	"strings"
 	"time"
-	"unicode/utf8"
 
-	"github.com/Unknwon/goconfig"
-	homedir "github.com/mitchellh/go-homedir"
+	"github.com/mitchellh/go-homedir"
 	"github.com/pkg/errors"
+
 	"github.com/rclone/rclone/fs"
-	"github.com/rclone/rclone/fs/accounting"
-	"github.com/rclone/rclone/fs/config/configmap"
-	"github.com/rclone/rclone/fs/config/configstruct"
 	"github.com/rclone/rclone/fs/config/obscure"
-	"github.com/rclone/rclone/fs/driveletter"
 	"github.com/rclone/rclone/fs/fspath"
 	"github.com/rclone/rclone/fs/rc"
 	"github.com/rclone/rclone/lib/random"
-	"github.com/rclone/rclone/lib/terminal"
-	"golang.org/x/crypto/nacl/secretbox"
-	"golang.org/x/text/unicode/norm"
 )
 
 const (
@@ -74,32 +56,58 @@ const (
 	ConfigAuthNoBrowser = "config_auth_no_browser"
 )
 
+// Storage defines an interface for loading and saving config to
+// persistent storage. Rclone provides a default implementation to
+// load and save to a config file when this is imported
+//
+// import "github.com/rclone/rclone/fs/config/configfile"
+// configfile.LoadConfig(ctx)
+type Storage interface {
+	// GetSectionList returns a slice of strings with names for all the
+	// sections
+	GetSectionList() []string
+
+	// HasSection returns true if section exists in the config file
+	HasSection(section string) bool
+
+	// DeleteSection removes the named section and all config from the
+	// config file
+	DeleteSection(section string)
+
+	// GetKeyList returns the keys in this section
+	GetKeyList(section string) []string
+
+	// GetValue returns the key in section with a found flag
+	GetValue(section string, key string) (value string, found bool)
+
+	// SetValue sets the value under key in section
+	SetValue(section string, key string, value string)
+
+	// DeleteKey removes the key under section
+	DeleteKey(section string, key string) bool
+
+	// Load the config from permanent storage
+	Load() error
+
+	// Save the config to permanent storage
+	Save() error
+
+	// Serialize the config into a string
+	Serialize() (string, error)
+}
+
 // Global
 var (
-	// configFile is the global config data structure. Don't read it directly, use getConfigData()
-	configFile *goconfig.ConfigFile
-
-	// ConfigPath points to the config file
-	ConfigPath = makeConfigPath()
+	// Data is the global config data structure
+	Data Storage = defaultStorage{}
 
 	// CacheDir points to the cache directory.  Users of this
 	// should make a subdirectory and use MkdirAll() to create it
 	// and any parents.
 	CacheDir = makeCacheDir()
 
-	// Key to use for password en/decryption.
-	// When nil, no encryption will be used for saving.
-	configKey []byte
-
-	// output of prompt for password
-	PasswordPromptOutput = os.Stderr
-
-	// If set to true, the configKey is obscured with obscure.Obscure and saved to a temp file when it is
-	// calculated from the password. The path of that temp file is then written to the environment variable
-	// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is present, password prompt is skipped and `RCLONE_CONFIG_PASS` ignored.
-	// For security reasons, the temp file is deleted once the configKey is successfully loaded.
-	// This can be used to pass the configKey to a child process.
-	PassConfigKeyForDaemonization = false
+	// ConfigPath points to the config file
+	ConfigPath = makeConfigPath()
 
 	// Password can be used to configure the random password generator
 	Password = random.Password
@@ -111,16 +119,8 @@ func init() {
 	fs.ConfigFileSet = SetValueAndSave
 }
 
-func getConfigData() *goconfig.ConfigFile {
-	if configFile == nil {
-		LoadConfig(context.Background())
-	}
-	return configFile
-}
-
 // Return the path to the configuration file
 func makeConfigPath() string {
-
 	// Use rclone.conf from rclone executable directory if already existing
 	exe, err := os.Executable()
 	if err == nil {
@@ -217,386 +217,17 @@ func LoadConfig(ctx context.Context) {
 	_ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(ConfigPath))
 
 	// Load configuration file.
-	var err error
-	configFile, err = loadConfigFile()
-	if err == errorConfigFileNotFound {
+	if err := Data.Load(); err == ErrorConfigFileNotFound {
 		fs.Logf(nil, "Config file %q not found - using defaults", ConfigPath)
-		configFile, _ = goconfig.LoadFromReader(&bytes.Buffer{})
 	} else if err != nil {
 		log.Fatalf("Failed to load config file %q: %v", ConfigPath, err)
 	} else {
 		fs.Debugf(nil, "Using config file from %q", ConfigPath)
 	}
-
-	// Start the token bucket limiter
-	accounting.TokenBucket.StartTokenBucket(ctx)
-
-	// Start the bandwidth update ticker
-	accounting.TokenBucket.StartTokenTicker(ctx)
-
-	// Start the transactions per second limiter
-	accounting.StartLimitTPS(ctx)
-}
-
-var errorConfigFileNotFound = errors.New("config file not found")
-
-// loadConfigFile will load a config file, and
-// automatically decrypt it.
-func loadConfigFile() (*goconfig.ConfigFile, error) {
-	ctx := context.Background()
-	ci := fs.GetConfig(ctx)
-	var usingPasswordCommand bool
-
-	b, err := ioutil.ReadFile(ConfigPath)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil, errorConfigFileNotFound
-		}
-		return nil, err
-	}
-	// Find first non-empty line
-	r := bufio.NewReader(bytes.NewBuffer(b))
-	for {
-		line, _, err := r.ReadLine()
-		if err != nil {
-			if err == io.EOF {
-				return goconfig.LoadFromReader(bytes.NewBuffer(b))
-			}
-			return nil, err
-		}
-		l := strings.TrimSpace(string(line))
-		if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
-			continue
-		}
-		// First non-empty or non-comment must be ENCRYPT_V0
-		if l == "RCLONE_ENCRYPT_V0:" {
-			break
-		}
-		if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
-			return nil, errors.New("unsupported configuration encryption - update rclone for support")
-		}
-		return goconfig.LoadFromReader(bytes.NewBuffer(b))
-	}
-
-	if len(configKey) == 0 {
-		if len(ci.PasswordCommand) != 0 {
-			var stdout bytes.Buffer
-			var stderr bytes.Buffer
-
-			cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
-
-			cmd.Stdout = &stdout
-			cmd.Stderr = &stderr
-			cmd.Stdin = os.Stdin
-
-			if err := cmd.Run(); err != nil {
-				// One does not always get the stderr returned in the wrapped error.
-				fs.Errorf(nil, "Using --password-command returned: %v", err)
-				if ers := strings.TrimSpace(stderr.String()); ers != "" {
-					fs.Errorf(nil, "--password-command stderr: %s", ers)
-				}
-				return nil, errors.Wrap(err, "password command failed")
-			}
-			if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
-				err := setConfigPassword(pass)
-				if err != nil {
-					return nil, errors.Wrap(err, "incorrect password")
-				}
-			} else {
-				return nil, errors.New("password-command returned empty string")
-			}
-
-			if len(configKey) == 0 {
-				return nil, errors.New("unable to decrypt configuration: incorrect password")
-			}
-			usingPasswordCommand = true
-		} else {
-			usingPasswordCommand = false
-
-			envpw := os.Getenv("RCLONE_CONFIG_PASS")
-
-			if envpw != "" {
-				err := setConfigPassword(envpw)
-				if err != nil {
-					fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
-				} else {
-					fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.")
-				}
-			}
-		}
-	}
-
-	// Encrypted content is base64 encoded.
-	dec := base64.NewDecoder(base64.StdEncoding, r)
-	box, err := ioutil.ReadAll(dec)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to load base64 encoded data")
-	}
-	if len(box) < 24+secretbox.Overhead {
-		return nil, errors.New("Configuration data too short")
-	}
-
-	var out []byte
-	for {
-		if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
-			fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
-			obscuredKey, err := ioutil.ReadFile(envKeyFile)
-			if err != nil {
-				errRemove := os.Remove(envKeyFile)
-				if errRemove != nil {
-					log.Fatalf("unable to read obscured config key and unable to delete the temp file: %v", err)
-				}
-				log.Fatalf("unable to read obscured config key: %v", err)
-			}
-			errRemove := os.Remove(envKeyFile)
-			if errRemove != nil {
-				log.Fatalf("unable to delete temp file with configKey: %v", err)
-			}
-			configKey = []byte(obscure.MustReveal(string(obscuredKey)))
-			fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
-		} else {
-			if len(configKey) == 0 {
-				if usingPasswordCommand {
-					return nil, errors.New("using --password-command derived password, unable to decrypt configuration")
-				}
-				if !ci.AskPassword {
-					return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
-				}
-				getConfigPassword("Enter configuration password:")
-			}
-		}
-
-		// Nonce is first 24 bytes of the ciphertext
-		var nonce [24]byte
-		copy(nonce[:], box[:24])
-		var key [32]byte
-		copy(key[:], configKey[:32])
-
-		// Attempt to decrypt
-		var ok bool
-		out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
-		if ok {
-			break
-		}
-
-		// Retry
-		fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
-		configKey = nil
-	}
-	return goconfig.LoadFromReader(bytes.NewBuffer(out))
-}
-
-// checkPassword normalises and validates the password
-func checkPassword(password string) (string, error) {
-	if !utf8.ValidString(password) {
-		return "", errors.New("password contains invalid utf8 characters")
-	}
-	// Check for leading/trailing whitespace
-	trimmedPassword := strings.TrimSpace(password)
-	// Warn user if password has leading+trailing whitespace
-	if len(password) != len(trimmedPassword) {
-		_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
-	}
-	// Normalize to reduce weird variations.
-	password = norm.NFKC.String(password)
-	if len(password) == 0 || len(trimmedPassword) == 0 {
-		return "", errors.New("no characters in password")
-	}
-	return password, nil
-}
-
-// GetPassword asks the user for a password with the prompt given.
-func GetPassword(prompt string) string {
-	_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
-	for {
-		_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
-		password := ReadPassword()
-		password, err := checkPassword(password)
-		if err == nil {
-			return password
-		}
-		_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
-	}
-}
-
-// ChangePassword will query the user twice for the named password. If
-// the same password is entered it is returned.
-func ChangePassword(name string) string {
-	for {
-		a := GetPassword(fmt.Sprintf("Enter %s password:", name))
-		b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
-		if a == b {
-			return a
-		}
-		fmt.Println("Passwords do not match!")
-	}
-}
-
-// getConfigPassword will query the user for a password the
-// first time it is required.
-func getConfigPassword(q string) {
-	if len(configKey) != 0 {
-		return
-	}
-	for {
-		password := GetPassword(q)
-		err := setConfigPassword(password)
-		if err == nil {
-			return
-		}
-		_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
-	}
-}
-
-// setConfigPassword will set the configKey to the hash of
-// the password. If the length of the password is
-// zero after trimming+normalization, an error is returned.
-func setConfigPassword(password string) error {
-	password, err := checkPassword(password)
-	if err != nil {
-		return err
-	}
-	// Create SHA256 has of the password
-	sha := sha256.New()
-	_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
-	if err != nil {
-		return err
-	}
-	configKey = sha.Sum(nil)
-	if PassConfigKeyForDaemonization {
-		tempFile, err := ioutil.TempFile("", "rclone")
-		if err != nil {
-			log.Fatalf("cannot create temp file to store configKey: %v", err)
-		}
-		_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
-		if err != nil {
-			errRemove := os.Remove(tempFile.Name())
-			if errRemove != nil {
-				log.Fatalf("error writing configKey to temp file and also error deleting it: %v", err)
-			}
-			log.Fatalf("error writing configKey to temp file: %v", err)
-		}
-		err = tempFile.Close()
-		if err != nil {
-			errRemove := os.Remove(tempFile.Name())
-			if errRemove != nil {
-				log.Fatalf("error closing temp file with configKey and also error deleting it: %v", err)
-			}
-			log.Fatalf("error closing temp file with configKey: %v", err)
-		}
-		fs.Debugf(nil, "saving configKey to temp file")
-		err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
-		if err != nil {
-			errRemove := os.Remove(tempFile.Name())
-			if errRemove != nil {
-				log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %v", err)
-			}
-			log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %v", err)
-		}
-	}
-	return nil
-}
-
-// changeConfigPassword will query the user twice
-// for a password. If the same password is entered
-// twice the key is updated.
-func changeConfigPassword() {
-	err := setConfigPassword(ChangePassword("NEW configuration"))
-	if err != nil {
-		fmt.Printf("Failed to set config password: %v\n", err)
-		return
-	}
 }
 
-// saveConfig saves configuration file.
-// if configKey has been set, the file will be encrypted.
-func saveConfig() error {
-	dir, name := filepath.Split(ConfigPath)
-	err := os.MkdirAll(dir, os.ModePerm)
-	if err != nil {
-		return errors.Wrap(err, "failed to create config directory")
-	}
-	f, err := ioutil.TempFile(dir, name)
-	if err != nil {
-		return errors.Errorf("Failed to create temp file for new config: %v", err)
-	}
-	defer func() {
-		if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
-			fs.Errorf(nil, "Failed to remove temp config file: %v", err)
-		}
-	}()
-
-	var buf bytes.Buffer
-	err = goconfig.SaveConfigData(getConfigData(), &buf)
-	if err != nil {
-		return errors.Errorf("Failed to save config file: %v", err)
-	}
-
-	if len(configKey) == 0 {
-		if _, err := buf.WriteTo(f); err != nil {
-			return errors.Errorf("Failed to write temp config file: %v", err)
-		}
-	} else {
-		_, _ = fmt.Fprintln(f, "# Encrypted rclone configuration File")
-		_, _ = fmt.Fprintln(f, "")
-		_, _ = fmt.Fprintln(f, "RCLONE_ENCRYPT_V0:")
-
-		// Generate new nonce and write it to the start of the ciphertext
-		var nonce [24]byte
-		n, _ := rand.Read(nonce[:])
-		if n != 24 {
-			return errors.Errorf("nonce short read: %d", n)
-		}
-		enc := base64.NewEncoder(base64.StdEncoding, f)
-		_, err = enc.Write(nonce[:])
-		if err != nil {
-			return errors.Errorf("Failed to write temp config file: %v", err)
-		}
-
-		var key [32]byte
-		copy(key[:], configKey[:32])
-
-		b := secretbox.Seal(nil, buf.Bytes(), &nonce, &key)
-		_, err = enc.Write(b)
-		if err != nil {
-			return errors.Errorf("Failed to write temp config file: %v", err)
-		}
-		_ = enc.Close()
-	}
-
-	_ = f.Sync()
-	err = f.Close()
-	if err != nil {
-		return errors.Errorf("Failed to close config file: %v", err)
-	}
-
-	var fileMode os.FileMode = 0600
-	info, err := os.Stat(ConfigPath)
-	if err != nil {
-		fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
-	} else if info.Mode() != fileMode {
-		fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
-		fileMode = info.Mode()
-	}
-
-	attemptCopyGroup(ConfigPath, f.Name())
-
-	err = os.Chmod(f.Name(), fileMode)
-	if err != nil {
-		fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
-	}
-
-	if err = os.Rename(ConfigPath, ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
-		return errors.Errorf("Failed to move previous config to backup location: %v", err)
-	}
-	if err = os.Rename(f.Name(), ConfigPath); err != nil {
-		return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
-	}
-	if err := os.Remove(ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
-		fs.Errorf(nil, "Failed to remove backup config file: %v", err)
-	}
-	return nil
-}
+// ErrorConfigFileNotFound is returned when the config file is not found
+var ErrorConfigFileNotFound = errors.New("config file not found")
 
 // SaveConfig calling function which saves configuration file.
 // if saveConfig returns error trying again after sleep.
@@ -605,7 +236,7 @@ func SaveConfig() {
 	ci := fs.GetConfig(ctx)
 	var err error
 	for i := 0; i < ci.LowLevelRetries+1; i++ {
-		if err = saveConfig(); err == nil {
+		if err = Data.Save(); err == nil {
 			return
 		}
 		waitingTimeMs := mathrand.Intn(1000)
@@ -619,419 +250,49 @@ func SaveConfig() {
 // SetValueAndSave sets the key to the value and saves just that
 // value in the config file.  It loads the old config file in from
 // disk first and overwrites the given value only.
-func SetValueAndSave(name, key, value string) (err error) {
+func SetValueAndSave(name, key, value string) error {
 	// Set the value in config in case we fail to reload it
-	getConfigData().SetValue(name, key, value)
+	Data.SetValue(name, key, value)
+
 	// Reload the config file
-	reloadedConfigFile, err := loadConfigFile()
-	if err == errorConfigFileNotFound {
+	err := Data.Load()
+	if err == ErrorConfigFileNotFound {
 		// Config file not written yet so ignore reload
 		return nil
 	} else if err != nil {
 		return err
 	}
-	_, err = reloadedConfigFile.GetSection(name)
-	if err != nil {
+	if !Data.HasSection(name) {
 		// Section doesn't exist yet so ignore reload
 		return nil
 	}
-	// Update the config file with the reloaded version
-	configFile = reloadedConfigFile
-	// Set the value in the reloaded version
-	reloadedConfigFile.SetValue(name, key, value)
 	// Save it again
 	SaveConfig()
 	return nil
 }
 
+// getWithDefault gets key out of section name returning defaultValue if not
+// found.
+func getWithDefault(name, key, defaultValue string) string {
+	value, found := Data.GetValue(name, key)
+	if !found {
+		return defaultValue
+	}
+	return value
+}
+
 // FileGetFresh reads the config key under section return the value or
 // an error if the config file was not found or that value couldn't be
 // read.
 func FileGetFresh(section, key string) (value string, err error) {
-	reloadedConfigFile, err := loadConfigFile()
-	if err != nil {
+	if err := Data.Load(); err != nil {
 		return "", err
 	}
-	return reloadedConfigFile.GetValue(section, key)
-}
-
-// ShowRemotes shows an overview of the config file
-func ShowRemotes() {
-	remotes := getConfigData().GetSectionList()
-	if len(remotes) == 0 {
-		return
-	}
-	sort.Strings(remotes)
-	fmt.Printf("%-20s %s\n", "Name", "Type")
-	fmt.Printf("%-20s %s\n", "====", "====")
-	for _, remote := range remotes {
-		fmt.Printf("%-20s %s\n", remote, FileGet(remote, "type"))
-	}
-}
-
-// ChooseRemote chooses a remote name
-func ChooseRemote() string {
-	remotes := getConfigData().GetSectionList()
-	sort.Strings(remotes)
-	return Choose("remote", remotes, nil, false)
-}
-
-// ReadLine reads some input
-var ReadLine = func() string {
-	buf := bufio.NewReader(os.Stdin)
-	line, err := buf.ReadString('\n')
-	if err != nil {
-		log.Fatalf("Failed to read line: %v", err)
-	}
-	return strings.TrimSpace(line)
-}
-
-// ReadNonEmptyLine prints prompt and calls Readline until non empty
-func ReadNonEmptyLine(prompt string) string {
-	result := ""
-	for result == "" {
-		fmt.Print(prompt)
-		result = strings.TrimSpace(ReadLine())
-	}
-	return result
-}
-
-// CommandDefault - choose one.  If return is pressed then it will
-// chose the defaultIndex if it is >= 0
-func CommandDefault(commands []string, defaultIndex int) byte {
-	opts := []string{}
-	for i, text := range commands {
-		def := ""
-		if i == defaultIndex {
-			def = " (default)"
-		}
-		fmt.Printf("%c) %s%s\n", text[0], text[1:], def)
-		opts = append(opts, text[:1])
-	}
-	optString := strings.Join(opts, "")
-	optHelp := strings.Join(opts, "/")
-	for {
-		fmt.Printf("%s> ", optHelp)
-		result := strings.ToLower(ReadLine())
-		if len(result) == 0 && defaultIndex >= 0 {
-			return optString[defaultIndex]
-		}
-		if len(result) != 1 {
-			continue
-		}
-		i := strings.Index(optString, string(result[0]))
-		if i >= 0 {
-			return result[0]
-		}
-	}
-}
-
-// Command - choose one
-func Command(commands []string) byte {
-	return CommandDefault(commands, -1)
-}
-
-// Confirm asks the user for Yes or No and returns true or false
-//
-// If the user presses enter then the Default will be used
-func Confirm(Default bool) bool {
-	defaultIndex := 0
-	if !Default {
-		defaultIndex = 1
-	}
-	return CommandDefault([]string{"yYes", "nNo"}, defaultIndex) == 'y'
-}
-
-// ConfirmWithConfig asks the user for Yes or No and returns true or
-// false.
-//
-// If AutoConfirm is set, it will look up the value in m and return
-// that, but if it isn't set then it will return the Default value
-// passed in
-func ConfirmWithConfig(ctx context.Context, m configmap.Getter, configName string, Default bool) bool {
-	ci := fs.GetConfig(ctx)
-	if ci.AutoConfirm {
-		configString, ok := m.Get(configName)
-		if ok {
-			configValue, err := strconv.ParseBool(configString)
-			if err != nil {
-				fs.Errorf(nil, "Failed to parse config parameter %s=%q as boolean - using default %v: %v", configName, configString, Default, err)
-			} else {
-				Default = configValue
-			}
-		}
-		answer := "No"
-		if Default {
-			answer = "Yes"
-		}
-		fmt.Printf("Auto confirm is set: answering %s, override by setting config parameter %s=%v\n", answer, configName, !Default)
-		return Default
-	}
-	return Confirm(Default)
-}
-
-// Choose one of the defaults or type a new string if newOk is set
-func Choose(what string, defaults, help []string, newOk bool) string {
-	valueDescription := "an existing"
-	if newOk {
-		valueDescription = "your own"
-	}
-	fmt.Printf("Choose a number from below, or type in %s value\n", valueDescription)
-	attributes := []string{terminal.HiRedFg, terminal.HiGreenFg}
-	for i, text := range defaults {
-		var lines []string
-		if help != nil {
-			parts := strings.Split(help[i], "\n")
-			lines = append(lines, parts...)
-		}
-		lines = append(lines, fmt.Sprintf("%q", text))
-		pos := i + 1
-		terminal.WriteString(attributes[i%len(attributes)])
-		if len(lines) == 1 {
-			fmt.Printf("%2d > %s\n", pos, text)
-		} else {
-			mid := (len(lines) - 1) / 2
-			for i, line := range lines {
-				var sep rune
-				switch i {
-				case 0:
-					sep = '/'
-				case len(lines) - 1:
-					sep = '\\'
-				default:
-					sep = '|'
-				}
-				number := "  "
-				if i == mid {
-					number = fmt.Sprintf("%2d", pos)
-				}
-				fmt.Printf("%s %c %s\n", number, sep, line)
-			}
-		}
-		terminal.WriteString(terminal.Reset)
-	}
-	for {
-		fmt.Printf("%s> ", what)
-		result := ReadLine()
-		i, err := strconv.Atoi(result)
-		if err != nil {
-			if newOk {
-				return result
-			}
-			for _, v := range defaults {
-				if result == v {
-					return result
-				}
-			}
-			continue
-		}
-		if i >= 1 && i <= len(defaults) {
-			return defaults[i-1]
-		}
-	}
-}
-
-// ChooseNumber asks the user to enter a number between min and max
-// inclusive prompting them with what.
-func ChooseNumber(what string, min, max int) int {
-	for {
-		fmt.Printf("%s> ", what)
-		result := ReadLine()
-		i, err := strconv.Atoi(result)
-		if err != nil {
-			fmt.Printf("Bad number: %v\n", err)
-			continue
-		}
-		if i < min || i > max {
-			fmt.Printf("Out of range - %d to %d inclusive\n", min, max)
-			continue
-		}
-		return i
-	}
-}
-
-// ShowRemote shows the contents of the remote
-func ShowRemote(name string) {
-	fmt.Printf("--------------------\n")
-	fmt.Printf("[%s]\n", name)
-	fs := MustFindByName(name)
-	for _, key := range getConfigData().GetKeyList(name) {
-		isPassword := false
-		for _, option := range fs.Options {
-			if option.Name == key && option.IsPassword {
-				isPassword = true
-				break
-			}
-		}
-		value := FileGet(name, key)
-		if isPassword && value != "" {
-			fmt.Printf("%s = *** ENCRYPTED ***\n", key)
-		} else {
-			fmt.Printf("%s = %s\n", key, value)
-		}
-	}
-	fmt.Printf("--------------------\n")
-}
-
-// OkRemote prints the contents of the remote and ask if it is OK
-func OkRemote(name string) bool {
-	ShowRemote(name)
-	switch i := CommandDefault([]string{"yYes this is OK", "eEdit this remote", "dDelete this remote"}, 0); i {
-	case 'y':
-		return true
-	case 'e':
-		return false
-	case 'd':
-		getConfigData().DeleteSection(name)
-		return true
-	default:
-		fs.Errorf(nil, "Bad choice %c", i)
-	}
-	return false
-}
-
-// MustFindByName finds the RegInfo for the remote name passed in or
-// exits with a fatal error.
-func MustFindByName(name string) *fs.RegInfo {
-	fsType := FileGet(name, "type")
-	if fsType == "" {
-		log.Fatalf("Couldn't find type of fs for %q", name)
-	}
-	return fs.MustFind(fsType)
-}
-
-// RemoteConfig runs the config helper for the remote if needed
-func RemoteConfig(ctx context.Context, name string) {
-	fmt.Printf("Remote config\n")
-	f := MustFindByName(name)
-	if f.Config != nil {
-		m := fs.ConfigMap(f, name)
-		f.Config(ctx, name, m)
-	}
-}
-
-// matchProvider returns true if provider matches the providerConfig string.
-//
-// The providerConfig string can either be a list of providers to
-// match, or if it starts with "!" it will be a list of providers not
-// to match.
-//
-// If either providerConfig or provider is blank then it will return true
-func matchProvider(providerConfig, provider string) bool {
-	if providerConfig == "" || provider == "" {
-		return true
-	}
-	negate := false
-	if strings.HasPrefix(providerConfig, "!") {
-		providerConfig = providerConfig[1:]
-		negate = true
-	}
-	providers := strings.Split(providerConfig, ",")
-	matched := false
-	for _, p := range providers {
-		if p == provider {
-			matched = true
-			break
-		}
-	}
-	if negate {
-		return !matched
-	}
-	return matched
-}
-
-// ChooseOption asks the user to choose an option
-func ChooseOption(o *fs.Option, name string) string {
-	var subProvider = getConfigData().MustValue(name, fs.ConfigProvider, "")
-	fmt.Println(o.Help)
-	if o.IsPassword {
-		actions := []string{"yYes type in my own password", "gGenerate random password"}
-		defaultAction := -1
-		if !o.Required {
-			defaultAction = len(actions)
-			actions = append(actions, "nNo leave this optional password blank")
-		}
-		var password string
-		var err error
-		switch i := CommandDefault(actions, defaultAction); i {
-		case 'y':
-			password = ChangePassword("the")
-		case 'g':
-			for {
-				fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n")
-				bits := ChooseNumber("Bits", 64, 1024)
-				password, err = Password(bits)
-				if err != nil {
-					log.Fatalf("Failed to make password: %v", err)
-				}
-				fmt.Printf("Your password is: %s\n", password)
-				fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
-					"password itself) will be stored under your \nconfiguration file, so keep this generated password " +
-					"in a safe place.\n")
-				if Confirm(true) {
-					break
-				}
-			}
-		case 'n':
-			return ""
-		default:
-			fs.Errorf(nil, "Bad choice %c", i)
-		}
-		return obscure.MustObscure(password)
-	}
-	what := fmt.Sprintf("%T value", o.Default)
-	switch o.Default.(type) {
-	case bool:
-		what = "boolean value (true or false)"
-	case fs.SizeSuffix:
-		what = "size with suffix k,M,G,T"
-	case fs.Duration:
-		what = "duration s,m,h,d,w,M,y"
-	case int, int8, int16, int32, int64:
-		what = "signed integer"
-	case uint, byte, uint16, uint32, uint64:
-		what = "unsigned integer"
-	}
-	var in string
-	for {
-		fmt.Printf("Enter a %s. Press Enter for the default (%q).\n", what, fmt.Sprint(o.Default))
-		if len(o.Examples) > 0 {
-			var values []string
-			var help []string
-			for _, example := range o.Examples {
-				if matchProvider(example.Provider, subProvider) {
-					values = append(values, example.Value)
-					help = append(help, example.Help)
-				}
-			}
-			in = Choose(o.Name, values, help, true)
-		} else {
-			fmt.Printf("%s> ", o.Name)
-			in = ReadLine()
-		}
-		if in == "" {
-			if o.Required && fmt.Sprint(o.Default) == "" {
-				fmt.Printf("This value is required and it has no default.\n")
-				continue
-			}
-			break
-		}
-		newIn, err := configstruct.StringToInterface(o.Default, in)
-		if err != nil {
-			fmt.Printf("Failed to parse %q: %v\n", in, err)
-			continue
-		}
-		in = fmt.Sprint(newIn) // canonicalise
-		break
+	value, found := Data.GetValue(section, key)
+	if !found {
+		return "", errors.New("value not found")
 	}
-	return in
-}
-
-// Suppress the confirm prompts by altering the context config
-func suppressConfirm(ctx context.Context) context.Context {
-	newCtx, ci := fs.AddConfig(ctx)
-	ci.AutoConfirm = true
-	return newCtx
+	return value, nil
 }
 
 // UpdateRemote adds the keyValues passed in to the remote of name.
@@ -1079,7 +340,7 @@ func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, doObscu
 				}
 			}
 		}
-		getConfigData().SetValue(name, k, vStr)
+		Data.SetValue(name, k, vStr)
 	}
 	RemoteConfig(ctx, name)
 	SaveConfig()
@@ -1095,9 +356,9 @@ func CreateRemote(ctx context.Context, name string, provider string, keyValues r
 		return err
 	}
 	// Delete the old config if it exists
-	getConfigData().DeleteSection(name)
+	Data.DeleteSection(name)
 	// Set the type
-	getConfigData().SetValue(name, "type", provider)
+	Data.SetValue(name, "type", provider)
 	// Set the remaining values
 	return UpdateRemote(ctx, name, keyValues, doObscure, noObscure)
 }
@@ -1147,306 +408,30 @@ func fsOption() *fs.Option {
 	return o
 }
 
-// NewRemoteName asks the user for a name for a new remote
-func NewRemoteName() (name string) {
-	for {
-		fmt.Printf("name> ")
-		name = ReadLine()
-		_, err := getConfigData().GetSection(name)
-		if err == nil {
-			fmt.Printf("Remote %q already exists.\n", name)
-			continue
-		}
-		err = fspath.CheckConfigName(name)
-		switch {
-		case name == "":
-			fmt.Printf("Can't use empty name.\n")
-		case driveletter.IsDriveLetter(name):
-			fmt.Printf("Can't use %q as it can be confused with a drive letter.\n", name)
-		case err != nil:
-			fmt.Printf("Can't use %q as %v.\n", name, err)
-		default:
-			return name
-		}
-	}
-}
-
-// editOptions edits the options.  If new is true then it just allows
-// entry and doesn't show any old values.
-func editOptions(ri *fs.RegInfo, name string, isNew bool) {
-	fmt.Printf("** See help for %s backend at: https://rclone.org/%s/ **\n\n", ri.Name, ri.FileName())
-	hasAdvanced := false
-	for _, advanced := range []bool{false, true} {
-		if advanced {
-			if !hasAdvanced {
-				break
-			}
-			fmt.Printf("Edit advanced config? (y/n)\n")
-			if !Confirm(false) {
-				break
-			}
-		}
-		for _, option := range ri.Options {
-			isVisible := option.Hide&fs.OptionHideConfigurator == 0
-			hasAdvanced = hasAdvanced || (option.Advanced && isVisible)
-			if option.Advanced != advanced {
-				continue
-			}
-			subProvider := getConfigData().MustValue(name, fs.ConfigProvider, "")
-			if matchProvider(option.Provider, subProvider) && isVisible {
-				if !isNew {
-					fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
-					fmt.Printf("Edit? (y/n)>\n")
-					if !Confirm(false) {
-						continue
-					}
-				}
-				FileSet(name, option.Name, ChooseOption(&option, name))
-			}
-		}
-	}
-}
-
-// NewRemote make a new remote from its name
-func NewRemote(ctx context.Context, name string) {
-	var (
-		newType string
-		ri      *fs.RegInfo
-		err     error
-	)
-
-	// Set the type first
-	for {
-		newType = ChooseOption(fsOption(), name)
-		ri, err = fs.Find(newType)
-		if err != nil {
-			fmt.Printf("Bad remote %q: %v\n", newType, err)
-			continue
-		}
-		break
-	}
-	getConfigData().SetValue(name, "type", newType)
-
-	editOptions(ri, name, true)
-	RemoteConfig(ctx, name)
-	if OkRemote(name) {
-		SaveConfig()
-		return
-	}
-	EditRemote(ctx, ri, name)
-}
-
-// EditRemote gets the user to edit a remote
-func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
-	ShowRemote(name)
-	fmt.Printf("Edit remote\n")
-	for {
-		editOptions(ri, name, false)
-		if OkRemote(name) {
-			break
-		}
-	}
-	SaveConfig()
-	RemoteConfig(ctx, name)
-}
-
-// DeleteRemote gets the user to delete a remote
-func DeleteRemote(name string) {
-	getConfigData().DeleteSection(name)
-	SaveConfig()
-}
-
-// copyRemote asks the user for a new remote name and copies name into
-// it. Returns the new name.
-func copyRemote(name string) string {
-	newName := NewRemoteName()
-	// Copy the keys
-	for _, key := range getConfigData().GetKeyList(name) {
-		value := getConfigData().MustValue(name, key, "")
-		getConfigData().SetValue(newName, key, value)
-	}
-	return newName
-}
-
-// RenameRemote renames a config section
-func RenameRemote(name string) {
-	fmt.Printf("Enter new name for %q remote.\n", name)
-	newName := copyRemote(name)
-	if name != newName {
-		getConfigData().DeleteSection(name)
-		SaveConfig()
-	}
-}
-
-// CopyRemote copies a config section
-func CopyRemote(name string) {
-	fmt.Printf("Enter name for copy of %q remote.\n", name)
-	copyRemote(name)
-	SaveConfig()
-}
-
-// ShowConfigLocation prints the location of the config file in use
-func ShowConfigLocation() {
-	if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
-		fmt.Println("Configuration file doesn't exist, but rclone will use this path:")
-	} else {
-		fmt.Println("Configuration file is stored at:")
-	}
-	fmt.Printf("%s\n", ConfigPath)
-}
-
-// ShowConfig prints the (unencrypted) config options
-func ShowConfig() {
-	var buf bytes.Buffer
-	if err := goconfig.SaveConfigData(getConfigData(), &buf); err != nil {
-		log.Fatalf("Failed to serialize config: %v", err)
-	}
-	str := buf.String()
-	if str == "" {
-		str = "; empty config\n"
-	}
-	fmt.Printf("%s", str)
-}
-
-// EditConfig edits the config file interactively
-func EditConfig(ctx context.Context) {
-	for {
-		haveRemotes := len(getConfigData().GetSectionList()) != 0
-		what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
-		if haveRemotes {
-			fmt.Printf("Current remotes:\n\n")
-			ShowRemotes()
-			fmt.Printf("\n")
-		} else {
-			fmt.Printf("No remotes found - make a new one\n")
-			// take 2nd item and last 2 items of menu list
-			what = append(what[1:2], what[len(what)-2:]...)
-		}
-		switch i := Command(what); i {
-		case 'e':
-			name := ChooseRemote()
-			fs := MustFindByName(name)
-			EditRemote(ctx, fs, name)
-		case 'n':
-			NewRemote(ctx, NewRemoteName())
-		case 'd':
-			name := ChooseRemote()
-			DeleteRemote(name)
-		case 'r':
-			RenameRemote(ChooseRemote())
-		case 'c':
-			CopyRemote(ChooseRemote())
-		case 's':
-			SetPassword()
-		case 'q':
-			return
-
-		}
-	}
-}
-
-// SetPassword will allow the user to modify the current
-// configuration encryption settings.
-func SetPassword() {
-	for {
-		if len(configKey) > 0 {
-			fmt.Println("Your configuration is encrypted.")
-			what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
-			switch i := Command(what); i {
-			case 'c':
-				changeConfigPassword()
-				SaveConfig()
-				fmt.Println("Password changed")
-				continue
-			case 'u':
-				configKey = nil
-				SaveConfig()
-				continue
-			case 'q':
-				return
-			}
-
-		} else {
-			fmt.Println("Your configuration is not encrypted.")
-			fmt.Println("If you add a password, you will protect your login information to cloud services.")
-			what := []string{"aAdd Password", "qQuit to main menu"}
-			switch i := Command(what); i {
-			case 'a':
-				changeConfigPassword()
-				SaveConfig()
-				fmt.Println("Password set")
-				continue
-			case 'q':
-				return
-			}
-		}
-	}
-}
-
-// Authorize is for remote authorization of headless machines.
-//
-// It expects 1 or 3 arguments
-//
-//   rclone authorize "fs name"
-//   rclone authorize "fs name" "client id" "client secret"
-func Authorize(ctx context.Context, args []string, noAutoBrowser bool) {
-	ctx = suppressConfirm(ctx)
-	switch len(args) {
-	case 1, 3:
-	default:
-		log.Fatalf("Invalid number of arguments: %d", len(args))
-	}
-	newType := args[0]
-	f := fs.MustFind(newType)
-	if f.Config == nil {
-		log.Fatalf("Can't authorize fs %q", newType)
-	}
-	// Name used for temporary fs
-	name := "**temp-fs**"
-
-	// Make sure we delete it
-	defer DeleteRemote(name)
-
-	// Indicate that we are running rclone authorize
-	getConfigData().SetValue(name, ConfigAuthorize, "true")
-	if noAutoBrowser {
-		getConfigData().SetValue(name, ConfigAuthNoBrowser, "true")
-	}
-
-	if len(args) == 3 {
-		getConfigData().SetValue(name, ConfigClientID, args[1])
-		getConfigData().SetValue(name, ConfigClientSecret, args[2])
-	}
-
-	m := fs.ConfigMap(f, name)
-	f.Config(ctx, name, m)
-}
-
 // FileGetFlag gets the config key under section returning the
 // the value and true if found and or ("", false) otherwise
 func FileGetFlag(section, key string) (string, bool) {
-	newValue, err := getConfigData().GetValue(section, key)
-	return newValue, err == nil
+	return Data.GetValue(section, key)
 }
 
-// FileGet gets the config key under section returning the
-// default or empty string if not set.
+// FileGet gets the config key under section returning the default if not set.
 //
 // It looks up defaults in the environment if they are present
-func FileGet(section, key string, defaultVal ...string) string {
+func FileGet(section, key string) string {
+	var defaultVal string
 	envKey := fs.ConfigToEnv(section, key)
 	newValue, found := os.LookupEnv(envKey)
 	if found {
-		defaultVal = []string{newValue}
+		defaultVal = newValue
 	}
-	return getConfigData().MustValue(section, key, defaultVal...)
+	return getWithDefault(section, key, defaultVal)
 }
 
 // FileSet sets the key in section to value.  It doesn't save
 // the config file.
 func FileSet(section, key, value string) {
 	if value != "" {
-		getConfigData().SetValue(section, key, value)
+		Data.SetValue(section, key, value)
 	} else {
 		FileDeleteKey(section, key)
 	}
@@ -1456,25 +441,20 @@ func FileSet(section, key, value string) {
 // It returns true if the key was deleted,
 // or returns false if the section or key didn't exist.
 func FileDeleteKey(section, key string) bool {
-	return getConfigData().DeleteKey(section, key)
+	return Data.DeleteKey(section, key)
 }
 
 var matchEnv = regexp.MustCompile(`^RCLONE_CONFIG_(.*?)_TYPE=.*$`)
 
 // FileRefresh ensures the latest configFile is loaded from disk
 func FileRefresh() error {
-	reloadedConfigFile, err := loadConfigFile()
-	if err != nil {
-		return err
-	}
-	configFile = reloadedConfigFile
-	return nil
+	return Data.Load()
 }
 
 // FileSections returns the sections in the config file
 // including any defined by environment variables.
 func FileSections() []string {
-	sections := getConfigData().GetSectionList()
+	sections := Data.GetSectionList()
 	for _, item := range os.Environ() {
 		matches := matchEnv.FindStringSubmatch(item)
 		if len(matches) == 2 {
@@ -1487,7 +467,7 @@ func FileSections() []string {
 // DumpRcRemote dumps the config for a single remote
 func DumpRcRemote(name string) (dump rc.Params) {
 	params := rc.Params{}
-	for _, key := range getConfigData().GetKeyList(name) {
+	for _, key := range Data.GetKeyList(name) {
 		params[key] = FileGet(name, key)
 	}
 	return params
@@ -1497,7 +477,7 @@ func DumpRcRemote(name string) (dump rc.Params) {
 // for the rc
 func DumpRcBlob() (dump rc.Params) {
 	dump = rc.Params{}
-	for _, name := range getConfigData().GetSectionList() {
+	for _, name := range Data.GetSectionList() {
 		dump[name] = DumpRcRemote(name)
 	}
 	return dump
diff --git a/fs/config/configfile/configfile.go b/fs/config/configfile/configfile.go
new file mode 100644
index 000000000..8a87ecb0d
--- /dev/null
+++ b/fs/config/configfile/configfile.go
@@ -0,0 +1,178 @@
+package configfile
+
+import (
+	"bytes"
+	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/Unknwon/goconfig"
+	"github.com/pkg/errors"
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config"
+)
+
+// LoadConfig installs the config file handler and calls config.LoadConfig
+func LoadConfig(ctx context.Context) {
+	config.Data = &Storage{}
+	config.LoadConfig(ctx)
+}
+
+// Storage implements config.Storage for saving and loading config
+// data in a simple INI based file.
+type Storage struct {
+	gc *goconfig.ConfigFile
+}
+
+// Load the config from permanent storage, decrypting if necessary
+func (s *Storage) Load() (err error) {
+	// Make sure we have a sensible default even when we error
+	defer func() {
+		if err != nil {
+			s.gc, _ = goconfig.LoadFromReader(bytes.NewReader([]byte{}))
+		}
+	}()
+
+	b, err := os.Open(config.ConfigPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return config.ErrorConfigFileNotFound
+		}
+		return err
+	}
+	defer fs.CheckClose(b, &err)
+
+	cryptReader, err := config.Decrypt(b)
+	if err != nil {
+		return err
+	}
+
+	gc, err := goconfig.LoadFromReader(cryptReader)
+	if err != nil {
+		return err
+	}
+	s.gc = gc
+
+	return nil
+}
+
+// Save the config to permanent storage, encrypting if necessary
+func (s *Storage) Save() error {
+	dir, name := filepath.Split(config.ConfigPath)
+	err := os.MkdirAll(dir, os.ModePerm)
+	if err != nil {
+		return errors.Wrap(err, "failed to create config directory")
+	}
+	f, err := ioutil.TempFile(dir, name)
+	if err != nil {
+		return errors.Errorf("Failed to create temp file for new config: %v", err)
+	}
+	defer func() {
+		_ = f.Close()
+		if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
+			fs.Errorf(nil, "Failed to remove temp config file: %v", err)
+		}
+	}()
+
+	var buf bytes.Buffer
+	if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
+		return errors.Errorf("Failed to save config file: %v", err)
+	}
+
+	if err := config.Encrypt(&buf, f); err != nil {
+		return err
+	}
+
+	_ = f.Sync()
+	err = f.Close()
+	if err != nil {
+		return errors.Errorf("Failed to close config file: %v", err)
+	}
+
+	var fileMode os.FileMode = 0600
+	info, err := os.Stat(config.ConfigPath)
+	if err != nil {
+		fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
+	} else if info.Mode() != fileMode {
+		fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
+		fileMode = info.Mode()
+	}
+
+	attemptCopyGroup(config.ConfigPath, f.Name())
+
+	err = os.Chmod(f.Name(), fileMode)
+	if err != nil {
+		fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
+	}
+
+	if err = os.Rename(config.ConfigPath, config.ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
+		return errors.Errorf("Failed to move previous config to backup location: %v", err)
+	}
+	if err = os.Rename(f.Name(), config.ConfigPath); err != nil {
+		return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
+	}
+	if err := os.Remove(config.ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
+		fs.Errorf(nil, "Failed to remove backup config file: %v", err)
+	}
+
+	return nil
+}
+
+// Serialize the config into a string
+func (s *Storage) Serialize() (string, error) {
+	var buf bytes.Buffer
+	if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
+		return "", errors.Errorf("Failed to save config file: %v", err)
+	}
+
+	return buf.String(), nil
+}
+
+// HasSection returns true if section exists in the config file
+func (s *Storage) HasSection(section string) bool {
+	_, err := s.gc.GetSection(section)
+	if err != nil {
+		return false
+	}
+	return true
+}
+
+// DeleteSection removes the named section and all config from the
+// config file
+func (s *Storage) DeleteSection(section string) {
+	s.gc.DeleteSection(section)
+}
+
+// GetSectionList returns a slice of strings with names for all the
+// sections
+func (s *Storage) GetSectionList() []string {
+	return s.gc.GetSectionList()
+}
+
+// GetKeyList returns the keys in this section
+func (s *Storage) GetKeyList(section string) []string {
+	return s.gc.GetKeyList(section)
+}
+
+// GetValue returns the key in section with a found flag
+func (s *Storage) GetValue(section string, key string) (value string, found bool) {
+	value, err := s.gc.GetValue(section, key)
+	if err != nil {
+		return "", false
+	}
+	return value, true
+}
+
+// SetValue sets the value under key in section
+func (s *Storage) SetValue(section string, key string, value string) {
+	s.gc.SetValue(section, key, value)
+}
+
+// DeleteKey removes the key under section
+func (s *Storage) DeleteKey(section string, key string) bool {
+	return s.gc.DeleteKey(section, key)
+}
+
+// Check the interface is satisfied
+var _ config.Storage = (*Storage)(nil)
diff --git a/fs/config/config_other.go b/fs/config/configfile/configfile_other.go
similarity index 94%
rename from fs/config/config_other.go
rename to fs/config/configfile/configfile_other.go
index e9024a801..a55dc26c5 100644
--- a/fs/config/config_other.go
+++ b/fs/config/configfile/configfile_other.go
@@ -3,7 +3,7 @@
 
 // +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
 
-package config
+package configfile
 
 // attemptCopyGroups tries to keep the group the same, which only makes sense
 // for system with user-group-world permission model.
diff --git a/fs/config/config_unix.go b/fs/config/configfile/configfile_unix.go
similarity index 97%
rename from fs/config/config_unix.go
rename to fs/config/configfile/configfile_unix.go
index 8db2c4c2b..bfdae94ef 100644
--- a/fs/config/config_unix.go
+++ b/fs/config/configfile/configfile_unix.go
@@ -3,7 +3,7 @@
 
 // +build darwin dragonfly freebsd linux netbsd openbsd solaris
 
-package config
+package configfile
 
 import (
 	"os"
diff --git a/fs/config/crypt.go b/fs/config/crypt.go
new file mode 100644
index 000000000..98015f6c8
--- /dev/null
+++ b/fs/config/crypt.go
@@ -0,0 +1,307 @@
+package config
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/pkg/errors"
+	"golang.org/x/crypto/nacl/secretbox"
+
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config/obscure"
+)
+
+var (
+	// Key to use for password en/decryption.
+	// When nil, no encryption will be used for saving.
+	configKey []byte
+
+	// PasswordPromptOutput is output of prompt for password
+	PasswordPromptOutput = os.Stderr
+
+	// PassConfigKeyForDaemonization if set to true, the configKey
+	// is obscured with obscure.Obscure and saved to a temp file
+	// when it is calculated from the password. The path of that
+	// temp file is then written to the environment variable
+	// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is
+	// present, password prompt is skipped and
+	// `RCLONE_CONFIG_PASS` ignored. For security reasons, the
+	// temp file is deleted once the configKey is successfully
+	// loaded. This can be used to pass the configKey to a child
+	// process.
+	PassConfigKeyForDaemonization = false
+)
+
+// Decrypt will automatically decrypt a reader
+func Decrypt(b io.ReadSeeker) (io.Reader, error) {
+	ctx := context.Background()
+	ci := fs.GetConfig(ctx)
+	var usingPasswordCommand bool
+
+	// Find first non-empty line
+	r := bufio.NewReader(b)
+	for {
+		line, _, err := r.ReadLine()
+		if err != nil {
+			if err == io.EOF {
+				if _, err := b.Seek(0, io.SeekStart); err != nil {
+					return nil, err
+				}
+				return b, nil
+			}
+			return nil, err
+		}
+		l := strings.TrimSpace(string(line))
+		if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
+			continue
+		}
+		// First non-empty or non-comment must be ENCRYPT_V0
+		if l == "RCLONE_ENCRYPT_V0:" {
+			break
+		}
+		if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
+			return nil, errors.New("unsupported configuration encryption - update rclone for support")
+		}
+		if _, err := b.Seek(0, io.SeekStart); err != nil {
+			return nil, err
+		}
+		return b, nil
+	}
+
+	if len(configKey) == 0 {
+		if len(ci.PasswordCommand) != 0 {
+			var stdout bytes.Buffer
+			var stderr bytes.Buffer
+
+			cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)
+
+			cmd.Stdout = &stdout
+			cmd.Stderr = &stderr
+			cmd.Stdin = os.Stdin
+
+			if err := cmd.Run(); err != nil {
+				// One does not always get the stderr returned in the wrapped error.
+				fs.Errorf(nil, "Using --password-command returned: %v", err)
+				if ers := strings.TrimSpace(stderr.String()); ers != "" {
+					fs.Errorf(nil, "--password-command stderr: %s", ers)
+				}
+				return nil, errors.Wrap(err, "password command failed")
+			}
+			if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
+				err := SetConfigPassword(pass)
+				if err != nil {
+					return nil, errors.Wrap(err, "incorrect password")
+				}
+			} else {
+				return nil, errors.New("password-command returned empty string")
+			}
+
+			if len(configKey) == 0 {
+				return nil, errors.New("unable to decrypt configuration: incorrect password")
+			}
+			usingPasswordCommand = true
+		} else {
+			usingPasswordCommand = false
+
+			envpw := os.Getenv("RCLONE_CONFIG_PASS")
+
+			if envpw != "" {
+				err := SetConfigPassword(envpw)
+				if err != nil {
+					fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
+				} else {
+					fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.")
+				}
+			}
+		}
+	}
+
+	// Encrypted content is base64 encoded.
+	dec := base64.NewDecoder(base64.StdEncoding, r)
+	box, err := ioutil.ReadAll(dec)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to load base64 encoded data")
+	}
+	if len(box) < 24+secretbox.Overhead {
+		return nil, errors.New("Configuration data too short")
+	}
+
+	var out []byte
+	for {
+		if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
+			fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
+			obscuredKey, err := ioutil.ReadFile(envKeyFile)
+			if err != nil {
+				errRemove := os.Remove(envKeyFile)
+				if errRemove != nil {
+					log.Fatalf("unable to read obscured config key and unable to delete the temp file: %v", err)
+				}
+				log.Fatalf("unable to read obscured config key: %v", err)
+			}
+			errRemove := os.Remove(envKeyFile)
+			if errRemove != nil {
+				log.Fatalf("unable to delete temp file with configKey: %v", err)
+			}
+			configKey = []byte(obscure.MustReveal(string(obscuredKey)))
+			fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
+		} else {
+			if len(configKey) == 0 {
+				if usingPasswordCommand {
+					return nil, errors.New("using --password-command derived password, unable to decrypt configuration")
+				}
+				if !ci.AskPassword {
+					return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
+				}
+				getConfigPassword("Enter configuration password:")
+			}
+		}
+
+		// Nonce is first 24 bytes of the ciphertext
+		var nonce [24]byte
+		copy(nonce[:], box[:24])
+		var key [32]byte
+		copy(key[:], configKey[:32])
+
+		// Attempt to decrypt
+		var ok bool
+		out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
+		if ok {
+			break
+		}
+
+		// Retry
+		fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
+		configKey = nil
+	}
+	return bytes.NewReader(out), nil
+}
+
+// Encrypt the config file
+func Encrypt(src io.Reader, dst io.Writer) error {
+	if len(configKey) == 0 {
+		_, err := io.Copy(dst, src)
+		return err
+	}
+
+	_, _ = fmt.Fprintln(dst, "# Encrypted rclone configuration File")
+	_, _ = fmt.Fprintln(dst, "")
+	_, _ = fmt.Fprintln(dst, "RCLONE_ENCRYPT_V0:")
+
+	// Generate new nonce and write it to the start of the ciphertext
+	var nonce [24]byte
+	n, _ := rand.Read(nonce[:])
+	if n != 24 {
+		return errors.Errorf("nonce short read: %d", n)
+	}
+	enc := base64.NewEncoder(base64.StdEncoding, dst)
+	_, err := enc.Write(nonce[:])
+	if err != nil {
+		return errors.Errorf("Failed to write config file: %v", err)
+	}
+
+	var key [32]byte
+	copy(key[:], configKey[:32])
+
+	data, err := ioutil.ReadAll(src)
+	if err != nil {
+		return err
+	}
+	b := secretbox.Seal(nil, data, &nonce, &key)
+	_, err = enc.Write(b)
+	if err != nil {
+		return errors.Errorf("Failed to write config file: %v", err)
+	}
+	return enc.Close()
+}
+
+// getConfigPassword will query the user for a password the
+// first time it is required.
+func getConfigPassword(q string) {
+	if len(configKey) != 0 {
+		return
+	}
+	for {
+		password := GetPassword(q)
+		err := SetConfigPassword(password)
+		if err == nil {
+			return
+		}
+		_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
+	}
+}
+
+// SetConfigPassword will set the configKey to the hash of
+// the password. If the length of the password is
+// zero after trimming+normalization, an error is returned.
+func SetConfigPassword(password string) error {
+	password, err := checkPassword(password)
+	if err != nil {
+		return err
+	}
+	// Create SHA256 has of the password
+	sha := sha256.New()
+	_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
+	if err != nil {
+		return err
+	}
+	configKey = sha.Sum(nil)
+	if PassConfigKeyForDaemonization {
+		tempFile, err := ioutil.TempFile("", "rclone")
+		if err != nil {
+			log.Fatalf("cannot create temp file to store configKey: %v", err)
+		}
+		_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
+		if err != nil {
+			errRemove := os.Remove(tempFile.Name())
+			if errRemove != nil {
+				log.Fatalf("error writing configKey to temp file and also error deleting it: %v", err)
+			}
+			log.Fatalf("error writing configKey to temp file: %v", err)
+		}
+		err = tempFile.Close()
+		if err != nil {
+			errRemove := os.Remove(tempFile.Name())
+			if errRemove != nil {
+				log.Fatalf("error closing temp file with configKey and also error deleting it: %v", err)
+			}
+			log.Fatalf("error closing temp file with configKey: %v", err)
+		}
+		fs.Debugf(nil, "saving configKey to temp file")
+		err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
+		if err != nil {
+			errRemove := os.Remove(tempFile.Name())
+			if errRemove != nil {
+				log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %v", err)
+			}
+			log.Fatalf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %v", err)
+		}
+	}
+	return nil
+}
+
+// ClearConfigPassword sets the current the password to empty
+func ClearConfigPassword() {
+	configKey = nil
+}
+
+// changeConfigPassword will query the user twice
+// for a password. If the same password is entered
+// twice the key is updated.
+func changeConfigPassword() {
+	err := SetConfigPassword(ChangePassword("NEW configuration"))
+	if err != nil {
+		fmt.Printf("Failed to set config password: %v\n", err)
+		return
+	}
+}
diff --git a/fs/config/default_storage.go b/fs/config/default_storage.go
new file mode 100644
index 000000000..9998dd76a
--- /dev/null
+++ b/fs/config/default_storage.go
@@ -0,0 +1,61 @@
+package config
+
+// Default config.Storage which panics with a useful error when used
+type defaultStorage struct{}
+
+var noConfigStorage = "internal error: no config file system found. Did you call configfile.LoadConfig(ctx)?"
+
+// GetSectionList returns a slice of strings with names for all the
+// sections
+func (defaultStorage) GetSectionList() []string {
+	panic(noConfigStorage)
+}
+
+// HasSection returns true if section exists in the config file
+func (defaultStorage) HasSection(section string) bool {
+	panic(noConfigStorage)
+}
+
+// DeleteSection removes the named section and all config from the
+// config file
+func (defaultStorage) DeleteSection(section string) {
+	panic(noConfigStorage)
+}
+
+// GetKeyList returns the keys in this section
+func (defaultStorage) GetKeyList(section string) []string {
+	panic(noConfigStorage)
+}
+
+// GetValue returns the key in section with a found flag
+func (defaultStorage) GetValue(section string, key string) (value string, found bool) {
+	panic(noConfigStorage)
+}
+
+// SetValue sets the value under key in section
+func (defaultStorage) SetValue(section string, key string, value string) {
+	panic(noConfigStorage)
+}
+
+// DeleteKey removes the key under section
+func (defaultStorage) DeleteKey(section string, key string) bool {
+	panic(noConfigStorage)
+}
+
+// Load the config from permanent storage
+func (defaultStorage) Load() error {
+	panic(noConfigStorage)
+}
+
+// Save the config to permanent storage
+func (defaultStorage) Save() error {
+	panic(noConfigStorage)
+}
+
+// Serialize the config into a string
+func (defaultStorage) Serialize() (string, error) {
+	panic(noConfigStorage)
+}
+
+// Check the interface is satisfied
+var _ Storage = defaultStorage{}
diff --git a/fs/config/rc.go b/fs/config/rc.go
index 3a08448ce..893291a2b 100644
--- a/fs/config/rc.go
+++ b/fs/config/rc.go
@@ -72,7 +72,7 @@ See the [listremotes command](/commands/rclone_listremotes/) command for more in
 // Return the a list of remotes in the config file
 func rcListRemotes(ctx context.Context, in rc.Params) (out rc.Params, err error) {
 	var remotes = []string{}
-	for _, remote := range getConfigData().GetSectionList() {
+	for _, remote := range Data.GetSectionList() {
 		remotes = append(remotes, remote)
 	}
 	out = rc.Params{
diff --git a/fs/config/ui.go b/fs/config/ui.go
new file mode 100644
index 000000000..595b99492
--- /dev/null
+++ b/fs/config/ui.go
@@ -0,0 +1,684 @@
+// Textual user interface parts of the config system
+
+package config
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"unicode/utf8"
+
+	"github.com/pkg/errors"
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config/configmap"
+	"github.com/rclone/rclone/fs/config/configstruct"
+	"github.com/rclone/rclone/fs/config/obscure"
+	"github.com/rclone/rclone/fs/driveletter"
+	"github.com/rclone/rclone/fs/fspath"
+	"github.com/rclone/rclone/lib/terminal"
+	"golang.org/x/text/unicode/norm"
+)
+
+// ReadLine reads some input
+var ReadLine = func() string {
+	buf := bufio.NewReader(os.Stdin)
+	line, err := buf.ReadString('\n')
+	if err != nil {
+		log.Fatalf("Failed to read line: %v", err)
+	}
+	return strings.TrimSpace(line)
+}
+
+// ReadNonEmptyLine prints prompt and calls Readline until non empty
+func ReadNonEmptyLine(prompt string) string {
+	result := ""
+	for result == "" {
+		fmt.Print(prompt)
+		result = strings.TrimSpace(ReadLine())
+	}
+	return result
+}
+
+// CommandDefault - choose one.  If return is pressed then it will
+// chose the defaultIndex if it is >= 0
+func CommandDefault(commands []string, defaultIndex int) byte {
+	opts := []string{}
+	for i, text := range commands {
+		def := ""
+		if i == defaultIndex {
+			def = " (default)"
+		}
+		fmt.Printf("%c) %s%s\n", text[0], text[1:], def)
+		opts = append(opts, text[:1])
+	}
+	optString := strings.Join(opts, "")
+	optHelp := strings.Join(opts, "/")
+	for {
+		fmt.Printf("%s> ", optHelp)
+		result := strings.ToLower(ReadLine())
+		if len(result) == 0 && defaultIndex >= 0 {
+			return optString[defaultIndex]
+		}
+		if len(result) != 1 {
+			continue
+		}
+		i := strings.Index(optString, string(result[0]))
+		if i >= 0 {
+			return result[0]
+		}
+	}
+}
+
+// Command - choose one
+func Command(commands []string) byte {
+	return CommandDefault(commands, -1)
+}
+
+// Confirm asks the user for Yes or No and returns true or false
+//
+// If the user presses enter then the Default will be used
+func Confirm(Default bool) bool {
+	defaultIndex := 0
+	if !Default {
+		defaultIndex = 1
+	}
+	return CommandDefault([]string{"yYes", "nNo"}, defaultIndex) == 'y'
+}
+
+// ConfirmWithConfig asks the user for Yes or No and returns true or
+// false.
+//
+// If AutoConfirm is set, it will look up the value in m and return
+// that, but if it isn't set then it will return the Default value
+// passed in
+func ConfirmWithConfig(ctx context.Context, m configmap.Getter, configName string, Default bool) bool {
+	ci := fs.GetConfig(ctx)
+	if ci.AutoConfirm {
+		configString, ok := m.Get(configName)
+		if ok {
+			configValue, err := strconv.ParseBool(configString)
+			if err != nil {
+				fs.Errorf(nil, "Failed to parse config parameter %s=%q as boolean - using default %v: %v", configName, configString, Default, err)
+			} else {
+				Default = configValue
+			}
+		}
+		answer := "No"
+		if Default {
+			answer = "Yes"
+		}
+		fmt.Printf("Auto confirm is set: answering %s, override by setting config parameter %s=%v\n", answer, configName, !Default)
+		return Default
+	}
+	return Confirm(Default)
+}
+
+// Choose one of the defaults or type a new string if newOk is set
+func Choose(what string, defaults, help []string, newOk bool) string {
+	valueDescription := "an existing"
+	if newOk {
+		valueDescription = "your own"
+	}
+	fmt.Printf("Choose a number from below, or type in %s value\n", valueDescription)
+	attributes := []string{terminal.HiRedFg, terminal.HiGreenFg}
+	for i, text := range defaults {
+		var lines []string
+		if help != nil {
+			parts := strings.Split(help[i], "\n")
+			lines = append(lines, parts...)
+		}
+		lines = append(lines, fmt.Sprintf("%q", text))
+		pos := i + 1
+		terminal.WriteString(attributes[i%len(attributes)])
+		if len(lines) == 1 {
+			fmt.Printf("%2d > %s\n", pos, text)
+		} else {
+			mid := (len(lines) - 1) / 2
+			for i, line := range lines {
+				var sep rune
+				switch i {
+				case 0:
+					sep = '/'
+				case len(lines) - 1:
+					sep = '\\'
+				default:
+					sep = '|'
+				}
+				number := "  "
+				if i == mid {
+					number = fmt.Sprintf("%2d", pos)
+				}
+				fmt.Printf("%s %c %s\n", number, sep, line)
+			}
+		}
+		terminal.WriteString(terminal.Reset)
+	}
+	for {
+		fmt.Printf("%s> ", what)
+		result := ReadLine()
+		i, err := strconv.Atoi(result)
+		if err != nil {
+			if newOk {
+				return result
+			}
+			for _, v := range defaults {
+				if result == v {
+					return result
+				}
+			}
+			continue
+		}
+		if i >= 1 && i <= len(defaults) {
+			return defaults[i-1]
+		}
+	}
+}
+
+// ChooseNumber asks the user to enter a number between min and max
+// inclusive prompting them with what.
+func ChooseNumber(what string, min, max int) int {
+	for {
+		fmt.Printf("%s> ", what)
+		result := ReadLine()
+		i, err := strconv.Atoi(result)
+		if err != nil {
+			fmt.Printf("Bad number: %v\n", err)
+			continue
+		}
+		if i < min || i > max {
+			fmt.Printf("Out of range - %d to %d inclusive\n", min, max)
+			continue
+		}
+		return i
+	}
+}
+
+// ShowRemotes shows an overview of the config file
+func ShowRemotes() {
+	remotes := Data.GetSectionList()
+	if len(remotes) == 0 {
+		return
+	}
+	sort.Strings(remotes)
+	fmt.Printf("%-20s %s\n", "Name", "Type")
+	fmt.Printf("%-20s %s\n", "====", "====")
+	for _, remote := range remotes {
+		fmt.Printf("%-20s %s\n", remote, FileGet(remote, "type"))
+	}
+}
+
+// ChooseRemote chooses a remote name
+func ChooseRemote() string {
+	remotes := Data.GetSectionList()
+	sort.Strings(remotes)
+	return Choose("remote", remotes, nil, false)
+}
+
+// mustFindByName finds the RegInfo for the remote name passed in or
+// exits with a fatal error.
+func mustFindByName(name string) *fs.RegInfo {
+	fsType := FileGet(name, "type")
+	if fsType == "" {
+		log.Fatalf("Couldn't find type of fs for %q", name)
+	}
+	return fs.MustFind(fsType)
+}
+
+// ShowRemote shows the contents of the remote
+func ShowRemote(name string) {
+	fmt.Printf("--------------------\n")
+	fmt.Printf("[%s]\n", name)
+	fs := mustFindByName(name)
+	for _, key := range Data.GetKeyList(name) {
+		isPassword := false
+		for _, option := range fs.Options {
+			if option.Name == key && option.IsPassword {
+				isPassword = true
+				break
+			}
+		}
+		value := FileGet(name, key)
+		if isPassword && value != "" {
+			fmt.Printf("%s = *** ENCRYPTED ***\n", key)
+		} else {
+			fmt.Printf("%s = %s\n", key, value)
+		}
+	}
+	fmt.Printf("--------------------\n")
+}
+
+// OkRemote prints the contents of the remote and ask if it is OK
+func OkRemote(name string) bool {
+	ShowRemote(name)
+	switch i := CommandDefault([]string{"yYes this is OK", "eEdit this remote", "dDelete this remote"}, 0); i {
+	case 'y':
+		return true
+	case 'e':
+		return false
+	case 'd':
+		Data.DeleteSection(name)
+		return true
+	default:
+		fs.Errorf(nil, "Bad choice %c", i)
+	}
+	return false
+}
+
+// RemoteConfig runs the config helper for the remote if needed
+func RemoteConfig(ctx context.Context, name string) {
+	fmt.Printf("Remote config\n")
+	f := mustFindByName(name)
+	if f.Config != nil {
+		m := fs.ConfigMap(f, name)
+		f.Config(ctx, name, m)
+	}
+}
+
+// matchProvider returns true if provider matches the providerConfig string.
+//
+// The providerConfig string can either be a list of providers to
+// match, or if it starts with "!" it will be a list of providers not
+// to match.
+//
+// If either providerConfig or provider is blank then it will return true
+func matchProvider(providerConfig, provider string) bool {
+	if providerConfig == "" || provider == "" {
+		return true
+	}
+	negate := false
+	if strings.HasPrefix(providerConfig, "!") {
+		providerConfig = providerConfig[1:]
+		negate = true
+	}
+	providers := strings.Split(providerConfig, ",")
+	matched := false
+	for _, p := range providers {
+		if p == provider {
+			matched = true
+			break
+		}
+	}
+	if negate {
+		return !matched
+	}
+	return matched
+}
+
+// ChooseOption asks the user to choose an option
+func ChooseOption(o *fs.Option, name string) string {
+	var subProvider = getWithDefault(name, fs.ConfigProvider, "")
+	fmt.Println(o.Help)
+	if o.IsPassword {
+		actions := []string{"yYes type in my own password", "gGenerate random password"}
+		defaultAction := -1
+		if !o.Required {
+			defaultAction = len(actions)
+			actions = append(actions, "nNo leave this optional password blank")
+		}
+		var password string
+		var err error
+		switch i := CommandDefault(actions, defaultAction); i {
+		case 'y':
+			password = ChangePassword("the")
+		case 'g':
+			for {
+				fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n")
+				bits := ChooseNumber("Bits", 64, 1024)
+				password, err = Password(bits)
+				if err != nil {
+					log.Fatalf("Failed to make password: %v", err)
+				}
+				fmt.Printf("Your password is: %s\n", password)
+				fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
+					"password itself) will be stored under your \nconfiguration file, so keep this generated password " +
+					"in a safe place.\n")
+				if Confirm(true) {
+					break
+				}
+			}
+		case 'n':
+			return ""
+		default:
+			fs.Errorf(nil, "Bad choice %c", i)
+		}
+		return obscure.MustObscure(password)
+	}
+	what := fmt.Sprintf("%T value", o.Default)
+	switch o.Default.(type) {
+	case bool:
+		what = "boolean value (true or false)"
+	case fs.SizeSuffix:
+		what = "size with suffix k,M,G,T"
+	case fs.Duration:
+		what = "duration s,m,h,d,w,M,y"
+	case int, int8, int16, int32, int64:
+		what = "signed integer"
+	case uint, byte, uint16, uint32, uint64:
+		what = "unsigned integer"
+	}
+	var in string
+	for {
+		fmt.Printf("Enter a %s. Press Enter for the default (%q).\n", what, fmt.Sprint(o.Default))
+		if len(o.Examples) > 0 {
+			var values []string
+			var help []string
+			for _, example := range o.Examples {
+				if matchProvider(example.Provider, subProvider) {
+					values = append(values, example.Value)
+					help = append(help, example.Help)
+				}
+			}
+			in = Choose(o.Name, values, help, true)
+		} else {
+			fmt.Printf("%s> ", o.Name)
+			in = ReadLine()
+		}
+		if in == "" {
+			if o.Required && fmt.Sprint(o.Default) == "" {
+				fmt.Printf("This value is required and it has no default.\n")
+				continue
+			}
+			break
+		}
+		newIn, err := configstruct.StringToInterface(o.Default, in)
+		if err != nil {
+			fmt.Printf("Failed to parse %q: %v\n", in, err)
+			continue
+		}
+		in = fmt.Sprint(newIn) // canonicalise
+		break
+	}
+	return in
+}
+
+// NewRemoteName asks the user for a name for a new remote
+func NewRemoteName() (name string) {
+	for {
+		fmt.Printf("name> ")
+		name = ReadLine()
+		if Data.HasSection(name) {
+			fmt.Printf("Remote %q already exists.\n", name)
+			continue
+		}
+		err := fspath.CheckConfigName(name)
+		switch {
+		case name == "":
+			fmt.Printf("Can't use empty name.\n")
+		case driveletter.IsDriveLetter(name):
+			fmt.Printf("Can't use %q as it can be confused with a drive letter.\n", name)
+		case err != nil:
+			fmt.Printf("Can't use %q as %v.\n", name, err)
+		default:
+			return name
+		}
+	}
+}
+
+// editOptions edits the options.  If new is true then it just allows
+// entry and doesn't show any old values.
+func editOptions(ri *fs.RegInfo, name string, isNew bool) {
+	fmt.Printf("** See help for %s backend at: https://rclone.org/%s/ **\n\n", ri.Name, ri.FileName())
+	hasAdvanced := false
+	for _, advanced := range []bool{false, true} {
+		if advanced {
+			if !hasAdvanced {
+				break
+			}
+			fmt.Printf("Edit advanced config? (y/n)\n")
+			if !Confirm(false) {
+				break
+			}
+		}
+		for _, option := range ri.Options {
+			isVisible := option.Hide&fs.OptionHideConfigurator == 0
+			hasAdvanced = hasAdvanced || (option.Advanced && isVisible)
+			if option.Advanced != advanced {
+				continue
+			}
+			subProvider := getWithDefault(name, fs.ConfigProvider, "")
+			if matchProvider(option.Provider, subProvider) && isVisible {
+				if !isNew {
+					fmt.Printf("Value %q = %q\n", option.Name, FileGet(name, option.Name))
+					fmt.Printf("Edit? (y/n)>\n")
+					if !Confirm(false) {
+						continue
+					}
+				}
+				FileSet(name, option.Name, ChooseOption(&option, name))
+			}
+		}
+	}
+}
+
+// NewRemote make a new remote from its name
+func NewRemote(ctx context.Context, name string) {
+	var (
+		newType string
+		ri      *fs.RegInfo
+		err     error
+	)
+
+	// Set the type first
+	for {
+		newType = ChooseOption(fsOption(), name)
+		ri, err = fs.Find(newType)
+		if err != nil {
+			fmt.Printf("Bad remote %q: %v\n", newType, err)
+			continue
+		}
+		break
+	}
+	Data.SetValue(name, "type", newType)
+
+	editOptions(ri, name, true)
+	RemoteConfig(ctx, name)
+	if OkRemote(name) {
+		SaveConfig()
+		return
+	}
+	EditRemote(ctx, ri, name)
+}
+
+// EditRemote gets the user to edit a remote
+func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) {
+	ShowRemote(name)
+	fmt.Printf("Edit remote\n")
+	for {
+		editOptions(ri, name, false)
+		if OkRemote(name) {
+			break
+		}
+	}
+	SaveConfig()
+	RemoteConfig(ctx, name)
+}
+
+// DeleteRemote gets the user to delete a remote
+func DeleteRemote(name string) {
+	Data.DeleteSection(name)
+	SaveConfig()
+}
+
+// copyRemote asks the user for a new remote name and copies name into
+// it. Returns the new name.
+func copyRemote(name string) string {
+	newName := NewRemoteName()
+	// Copy the keys
+	for _, key := range Data.GetKeyList(name) {
+		value := getWithDefault(name, key, "")
+		Data.SetValue(newName, key, value)
+	}
+	return newName
+}
+
+// RenameRemote renames a config section
+func RenameRemote(name string) {
+	fmt.Printf("Enter new name for %q remote.\n", name)
+	newName := copyRemote(name)
+	if name != newName {
+		Data.DeleteSection(name)
+		SaveConfig()
+	}
+}
+
+// CopyRemote copies a config section
+func CopyRemote(name string) {
+	fmt.Printf("Enter name for copy of %q remote.\n", name)
+	copyRemote(name)
+	SaveConfig()
+}
+
+// ShowConfigLocation prints the location of the config file in use
+func ShowConfigLocation() {
+	if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
+		fmt.Println("Configuration file doesn't exist, but rclone will use this path:")
+	} else {
+		fmt.Println("Configuration file is stored at:")
+	}
+	fmt.Printf("%s\n", ConfigPath)
+}
+
+// ShowConfig prints the (unencrypted) config options
+func ShowConfig() {
+	str, err := Data.Serialize()
+	if err != nil {
+		log.Fatalf("Failed to serialize config: %v", err)
+	}
+	if str == "" {
+		str = "; empty config\n"
+	}
+	fmt.Printf("%s", str)
+}
+
+// EditConfig edits the config file interactively
+func EditConfig(ctx context.Context) {
+	for {
+		haveRemotes := len(Data.GetSectionList()) != 0
+		what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"}
+		if haveRemotes {
+			fmt.Printf("Current remotes:\n\n")
+			ShowRemotes()
+			fmt.Printf("\n")
+		} else {
+			fmt.Printf("No remotes found - make a new one\n")
+			// take 2nd item and last 2 items of menu list
+			what = append(what[1:2], what[len(what)-2:]...)
+		}
+		switch i := Command(what); i {
+		case 'e':
+			name := ChooseRemote()
+			fs := mustFindByName(name)
+			EditRemote(ctx, fs, name)
+		case 'n':
+			NewRemote(ctx, NewRemoteName())
+		case 'd':
+			name := ChooseRemote()
+			DeleteRemote(name)
+		case 'r':
+			RenameRemote(ChooseRemote())
+		case 'c':
+			CopyRemote(ChooseRemote())
+		case 's':
+			SetPassword()
+		case 'q':
+			return
+
+		}
+	}
+}
+
+// Suppress the confirm prompts by altering the context config
+func suppressConfirm(ctx context.Context) context.Context {
+	newCtx, ci := fs.AddConfig(ctx)
+	ci.AutoConfirm = true
+	return newCtx
+}
+
+// checkPassword normalises and validates the password
+func checkPassword(password string) (string, error) {
+	if !utf8.ValidString(password) {
+		return "", errors.New("password contains invalid utf8 characters")
+	}
+	// Check for leading/trailing whitespace
+	trimmedPassword := strings.TrimSpace(password)
+	// Warn user if password has leading+trailing whitespace
+	if len(password) != len(trimmedPassword) {
+		_, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped")
+	}
+	// Normalize to reduce weird variations.
+	password = norm.NFKC.String(password)
+	if len(password) == 0 || len(trimmedPassword) == 0 {
+		return "", errors.New("no characters in password")
+	}
+	return password, nil
+}
+
+// GetPassword asks the user for a password with the prompt given.
+func GetPassword(prompt string) string {
+	_, _ = fmt.Fprintln(PasswordPromptOutput, prompt)
+	for {
+		_, _ = fmt.Fprint(PasswordPromptOutput, "password:")
+		password := ReadPassword()
+		password, err := checkPassword(password)
+		if err == nil {
+			return password
+		}
+		_, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err)
+	}
+}
+
+// ChangePassword will query the user twice for the named password. If
+// the same password is entered it is returned.
+func ChangePassword(name string) string {
+	for {
+		a := GetPassword(fmt.Sprintf("Enter %s password:", name))
+		b := GetPassword(fmt.Sprintf("Confirm %s password:", name))
+		if a == b {
+			return a
+		}
+		fmt.Println("Passwords do not match!")
+	}
+}
+
+// SetPassword will allow the user to modify the current
+// configuration encryption settings.
+func SetPassword() {
+	for {
+		if len(configKey) > 0 {
+			fmt.Println("Your configuration is encrypted.")
+			what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
+			switch i := Command(what); i {
+			case 'c':
+				changeConfigPassword()
+				SaveConfig()
+				fmt.Println("Password changed")
+				continue
+			case 'u':
+				configKey = nil
+				SaveConfig()
+				continue
+			case 'q':
+				return
+			}
+
+		} else {
+			fmt.Println("Your configuration is not encrypted.")
+			fmt.Println("If you add a password, you will protect your login information to cloud services.")
+			what := []string{"aAdd Password", "qQuit to main menu"}
+			switch i := Command(what); i {
+			case 'a':
+				changeConfigPassword()
+				SaveConfig()
+				fmt.Println("Password set")
+				continue
+			case 'q':
+				return
+			}
+		}
+	}
+}
diff --git a/fs/dir.go b/fs/dir.go
index d876d89a6..63c51a310 100644
--- a/fs/dir.go
+++ b/fs/dir.go
@@ -12,6 +12,7 @@ type Dir struct {
 	size    int64     // size of directory and contents or -1 if unknown
 	items   int64     // number of objects or -1 for unknown
 	id      string    // optional ID
+	parent  string    // optional parent directory ID
 }
 
 // NewDir creates an unspecialized Directory object
@@ -62,6 +63,17 @@ func (d *Dir) SetID(id string) *Dir {
 	return d
 }
 
+// ParentID returns the IDs of the Dir parent if known
+func (d *Dir) ParentID() string {
+	return d.parent
+}
+
+// SetParentID sets the optional parent ID of the Dir
+func (d *Dir) SetParentID(parent string) *Dir {
+	d.parent = parent
+	return d
+}
+
 // ModTime returns the modification date of the file
 // It should return a best guess if one isn't available
 func (d *Dir) ModTime(ctx context.Context) time.Time {
diff --git a/fs/fs.go b/fs/fs.go
index 5ec968936..0e4e73b58 100644
--- a/fs/fs.go
+++ b/fs/fs.go
@@ -396,6 +396,12 @@ type IDer interface {
 	ID() string
 }
 
+// ParentIDer is an optional interface for Object
+type ParentIDer interface {
+	// ParentID returns the ID of the parent directory if known or nil if not
+	ParentID() string
+}
+
 // ObjectUnWrapper is an optional interface for Object
 type ObjectUnWrapper interface {
 	// UnWrap returns the Object that this Object is wrapping or
diff --git a/fs/operations/dedupe.go b/fs/operations/dedupe.go
index df6d37f24..11ad82dd8 100644
--- a/fs/operations/dedupe.go
+++ b/fs/operations/dedupe.go
@@ -247,20 +247,82 @@ func (x *DeduplicateMode) Type() string {
 	return "string"
 }
 
+// Directory with entry count and links to parents
+type dedupeDir struct {
+	dir    fs.Directory
+	parent string
+	count  int
+}
+
+// Map of directories by ID with recursive counts
+type dedupeDirsMap map[string]*dedupeDir
+
+func (dm dedupeDirsMap) get(id string) *dedupeDir {
+	d := dm[id]
+	if d == nil {
+		d = &dedupeDir{}
+		dm[id] = d
+	}
+	return d
+}
+
+func (dm dedupeDirsMap) increment(parent string) {
+	if parent != "" {
+		d := dm.get(parent)
+		d.count++
+		dm.increment(d.parent)
+	}
+}
+
 // dedupeFindDuplicateDirs scans f for duplicate directories
-func dedupeFindDuplicateDirs(ctx context.Context, f fs.Fs) ([][]fs.Directory, error) {
+func dedupeFindDuplicateDirs(ctx context.Context, f fs.Fs) (duplicateDirs [][]*dedupeDir, err error) {
+	dirsByID := dedupeDirsMap{}
+	dirs := map[string][]*dedupeDir{}
+
 	ci := fs.GetConfig(ctx)
-	dirs := map[string][]fs.Directory{}
-	err := walk.ListR(ctx, f, "", true, ci.MaxDepth, walk.ListDirs, func(entries fs.DirEntries) error {
-		entries.ForDir(func(d fs.Directory) {
-			dirs[d.Remote()] = append(dirs[d.Remote()], d)
-		})
+	err = walk.ListR(ctx, f, "", true, ci.MaxDepth, walk.ListAll, func(entries fs.DirEntries) error {
+		for _, entry := range entries {
+			remote := entry.Remote()
+			parentRemote := path.Dir(remote)
+			if parentRemote == "." {
+				parentRemote = ""
+			}
+
+			// Obtain ID of the object parent, if known.
+			// (This usually means that backend allows duplicate paths)
+			// Fall back to remote parent path, if unavailable.
+			var parent string
+			if entryParentIDer, ok := entry.(fs.ParentIDer); ok {
+				parent = entryParentIDer.ParentID()
+			}
+			if parent == "" {
+				parent = parentRemote
+			}
+
+			var ID string
+			if entryIDer, ok := entry.(fs.IDer); ok {
+				ID = entryIDer.ID()
+			}
+			if ID == "" {
+				ID = remote
+			}
+
+			if fsDir, ok := entry.(fs.Directory); ok {
+				d := dirsByID.get(ID)
+				d.dir = fsDir
+				d.parent = parent
+				dirs[remote] = append(dirs[remote], d)
+			}
+
+			dirsByID.increment(parent)
+		}
 		return nil
 	})
 	if err != nil {
 		return nil, errors.Wrap(err, "find duplicate dirs")
 	}
-	// make sure parents are before children
+
+	// Make sure parents are before children
 	duplicateNames := []string{}
 	for name, ds := range dirs {
 		if len(ds) > 1 {
@@ -268,15 +330,15 @@ func dedupeFindDuplicateDirs(ctx context.Context, f fs.Fs) ([][]fs.Directory, er
 		}
 	}
 	sort.Strings(duplicateNames)
-	duplicateDirs := [][]fs.Directory{}
 	for _, name := range duplicateNames {
 		duplicateDirs = append(duplicateDirs, dirs[name])
 	}
-	return duplicateDirs, nil
+
+	return
 }
 
 // dedupeMergeDuplicateDirs merges all the duplicate directories found
-func dedupeMergeDuplicateDirs(ctx context.Context, f fs.Fs, duplicateDirs [][]fs.Directory) error {
+func dedupeMergeDuplicateDirs(ctx context.Context, f fs.Fs, duplicateDirs [][]*dedupeDir) error {
 	mergeDirs := f.Features().MergeDirs
 	if mergeDirs == nil {
 		return errors.Errorf("%v: can't merge directories", f)
@@ -285,15 +347,30 @@ func dedupeMergeDuplicateDirs(ctx context.Context, f fs.Fs, duplicateDirs [][]fs
 	if dirCacheFlush == nil {
 		return errors.Errorf("%v: can't flush dir cache", f)
 	}
-	for _, dirs := range duplicateDirs {
-		if !SkipDestructive(ctx, dirs[0], "merge duplicate directories") {
-			fs.Infof(dirs[0], "Merging contents of duplicate directories")
-			err := mergeDirs(ctx, dirs)
-			if err != nil {
-				err = fs.CountError(err)
-				fs.Errorf(nil, "merge duplicate dirs: %v", err)
+	for _, dedupeDirs := range duplicateDirs {
+		if SkipDestructive(ctx, dedupeDirs[0].dir, "merge duplicate directories") {
+			continue
+		}
+
+		// Put largest directory in front to minimize movements
+		fsDirs := []fs.Directory{}
+		largestCount := -1
+		largestIdx := 0
+		for i, d := range dedupeDirs {
+			fsDirs = append(fsDirs, d.dir)
+			if d.count > largestCount {
+				largestIdx = i
+				largestCount = d.count
 			}
 		}
+		fsDirs[largestIdx], fsDirs[0] = fsDirs[0], fsDirs[largestIdx]
+
+		fs.Infof(fsDirs[0], "Merging contents of duplicate directories")
+		err := mergeDirs(ctx, fsDirs)
+		if err != nil {
+			err = fs.CountError(err)
+			fs.Errorf(nil, "merge duplicate dirs: %v", err)
+		}
 	}
 	dirCacheFlush()
 	return nil
@@ -335,15 +412,16 @@ func Deduplicate(ctx context.Context, f fs.Fs, mode DeduplicateMode, byHash bool
 		if err != nil {
 			return err
 		}
-		if len(duplicateDirs) != 0 {
+		if len(duplicateDirs) > 0 {
 			if mode != DeduplicateList {
 				err = dedupeMergeDuplicateDirs(ctx, f, duplicateDirs)
 				if err != nil {
 					return err
 				}
 			} else {
-				for _, dir := range duplicateDirs {
-					fmt.Printf("%s: %d duplicates of this directory\n", dir[0].Remote(), len(dir))
+				for _, dedupeDirs := range duplicateDirs {
+					remote := dedupeDirs[0].dir.Remote()
+					fmt.Printf("%s: %d duplicates of this directory\n", remote, len(dedupeDirs))
 				}
 			}
 		}
@@ -375,42 +453,43 @@ func Deduplicate(ctx context.Context, f fs.Fs, mode DeduplicateMode, byHash bool
 	}
 
 	for remote, objs := range files {
-		if len(objs) > 1 {
-			fs.Logf(remote, "Found %d files with duplicate %s", len(objs), what)
-			if !byHash && mode != DeduplicateList {
-				objs = dedupeDeleteIdentical(ctx, ht, remote, objs)
-				if len(objs) <= 1 {
-					fs.Logf(remote, "All duplicates removed")
-					continue
-				}
-			}
-			switch mode {
-			case DeduplicateInteractive:
-				dedupeInteractive(ctx, f, ht, remote, objs, byHash)
-			case DeduplicateFirst:
-				dedupeDeleteAllButOne(ctx, 0, remote, objs)
-			case DeduplicateNewest:
-				sortOldestFirst(objs)
-				dedupeDeleteAllButOne(ctx, len(objs)-1, remote, objs)
-			case DeduplicateOldest:
-				sortOldestFirst(objs)
-				dedupeDeleteAllButOne(ctx, 0, remote, objs)
-			case DeduplicateRename:
-				dedupeRename(ctx, f, remote, objs)
-			case DeduplicateLargest:
-				sortSmallestFirst(objs)
-				dedupeDeleteAllButOne(ctx, len(objs)-1, remote, objs)
-			case DeduplicateSmallest:
-				sortSmallestFirst(objs)
-				dedupeDeleteAllButOne(ctx, 0, remote, objs)
-			case DeduplicateSkip:
-				fs.Logf(remote, "Skipping %d files with duplicate %s", len(objs), what)
-			case DeduplicateList:
-				dedupeList(ctx, f, ht, remote, objs, byHash)
-			default:
-				//skip
+		if len(objs) <= 1 {
+			continue
+		}
+		fs.Logf(remote, "Found %d files with duplicate %s", len(objs), what)
+		if !byHash && mode != DeduplicateList {
+			objs = dedupeDeleteIdentical(ctx, ht, remote, objs)
+			if len(objs) <= 1 {
+				fs.Logf(remote, "All duplicates removed")
+				continue
 			}
 		}
+		switch mode {
+		case DeduplicateInteractive:
+			dedupeInteractive(ctx, f, ht, remote, objs, byHash)
+		case DeduplicateFirst:
+			dedupeDeleteAllButOne(ctx, 0, remote, objs)
+		case DeduplicateNewest:
+			sortOldestFirst(objs)
+			dedupeDeleteAllButOne(ctx, len(objs)-1, remote, objs)
+		case DeduplicateOldest:
+			sortOldestFirst(objs)
+			dedupeDeleteAllButOne(ctx, 0, remote, objs)
+		case DeduplicateRename:
+			dedupeRename(ctx, f, remote, objs)
+		case DeduplicateLargest:
+			sortSmallestFirst(objs)
+			dedupeDeleteAllButOne(ctx, len(objs)-1, remote, objs)
+		case DeduplicateSmallest:
+			sortSmallestFirst(objs)
+			dedupeDeleteAllButOne(ctx, 0, remote, objs)
+		case DeduplicateSkip:
+			fs.Logf(remote, "Skipping %d files with duplicate %s", len(objs), what)
+		case DeduplicateList:
+			dedupeList(ctx, f, ht, remote, objs, byHash)
+		default:
+			//skip
+		}
 	}
 	return nil
 }


Test patch:
diff --git a/backend/alias/alias_internal_test.go b/backend/alias/alias_internal_test.go
index 5839a9fff..8b4efa2ec 100644
--- a/backend/alias/alias_internal_test.go
+++ b/backend/alias/alias_internal_test.go
@@ -11,6 +11,7 @@ import (
 	_ "github.com/rclone/rclone/backend/local" // pull in test backend
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/stretchr/testify/require"
 )
 
@@ -19,7 +20,7 @@ var (
 )
 
 func prepare(t *testing.T, root string) {
-	config.LoadConfig(context.Background())
+	configfile.LoadConfig(context.Background())
 
 	// Configure the remote
 	config.FileSet(remoteName, "type", "alias")
diff --git a/backend/cache/cache_internal_test.go b/backend/cache/cache_internal_test.go
index d5351899a..d15a39866 100644
--- a/backend/cache/cache_internal_test.go
+++ b/backend/cache/cache_internal_test.go
@@ -892,7 +892,7 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
 		m.Set("type", "cache")
 		m.Set("remote", localRemote+":"+filepath.Join(os.TempDir(), localRemote))
 	} else {
-		remoteType := config.FileGet(remote, "type", "")
+		remoteType := config.FileGet(remote, "type")
 		if remoteType == "" {
 			t.Skipf("skipped due to invalid remote type for %v", remote)
 			return nil, nil
@@ -903,14 +903,14 @@ func (r *run) newCacheFs(t *testing.T, remote, id string, needRemote, purge bool
 				m.Set("password", cryptPassword1)
 				m.Set("password2", cryptPassword2)
 			}
-			remoteRemote := config.FileGet(remote, "remote", "")
+			remoteRemote := config.FileGet(remote, "remote")
 			if remoteRemote == "" {
 				t.Skipf("skipped due to invalid remote wrapper for %v", remote)
 				return nil, nil
 			}
 			remoteRemoteParts := strings.Split(remoteRemote, ":")
 			remoteWrapping := remoteRemoteParts[0]
-			remoteType := config.FileGet(remoteWrapping, "type", "")
+			remoteType := config.FileGet(remoteWrapping, "type")
 			if remoteType != "cache" {
 				t.Skipf("skipped due to invalid remote type for %v: '%v'", remoteWrapping, remoteType)
 				return nil, nil
diff --git a/backend/http/http_internal_test.go b/backend/http/http_internal_test.go
index 55d1bbc70..43e78444e 100644
--- a/backend/http/http_internal_test.go
+++ b/backend/http/http_internal_test.go
@@ -15,7 +15,7 @@ import (
 	"time"
 
 	"github.com/rclone/rclone/fs"
-	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/config/configmap"
 	"github.com/rclone/rclone/fstest"
 	"github.com/rclone/rclone/lib/rest"
@@ -47,7 +47,7 @@ func prepareServer(t *testing.T) (configmap.Simple, func()) {
 	ts := httptest.NewServer(handler)
 
 	// Configure the remote
-	config.LoadConfig(context.Background())
+	configfile.LoadConfig(context.Background())
 	// fs.Config.LogLevel = fs.LogLevelDebug
 	// fs.Config.DumpHeaders = true
 	// fs.Config.DumpBodies = true
diff --git a/cmd/mountlib/rc_test.go b/cmd/mountlib/rc_test.go
index de725d7b1..22c354bc5 100644
--- a/cmd/mountlib/rc_test.go
+++ b/cmd/mountlib/rc_test.go
@@ -13,6 +13,7 @@ import (
 	_ "github.com/rclone/rclone/cmd/cmount"
 	_ "github.com/rclone/rclone/cmd/mount"
 	_ "github.com/rclone/rclone/cmd/mount2"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/rc"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -20,6 +21,7 @@ import (
 
 func TestRc(t *testing.T) {
 	ctx := context.Background()
+	configfile.LoadConfig(ctx)
 	mount := rc.Calls.Get("mount/mount")
 	assert.NotNil(t, mount)
 	unmount := rc.Calls.Get("mount/unmount")
diff --git a/cmd/selfupdate/selfupdate_test.go b/cmd/selfupdate/selfupdate_test.go
new file mode 100644
index 000000000..567643716
--- /dev/null
+++ b/cmd/selfupdate/selfupdate_test.go
@@ -0,0 +1,198 @@
+package selfupdate
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"testing"
+	"time"
+
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fstest/testy"
+	"github.com/rclone/rclone/lib/random"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetVersion(t *testing.T) {
+	testy.SkipUnreliable(t)
+
+	ctx := context.Background()
+
+	// a beta version can only have "v" prepended
+	resultVer, _, err := GetVersion(ctx, true, "1.2.3.4")
+	assert.NoError(t, err)
+	assert.Equal(t, "v1.2.3.4", resultVer)
+
+	// but a stable version syntax should be checked
+	_, _, err = GetVersion(ctx, false, "1")
+	assert.Error(t, err)
+	_, _, err = GetVersion(ctx, false, "1.")
+	assert.Error(t, err)
+	_, _, err = GetVersion(ctx, false, "1.2.")
+	assert.Error(t, err)
+	_, _, err = GetVersion(ctx, false, "1.2.3.4")
+	assert.Error(t, err)
+
+	// incomplete stable version should have micro release added
+	resultVer, _, err = GetVersion(ctx, false, "1.52")
+	assert.NoError(t, err)
+	assert.Equal(t, "v1.52.3", resultVer)
+}
+
+func makeTestDir() (testDir string, err error) {
+	const maxAttempts = 5
+	testDirBase := filepath.Join(os.TempDir(), "rclone-test-selfupdate.")
+
+	for attempt := 0; attempt < maxAttempts; attempt++ {
+		testDir = testDirBase + random.String(4)
+		err = os.MkdirAll(testDir, os.ModePerm)
+		if err == nil {
+			break
+		}
+	}
+	return
+}
+
+func TestInstallOnLinux(t *testing.T) {
+	testy.SkipUnreliable(t)
+	if runtime.GOOS != "linux" {
+		t.Skip("this is a Linux only test")
+	}
+
+	// Prepare for test
+	ctx := context.Background()
+	testDir, err := makeTestDir()
+	assert.NoError(t, err)
+	path := filepath.Join(testDir, "rclone")
+	defer func() {
+		_ = os.Chmod(path, 0644)
+		_ = os.RemoveAll(testDir)
+	}()
+
+	regexVer := regexp.MustCompile(`v[0-9]\S+`)
+
+	betaVer, _, err := GetVersion(ctx, true, "")
+	assert.NoError(t, err)
+
+	// Must do nothing if version isn't changing
+	assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path, Version: fs.Version}))
+
+	// Must fail on non-writable file
+	assert.NoError(t, ioutil.WriteFile(path, []byte("test"), 0644))
+	assert.NoError(t, os.Chmod(path, 0000))
+	err = (InstallUpdate(ctx, &Options{Beta: true, Output: path}))
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "run self-update as root")
+
+	// Must keep non-standard permissions
+	assert.NoError(t, os.Chmod(path, 0644))
+	assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
+
+	info, err := os.Stat(path)
+	assert.NoError(t, err)
+	assert.Equal(t, os.FileMode(0644), info.Mode().Perm())
+
+	// Must remove temporary files
+	files, err := ioutil.ReadDir(testDir)
+	assert.NoError(t, err)
+	assert.Equal(t, 1, len(files))
+
+	// Must contain valid executable
+	assert.NoError(t, os.Chmod(path, 0755))
+	cmd := exec.Command(path, "version")
+	output, err := cmd.CombinedOutput()
+	assert.NoError(t, err)
+	assert.True(t, cmd.ProcessState.Success())
+	assert.Equal(t, betaVer, regexVer.FindString(string(output)))
+}
+
+func TestRenameOnWindows(t *testing.T) {
+	testy.SkipUnreliable(t)
+	if runtime.GOOS != "windows" {
+		t.Skip("this is a Windows only test")
+	}
+
+	// Prepare for test
+	ctx := context.Background()
+
+	testDir, err := makeTestDir()
+	assert.NoError(t, err)
+	defer func() {
+		_ = os.RemoveAll(testDir)
+	}()
+
+	path := filepath.Join(testDir, "rclone.exe")
+	regexVer := regexp.MustCompile(`v[0-9]\S+`)
+
+	stableVer, _, err := GetVersion(ctx, false, "")
+	assert.NoError(t, err)
+
+	betaVer, _, err := GetVersion(ctx, true, "")
+	assert.NoError(t, err)
+
+	// Must not create temporary files when target doesn't exist
+	assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
+
+	files, err := ioutil.ReadDir(testDir)
+	assert.NoError(t, err)
+	assert.Equal(t, 1, len(files))
+
+	// Must save running executable as the "old" file
+	cmdWait := exec.Command(path, "config")
+	stdinWait, err := cmdWait.StdinPipe() // Make it run waiting for input
+	assert.NoError(t, err)
+	assert.NoError(t, cmdWait.Start())
+
+	assert.NoError(t, InstallUpdate(ctx, &Options{Beta: false, Output: path}))
+	files, err = ioutil.ReadDir(testDir)
+	assert.NoError(t, err)
+	assert.Equal(t, 2, len(files))
+
+	pathOld := filepath.Join(testDir, "rclone.old.exe")
+	_, err = os.Stat(pathOld)
+	assert.NoError(t, err)
+
+	cmd := exec.Command(path, "version")
+	output, err := cmd.CombinedOutput()
+	assert.NoError(t, err)
+	assert.True(t, cmd.ProcessState.Success())
+	assert.Equal(t, stableVer, regexVer.FindString(string(output)))
+
+	cmdOld := exec.Command(pathOld, "version")
+	output, err = cmdOld.CombinedOutput()
+	assert.NoError(t, err)
+	assert.True(t, cmdOld.ProcessState.Success())
+	assert.Equal(t, betaVer, regexVer.FindString(string(output)))
+
+	// Stop previous waiting executable, run new and saved executables
+	_ = stdinWait.Close()
+	_ = cmdWait.Wait()
+	time.Sleep(100 * time.Millisecond)
+
+	cmdWait = exec.Command(path, "config")
+	stdinWait, err = cmdWait.StdinPipe()
+	assert.NoError(t, err)
+	assert.NoError(t, cmdWait.Start())
+
+	cmdWaitOld := exec.Command(pathOld, "config")
+	stdinWaitOld, err := cmdWaitOld.StdinPipe()
+	assert.NoError(t, err)
+	assert.NoError(t, cmdWaitOld.Start())
+
+	// Updating when the "old" executable is running must produce a random "old" file
+	assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
+	files, err = ioutil.ReadDir(testDir)
+	assert.NoError(t, err)
+	assert.Equal(t, 3, len(files))
+
+	// Stop all waiting executables
+	_ = stdinWait.Close()
+	_ = cmdWait.Wait()
+	_ = stdinWaitOld.Close()
+	_ = cmdWaitOld.Wait()
+	time.Sleep(100 * time.Millisecond)
+}
diff --git a/cmd/serve/dlna/dlna_test.go b/cmd/serve/dlna/dlna_test.go
index 883255e19..535c85f26 100644
--- a/cmd/serve/dlna/dlna_test.go
+++ b/cmd/serve/dlna/dlna_test.go
@@ -13,12 +13,12 @@ import (
 
 	"github.com/anacrolix/dms/soap"
 
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/vfs"
 
 	_ "github.com/rclone/rclone/backend/local"
 	"github.com/rclone/rclone/cmd/serve/dlna/dlnaflags"
 	"github.com/rclone/rclone/fs"
-	"github.com/rclone/rclone/fs/config"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -41,7 +41,7 @@ func startServer(t *testing.T, f fs.Fs) {
 }
 
 func TestInit(t *testing.T) {
-	config.LoadConfig(context.Background())
+	configfile.LoadConfig(context.Background())
 
 	f, err := fs.NewFs(context.Background(), "testdata/files")
 	l, _ := f.List(context.Background(), "")
diff --git a/cmd/serve/http/http_test.go b/cmd/serve/http/http_test.go
index ed76fa879..cec0bd36b 100644
--- a/cmd/serve/http/http_test.go
+++ b/cmd/serve/http/http_test.go
@@ -12,7 +12,7 @@ import (
 	_ "github.com/rclone/rclone/backend/local"
 	"github.com/rclone/rclone/cmd/serve/httplib"
 	"github.com/rclone/rclone/fs"
-	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/filter"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -61,7 +61,7 @@ var (
 func TestInit(t *testing.T) {
 	ctx := context.Background()
 	// Configure the remote
-	config.LoadConfig(context.Background())
+	configfile.LoadConfig(context.Background())
 	// fs.Config.LogLevel = fs.LogLevelDebug
 	// fs.Config.DumpHeaders = true
 	// fs.Config.DumpBodies = true
diff --git a/cmd/serve/restic/restic_appendonly_test.go b/cmd/serve/restic/restic_appendonly_test.go
index 9087f941d..74b1ec1d3 100644
--- a/cmd/serve/restic/restic_appendonly_test.go
+++ b/cmd/serve/restic/restic_appendonly_test.go
@@ -1,6 +1,7 @@
 package restic
 
 import (
+	"context"
 	"crypto/rand"
 	"encoding/hex"
 	"io"
@@ -12,6 +13,7 @@ import (
 
 	"github.com/rclone/rclone/cmd"
 	"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/stretchr/testify/require"
 )
 
@@ -63,6 +65,8 @@ func createOverwriteDeleteSeq(t testing.TB, path string) []TestRequest {
 
 // TestResticHandler runs tests on the restic handler code, especially in append-only mode.
 func TestResticHandler(t *testing.T) {
+	ctx := context.Background()
+	configfile.LoadConfig(ctx)
 	buf := make([]byte, 32)
 	_, err := io.ReadFull(rand.Reader, buf)
 	require.NoError(t, err)
diff --git a/fs/config/config_test.go b/fs/config/config_test.go
index 6713e9d17..486416451 100644
--- a/fs/config/config_test.go
+++ b/fs/config/config_test.go
@@ -1,455 +1,52 @@
-package config
+// These are in an external package because we need to import configfile
+
+package config_test
 
 import (
 	"bytes"
 	"context"
-	"fmt"
 	"io/ioutil"
-	"os"
 	"testing"
 
-	"github.com/rclone/rclone/fs"
-	"github.com/rclone/rclone/fs/config/obscure"
+	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/rc"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-func testConfigFile(t *testing.T, configFileName string) func() {
-	ctx := context.Background()
-	ci := fs.GetConfig(ctx)
-	configKey = nil // reset password
-	_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
-	_ = os.Unsetenv("RCLONE_CONFIG_PASS")
-	// create temp config file
-	tempFile, err := ioutil.TempFile("", configFileName)
-	assert.NoError(t, err)
-	path := tempFile.Name()
-	assert.NoError(t, tempFile.Close())
-
-	// temporarily adapt configuration
-	oldOsStdout := os.Stdout
-	oldConfigPath := ConfigPath
-	oldConfig := *ci
-	oldConfigFile := configFile
-	oldReadLine := ReadLine
-	oldPassword := Password
-	os.Stdout = nil
-	ConfigPath = path
-	ci = &fs.ConfigInfo{}
-	configFile = nil
-
-	LoadConfig(ctx)
-	assert.Equal(t, []string{}, getConfigData().GetSectionList())
-
-	// Fake a remote
-	fs.Register(&fs.RegInfo{
-		Name: "config_test_remote",
-		Options: fs.Options{
-			{
-				Name:       "bool",
-				Default:    false,
-				IsPassword: false,
-			},
-			{
-				Name:       "pass",
-				Default:    "",
-				IsPassword: true,
-			},
-		},
-	})
-
-	// Undo the above
-	return func() {
-		err := os.Remove(path)
-		assert.NoError(t, err)
-
-		os.Stdout = oldOsStdout
-		ConfigPath = oldConfigPath
-		ReadLine = oldReadLine
-		Password = oldPassword
-		*ci = oldConfig
-		configFile = oldConfigFile
-
-		_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
-		_ = os.Unsetenv("RCLONE_CONFIG_PASS")
-	}
-}
-
-// makeReadLine makes a simple readLine which returns a fixed list of
-// strings
-func makeReadLine(answers []string) func() string {
-	i := 0
-	return func() string {
-		i = i + 1
-		return answers[i-1]
-	}
-}
-
-func TestCRUD(t *testing.T) {
-	defer testConfigFile(t, "crud.conf")()
-	ctx := context.Background()
-
-	// script for creating remote
-	ReadLine = makeReadLine([]string{
-		"config_test_remote", // type
-		"true",               // bool value
-		"y",                  // type my own password
-		"secret",             // password
-		"secret",             // repeat
-		"y",                  // looks good, save
-	})
-	NewRemote(ctx, "test")
-
-	assert.Equal(t, []string{"test"}, configFile.GetSectionList())
-	assert.Equal(t, "config_test_remote", FileGet("test", "type"))
-	assert.Equal(t, "true", FileGet("test", "bool"))
-	assert.Equal(t, "secret", obscure.MustReveal(FileGet("test", "pass")))
-
-	// normal rename, test → asdf
-	ReadLine = makeReadLine([]string{
-		"asdf",
-		"asdf",
-		"asdf",
-	})
-	RenameRemote("test")
-
-	assert.Equal(t, []string{"asdf"}, configFile.GetSectionList())
-	assert.Equal(t, "config_test_remote", FileGet("asdf", "type"))
-	assert.Equal(t, "true", FileGet("asdf", "bool"))
-	assert.Equal(t, "secret", obscure.MustReveal(FileGet("asdf", "pass")))
-
-	// delete remote
-	DeleteRemote("asdf")
-	assert.Equal(t, []string{}, configFile.GetSectionList())
-}
-
-func TestChooseOption(t *testing.T) {
-	defer testConfigFile(t, "crud.conf")()
-	ctx := context.Background()
-
-	// script for creating remote
-	ReadLine = makeReadLine([]string{
-		"config_test_remote", // type
-		"false",              // bool value
-		"x",                  // bad choice
-		"g",                  // generate password
-		"1024",               // very big
-		"y",                  // password OK
-		"y",                  // looks good, save
-	})
-	Password = func(bits int) (string, error) {
-		assert.Equal(t, 1024, bits)
-		return "not very random password", nil
-	}
-	NewRemote(ctx, "test")
-
-	assert.Equal(t, "false", FileGet("test", "bool"))
-	assert.Equal(t, "not very random password", obscure.MustReveal(FileGet("test", "pass")))
-
-	// script for creating remote
-	ReadLine = makeReadLine([]string{
-		"config_test_remote", // type
-		"true",               // bool value
-		"n",                  // not required
-		"y",                  // looks good, save
-	})
-	NewRemote(ctx, "test")
-
-	assert.Equal(t, "true", FileGet("test", "bool"))
-	assert.Equal(t, "", FileGet("test", "pass"))
-}
-
-func TestNewRemoteName(t *testing.T) {
-	defer testConfigFile(t, "crud.conf")()
-	ctx := context.Background()
-
-	// script for creating remote
-	ReadLine = makeReadLine([]string{
-		"config_test_remote", // type
-		"true",               // bool value
-		"n",                  // not required
-		"y",                  // looks good, save
-	})
-	NewRemote(ctx, "test")
-
-	ReadLine = makeReadLine([]string{
-		"test",           // already exists
-		"",               // empty string not allowed
-		"bad@characters", // bad characters
-		"newname",        // OK
-	})
-
-	assert.Equal(t, "newname", NewRemoteName())
-}
-
-func TestCreateUpdatePasswordRemote(t *testing.T) {
-	ctx := context.Background()
-	defer testConfigFile(t, "update.conf")()
-
-	for _, doObscure := range []bool{false, true} {
-		for _, noObscure := range []bool{false, true} {
-			if doObscure && noObscure {
-				break
-			}
-			t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
-				require.NoError(t, CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
-					"bool": true,
-					"pass": "potato",
-				}, doObscure, noObscure))
-
-				assert.Equal(t, []string{"test2"}, configFile.GetSectionList())
-				assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
-				assert.Equal(t, "true", FileGet("test2", "bool"))
-				gotPw := FileGet("test2", "pass")
-				if !noObscure {
-					gotPw = obscure.MustReveal(gotPw)
-				}
-				assert.Equal(t, "potato", gotPw)
-
-				wantPw := obscure.MustObscure("potato2")
-				require.NoError(t, UpdateRemote(ctx, "test2", rc.Params{
-					"bool":  false,
-					"pass":  wantPw,
-					"spare": "spare",
-				}, doObscure, noObscure))
-
-				assert.Equal(t, []string{"test2"}, configFile.GetSectionList())
-				assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
-				assert.Equal(t, "false", FileGet("test2", "bool"))
-				gotPw = FileGet("test2", "pass")
-				if doObscure {
-					gotPw = obscure.MustReveal(gotPw)
-				}
-				assert.Equal(t, wantPw, gotPw)
-
-				require.NoError(t, PasswordRemote(ctx, "test2", rc.Params{
-					"pass": "potato3",
-				}))
-
-				assert.Equal(t, []string{"test2"}, configFile.GetSectionList())
-				assert.Equal(t, "config_test_remote", FileGet("test2", "type"))
-				assert.Equal(t, "false", FileGet("test2", "bool"))
-				assert.Equal(t, "potato3", obscure.MustReveal(FileGet("test2", "pass")))
-			})
-		}
-	}
-
-}
-
-// Test some error cases
-func TestReveal(t *testing.T) {
-	for _, test := range []struct {
-		in      string
-		wantErr string
-	}{
-		{"YmJiYmJiYmJiYmJiYmJiYp*gcEWbAw", "base64 decode failed when revealing password - is it obscured?: illegal base64 data at input byte 22"},
-		{"aGVsbG8", "input too short when revealing password - is it obscured?"},
-		{"", "input too short when revealing password - is it obscured?"},
-	} {
-		gotString, gotErr := obscure.Reveal(test.in)
-		assert.Equal(t, "", gotString)
-		assert.Equal(t, test.wantErr, gotErr.Error())
-	}
-}
-
 func TestConfigLoad(t *testing.T) {
-	oldConfigPath := ConfigPath
-	ConfigPath = "./testdata/plain.conf"
+	oldConfigPath := config.ConfigPath
+	config.ConfigPath = "./testdata/plain.conf"
 	defer func() {
-		ConfigPath = oldConfigPath
+		config.ConfigPath = oldConfigPath
 	}()
-	configKey = nil // reset password
-	c, err := loadConfigFile()
-	if err != nil {
-		t.Fatal(err)
-	}
-	sections := c.GetSectionList()
+	config.ClearConfigPassword()
+	configfile.LoadConfig(context.Background())
+	sections := config.Data.GetSectionList()
 	var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
 	assert.Equal(t, expect, sections)
 
-	keys := c.GetKeyList("nounc")
+	keys := config.Data.GetKeyList("nounc")
 	expect = []string{"type", "nounc"}
 	assert.Equal(t, expect, keys)
 }
 
-func TestConfigLoadEncrypted(t *testing.T) {
-	var err error
-	oldConfigPath := ConfigPath
-	ConfigPath = "./testdata/encrypted.conf"
-	defer func() {
-		ConfigPath = oldConfigPath
-		configKey = nil // reset password
-	}()
-
-	// Set correct password
-	err = setConfigPassword("asdf")
-	require.NoError(t, err)
-	c, err := loadConfigFile()
-	require.NoError(t, err)
-	sections := c.GetSectionList()
-	var expect = []string{"nounc", "unc"}
-	assert.Equal(t, expect, sections)
-
-	keys := c.GetKeyList("nounc")
-	expect = []string{"type", "nounc"}
-	assert.Equal(t, expect, keys)
-}
-
-func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
-	ctx := context.Background()
-	ci := fs.GetConfig(ctx)
-	oldConfigPath := ConfigPath
-	oldConfig := *ci
-	ConfigPath = "./testdata/encrypted.conf"
-	// using ci.PasswordCommand, correct password
-	ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
-	defer func() {
-		ConfigPath = oldConfigPath
-		configKey = nil // reset password
-		*ci = oldConfig
-		ci.PasswordCommand = nil
-	}()
-
-	configKey = nil // reset password
-
-	c, err := loadConfigFile()
-	require.NoError(t, err)
-
-	sections := c.GetSectionList()
-	var expect = []string{"nounc", "unc"}
-	assert.Equal(t, expect, sections)
-
-	keys := c.GetKeyList("nounc")
-	expect = []string{"type", "nounc"}
-	assert.Equal(t, expect, keys)
-}
-
-func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
-	ctx := context.Background()
-	ci := fs.GetConfig(ctx)
-	oldConfigPath := ConfigPath
-	oldConfig := *ci
-	ConfigPath = "./testdata/encrypted.conf"
-	// using ci.PasswordCommand, incorrect password
-	ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"}
-	defer func() {
-		ConfigPath = oldConfigPath
-		configKey = nil // reset password
-		*ci = oldConfig
-		ci.PasswordCommand = nil
-	}()
-
-	configKey = nil // reset password
-
-	_, err := loadConfigFile()
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "using --password-command derived password")
-}
-
-func TestConfigLoadEncryptedFailures(t *testing.T) {
-	var err error
-
-	// This file should be too short to be decoded.
-	oldConfigPath := ConfigPath
-	ConfigPath = "./testdata/enc-short.conf"
-	defer func() { ConfigPath = oldConfigPath }()
-	_, err = loadConfigFile()
-	require.Error(t, err)
-
-	// This file contains invalid base64 characters.
-	ConfigPath = "./testdata/enc-invalid.conf"
-	_, err = loadConfigFile()
-	require.Error(t, err)
-
-	// This file contains invalid base64 characters.
-	ConfigPath = "./testdata/enc-too-new.conf"
-	_, err = loadConfigFile()
-	require.Error(t, err)
-
-	// This file does not exist.
-	ConfigPath = "./testdata/filenotfound.conf"
-	c, err := loadConfigFile()
-	assert.Equal(t, errorConfigFileNotFound, err)
-	assert.Nil(t, c)
-}
-
-func TestPassword(t *testing.T) {
-	defer func() {
-		configKey = nil // reset password
-	}()
-	var err error
-	// Empty password should give error
-	err = setConfigPassword("  \t  ")
-	require.Error(t, err)
-
-	// Test invalid utf8 sequence
-	err = setConfigPassword(string([]byte{0xff, 0xfe, 0xfd}) + "abc")
-	require.Error(t, err)
-
-	// Simple check of wrong passwords
-	hashedKeyCompare(t, "mis", "match", false)
-
-	// Check that passwords match after unicode normalization
-	hashedKeyCompare(t, "ﬀ\u0041\u030A", "ffÅ", true)
-
-	// Check that passwords preserves case
-	hashedKeyCompare(t, "abcdef", "ABCDEF", false)
-
-}
-
-func hashedKeyCompare(t *testing.T, a, b string, shouldMatch bool) {
-	err := setConfigPassword(a)
-	require.NoError(t, err)
-	k1 := configKey
-
-	err = setConfigPassword(b)
-	require.NoError(t, err)
-	k2 := configKey
-
-	if shouldMatch {
-		assert.Equal(t, k1, k2)
-	} else {
-		assert.NotEqual(t, k1, k2)
-	}
-}
-
-func TestMatchProvider(t *testing.T) {
-	for _, test := range []struct {
-		config   string
-		provider string
-		want     bool
-	}{
-		{"", "", true},
-		{"one", "one", true},
-		{"one,two", "two", true},
-		{"one,two,three", "two", true},
-		{"one", "on", false},
-		{"one,two,three", "tw", false},
-		{"!one,two,three", "two", false},
-		{"!one,two,three", "four", true},
-	} {
-		what := fmt.Sprintf("%q,%q", test.config, test.provider)
-		got := matchProvider(test.config, test.provider)
-		assert.Equal(t, test.want, got, what)
-	}
-}
-
 func TestFileRefresh(t *testing.T) {
 	ctx := context.Background()
 	defer testConfigFile(t, "refresh.conf")()
-	require.NoError(t, CreateRemote(ctx, "refresh_test", "config_test_remote", rc.Params{
+	require.NoError(t, config.CreateRemote(ctx, "refresh_test", "config_test_remote", rc.Params{
 		"bool": true,
 	}, false, false))
-	b, err := ioutil.ReadFile(ConfigPath)
+	b, err := ioutil.ReadFile(config.ConfigPath)
 	assert.NoError(t, err)
 
 	b = bytes.Replace(b, []byte("refresh_test"), []byte("refreshed_test"), 1)
-	err = ioutil.WriteFile(ConfigPath, b, 0644)
+	err = ioutil.WriteFile(config.ConfigPath, b, 0644)
 	assert.NoError(t, err)
 
-	assert.NotEqual(t, []string{"refreshed_test"}, configFile.GetSectionList())
-	err = FileRefresh()
+	assert.NotEqual(t, []string{"refreshed_test"}, config.Data.GetSectionList())
+	err = config.FileRefresh()
 	assert.NoError(t, err)
-	assert.Equal(t, []string{"refreshed_test"}, configFile.GetSectionList())
+	assert.Equal(t, []string{"refreshed_test"}, config.Data.GetSectionList())
 }
diff --git a/fs/config/crypt_internal_test.go b/fs/config/crypt_internal_test.go
new file mode 100644
index 000000000..05dc71e0f
--- /dev/null
+++ b/fs/config/crypt_internal_test.go
@@ -0,0 +1,48 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func hashedKeyCompare(t *testing.T, a, b string, shouldMatch bool) {
+	err := SetConfigPassword(a)
+	require.NoError(t, err)
+	k1 := configKey
+
+	err = SetConfigPassword(b)
+	require.NoError(t, err)
+	k2 := configKey
+
+	if shouldMatch {
+		assert.Equal(t, k1, k2)
+	} else {
+		assert.NotEqual(t, k1, k2)
+	}
+}
+
+func TestPassword(t *testing.T) {
+	defer func() {
+		configKey = nil // reset password
+	}()
+	var err error
+	// Empty password should give error
+	err = SetConfigPassword("  \t  ")
+	require.Error(t, err)
+
+	// Test invalid utf8 sequence
+	err = SetConfigPassword(string([]byte{0xff, 0xfe, 0xfd}) + "abc")
+	require.Error(t, err)
+
+	// Simple check of wrong passwords
+	hashedKeyCompare(t, "mis", "match", false)
+
+	// Check that passwords match after unicode normalization
+	hashedKeyCompare(t, "ﬀ\u0041\u030A", "ffÅ", true)
+
+	// Check that passwords preserves case
+	hashedKeyCompare(t, "abcdef", "ABCDEF", false)
+
+}
diff --git a/fs/config/crypt_test.go b/fs/config/crypt_test.go
new file mode 100644
index 000000000..7febbfc6d
--- /dev/null
+++ b/fs/config/crypt_test.go
@@ -0,0 +1,115 @@
+// These are in an external package because we need to import configfile
+//
+// Internal tests are in crypt_internal_test.go
+
+package config_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestConfigLoadEncrypted(t *testing.T) {
+	var err error
+	oldConfigPath := config.ConfigPath
+	config.ConfigPath = "./testdata/encrypted.conf"
+	defer func() {
+		config.ConfigPath = oldConfigPath
+		config.ClearConfigPassword()
+	}()
+
+	// Set correct password
+	err = config.SetConfigPassword("asdf")
+	require.NoError(t, err)
+	err = config.Data.Load()
+	require.NoError(t, err)
+	sections := config.Data.GetSectionList()
+	var expect = []string{"nounc", "unc"}
+	assert.Equal(t, expect, sections)
+
+	keys := config.Data.GetKeyList("nounc")
+	expect = []string{"type", "nounc"}
+	assert.Equal(t, expect, keys)
+}
+
+func TestConfigLoadEncryptedWithValidPassCommand(t *testing.T) {
+	ctx := context.Background()
+	ci := fs.GetConfig(ctx)
+	oldConfigPath := config.ConfigPath
+	oldConfig := *ci
+	config.ConfigPath = "./testdata/encrypted.conf"
+	// using ci.PasswordCommand, correct password
+	ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
+	defer func() {
+		config.ConfigPath = oldConfigPath
+		config.ClearConfigPassword()
+		*ci = oldConfig
+		ci.PasswordCommand = nil
+	}()
+
+	config.ClearConfigPassword()
+
+	err := config.Data.Load()
+	require.NoError(t, err)
+
+	sections := config.Data.GetSectionList()
+	var expect = []string{"nounc", "unc"}
+	assert.Equal(t, expect, sections)
+
+	keys := config.Data.GetKeyList("nounc")
+	expect = []string{"type", "nounc"}
+	assert.Equal(t, expect, keys)
+}
+
+func TestConfigLoadEncryptedWithInvalidPassCommand(t *testing.T) {
+	ctx := context.Background()
+	ci := fs.GetConfig(ctx)
+	oldConfigPath := config.ConfigPath
+	oldConfig := *ci
+	config.ConfigPath = "./testdata/encrypted.conf"
+	// using ci.PasswordCommand, incorrect password
+	ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf-blurfl"}
+	defer func() {
+		config.ConfigPath = oldConfigPath
+		config.ClearConfigPassword()
+		*ci = oldConfig
+		ci.PasswordCommand = nil
+	}()
+
+	config.ClearConfigPassword()
+
+	err := config.Data.Load()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "using --password-command derived password")
+}
+
+func TestConfigLoadEncryptedFailures(t *testing.T) {
+	var err error
+
+	// This file should be too short to be decoded.
+	oldConfigPath := config.ConfigPath
+	config.ConfigPath = "./testdata/enc-short.conf"
+	defer func() { config.ConfigPath = oldConfigPath }()
+	err = config.Data.Load()
+	require.Error(t, err)
+
+	// This file contains invalid base64 characters.
+	config.ConfigPath = "./testdata/enc-invalid.conf"
+	err = config.Data.Load()
+	require.Error(t, err)
+
+	// This file contains invalid base64 characters.
+	config.ConfigPath = "./testdata/enc-too-new.conf"
+	err = config.Data.Load()
+	require.Error(t, err)
+
+	// This file does not exist.
+	config.ConfigPath = "./testdata/filenotfound.conf"
+	err = config.Data.Load()
+	assert.Equal(t, config.ErrorConfigFileNotFound, err)
+}
diff --git a/fs/config/obscure/obscure_test.go b/fs/config/obscure/obscure_test.go
index ff9dedb04..d7ea43121 100644
--- a/fs/config/obscure/obscure_test.go
+++ b/fs/config/obscure/obscure_test.go
@@ -58,3 +58,19 @@ func TestReveal(t *testing.T) {
 
 	}
 }
+
+// Test some error cases
+func TestRevealErrors(t *testing.T) {
+	for _, test := range []struct {
+		in      string
+		wantErr string
+	}{
+		{"YmJiYmJiYmJiYmJiYmJiYp*gcEWbAw", "base64 decode failed when revealing password - is it obscured?: illegal base64 data at input byte 22"},
+		{"aGVsbG8", "input too short when revealing password - is it obscured?"},
+		{"", "input too short when revealing password - is it obscured?"},
+	} {
+		gotString, gotErr := Reveal(test.in)
+		assert.Equal(t, "", gotString)
+		assert.Equal(t, test.wantErr, gotErr.Error())
+	}
+}
diff --git a/fs/config/rc_test.go b/fs/config/rc_test.go
index 5cc8f9b2b..89d3bb9e9 100644
--- a/fs/config/rc_test.go
+++ b/fs/config/rc_test.go
@@ -7,6 +7,7 @@ import (
 	_ "github.com/rclone/rclone/backend/local"
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/config/obscure"
 	"github.com/rclone/rclone/fs/rc"
 	"github.com/stretchr/testify/assert"
@@ -16,6 +17,8 @@ import (
 const testName = "configTestNameForRc"
 
 func TestRc(t *testing.T) {
+	ctx := context.Background()
+	configfile.LoadConfig(ctx)
 	// Create the test remote
 	call := rc.Calls.Get("config/create")
 	assert.NotNil(t, call)
@@ -26,7 +29,7 @@ func TestRc(t *testing.T) {
 			"test_key": "sausage",
 		},
 	}
-	out, err := call.Fn(context.Background(), in)
+	out, err := call.Fn(ctx, in)
 	require.NoError(t, err)
 	require.Nil(t, out)
 	assert.Equal(t, "local", config.FileGet(testName, "type"))
diff --git a/fs/config/ui_internal_test.go b/fs/config/ui_internal_test.go
new file mode 100644
index 000000000..98568d47a
--- /dev/null
+++ b/fs/config/ui_internal_test.go
@@ -0,0 +1,29 @@
+package config
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMatchProvider(t *testing.T) {
+	for _, test := range []struct {
+		config   string
+		provider string
+		want     bool
+	}{
+		{"", "", true},
+		{"one", "one", true},
+		{"one,two", "two", true},
+		{"one,two,three", "two", true},
+		{"one", "on", false},
+		{"one,two,three", "tw", false},
+		{"!one,two,three", "two", false},
+		{"!one,two,three", "four", true},
+	} {
+		what := fmt.Sprintf("%q,%q", test.config, test.provider)
+		got := matchProvider(test.config, test.provider)
+		assert.Equal(t, test.want, got, what)
+	}
+}
diff --git a/fs/config/ui_test.go b/fs/config/ui_test.go
new file mode 100644
index 000000000..7bbaeab91
--- /dev/null
+++ b/fs/config/ui_test.go
@@ -0,0 +1,242 @@
+// These are in an external package because we need to import configfile
+//
+// Internal tests are in ui_internal_test.go
+
+package config_test
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
+	"github.com/rclone/rclone/fs/config/obscure"
+	"github.com/rclone/rclone/fs/rc"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func testConfigFile(t *testing.T, configFileName string) func() {
+	ctx := context.Background()
+	ci := fs.GetConfig(ctx)
+	config.ClearConfigPassword()
+	_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
+	_ = os.Unsetenv("RCLONE_CONFIG_PASS")
+	// create temp config file
+	tempFile, err := ioutil.TempFile("", configFileName)
+	assert.NoError(t, err)
+	path := tempFile.Name()
+	assert.NoError(t, tempFile.Close())
+
+	// temporarily adapt configuration
+	oldOsStdout := os.Stdout
+	oldConfigPath := config.ConfigPath
+	oldConfig := *ci
+	oldConfigFile := config.Data
+	oldReadLine := config.ReadLine
+	oldPassword := config.Password
+	os.Stdout = nil
+	config.ConfigPath = path
+	ci = &fs.ConfigInfo{}
+
+	configfile.LoadConfig(ctx)
+	assert.Equal(t, []string{}, config.Data.GetSectionList())
+
+	// Fake a remote
+	fs.Register(&fs.RegInfo{
+		Name: "config_test_remote",
+		Options: fs.Options{
+			{
+				Name:       "bool",
+				Default:    false,
+				IsPassword: false,
+			},
+			{
+				Name:       "pass",
+				Default:    "",
+				IsPassword: true,
+			},
+		},
+	})
+
+	// Undo the above
+	return func() {
+		err := os.Remove(path)
+		assert.NoError(t, err)
+
+		os.Stdout = oldOsStdout
+		config.ConfigPath = oldConfigPath
+		config.ReadLine = oldReadLine
+		config.Password = oldPassword
+		*ci = oldConfig
+		config.Data = oldConfigFile
+
+		_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
+		_ = os.Unsetenv("RCLONE_CONFIG_PASS")
+	}
+}
+
+// makeReadLine makes a simple readLine which returns a fixed list of
+// strings
+func makeReadLine(answers []string) func() string {
+	i := 0
+	return func() string {
+		i = i + 1
+		return answers[i-1]
+	}
+}
+
+func TestCRUD(t *testing.T) {
+	defer testConfigFile(t, "crud.conf")()
+	ctx := context.Background()
+
+	// script for creating remote
+	config.ReadLine = makeReadLine([]string{
+		"config_test_remote", // type
+		"true",               // bool value
+		"y",                  // type my own password
+		"secret",             // password
+		"secret",             // repeat
+		"y",                  // looks good, save
+	})
+	config.NewRemote(ctx, "test")
+
+	assert.Equal(t, []string{"test"}, config.Data.GetSectionList())
+	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
+	assert.Equal(t, "true", config.FileGet("test", "bool"))
+	assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("test", "pass")))
+
+	// normal rename, test → asdf
+	config.ReadLine = makeReadLine([]string{
+		"asdf",
+		"asdf",
+		"asdf",
+	})
+	config.RenameRemote("test")
+
+	assert.Equal(t, []string{"asdf"}, config.Data.GetSectionList())
+	assert.Equal(t, "config_test_remote", config.FileGet("asdf", "type"))
+	assert.Equal(t, "true", config.FileGet("asdf", "bool"))
+	assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("asdf", "pass")))
+
+	// delete remote
+	config.DeleteRemote("asdf")
+	assert.Equal(t, []string{}, config.Data.GetSectionList())
+}
+
+func TestChooseOption(t *testing.T) {
+	defer testConfigFile(t, "crud.conf")()
+	ctx := context.Background()
+
+	// script for creating remote
+	config.ReadLine = makeReadLine([]string{
+		"config_test_remote", // type
+		"false",              // bool value
+		"x",                  // bad choice
+		"g",                  // generate password
+		"1024",               // very big
+		"y",                  // password OK
+		"y",                  // looks good, save
+	})
+	config.Password = func(bits int) (string, error) {
+		assert.Equal(t, 1024, bits)
+		return "not very random password", nil
+	}
+	config.NewRemote(ctx, "test")
+
+	assert.Equal(t, "false", config.FileGet("test", "bool"))
+	assert.Equal(t, "not very random password", obscure.MustReveal(config.FileGet("test", "pass")))
+
+	// script for creating remote
+	config.ReadLine = makeReadLine([]string{
+		"config_test_remote", // type
+		"true",               // bool value
+		"n",                  // not required
+		"y",                  // looks good, save
+	})
+	config.NewRemote(ctx, "test")
+
+	assert.Equal(t, "true", config.FileGet("test", "bool"))
+	assert.Equal(t, "", config.FileGet("test", "pass"))
+}
+
+func TestNewRemoteName(t *testing.T) {
+	defer testConfigFile(t, "crud.conf")()
+	ctx := context.Background()
+
+	// script for creating remote
+	config.ReadLine = makeReadLine([]string{
+		"config_test_remote", // type
+		"true",               // bool value
+		"n",                  // not required
+		"y",                  // looks good, save
+	})
+	config.NewRemote(ctx, "test")
+
+	config.ReadLine = makeReadLine([]string{
+		"test",           // already exists
+		"",               // empty string not allowed
+		"bad@characters", // bad characters
+		"newname",        // OK
+	})
+
+	assert.Equal(t, "newname", config.NewRemoteName())
+}
+
+func TestCreateUpdatePasswordRemote(t *testing.T) {
+	ctx := context.Background()
+	defer testConfigFile(t, "update.conf")()
+
+	for _, doObscure := range []bool{false, true} {
+		for _, noObscure := range []bool{false, true} {
+			if doObscure && noObscure {
+				break
+			}
+			t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
+				require.NoError(t, config.CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
+					"bool": true,
+					"pass": "potato",
+				}, doObscure, noObscure))
+
+				assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
+				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
+				assert.Equal(t, "true", config.FileGet("test2", "bool"))
+				gotPw := config.FileGet("test2", "pass")
+				if !noObscure {
+					gotPw = obscure.MustReveal(gotPw)
+				}
+				assert.Equal(t, "potato", gotPw)
+
+				wantPw := obscure.MustObscure("potato2")
+				require.NoError(t, config.UpdateRemote(ctx, "test2", rc.Params{
+					"bool":  false,
+					"pass":  wantPw,
+					"spare": "spare",
+				}, doObscure, noObscure))
+
+				assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
+				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
+				assert.Equal(t, "false", config.FileGet("test2", "bool"))
+				gotPw = config.FileGet("test2", "pass")
+				if doObscure {
+					gotPw = obscure.MustReveal(gotPw)
+				}
+				assert.Equal(t, wantPw, gotPw)
+
+				require.NoError(t, config.PasswordRemote(ctx, "test2", rc.Params{
+					"pass": "potato3",
+				}))
+
+				assert.Equal(t, []string{"test2"}, config.Data.GetSectionList())
+				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
+				assert.Equal(t, "false", config.FileGet("test2", "bool"))
+				assert.Equal(t, "potato3", obscure.MustReveal(config.FileGet("test2", "pass")))
+			})
+		}
+	}
+
+}
diff --git a/fs/rc/rcserver/rcserver_test.go b/fs/rc/rcserver/rcserver_test.go
index 9ada37a92..b17f3dd0b 100644
--- a/fs/rc/rcserver/rcserver_test.go
+++ b/fs/rc/rcserver/rcserver_test.go
@@ -20,6 +20,7 @@ import (
 	_ "github.com/rclone/rclone/backend/local"
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/accounting"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/rc"
 )
 
@@ -101,9 +102,11 @@ type testRun struct {
 
 // Run a suite of tests
 func testServer(t *testing.T, tests []testRun, opt *rc.Options) {
+	ctx := context.Background()
+	configfile.LoadConfig(ctx)
 	mux := http.NewServeMux()
 	opt.HTTPOptions.Template = testTemplate
-	rcServer := newServer(context.Background(), opt, mux)
+	rcServer := newServer(ctx, opt, mux)
 	for _, test := range tests {
 		t.Run(test.Name, func(t *testing.T) {
 			method := test.Method
diff --git a/fstest/fstest.go b/fstest/fstest.go
index b996d2259..f97bc2cae 100644
--- a/fstest/fstest.go
+++ b/fstest/fstest.go
@@ -25,6 +25,7 @@ import (
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/accounting"
 	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/fs/hash"
 	"github.com/rclone/rclone/fs/walk"
 	"github.com/rclone/rclone/lib/random"
@@ -70,7 +71,8 @@ func Initialise() {
 	if envConfig := os.Getenv("RCLONE_CONFIG"); envConfig != "" {
 		config.ConfigPath = envConfig
 	}
-	config.LoadConfig(ctx)
+	configfile.LoadConfig(ctx)
+	accounting.Start(ctx)
 	if *Verbose {
 		ci.LogLevel = fs.LogLevelDebug
 	}