Repository: rclone/rclone
Pull request #8039 (2024-08-29 09:49:42)

swift: add total/free space info in about command.

#### What is the purpose of this change?

With the enhancement in version [v2.0.3](https://github.com/ncw/swift/releases/tag/v2.0.3) of ncw/swift library, `about` command can now get Total and Free space info from swift remotes that support this feature (ex. [Blomp storage](https://www.blomp.com/cloud-storage/))

#### Was the change discussed in an issue or in the forum before?

Pull request [#186](https://github.com/ncw/swift/pull/186) in ncw/swift 

#### Checklist

- [x] I have read the [contribution guidelines](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#submitting-a-new-feature-or-bug-fix).
- [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 :-)



Patch:
diff --git a/backend/s3/s3.go b/backend/s3/s3.go
index 2ca9b4fb3..e45a7b299 100644
--- a/backend/s3/s3.go
+++ b/backend/s3/s3.go
@@ -5988,7 +5988,13 @@ func (w *s3ChunkWriter) WriteChunk(ctx context.Context, chunkNumber int, reader
 	if do, ok := reader.(pool.DelayAccountinger); ok {
 		// To figure out this number, do a transfer and if the accounted size is 0 or a
 		// multiple of what it should be, increase or decrease this number.
-		do.DelayAccounting(3)
+		//
+		// For transfers over https the SDK does not sign the body whereas over http it does
+		if len(w.f.opt.Endpoint) >= 5 && strings.EqualFold(w.f.opt.Endpoint[:5], "http:") {
+			do.DelayAccounting(3)
+		} else {
+			do.DelayAccounting(2)
+		}
 	}
 
 	// create checksum of buffer for integrity checking
diff --git a/backend/swift/swift.go b/backend/swift/swift.go
index edc774248..452d5f11a 100644
--- a/backend/swift/swift.go
+++ b/backend/swift/swift.go
@@ -883,7 +883,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
 
 // About gets quota information
 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
-	var total, objects int64
+	var used, objects, total int64
 	if f.rootContainer != "" {
 		var container swift.Container
 		err = f.pacer.Call(func() (bool, error) {
@@ -893,8 +893,9 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
 		if err != nil {
 			return nil, fmt.Errorf("container info failed: %w", err)
 		}
-		total = container.Bytes
+		used = container.Bytes
 		objects = container.Count
+		total = container.QuotaBytes
 	} else {
 		var containers []swift.Container
 		err = f.pacer.Call(func() (bool, error) {
@@ -905,14 +906,19 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
 			return nil, fmt.Errorf("container listing failed: %w", err)
 		}
 		for _, c := range containers {
-			total += c.Bytes
+			used += c.Bytes
 			objects += c.Count
+			total += c.QuotaBytes
 		}
 	}
 	usage = &fs.Usage{
-		Used:    fs.NewUsageValue(total),   // bytes in use
+		Used:    fs.NewUsageValue(used),    // bytes in use
 		Objects: fs.NewUsageValue(objects), // objects in use
 	}
+	if total > 0 {
+		usage.Total = fs.NewUsageValue(total)
+		usage.Free = fs.NewUsageValue(total - used)
+	}
 	return usage, nil
 }
 
diff --git a/backend/yandex/yandex.go b/backend/yandex/yandex.go
index 798cccd6c..aabb2f8db 100644
--- a/backend/yandex/yandex.go
+++ b/backend/yandex/yandex.go
@@ -26,6 +26,7 @@ import (
 	"github.com/rclone/rclone/lib/encoder"
 	"github.com/rclone/rclone/lib/oauthutil"
 	"github.com/rclone/rclone/lib/pacer"
+	"github.com/rclone/rclone/lib/random"
 	"github.com/rclone/rclone/lib/readers"
 	"github.com/rclone/rclone/lib/rest"
 	"golang.org/x/oauth2"
@@ -39,6 +40,8 @@ const (
 	minSleep                    = 10 * time.Millisecond
 	maxSleep                    = 2 * time.Second // may needs to be increased, testing needed
 	decayConstant               = 2               // bigger for slower decay, exponential
+
+	userAgentTemplae = `Yandex.Disk {"os":"windows","dtype":"ydisk3","vsn":"3.2.37.4977","id":"6BD01244C7A94456BBCEE7EEC990AEAD","id2":"0F370CD40C594A4783BC839C846B999C","session_id":"%s"}`
 )
 
 // Globals
@@ -79,15 +82,22 @@ func init() {
 			// it doesn't seem worth making an exception for this
 			Default: (encoder.Display |
 				encoder.EncodeInvalidUtf8),
+		}, {
+			Name:     "spoof_ua",
+			Help:     "Set the user agent to match an official version of the yandex disk client. May help with upload performance.",
+			Default:  true,
+			Advanced: true,
+			Hide:     fs.OptionHideConfigurator,
 		}}...),
 	})
 }
 
 // Options defines the configuration for this backend
 type Options struct {
-	Token      string               `config:"token"`
-	HardDelete bool                 `config:"hard_delete"`
-	Enc        encoder.MultiEncoder `config:"encoding"`
+	Token          string               `config:"token"`
+	HardDelete     bool                 `config:"hard_delete"`
+	Enc            encoder.MultiEncoder `config:"encoding"`
+	SpoofUserAgent bool                 `config:"spoof_ua"`
 }
 
 // Fs represents a remote yandex
@@ -254,6 +264,12 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 		return nil, err
 	}
 
+	ctx, ci := fs.AddConfig(ctx)
+	if fs.ConfigOptionsInfo.Get("user_agent").IsDefault() && opt.SpoofUserAgent {
+		randomSessionID, _ := random.Password(128)
+		ci.UserAgent = fmt.Sprintf(userAgentTemplae, randomSessionID)
+	}
+
 	token, err := oauthutil.GetToken(name, m)
 	if err != nil {
 		return nil, fmt.Errorf("couldn't read OAuth token: %w", err)
@@ -274,7 +290,6 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 		return nil, fmt.Errorf("failed to configure Yandex: %w", err)
 	}
 
-	ci := fs.GetConfig(ctx)
 	f := &Fs{
 		name:  name,
 		opt:   *opt,
diff --git a/backend/zoho/api/types.go b/backend/zoho/api/types.go
index 5a516b6fb..ed6751a44 100644
--- a/backend/zoho/api/types.go
+++ b/backend/zoho/api/types.go
@@ -2,6 +2,8 @@
 package api
 
 import (
+	"encoding/json"
+	"fmt"
 	"strconv"
 	"time"
 )
@@ -12,7 +14,12 @@ type Time time.Time
 
 // UnmarshalJSON turns JSON into a Time
 func (t *Time) UnmarshalJSON(data []byte) error {
-	millis, err := strconv.ParseInt(string(data), 10, 64)
+	s := string(data)
+	// If the time is a quoted string, strip quotes
+	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
+		s = s[1 : len(s)-1]
+	}
+	millis, err := strconv.ParseInt(s, 10, 64)
 	if err != nil {
 		return err
 	}
@@ -84,6 +91,73 @@ type ItemList struct {
 	Items []Item `json:"data"`
 }
 
+// UploadFileInfo is what the FileInfo field in the UnloadInfo struct decodes to
+type UploadFileInfo struct {
+	OrgID           string `json:"ORG_ID"`
+	ResourceID      string `json:"RESOURCE_ID"`
+	LibraryID       string `json:"LIBRARY_ID"`
+	Md5Checksum     string `json:"MD5_CHECKSUM"`
+	ParentModelID   string `json:"PARENT_MODEL_ID"`
+	ParentID        string `json:"PARENT_ID"`
+	ResourceType    int    `json:"RESOURCE_TYPE"`
+	WmsSentTime     string `json:"WMS_SENT_TIME"`
+	TabID           string `json:"TAB_ID"`
+	Owner           string `json:"OWNER"`
+	ResourceGroup   string `json:"RESOURCE_GROUP"`
+	ParentModelName string `json:"PARENT_MODEL_NAME"`
+	Size            int64  `json:"size"`
+	Operation       string `json:"OPERATION"`
+	EventID         string `json:"EVENT_ID"`
+	AuditInfo       struct {
+		VersionInfo struct {
+			VersionAuthors    []string `json:"versionAuthors"`
+			VersionID         string   `json:"versionId"`
+			IsMinorVersion    bool     `json:"isMinorVersion"`
+			VersionTime       Time     `json:"versionTime"`
+			VersionAuthorZuid []string `json:"versionAuthorZuid"`
+			VersionNotes      string   `json:"versionNotes"`
+			VersionNumber     string   `json:"versionNumber"`
+		} `json:"versionInfo"`
+		Resource struct {
+			Owner            string `json:"owner"`
+			CreatedTime      Time   `json:"created_time"`
+			Creator          string `json:"creator"`
+			ServiceType      int    `json:"service_type"`
+			Extension        string `json:"extension"`
+			StatusChangeTime Time   `json:"status_change_time"`
+			ResourceType     int    `json:"resource_type"`
+			Name             string `json:"name"`
+		} `json:"resource"`
+		ParentInfo struct {
+			ParentName string `json:"parentName"`
+			ParentID   string `json:"parentId"`
+			ParentType int    `json:"parentType"`
+		} `json:"parentInfo"`
+		LibraryInfo struct {
+			LibraryName string `json:"libraryName"`
+			LibraryID   string `json:"libraryId"`
+			LibraryType int    `json:"libraryType"`
+		} `json:"libraryInfo"`
+		UpdateType string `json:"updateType"`
+		StatusCode string `json:"statusCode"`
+	} `json:"AUDIT_INFO"`
+	ZUID   int64  `json:"ZUID"`
+	TeamID string `json:"TEAM_ID"`
+}
+
+// GetModTime fetches the modification time of the upload
+//
+// This tries a few places and if all fails returns the current time
+func (ufi *UploadFileInfo) GetModTime() Time {
+	if t := ufi.AuditInfo.Resource.CreatedTime; !time.Time(t).IsZero() {
+		return t
+	}
+	if t := ufi.AuditInfo.Resource.StatusChangeTime; !time.Time(t).IsZero() {
+		return t
+	}
+	return Time(time.Now())
+}
+
 // UploadInfo is a simplified and slightly different version of
 // the Item struct only used in the response to uploads
 type UploadInfo struct {
@@ -91,9 +165,21 @@ type UploadInfo struct {
 		ParentID    string `json:"parent_id"`
 		FileName    string `json:"notes.txt"`
 		RessourceID string `json:"resource_id"`
+		Permalink   string `json:"Permalink"`
+		FileInfo    string `json:"File INFO"` // JSON encoded UploadFileInfo
 	} `json:"attributes"`
 }
 
+// GetUploadFileInfo decodes the embedded FileInfo
+func (ui *UploadInfo) GetUploadFileInfo() (*UploadFileInfo, error) {
+	var ufi UploadFileInfo
+	err := json.Unmarshal([]byte(ui.Attributes.FileInfo), &ufi)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode FileInfo: %w", err)
+	}
+	return &ufi, nil
+}
+
 // UploadResponse is the response to a file Upload
 type UploadResponse struct {
 	Uploads []UploadInfo `json:"data"`
diff --git a/backend/zoho/zoho.go b/backend/zoho/zoho.go
index e4e69353f..207289c71 100644
--- a/backend/zoho/zoho.go
+++ b/backend/zoho/zoho.go
@@ -677,25 +677,26 @@ func (f *Fs) upload(ctx context.Context, name string, parent string, size int64,
 	if len(uploadResponse.Uploads) != 1 {
 		return nil, errors.New("upload: invalid response")
 	}
-	// Received meta data is missing size so we have to read it again.
-	// It doesn't always appear on first read so try again if necessary
-	var info *api.Item
-	const maxTries = 10
-	sleepTime := 100 * time.Millisecond
-	for i := 0; i < maxTries; i++ {
-		info, err = f.readMetaDataForID(ctx, uploadResponse.Uploads[0].Attributes.RessourceID)
-		if err != nil {
-			return nil, err
-		}
-		if info.Attributes.StorageInfo.Size != 0 || size == 0 {
-			break
-		}
-		fs.Debugf(f, "Size not available yet for %q - try again in %v (try %d/%d)", name, sleepTime, i+1, maxTries)
-		time.Sleep(sleepTime)
-		sleepTime *= 2
+	upload := uploadResponse.Uploads[0]
+	uploadInfo, err := upload.GetUploadFileInfo()
+	if err != nil {
+		return nil, fmt.Errorf("upload error: %w", err)
 	}
 
-	return info, nil
+	// Fill in the api.Item from the api.UploadFileInfo
+	var info api.Item
+	info.ID = upload.Attributes.RessourceID
+	info.Attributes.Name = upload.Attributes.FileName
+	// info.Attributes.Type = not used
+	info.Attributes.IsFolder = false
+	// info.Attributes.CreatedTime = not used
+	info.Attributes.ModifiedTime = uploadInfo.GetModTime()
+	// info.Attributes.UploadedTime = 0 not used
+	info.Attributes.StorageInfo.Size = uploadInfo.Size
+	info.Attributes.StorageInfo.FileCount = 0
+	info.Attributes.StorageInfo.FolderCount = 0
+
+	return &info, nil
 }
 
 // Put the object into the container
diff --git a/cmd/config/config.go b/cmd/config/config.go
index 0300fd76e..4aefe62c0 100644
--- a/cmd/config/config.go
+++ b/cmd/config/config.go
@@ -36,6 +36,7 @@ func init() {
 	configCommand.AddCommand(configReconnectCommand)
 	configCommand.AddCommand(configDisconnectCommand)
 	configCommand.AddCommand(configUserInfoCommand)
+	configCommand.AddCommand(configEncryptionCommand)
 }
 
 var configCommand = &cobra.Command{
@@ -518,3 +519,91 @@ system.
 		return nil
 	},
 }
+
+func init() {
+	configEncryptionCommand.AddCommand(configEncryptionSetCommand)
+	configEncryptionCommand.AddCommand(configEncryptionRemoveCommand)
+	configEncryptionCommand.AddCommand(configEncryptionCheckCommand)
+}
+
+var configEncryptionCommand = &cobra.Command{
+	Use:   "encryption",
+	Short: `set, remove and check the encryption for the config file`,
+	Long: `This command sets, clears and checks the encryption for the config file using
+the subcommands below.
+`,
+}
+
+var configEncryptionSetCommand = &cobra.Command{
+	Use:   "set",
+	Short: `Set or change the config file encryption password`,
+	Long: strings.ReplaceAll(`This command sets or changes the config file encryption password.
+
+If there was no config password set then it sets a new one, otherwise
+it changes the existing config password.
+
+Note that if you are changing an encryption password using
+|--password-command| then this will be called once to decrypt the
+config using the old password and then again to read the new
+password to re-encrypt the config.
+
+When |--password-command| is called to change the password then the
+environment variable |RCLONE_PASSWORD_CHANGE=1| will be set. So if
+changing passwords programatically you can use the environment
+variable to distinguish which password you must supply.
+
+Alternatively you can remove the password first (with |rclone config
+encryption remove|), then set it again with this command which may be
+easier if you don't mind the unecrypted config file being on the disk
+briefly.
+`, "|", "`"),
+	RunE: func(command *cobra.Command, args []string) error {
+		cmd.CheckArgs(0, 0, command, args)
+		config.LoadedData()
+		config.ChangeConfigPasswordAndSave()
+		return nil
+	},
+}
+
+var configEncryptionRemoveCommand = &cobra.Command{
+	Use:   "remove",
+	Short: `Remove the config file encryption password`,
+	Long: strings.ReplaceAll(`Remove the config file encryption password
+
+This removes the config file encryption, returning it to un-encrypted.
+
+If |--password-command| is in use, this will be called to supply the old config
+password.
+
+If the config was not encrypted then no error will be returned and
+this command will do nothing.
+`, "|", "`"),
+	RunE: func(command *cobra.Command, args []string) error {
+		cmd.CheckArgs(0, 0, command, args)
+		config.LoadedData()
+		config.RemoveConfigPasswordAndSave()
+		return nil
+	},
+}
+
+var configEncryptionCheckCommand = &cobra.Command{
+	Use:   "check",
+	Short: `Check that the config file is encrypted`,
+	Long: strings.ReplaceAll(`This checks the config file is encrypted and that you can decrypt it.
+
+It will attempt to decrypt the config using the password you supply.
+
+If decryption fails it will return a non-zero exit code if using
+|--password-command|, otherwise it will prompt again for the password.
+
+If the config file is not encrypted it will return a non zero exit code.
+`, "|", "`"),
+	RunE: func(command *cobra.Command, args []string) error {
+		cmd.CheckArgs(0, 0, command, args)
+		config.LoadedData()
+		if !config.IsEncrypted() {
+			return errors.New("config file is NOT encrypted")
+		}
+		return nil
+	},
+}
diff --git a/cmd/serve/nfs/filesystem.go b/cmd/serve/nfs/filesystem.go
index 17e044856..db918d895 100644
--- a/cmd/serve/nfs/filesystem.go
+++ b/cmd/serve/nfs/filesystem.go
@@ -3,6 +3,7 @@
 package nfs
 
 import (
+	"math"
 	"os"
 	"path"
 	"strings"
@@ -13,8 +14,34 @@ import (
 	"github.com/rclone/rclone/fs/log"
 	"github.com/rclone/rclone/vfs"
 	"github.com/rclone/rclone/vfs/vfscommon"
+	"github.com/willscott/go-nfs/file"
 )
 
+// setSys sets the Sys() call up for the vfs.Node passed in
+//
+// The billy abstraction layer does not extend to exposing `uid` and `gid`
+// ownership of files. If ownership is important to your file system, you
+// will need to ensure that the `os.FileInfo` meets additional constraints.
+// In particular, the `Sys()` escape hatch is queried by this library, and
+// if your file system populates a [`syscall.Stat_t`](https://golang.org/pkg/syscall/#Stat_t)
+// concrete struct, the ownership specified in that object will be used.
+// It can also return a file.FileInfo which is easier to manage cross platform
+func setSys(fi os.FileInfo) {
+	node, ok := fi.(vfs.Node)
+	if !ok {
+		fs.Errorf(fi, "internal error: %T is not a vfs.Node", fi)
+	}
+	vfs := node.VFS()
+	// Set the UID and GID for the node passed in from the VFS defaults.
+	stat := file.FileInfo{
+		Nlink:  1,
+		UID:    vfs.Opt.UID,
+		GID:    vfs.Opt.GID,
+		Fileid: math.MaxUint64, // without this mounting doesn't work on Linux
+	}
+	node.SetSys(&stat)
+}
+
 // FS is our wrapper around the VFS to properly support billy.Filesystem interface
 type FS struct {
 	vfs *vfs.VFS
@@ -23,7 +50,14 @@ type FS struct {
 // ReadDir implements read dir
 func (f *FS) ReadDir(path string) (dir []os.FileInfo, err error) {
 	defer log.Trace(path, "")("items=%d, err=%v", &dir, &err)
-	return f.vfs.ReadDir(path)
+	dir, err = f.vfs.ReadDir(path)
+	if err != nil {
+		return nil, err
+	}
+	for _, fi := range dir {
+		setSys(fi)
+	}
+	return dir, nil
 }
 
 // Create implements creating new files
@@ -47,7 +81,12 @@ func (f *FS) OpenFile(filename string, flag int, perm os.FileMode) (node billy.F
 // Stat gets the file stat
 func (f *FS) Stat(filename string) (fi os.FileInfo, err error) {
 	defer log.Trace(filename, "")("fi=%v, err=%v", &fi, &err)
-	return f.vfs.Stat(filename)
+	fi, err = f.vfs.Stat(filename)
+	if err != nil {
+		return nil, err
+	}
+	setSys(fi)
+	return fi, nil
 }
 
 // Rename renames a file
@@ -95,7 +134,12 @@ func (f *FS) MkdirAll(filename string, perm os.FileMode) (err error) {
 // Lstat gets the stats for symlink
 func (f *FS) Lstat(filename string) (fi os.FileInfo, err error) {
 	defer log.Trace(filename, "")("fi=%v, err=%v", &fi, &err)
-	return f.vfs.Stat(filename)
+	fi, err = f.vfs.Stat(filename)
+	if err != nil {
+		return nil, err
+	}
+	setSys(fi)
+	return fi, nil
 }
 
 // Symlink is not supported over NFS
diff --git a/cmd/serve/nfs/handler.go b/cmd/serve/nfs/handler.go
index de419d4f9..921bd89dc 100644
--- a/cmd/serve/nfs/handler.go
+++ b/cmd/serve/nfs/handler.go
@@ -24,7 +24,8 @@ type Handler struct {
 }
 
 // NewHandler creates a handler for the provided filesystem
-func NewHandler(vfs *vfs.VFS, opt *Options) (handler nfs.Handler, err error) {
+func NewHandler(ctx context.Context, vfs *vfs.VFS, opt *Options) (handler nfs.Handler, err error) {
+	ci := fs.GetConfig(ctx)
 	h := &Handler{
 		vfs:     vfs,
 		opt:     *opt,
@@ -35,7 +36,20 @@ func NewHandler(vfs *vfs.VFS, opt *Options) (handler nfs.Handler, err error) {
 	if err != nil {
 		return nil, fmt.Errorf("failed to make cache: %w", err)
 	}
-	nfs.SetLogger(&logIntercepter{Level: nfs.DebugLevel})
+	var level nfs.LogLevel
+	switch {
+	case ci.LogLevel >= fs.LogLevelDebug: // Debug level, needs -vv
+		level = nfs.TraceLevel
+	case ci.LogLevel >= fs.LogLevelInfo: // Transfers, needs -v
+		level = nfs.InfoLevel
+	case ci.LogLevel >= fs.LogLevelNotice: // Normal logging, -q suppresses
+		level = nfs.WarnLevel
+	case ci.LogLevel >= fs.LogLevelError: // Error - can't be suppressed
+		level = nfs.ErrorLevel
+	default:
+		level = nfs.WarnLevel
+	}
+	nfs.SetLogger(&logger{level: level})
 	return h, nil
 }
 
@@ -108,120 +122,167 @@ func onUnmount() {
 	}
 }
 
-// logIntercepter intercepts noisy go-nfs logs and reroutes them to DEBUG
-type logIntercepter struct {
-	Level nfs.LogLevel
+// logger handles go-nfs logs and reroutes them to rclone's logging system
+type logger struct {
+	level nfs.LogLevel
 }
 
-// Intercept intercepts go-nfs logs and calls fs.Debugf instead
-func (l *logIntercepter) Intercept(args ...interface{}) {
-	args = append([]interface{}{"[NFS DEBUG] "}, args...)
-	argsS := fmt.Sprint(args...)
-	fs.Debugf(nil, "%v", argsS)
+// logPrint intercepts go-nfs logs and calls rclone's log system instead
+func (l *logger) logPrint(level fs.LogLevel, args ...interface{}) {
+	fs.LogPrintf(level, "nfs", "%s", fmt.Sprint(args...))
 }
 
-// Interceptf intercepts go-nfs logs and calls fs.Debugf instead
-func (l *logIntercepter) Interceptf(format string, args ...interface{}) {
-	argsS := fmt.Sprint(args...)
-	// bit of a workaround... the real fix is probably https://github.com/willscott/go-nfs/pull/28
-	if strings.Contains(argsS, "mount.Umnt") {
-		onUnmount()
-	}
-
-	fs.Debugf(nil, "[NFS DEBUG] "+format, args...)
+// logPrintf intercepts go-nfs logs and calls rclone's log system instead
+func (l *logger) logPrintf(level fs.LogLevel, format string, args ...interface{}) {
+	fs.LogPrintf(level, "nfs", format, args...)
 }
 
 // Debug reroutes go-nfs Debug messages to Intercept
-func (l *logIntercepter) Debug(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Debug(args ...interface{}) {
+	if l.level < nfs.DebugLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelDebug, args...)
 }
 
-// Debugf reroutes go-nfs Debugf messages to Interceptf
-func (l *logIntercepter) Debugf(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Debugf reroutes go-nfs Debugf messages to logPrintf
+func (l *logger) Debugf(format string, args ...interface{}) {
+	if l.level < nfs.DebugLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelDebug, format, args...)
 }
 
 // Error reroutes go-nfs Error messages to Intercept
-func (l *logIntercepter) Error(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Error(args ...interface{}) {
+	if l.level < nfs.ErrorLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelError, args...)
 }
 
-// Errorf reroutes go-nfs Errorf messages to Interceptf
-func (l *logIntercepter) Errorf(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Errorf reroutes go-nfs Errorf messages to logPrintf
+func (l *logger) Errorf(format string, args ...interface{}) {
+	if l.level < nfs.ErrorLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelError, format, args...)
 }
 
 // Fatal reroutes go-nfs Fatal messages to Intercept
-func (l *logIntercepter) Fatal(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Fatal(args ...interface{}) {
+	if l.level < nfs.FatalLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelError, args...)
 }
 
-// Fatalf reroutes go-nfs Fatalf messages to Interceptf
-func (l *logIntercepter) Fatalf(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Fatalf reroutes go-nfs Fatalf messages to logPrintf
+func (l *logger) Fatalf(format string, args ...interface{}) {
+	if l.level < nfs.FatalLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelError, format, args...)
 }
 
 // GetLevel returns the nfs.LogLevel
-func (l *logIntercepter) GetLevel() nfs.LogLevel {
-	return l.Level
+func (l *logger) GetLevel() nfs.LogLevel {
+	return l.level
 }
 
 // Info reroutes go-nfs Info messages to Intercept
-func (l *logIntercepter) Info(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Info(args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelInfo, args...)
 }
 
-// Infof reroutes go-nfs Infof messages to Interceptf
-func (l *logIntercepter) Infof(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Infof reroutes go-nfs Infof messages to logPrintf
+func (l *logger) Infof(format string, args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelInfo, format, args...)
 }
 
 // Panic reroutes go-nfs Panic messages to Intercept
-func (l *logIntercepter) Panic(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Panic(args ...interface{}) {
+	if l.level < nfs.PanicLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelError, args...)
 }
 
-// Panicf reroutes go-nfs Panicf messages to Interceptf
-func (l *logIntercepter) Panicf(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Panicf reroutes go-nfs Panicf messages to logPrintf
+func (l *logger) Panicf(format string, args ...interface{}) {
+	if l.level < nfs.PanicLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelError, format, args...)
 }
 
 // ParseLevel parses the nfs.LogLevel
-func (l *logIntercepter) ParseLevel(level string) (nfs.LogLevel, error) {
+func (l *logger) ParseLevel(level string) (nfs.LogLevel, error) {
 	return nfs.Log.ParseLevel(level)
 }
 
 // Print reroutes go-nfs Print messages to Intercept
-func (l *logIntercepter) Print(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Print(args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelInfo, args...)
 }
 
 // Printf reroutes go-nfs Printf messages to Intercept
-func (l *logIntercepter) Printf(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+func (l *logger) Printf(format string, args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelInfo, format, args...)
 }
 
 // SetLevel sets the nfs.LogLevel
-func (l *logIntercepter) SetLevel(level nfs.LogLevel) {
-	l.Level = level
+func (l *logger) SetLevel(level nfs.LogLevel) {
+	l.level = level
 }
 
 // Trace reroutes go-nfs Trace messages to Intercept
-func (l *logIntercepter) Trace(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Trace(args ...interface{}) {
+	if l.level < nfs.DebugLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelDebug, args...)
 }
 
-// Tracef reroutes go-nfs Tracef messages to Interceptf
-func (l *logIntercepter) Tracef(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Tracef reroutes go-nfs Tracef messages to logPrintf
+func (l *logger) Tracef(format string, args ...interface{}) {
+	// FIXME BODGE ... the real fix is probably https://github.com/willscott/go-nfs/pull/28
+	// This comes from `Log.Tracef("request: %v", w.req)` in conn.go
+	// DEBUG : nfs: request: RPC #3285799202 (mount.Umnt)
+	argsS := fmt.Sprint(args...)
+	if strings.Contains(argsS, "mount.Umnt") {
+		onUnmount()
+	}
+	if l.level < nfs.DebugLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelDebug, format, args...)
 }
 
 // Warn reroutes go-nfs Warn messages to Intercept
-func (l *logIntercepter) Warn(args ...interface{}) {
-	l.Intercept(args...)
+func (l *logger) Warn(args ...interface{}) {
+	if l.level < nfs.WarnLevel {
+		return
+	}
+	l.logPrint(fs.LogLevelNotice, args...)
 }
 
-// Warnf reroutes go-nfs Warnf messages to Interceptf
-func (l *logIntercepter) Warnf(format string, args ...interface{}) {
-	l.Interceptf(format, args...)
+// Warnf reroutes go-nfs Warnf messages to logPrintf
+func (l *logger) Warnf(format string, args ...interface{}) {
+	if l.level < nfs.WarnLevel {
+		return
+	}
+	l.logPrintf(fs.LogLevelNotice, format, args...)
 }
diff --git a/cmd/serve/nfs/server.go b/cmd/serve/nfs/server.go
index baa8acdf6..47584ecd0 100644
--- a/cmd/serve/nfs/server.go
+++ b/cmd/serve/nfs/server.go
@@ -37,7 +37,7 @@ func NewServer(ctx context.Context, vfs *vfs.VFS, opt *Options) (s *Server, err
 		ctx: ctx,
 		opt: *opt,
 	}
-	s.handler, err = NewHandler(vfs, opt)
+	s.handler, err = NewHandler(ctx, vfs, opt)
 	if err != nil {
 		return nil, fmt.Errorf("failed to make NFS handler: %w", err)
 	}
diff --git a/docs/content/authors.md b/docs/content/authors.md
index 78b27fb00..0b1632c20 100644
--- a/docs/content/authors.md
+++ b/docs/content/authors.md
@@ -883,3 +883,6 @@ put them back in again.` >}}
   * Georg Welzel <gwelzel@mailbox.org>
   * John Oxley <john.oxley@gmail.com> <joxley@meta.com>
   * Pawel Palucha <pawel.palucha@aetion.com>
+  * crystalstall <crystalruby@qq.com>
+  * nipil <nipil@users.noreply.github.com>
+  * yuval-cloudinary <46710068+yuval-cloudinary@users.noreply.github.com>
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 06cde0a24..f8f5153d9 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -1924,11 +1924,12 @@ Suffix length limit is 16 characters.
 
 The default is `.partial`.
 
-### --password-command SpaceSepList ###
+### --password-command SpaceSepList {#password-command}
 
 This flag supplies a program which should supply the config password
 when run. This is an alternative to rclone prompting for the password
-or setting the `RCLONE_CONFIG_PASS` variable.
+or setting the `RCLONE_CONFIG_PASS` variable. It is also used when
+setting the config password for the first time.
 
 The argument to this should be a command with a space separated list
 of arguments. If one of the arguments has a space in then enclose it
@@ -1942,6 +1943,11 @@ Eg
     --password-command 'echo "hello with space"'
     --password-command 'echo "hello with ""quotes"" and space"'
 
+Note that when changing the configuration password the environment
+variable `RCLONE_PASSWORD_CHANGE=1` will be set. This can be used to
+distinguish initial decryption of the config file from the new
+password.
+
 See the [Configuration Encryption](#configuration-encryption) for more info.
 
 See a [Windows PowerShell example on the Wiki](https://github.com/rclone/rclone/wiki/Windows-Powershell-use-rclone-password-command-for-Config-file-password).
@@ -2545,6 +2551,12 @@ encryption from your configuration.
 
 There is no way to recover the configuration if you lose your password.
 
+You can also use
+
+- [rclone config encryption set](/commands/rclone_config_encryption_set/) to set the config encryption directly
+- [rclone config encryption remove](/commands/rclone_config_encryption_remove/) to remove it
+- [rclone config encryption check](/commands/rclone_config_encryption_check/) to check that it is encrypted properly.
+
 rclone uses [nacl secretbox](https://godoc.org/golang.org/x/crypto/nacl/secretbox)
 which in turn uses XSalsa20 and Poly1305 to encrypt and authenticate
 your configuration with secret-key cryptography.
@@ -2577,7 +2589,7 @@ An alternate means of supplying the password is to provide a script
 which will retrieve the password and print on standard output.  This
 script should have a fully specified path name and not rely on any
 environment variables.  The script is supplied either via
-`--password-command="..."` command line argument or via the
+[`--password-command="..."`](#password-command) command line argument or via the
 `RCLONE_PASSWORD_COMMAND` environment variable.
 
 One useful example of this is using the `passwordstore` application
@@ -2620,6 +2632,57 @@ general, but are used without referencing a stored remote, e.g.
 listing local filesystem paths, or
 [connection strings](#connection-strings): `rclone --config="" ls .`
 
+Configuration Encryption Cheatsheet
+-----------------------------------
+You can quickly apply a configuration encryption without plain-text
+at rest or transfer. Detailed instructions for popular OSes:
+
+### Mac ###
+
+* Generate and store a password
+
+`security add-generic-password -a rclone -s config -w $(openssl rand -base64 40)`
+
+* Add the retrieval instruction to your .zprofile / .profile
+
+`export RCLONE_PASSWORD_COMMAND="/usr/bin/security find-generic-password -a rclone -s config -w"`
+
+### Linux ###
+
+* Prerequisite
+
+Linux doesn't come with a default password manager. Let's install
+the "pass" utility using a package manager, e.g. `apt install pass`,
+ `yum install pass`,
+ [etc.](https://www.passwordstore.org/#download); then initialize a
+ password store:
+
+`pass init rclone`
+
+* Generate and store a password
+
+`echo $(openssl rand -base64 40) | pass insert -m rclone/config`
+
+* Add the retrieval instruction
+
+`export RCLONE_PASSWORD_COMMAND="/usr/bin/pass rclone/config"`
+
+### Windows ###
+
+* Generate and store a password
+
+`New-Object -TypeName PSCredential -ArgumentList "rclone", (ConvertTo-SecureString -String ([System.Web.Security.Membership]::GeneratePassword(40, 10)) -AsPlainText -Force) | Export-Clixml -Path "rclone-credential.xml"`
+
+* Add the password retrieval instruction
+
+`[Environment]::SetEnvironmentVariable("RCLONE_PASSWORD_COMMAND", "[System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR((Import-Clixml -Path "rclone-credential.xml").Password))")`
+
+### Encrypt the config file (all systems) ###
+
+* Execute `rclone config` -> `s`
+
+* Add/update the password from previous steps
+
 Developer options
 -----------------
 
diff --git a/docs/content/filtering.md b/docs/content/filtering.md
index db16cf7cc..ffc898c25 100644
--- a/docs/content/filtering.md
+++ b/docs/content/filtering.md
@@ -673,7 +673,7 @@ remote or flag value. The fix then is to quote values containing spaces.
 ### `--min-size` - Don't transfer any file smaller than this
 
 Controls the minimum size file within the scope of an rclone command.
-Default units are `KiB` but abbreviations `K`, `M`, `G`, `T` or `P` are valid.
+Default units are `KiB` but abbreviations `B`, `K`, `M`, `G`, `T` or `P` are valid.
 
 E.g. `rclone ls remote: --min-size 50k` lists files on `remote:` of 50 KiB
 size or larger.
@@ -683,7 +683,7 @@ See [the size option docs](/docs/#size-option) for more info.
 ### `--max-size` - Don't transfer any file larger than this
 
 Controls the maximum size file within the scope of an rclone command.
-Default units are `KiB` but abbreviations `K`, `M`, `G`, `T` or `P` are valid.
+Default units are `KiB` but abbreviations `B`, `K`, `M`, `G`, `T` or `P` are valid.
 
 E.g. `rclone ls remote: --max-size 1G` lists files on `remote:` of 1 GiB
 size or smaller.
diff --git a/docs/content/local.md b/docs/content/local.md
index 849e2cc54..8aeb35151 100644
--- a/docs/content/local.md
+++ b/docs/content/local.md
@@ -130,12 +130,8 @@ 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)).
-
-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.
+Using the same prefix `\\?\` it is also possible to specify path to volumes
+identified by their GUID, e.g. `\\?\Volume{b75e2c83-0000-0000-0000-602f00000000}\some\path`.
 
 #### Long paths ####
 
diff --git a/docs/content/release_signing.md b/docs/content/release_signing.md
index 8e19303dc..824d42d6b 100644
--- a/docs/content/release_signing.md
+++ b/docs/content/release_signing.md
@@ -149,7 +149,7 @@ $ rclone hashsum sha256 -C SHA256SUMS rclone-v1.63.1-windows-amd64.zip
 You can verify the signatures and hashes in one command line like this:
 
 ```
-$ gpg --decrypt SHA256SUMS | sha256sum -c --ignore-missing
+$ h=$(gpg --decrypt SHA256SUMS) && echo "$h" | sha256sum - -c --ignore-missing
 gpg: Signature made Mon 17 Jul 2023 15:03:17 BST
 gpg:                using DSA key FBF737ECE9F8AB18604BD2AC93935E02FF3B54FA
 gpg: Good signature from "Nick Craig-Wood <nick@craig-wood.com>" [ultimate]
diff --git a/docs/content/yandex.md b/docs/content/yandex.md
index 45039f0d7..6c65b1adc 100644
--- a/docs/content/yandex.md
+++ b/docs/content/yandex.md
@@ -210,6 +210,17 @@ Properties:
 - Type:        Encoding
 - Default:     Slash,Del,Ctl,InvalidUtf8,Dot
 
+#### --yandex-spoof-ua
+
+Set the user agent to match an official version of the yandex disk client. May help with upload performance.
+
+Properties:
+
+- Config:      spoof_ua
+- Env Var:     RCLONE_YANDEX_SPOOF_UA
+- Type:        bool
+- Default:     true
+
 #### --yandex-description
 
 Description of the remote.
diff --git a/fs/config/configfile/configfile_other.go b/fs/config/configfile/configfile_other.go
index ef509430c..c70a51603 100644
--- a/fs/config/configfile/configfile_other.go
+++ b/fs/config/configfile/configfile_other.go
@@ -5,6 +5,6 @@
 
 package configfile
 
-// attemptCopyGroups tries to keep the group the same, which only makes sense
+// attemptCopyGroup tries to keep the group the same, which only makes sense
 // for system with user-group-world permission model.
 func attemptCopyGroup(fromPath, toPath string) {}
diff --git a/fs/config/configfile/configfile_unix.go b/fs/config/configfile/configfile_unix.go
index 542e24621..d5e8ee3dd 100644
--- a/fs/config/configfile/configfile_unix.go
+++ b/fs/config/configfile/configfile_unix.go
@@ -14,7 +14,7 @@ import (
 	"github.com/rclone/rclone/fs"
 )
 
-// attemptCopyGroups tries to keep the group the same. User will be the one
+// attemptCopyGroup tries to keep the group the same. User will be the one
 // who is currently running this process.
 func attemptCopyGroup(fromPath, toPath string) {
 	info, err := os.Stat(fromPath)
diff --git a/fs/config/configflags/configflags.go b/fs/config/configflags/configflags.go
index d3beb9a3e..483e41a67 100644
--- a/fs/config/configflags/configflags.go
+++ b/fs/config/configflags/configflags.go
@@ -210,7 +210,7 @@ func SetFlags(ci *fs.ConfigInfo) {
 	}
 }
 
-// parseHeaders converts DSCP names to value
+// parseDSCP converts DSCP names to value
 func parseDSCP(dscp string) (uint8, bool) {
 	if s, err := strconv.ParseUint(dscp, 10, 6); err == nil {
 		return uint8(s), true
diff --git a/fs/config/crypt.go b/fs/config/crypt.go
index 8fc476f82..4a68d4159 100644
--- a/fs/config/crypt.go
+++ b/fs/config/crypt.go
@@ -41,6 +41,11 @@ var (
 	PassConfigKeyForDaemonization = false
 )
 
+// IsEncrypted returns true if the config file is encrypted
+func IsEncrypted() bool {
+	return len(configKey) > 0
+}
+
 // Decrypt will automatically decrypt a reader
 func Decrypt(b io.ReadSeeker) (io.Reader, error) {
 	ctx := context.Background()
@@ -78,37 +83,19 @@ func Decrypt(b io.ReadSeeker) (io.Reader, error) {
 	}
 
 	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, fmt.Errorf("password command failed: %w", err)
-			}
-			if pass := strings.Trim(stdout.String(), "\r\n"); pass != "" {
-				err := SetConfigPassword(pass)
-				if err != nil {
-					return nil, fmt.Errorf("incorrect password: %w", err)
-				}
-			} else {
-				return nil, errors.New("password-command returned empty string")
+		pass, err := GetPasswordCommand(ctx)
+		if err != nil {
+			return nil, err
+		}
+		if pass != "" {
+			usingPasswordCommand = true
+			err = SetConfigPassword(pass)
+			if err != nil {
+				return nil, fmt.Errorf("incorrect password: %w", err)
 			}
-
 			if len(configKey) == 0 {
 				return nil, errors.New("unable to decrypt configuration: incorrect password")
 			}
-			usingPasswordCommand = true
 		} else {
 			usingPasswordCommand = false
 
@@ -183,6 +170,40 @@ func Decrypt(b io.ReadSeeker) (io.Reader, error) {
 	return bytes.NewReader(out), nil
 }
 
+// GetPasswordCommand gets the password using the --password-command setting
+//
+// If the the --password-command flag was not in use it returns "", nil
+func GetPasswordCommand(ctx context.Context) (pass string, err error) {
+	ci := fs.GetConfig(ctx)
+	if len(ci.PasswordCommand) == 0 {
+		return "", nil
+	}
+
+	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
+
+	err = cmd.Run()
+	if 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 pass, fmt.Errorf("password command failed: %w", err)
+	}
+	pass = strings.Trim(stdout.String(), "\r\n")
+	if pass == "" {
+		return pass, errors.New("--password-command returned empty string")
+	}
+	return pass, nil
+}
+
 // Encrypt the config file
 func Encrypt(src io.Reader, dst io.Writer) error {
 	if len(configKey) == 0 {
@@ -294,10 +315,46 @@ func ClearConfigPassword() {
 // changeConfigPassword will query the user twice
 // for a password. If the same password is entered
 // twice the key is updated.
+//
+// This will use --password-command if configured to read the password.
 func changeConfigPassword() {
-	err := SetConfigPassword(ChangePassword("NEW configuration"))
+	// Set RCLONE_PASSWORD_CHANGE to "1" when calling the --password-command tool
+	_ = os.Setenv("RCLONE_PASSWORD_CHANGE", "1")
+	defer func() {
+		_ = os.Unsetenv("RCLONE_PASSWORD_CHANGE")
+	}()
+	pass, err := GetPasswordCommand(context.Background())
+	if err != nil {
+		fmt.Printf("Failed to read new password with --password-command: %v\n", err)
+		return
+	}
+	if pass == "" {
+		pass = ChangePassword("NEW configuration")
+	} else {
+		fmt.Printf("Read password using --password-command\n")
+	}
+	err = SetConfigPassword(pass)
 	if err != nil {
 		fmt.Printf("Failed to set config password: %v\n", err)
 		return
 	}
 }
+
+// ChangeConfigPasswordAndSave will query the user twice
+// for a password. If the same password is entered
+// twice the key is updated.
+//
+// This will use --password-command if configured to read the password.
+//
+// It will then save the config
+func ChangeConfigPasswordAndSave() {
+	changeConfigPassword()
+	SaveConfig()
+}
+
+// RemoveConfigPasswordAndSave will clear the config password and save
+// the unencrypted config file.
+func RemoveConfigPasswordAndSave() {
+	configKey = nil
+	SaveConfig()
+}
diff --git a/fs/config/ui.go b/fs/config/ui.go
index ff8acda0f..b08e9481c 100644
--- a/fs/config/ui.go
+++ b/fs/config/ui.go
@@ -797,13 +797,11 @@ func SetPassword() {
 			what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"}
 			switch i := Command(what); i {
 			case 'c':
-				changeConfigPassword()
-				SaveConfig()
+				ChangeConfigPasswordAndSave()
 				fmt.Println("Password changed")
 				continue
 			case 'u':
-				configKey = nil
-				SaveConfig()
+				RemoveConfigPasswordAndSave()
 				continue
 			case 'q':
 				return
@@ -815,8 +813,7 @@ func SetPassword() {
 			what := []string{"aAdd Password", "qQuit to main menu"}
 			switch i := Command(what); i {
 			case 'a':
-				changeConfigPassword()
-				SaveConfig()
+				ChangeConfigPasswordAndSave()
 				fmt.Println("Password set")
 				continue
 			case 'q':
diff --git a/go.mod b/go.mod
index c26d5f49b..e860ff31a 100644
--- a/go.mod
+++ b/go.mod
@@ -51,7 +51,7 @@ require (
 	github.com/minio/minio-go/v7 v7.0.74
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/moby/sys/mountinfo v0.7.2
-	github.com/ncw/swift/v2 v2.0.2
+	github.com/ncw/swift/v2 v2.0.3
 	github.com/oracle/oci-go-sdk/v65 v65.69.2
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/pkg/sftp v1.13.6
diff --git a/go.sum b/go.sum
index 62cebef03..3b48fdf64 100644
--- a/go.sum
+++ b/go.sum
@@ -461,8 +461,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/ncw/swift/v2 v2.0.2 h1:jx282pcAKFhmoZBSdMcCRFn9VWkoBIRsCpe+yZq7vEk=
-github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItcb+Kg=
+github.com/ncw/swift/v2 v2.0.3 h1:8R9dmgFIWs+RiVlisCEfiQiik1hjuR0JnOkLxaP9ihg=
+github.com/ncw/swift/v2 v2.0.3/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
diff --git a/lib/file/mkdir_other.go b/lib/file/mkdir_other.go
index 6baa7566b..3803885fb 100644
--- a/lib/file/mkdir_other.go
+++ b/lib/file/mkdir_other.go
@@ -1,10 +1,10 @@
-//go:build !windows
+//go:build !windows || go1.22
 
 package file
 
 import "os"
 
-// MkdirAll just calls os.MkdirAll on non-Windows.
+// MkdirAll just calls os.MkdirAll on non-Windows and with go1.22 or newer on Windows
 func MkdirAll(path string, perm os.FileMode) error {
 	return os.MkdirAll(path, perm)
 }
diff --git a/lib/file/mkdir_windows.go b/lib/file/mkdir_windows.go
index 4b5ba4f12..0da50c0c8 100644
--- a/lib/file/mkdir_windows.go
+++ b/lib/file/mkdir_windows.go
@@ -1,4 +1,4 @@
-//go:build windows
+//go:build windows && !go1.22
 
 package file
 


Test patch:
diff --git a/fs/config/config_test.go b/fs/config/config_test.go
index 612105a6c..7c4a503bf 100644
--- a/fs/config/config_test.go
+++ b/fs/config/config_test.go
@@ -10,6 +10,10 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func init() {
+	configfile.Install()
+}
+
 func TestConfigLoad(t *testing.T) {
 	oldConfigPath := config.GetConfigPath()
 	assert.NoError(t, config.SetConfigPath("./testdata/plain.conf"))
@@ -17,7 +21,6 @@ func TestConfigLoad(t *testing.T) {
 		assert.NoError(t, config.SetConfigPath(oldConfigPath))
 	}()
 	config.ClearConfigPassword()
-	configfile.Install()
 	sections := config.Data().GetSectionList()
 	var expect = []string{"RCLONE_ENCRYPT_V0", "nounc", "unc"}
 	assert.Equal(t, expect, sections)
diff --git a/fs/config/crypt_internal_test.go b/fs/config/crypt_internal_test.go
index 05dc71e0f..2d19643c5 100644
--- a/fs/config/crypt_internal_test.go
+++ b/fs/config/crypt_internal_test.go
@@ -1,8 +1,12 @@
 package config
 
 import (
+	"context"
+	"os"
+	"path/filepath"
 	"testing"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -46,3 +50,57 @@ func TestPassword(t *testing.T) {
 	hashedKeyCompare(t, "abcdef", "ABCDEF", false)
 
 }
+
+func TestChangeConfigPassword(t *testing.T) {
+	ci := fs.GetConfig(context.Background())
+
+	var err error
+	oldConfigPath := GetConfigPath()
+	assert.NoError(t, SetConfigPath("./testdata/encrypted.conf"))
+	defer func() {
+		assert.NoError(t, SetConfigPath(oldConfigPath))
+		ClearConfigPassword()
+		ci.PasswordCommand = nil
+	}()
+
+	// Get rid of any config password
+	ClearConfigPassword()
+
+	// Return the password, checking the state of the environment variable
+	checkCode := `
+package main
+
+import (
+	"fmt"
+	"os"
+	"log"
+)
+
+func main() {
+	v := os.Getenv("RCLONE_PASSWORD_CHANGE")
+	if v == "" {
+		log.Fatal("Env var not found")
+	} else if v != "1" {
+		log.Fatal("Env var wrong value")
+	} else {
+		fmt.Println("asdf")
+	}
+}
+`
+	dir := t.TempDir()
+	code := filepath.Join(dir, "file.go")
+	require.NoError(t, os.WriteFile(code, []byte(checkCode), 0777))
+
+	// Set correct password using --password-command
+	ci.PasswordCommand = fs.SpaceSepList{"go", "run", code}
+	changeConfigPassword()
+	err = Data().Load()
+	require.NoError(t, err)
+	sections := Data().GetSectionList()
+	var expect = []string{"nounc", "unc"}
+	assert.Equal(t, expect, sections)
+
+	keys := Data().GetKeyList("nounc")
+	expect = []string{"type", "nounc"}
+	assert.Equal(t, expect, keys)
+}
diff --git a/fs/config/crypt_test.go b/fs/config/crypt_test.go
index 04f54864d..73c3bf6f6 100644
--- a/fs/config/crypt_test.go
+++ b/fs/config/crypt_test.go
@@ -6,6 +6,8 @@ package config_test
 
 import (
 	"context"
+	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/rclone/rclone/fs"
@@ -24,8 +26,10 @@ func TestConfigLoadEncrypted(t *testing.T) {
 	}()
 
 	// Set correct password
+	assert.False(t, config.IsEncrypted())
 	err = config.SetConfigPassword("asdf")
 	require.NoError(t, err)
+	assert.True(t, config.IsEncrypted())
 	err = config.Data().Load()
 	require.NoError(t, err)
 	sections := config.Data().GetSectionList()
@@ -113,3 +117,56 @@ func TestConfigLoadEncryptedFailures(t *testing.T) {
 	err = config.Data().Load()
 	assert.Equal(t, config.ErrorConfigFileNotFound, err)
 }
+
+func TestGetPasswordCommand(t *testing.T) {
+	ctx, ci := fs.AddConfig(context.Background())
+
+	// Not configured
+	ci.PasswordCommand = fs.SpaceSepList{}
+	pass, err := config.GetPasswordCommand(ctx)
+	require.NoError(t, err)
+	assert.Equal(t, "", pass)
+
+	// With password - happy path
+	ci.PasswordCommand = fs.SpaceSepList{"echo", "asdf"}
+	pass, err = config.GetPasswordCommand(ctx)
+	require.NoError(t, err)
+	assert.Equal(t, "asdf", pass)
+
+	// Empty password returned
+	ci.PasswordCommand = fs.SpaceSepList{"echo", ""}
+	_, err = config.GetPasswordCommand(ctx)
+	assert.ErrorContains(t, err, "returned empty string")
+
+	// Error when running command
+	ci.PasswordCommand = fs.SpaceSepList{"XXX non-existent command XXX", ""}
+	_, err = config.GetPasswordCommand(ctx)
+	assert.ErrorContains(t, err, "not found")
+
+	// Check the state of the environment variable in --password-command
+	checkCode := `
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	if _, found := os.LookupEnv("RCLONE_PASSWORD_CHANGE"); found {
+		fmt.Println("Env var set")
+	} else {
+		fmt.Println("OK")
+	}
+}
+`
+	dir := t.TempDir()
+	code := filepath.Join(dir, "file.go")
+	require.NoError(t, os.WriteFile(code, []byte(checkCode), 0777))
+
+	// Check the environment variable unset when called directly
+	ci.PasswordCommand = fs.SpaceSepList{"go", "run", code}
+	pass, err = config.GetPasswordCommand(ctx)
+	require.NoError(t, err)
+	assert.Equal(t, "OK", pass)
+}
diff --git a/lib/file/mkdir_windows_go122_test.go b/lib/file/mkdir_windows_go122_test.go
new file mode 100644
index 000000000..eb9bbbef9
--- /dev/null
+++ b/lib/file/mkdir_windows_go122_test.go
@@ -0,0 +1,122 @@
+//go:build windows && go1.22
+
+package file
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func checkMkdirAll(t *testing.T, path string, valid bool, errormsgs ...string) {
+	if valid {
+		assert.NoError(t, os.MkdirAll(path, 0777))
+	} else {
+		err := os.MkdirAll(path, 0777)
+		assert.Error(t, err)
+		ok := false
+		for _, msg := range errormsgs {
+			if err.Error() == msg {
+				ok = true
+			}
+		}
+		assert.True(t, ok, fmt.Sprintf("Error message '%v' didn't match any of %v", err, errormsgs))
+	}
+}
+
+func checkMkdirAllSubdirs(t *testing.T, path string, valid bool, errormsgs ...string) {
+	checkMkdirAll(t, path, valid, errormsgs...)
+	checkMkdirAll(t, path+`\`, valid, errormsgs...)
+	checkMkdirAll(t, path+`\parent`, valid, errormsgs...)
+	checkMkdirAll(t, path+`\parent\`, valid, errormsgs...)
+	checkMkdirAll(t, path+`\parent\child`, valid, errormsgs...)
+	checkMkdirAll(t, path+`\parent\child\`, valid, errormsgs...)
+}
+
+// Testing paths on existing drive
+func TestMkdirAllOnDrive(t *testing.T) {
+	path := t.TempDir()
+
+	dir, err := os.Stat(path)
+	require.NoError(t, err)
+	require.True(t, dir.IsDir())
+
+	drive := filepath.VolumeName(path)
+
+	checkMkdirAll(t, drive, true, "")
+	checkMkdirAll(t, drive+`\`, true, "")
+	checkMkdirAll(t, `\\?\`+drive, false, fmt.Sprintf(`mkdir \\?\%s: Access is denied.`, drive)) // This isn't actually a valid Windows path, it worked under go1.21.3 but fails under go1.21.4 and newer
+	checkMkdirAll(t, `\\?\`+drive+`\`, true, "")
+	checkMkdirAllSubdirs(t, path, true, "")
+	checkMkdirAllSubdirs(t, `\\?\`+path, true, "")
+}
+
+// Testing paths on unused drive
+// This covers the cases that we wanted to improve with our own custom version
+// of golang's os.MkdirAll, introduced in PR #5401. Before go1.22 the original
+// os.MkdirAll would recurse extended-length paths down to the "\\?" prefix and
+// return the noninformative error:
+// "mkdir \\?: The filename, directory name, or volume label syntax is incorrect."
+// Our version stopped the recursion at drive's root directory, and reported,
+// before go1.21.4:
+// "mkdir \\?\A:\: The system cannot find the path specified."
+// or, starting with go1.21.4:
+// "mkdir \\?\A:: The system cannot find the path specified."
+// See https://github.com/rclone/rclone/pull/5401.
+// Starting with go1.22 golang's os.MkdirAll have similar improvements that made
+// our custom version no longer necessary.
+func TestMkdirAllOnUnusedDrive(t *testing.T) {
+	letter := FindUnusedDriveLetter()
+	require.NotEqual(t, letter, 0)
+	drive := string(letter) + ":"
+	checkMkdirAll(t, drive, false, fmt.Sprintf(`mkdir %s: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\`, false, fmt.Sprintf(`mkdir %s\: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent`, false, fmt.Sprintf(`mkdir %s\parent: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent\`, false, fmt.Sprintf(`mkdir %s\parent\: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent\child`, false, fmt.Sprintf(`mkdir %s\parent: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent\child\`, false, fmt.Sprintf(`mkdir %s\parent: The system cannot find the path specified.`, drive))
+
+	drive = `\\?\` + drive
+	checkMkdirAll(t, drive, false, fmt.Sprintf(`mkdir %s: The system cannot find the file specified.`, drive))
+	checkMkdirAll(t, drive+`\`, false, fmt.Sprintf(`mkdir %s\: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent`, false, fmt.Sprintf(`mkdir %s\parent: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent\`, false, fmt.Sprintf(`mkdir %s\parent\: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent\child`, false, fmt.Sprintf(`mkdir %s\parent: The system cannot find the path specified.`, drive))
+	checkMkdirAll(t, drive+`\parent\child\`, false, fmt.Sprintf(`mkdir %s\parent: The system cannot find the path specified.`, drive))
+}
+
+// Testing paths on unknown network host
+// This covers more cases that we wanted to improve in our custom version of
+// golang's os.MkdirAll, extending that explained in TestMkdirAllOnUnusedDrive.
+// With our first fix, stopping it from recursing extended-length paths down to
+// the "\\?" prefix, it would now stop at `\\?\UNC`, because that is what
+// filepath.VolumeName returns (which is wrong, that is not a volume name!),
+// and still return a noninformative error:
+// "mkdir \\?\UNC\\: The filename, directory name, or volume label syntax is incorrect."
+// Our version stopped the recursion at level before this, and reports:
+// "mkdir \\?\UNC\0.0.0.0: The specified path is invalid."
+// See https://github.com/rclone/rclone/pull/6420.
+// Starting with go1.22 golang's os.MkdirAll have similar improvements that made
+// our custom version no longer necessary.
+func TestMkdirAllOnUnusedNetworkHost(t *testing.T) {
+	sharePath := `\\0.0.0.0\share`
+	checkMkdirAll(t, sharePath, false, fmt.Sprintf(`mkdir %s: The format of the specified network name is invalid.`, sharePath))
+	checkMkdirAll(t, sharePath+`\`, false, fmt.Sprintf(`mkdir %s\: The format of the specified network name is invalid.`, sharePath))
+	checkMkdirAll(t, sharePath+`\parent`, false, fmt.Sprintf(`mkdir %s\parent: The format of the specified network name is invalid.`, sharePath))
+	checkMkdirAll(t, sharePath+`\parent\`, false, fmt.Sprintf(`mkdir %s\parent\: The format of the specified network name is invalid.`, sharePath))
+	checkMkdirAll(t, sharePath+`\parent\child`, false, fmt.Sprintf(`mkdir %s\parent: The format of the specified network name is invalid.`, sharePath))
+	checkMkdirAll(t, sharePath+`\parent\child\`, false, fmt.Sprintf(`mkdir %s\parent: The format of the specified network name is invalid.`, sharePath))
+
+	serverPath := `\\?\UNC\0.0.0.0`
+	sharePath = serverPath + `\share`
+	checkMkdirAll(t, sharePath, false, fmt.Sprintf(`mkdir %s: The specified path is invalid.`, serverPath))
+	checkMkdirAll(t, sharePath+`\`, false, fmt.Sprintf(`mkdir %s: The specified path is invalid.`, serverPath))
+	checkMkdirAll(t, sharePath+`\parent`, false, fmt.Sprintf(`mkdir %s: The specified path is invalid.`, serverPath))
+	checkMkdirAll(t, sharePath+`\parent\`, false, fmt.Sprintf(`mkdir %s: The specified path is invalid.`, serverPath))
+	checkMkdirAll(t, sharePath+`\parent\child`, false, fmt.Sprintf(`mkdir %s: The specified path is invalid.`, serverPath))
+	checkMkdirAll(t, sharePath+`\parent\child\`, false, fmt.Sprintf(`mkdir %s: The specified path is invalid.`, serverPath))
+}
diff --git a/lib/file/mkdir_windows_test.go b/lib/file/mkdir_windows_test.go
index 75e050bf7..e6d4bd389 100644
--- a/lib/file/mkdir_windows_test.go
+++ b/lib/file/mkdir_windows_test.go
@@ -1,4 +1,4 @@
-//go:build windows
+//go:build windows && !go1.22
 
 package file