Repository: rclone/rclone
Pull request #7459 (2023-11-27 12:46:01)

azure-files: fix storage base url
<!--
Thank you very much for contributing code or documentation to rclone! Please
fill out the following questions to make it easier for us to review your
changes.

You do not need to check all the boxes below all at once, feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box.
-->

#### What is the purpose of this change?

Azure files are not working because of the invalid base URL. Now it's "core.windows.net", but according to documentation (proved with testing), it should be "file.core.windows.net" - see https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview (Azure Files  - `https://<storage-account>.file.core.windows.net`)
#### Was the change discussed in an issue or in the forum before?

<!--
Link issues and relevant forum posts here.
-->

#### Checklist

- [x] I have read the [contribution guidelines](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#submitting-a-new-feature-or-bug-fix).
- [ ] I have added tests for all changes in this PR if appropriate.
- [ ] 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/.github/workflows/build.yml b/.github/workflows/build.yml
index d12696daf..f2b3a0bd2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -104,7 +104,7 @@ jobs:
           fetch-depth: 0
 
       - name: Install Go
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
           go-version: ${{ matrix.go }}
           check-latest: true
@@ -241,7 +241,7 @@ jobs:
 
       # Run govulncheck on the latest go version, the one we build binaries with
       - name: Install Go
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
           go-version: '1.21'
           check-latest: true
@@ -266,7 +266,7 @@ jobs:
 
       # Upgrade together with NDK version
       - name: Set up Go
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
           go-version: '1.21'
 
diff --git a/backend/amazonclouddrive/amazonclouddrive.go b/backend/amazonclouddrive/amazonclouddrive.go
index 1bc6bcb84..c5a123915 100644
--- a/backend/amazonclouddrive/amazonclouddrive.go
+++ b/backend/amazonclouddrive/amazonclouddrive.go
@@ -1346,6 +1346,12 @@ func (f *Fs) changeNotifyRunner(notifyFunc func(string, fs.EntryType), checkpoin
 	return checkpoint
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // ID returns the ID of the Object if known, or "" if not
 func (o *Object) ID() string {
 	if o.info.Id == nil {
@@ -1363,6 +1369,7 @@ var (
 	_ fs.DirMover        = (*Fs)(nil)
 	_ fs.DirCacheFlusher = (*Fs)(nil)
 	_ fs.ChangeNotifier  = (*Fs)(nil)
+	_ fs.Shutdowner      = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.MimeTyper       = &Object{}
 	_ fs.IDer            = &Object{}
diff --git a/backend/azurefiles/azurefiles.go b/backend/azurefiles/azurefiles.go
index fd8b4a06e..95275f6d7 100644
--- a/backend/azurefiles/azurefiles.go
+++ b/backend/azurefiles/azurefiles.go
@@ -65,7 +65,7 @@ import (
 const (
 	maxFileSize           = 4 * fs.Tebi
 	defaultChunkSize      = 4 * fs.Mebi
-	storageDefaultBaseURL = "core.windows.net" // FIXME not sure this is correct
+	storageDefaultBaseURL = "file.core.windows.net"
 )
 
 func init() {
diff --git a/backend/box/box.go b/backend/box/box.go
index 26e2cb356..4f38955eb 100644
--- a/backend/box/box.go
+++ b/backend/box/box.go
@@ -1207,6 +1207,12 @@ func (f *Fs) CleanUp(ctx context.Context) (err error) {
 	return err
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // ChangeNotify calls the passed function with a path that has had changes.
 // If the implementation uses polling, it should adhere to the given interval.
 //
@@ -1719,6 +1725,7 @@ var (
 	_ fs.DirCacheFlusher = (*Fs)(nil)
 	_ fs.PublicLinker    = (*Fs)(nil)
 	_ fs.CleanUpper      = (*Fs)(nil)
+	_ fs.Shutdowner      = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.IDer            = (*Object)(nil)
 )
diff --git a/backend/chunker/chunker.go b/backend/chunker/chunker.go
index bb4b49812..baf4656ed 100644
--- a/backend/chunker/chunker.go
+++ b/backend/chunker/chunker.go
@@ -325,6 +325,14 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
 		}
 	}
 
+	// Correct root if definitely pointing to a file
+	if err == fs.ErrorIsFile {
+		f.root = path.Dir(f.root)
+		if f.root == "." || f.root == "/" {
+			f.root = ""
+		}
+	}
+
 	// Note 1: the features here are ones we could support, and they are
 	// ANDed with the ones from wrappedFs.
 	// Note 2: features.Fill() points features.PutStream to our PutStream,
diff --git a/backend/compress/compress.go b/backend/compress/compress.go
index f635c5e59..b477512e7 100644
--- a/backend/compress/compress.go
+++ b/backend/compress/compress.go
@@ -14,6 +14,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path"
 	"regexp"
 	"strings"
 	"time"
@@ -172,6 +173,13 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
 		opt:  *opt,
 		mode: compressionModeFromName(opt.CompressionMode),
 	}
+	// Correct root if definitely pointing to a file
+	if err == fs.ErrorIsFile {
+		f.root = path.Dir(f.root)
+		if f.root == "." || f.root == "/" {
+			f.root = ""
+		}
+	}
 	// the features here are ones we could support, and they are
 	// ANDed with the ones from wrappedFs
 	f.features = (&fs.Features{
diff --git a/backend/crypt/crypt.go b/backend/crypt/crypt.go
index 281805fd5..54b1335dc 100644
--- a/backend/crypt/crypt.go
+++ b/backend/crypt/crypt.go
@@ -253,6 +253,13 @@ func NewFs(ctx context.Context, name, rpath string, m configmap.Mapper) (fs.Fs,
 		cipher: cipher,
 	}
 	cache.PinUntilFinalized(f.Fs, f)
+	// Correct root if definitely pointing to a file
+	if err == fs.ErrorIsFile {
+		f.root = path.Dir(f.root)
+		if f.root == "." || f.root == "/" {
+			f.root = ""
+		}
+	}
 	// the features here are ones we could support, and they are
 	// ANDed with the ones from wrappedFs
 	f.features = (&fs.Features{
diff --git a/backend/dropbox/dropbox.go b/backend/dropbox/dropbox.go
index 1b647af08..0eea08c53 100644
--- a/backend/dropbox/dropbox.go
+++ b/backend/dropbox/dropbox.go
@@ -1231,18 +1231,21 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
 		return nil, err
 	}
 	var total uint64
+	var used = q.Used
 	if q.Allocation != nil {
 		if q.Allocation.Individual != nil {
 			total += q.Allocation.Individual.Allocated
 		}
 		if q.Allocation.Team != nil {
 			total += q.Allocation.Team.Allocated
+			// Override used with Team.Used as this includes q.Used already
+			used = q.Allocation.Team.Used
 		}
 	}
 	usage = &fs.Usage{
-		Total: fs.NewUsageValue(int64(total)),          // quota of bytes that can be used
-		Used:  fs.NewUsageValue(int64(q.Used)),         // bytes in use
-		Free:  fs.NewUsageValue(int64(total - q.Used)), // bytes which can be uploaded before reaching the quota
+		Total: fs.NewUsageValue(int64(total)),        // quota of bytes that can be used
+		Used:  fs.NewUsageValue(int64(used)),         // bytes in use
+		Free:  fs.NewUsageValue(int64(total - used)), // bytes which can be uploaded before reaching the quota
 	}
 	return usage, nil
 }
diff --git a/backend/hasher/hasher.go b/backend/hasher/hasher.go
index 3428b4dc2..325f7f619 100644
--- a/backend/hasher/hasher.go
+++ b/backend/hasher/hasher.go
@@ -114,6 +114,13 @@ func NewFs(ctx context.Context, fsname, rpath string, cmap configmap.Mapper) (fs
 		root: rpath,
 		opt:  opt,
 	}
+	// Correct root if definitely pointing to a file
+	if err == fs.ErrorIsFile {
+		f.root = path.Dir(f.root)
+		if f.root == "." || f.root == "/" {
+			f.root = ""
+		}
+	}
 	baseFeatures := baseFs.Features()
 	f.fpTime = baseFs.Precision() != fs.ModTimeNotSupported
 
diff --git a/backend/hidrive/hidrive.go b/backend/hidrive/hidrive.go
index 500e6f7cf..6f690f629 100644
--- a/backend/hidrive/hidrive.go
+++ b/backend/hidrive/hidrive.go
@@ -762,6 +762,12 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
 	return nil
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // ------------------------------------------------------------
 
 // Fs returns the parent Fs.
@@ -997,6 +1003,7 @@ var (
 	_ fs.Copier         = (*Fs)(nil)
 	_ fs.Mover          = (*Fs)(nil)
 	_ fs.DirMover       = (*Fs)(nil)
+	_ fs.Shutdowner     = (*Fs)(nil)
 	_ fs.Object         = (*Object)(nil)
 	_ fs.IDer           = (*Object)(nil)
 )
diff --git a/backend/jottacloud/jottacloud.go b/backend/jottacloud/jottacloud.go
index e9176f5b0..6fb68b289 100644
--- a/backend/jottacloud/jottacloud.go
+++ b/backend/jottacloud/jottacloud.go
@@ -1680,6 +1680,12 @@ func (f *Fs) CleanUp(ctx context.Context) error {
 	return nil
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // Hashes returns the supported hash sets.
 func (f *Fs) Hashes() hash.Set {
 	return hash.Set(hash.MD5)
@@ -2104,6 +2110,7 @@ var (
 	_ fs.Abouter      = (*Fs)(nil)
 	_ fs.UserInfoer   = (*Fs)(nil)
 	_ fs.CleanUpper   = (*Fs)(nil)
+	_ fs.Shutdowner   = (*Fs)(nil)
 	_ fs.Object       = (*Object)(nil)
 	_ fs.MimeTyper    = (*Object)(nil)
 	_ fs.Metadataer   = (*Object)(nil)
diff --git a/backend/onedrive/onedrive.go b/backend/onedrive/onedrive.go
index fbbdcac47..5d8c68d03 100644
--- a/backend/onedrive/onedrive.go
+++ b/backend/onedrive/onedrive.go
@@ -1241,10 +1241,14 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
 	}
 	err = f.listAll(ctx, directoryID, false, false, func(info *api.Item) error {
 		entry, err := f.itemToDirEntry(ctx, dir, info)
-		if err == nil {
-			entries = append(entries, entry)
+		if err != nil {
+			return err
 		}
-		return err
+		if entry == nil {
+			return nil
+		}
+		entries = append(entries, entry)
+		return nil
 	})
 	if err != nil {
 		return nil, err
@@ -1339,6 +1343,9 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
 		if err != nil {
 			return err
 		}
+		if entry == nil {
+			return nil
+		}
 		err = list.Add(entry)
 		if err != nil {
 			return err
@@ -1370,6 +1377,12 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
 
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // Creates from the parameters passed in a half finished Object which
 // must have setMetaData called on it
 //
@@ -2748,6 +2761,7 @@ var (
 	_ fs.PublicLinker    = (*Fs)(nil)
 	_ fs.CleanUpper      = (*Fs)(nil)
 	_ fs.ListRer         = (*Fs)(nil)
+	_ fs.Shutdowner      = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.MimeTyper       = &Object{}
 	_ fs.IDer            = &Object{}
diff --git a/backend/oracleobjectstorage/client.go b/backend/oracleobjectstorage/client.go
index 6be770d42..cd28ef66a 100644
--- a/backend/oracleobjectstorage/client.go
+++ b/backend/oracleobjectstorage/client.go
@@ -70,6 +70,9 @@ func newObjectStorageClient(ctx context.Context, opt *Options) (*objectstorage.O
 	if opt.Region != "" {
 		client.SetRegion(opt.Region)
 	}
+	if opt.Endpoint != "" {
+		client.Host = opt.Endpoint
+	}
 	modifyClient(ctx, opt, &client.BaseClient)
 	return &client, err
 }
diff --git a/backend/oracleobjectstorage/multipart.go b/backend/oracleobjectstorage/multipart.go
index d57a9092a..93c7a9c3d 100644
--- a/backend/oracleobjectstorage/multipart.go
+++ b/backend/oracleobjectstorage/multipart.go
@@ -399,13 +399,17 @@ func (o *Object) prepareUpload(ctx context.Context, src fs.ObjectInfo, options [
 func (o *Object) createMultipartUpload(ctx context.Context, putReq *objectstorage.PutObjectRequest) (
 	uploadID string, existingParts map[int]objectstorage.MultipartUploadPartSummary, err error) {
 	bucketName, bucketPath := o.split()
-	f := o.fs
-	if f.opt.AttemptResumeUpload {
+	err = o.fs.makeBucket(ctx, bucketName)
+	if err != nil {
+		fs.Errorf(o, "failed to create bucket: %v, err: %v", bucketName, err)
+		return uploadID, existingParts, err
+	}
+	if o.fs.opt.AttemptResumeUpload {
 		fs.Debugf(o, "attempting to resume upload for %v (if any)", o.remote)
 		resumeUploads, err := o.fs.findLatestMultipartUpload(ctx, bucketName, bucketPath)
 		if err == nil && len(resumeUploads) > 0 {
 			uploadID = *resumeUploads[0].UploadId
-			existingParts, err = f.listMultipartUploadParts(ctx, bucketName, bucketPath, uploadID)
+			existingParts, err = o.fs.listMultipartUploadParts(ctx, bucketName, bucketPath, uploadID)
 			if err == nil {
 				fs.Debugf(o, "resuming with existing upload id: %v", uploadID)
 				return uploadID, existingParts, err
diff --git a/backend/pcloud/pcloud.go b/backend/pcloud/pcloud.go
index 9d6256b1a..64a48c25c 100644
--- a/backend/pcloud/pcloud.go
+++ b/backend/pcloud/pcloud.go
@@ -948,6 +948,12 @@ func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
 	return usage, nil
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // Hashes returns the supported hash sets.
 func (f *Fs) Hashes() hash.Set {
 	// EU region supports SHA1 and SHA256 (but rclone doesn't
@@ -1280,6 +1286,7 @@ var (
 	_ fs.DirCacheFlusher = (*Fs)(nil)
 	_ fs.PublicLinker    = (*Fs)(nil)
 	_ fs.Abouter         = (*Fs)(nil)
+	_ fs.Shutdowner      = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.IDer            = (*Object)(nil)
 )
diff --git a/backend/premiumizeme/premiumizeme.go b/backend/premiumizeme/premiumizeme.go
index 4adaa5af2..ef54aac9f 100644
--- a/backend/premiumizeme/premiumizeme.go
+++ b/backend/premiumizeme/premiumizeme.go
@@ -770,6 +770,12 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
 	return o.(*Object).url, nil
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // About gets quota information
 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
 	var resp *http.Response
@@ -1110,6 +1116,7 @@ var (
 	_ fs.DirCacheFlusher = (*Fs)(nil)
 	_ fs.Abouter         = (*Fs)(nil)
 	_ fs.PublicLinker    = (*Fs)(nil)
+	_ fs.Shutdowner      = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.MimeTyper       = (*Object)(nil)
 	_ fs.IDer            = (*Object)(nil)
diff --git a/backend/s3/s3.go b/backend/s3/s3.go
index 8f690b38d..d7ca47a77 100644
--- a/backend/s3/s3.go
+++ b/backend/s3/s3.go
@@ -2219,6 +2219,13 @@ If it is set then rclone will use v2 authentication.
 Use this only if v4 signatures don't work, e.g. pre Jewel/v10 CEPH.`,
 			Default:  false,
 			Advanced: true,
+		}, {
+			Name: "use_dual_stack",
+			Help: `If true use AWS S3 dual-stack endpoint (IPv6 support).
+
+See [AWS Docs on Dualstack Endpoints](https://docs.aws.amazon.com/AmazonS3/latest/userguide/dual-stack-endpoints.html)`,
+			Default:  false,
+			Advanced: true,
 		}, {
 			Name:     "use_accelerate_endpoint",
 			Provider: "AWS",
@@ -2426,6 +2433,19 @@ See [the time option docs](/docs/#time-option) for valid formats.
 `,
 			Default:  fs.Time{},
 			Advanced: true,
+		}, {
+			Name: "version_deleted",
+			Help: `Show deleted file markers when using versions.
+
+This shows deleted file markers in the listing when using versions. These will appear
+as 0 size files. The only operation which can be performed on them is deletion.
+
+Deleting a delete marker will reveal the previous version.
+
+Deleted files will always show with a timestamp.
+`,
+			Default:  false,
+			Advanced: true,
 		}, {
 			Name: "decompress",
 			Help: `If set this will decompress gzip encoded objects.
@@ -2615,6 +2635,7 @@ type Options struct {
 	Region                string               `config:"region"`
 	Endpoint              string               `config:"endpoint"`
 	STSEndpoint           string               `config:"sts_endpoint"`
+	UseDualStack          bool                 `config:"use_dual_stack"`
 	LocationConstraint    string               `config:"location_constraint"`
 	ACL                   string               `config:"acl"`
 	BucketACL             string               `config:"bucket_acl"`
@@ -2653,6 +2674,7 @@ type Options struct {
 	UsePresignedRequest   bool                 `config:"use_presigned_request"`
 	Versions              bool                 `config:"versions"`
 	VersionAt             fs.Time              `config:"version_at"`
+	VersionDeleted        bool                 `config:"version_deleted"`
 	Decompress            bool                 `config:"decompress"`
 	MightGzip             fs.Tristate          `config:"might_gzip"`
 	UseAcceptEncodingGzip fs.Tristate          `config:"use_accept_encoding_gzip"`
@@ -2942,6 +2964,9 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
 		r.addService("sts", opt.STSEndpoint)
 		awsConfig.WithEndpointResolver(r)
 	}
+	if opt.UseDualStack {
+		awsConfig.UseDualStackEndpoint = endpoints.DualStackEndpointStateEnabled
+	}
 
 	// awsConfig.WithLogLevel(aws.LogDebugWithSigning)
 	awsSessionOpts := session.Options{
@@ -3420,6 +3445,7 @@ func (f *Fs) getMetaDataListing(ctx context.Context, wantRemote string) (info *s
 		withVersions: f.opt.Versions,
 		findFile:     true,
 		versionAt:    f.opt.VersionAt,
+		hidden:       f.opt.VersionDeleted,
 	}, func(gotRemote string, object *s3.Object, objectVersionID *string, isDirectory bool) error {
 		if isDirectory {
 			return nil
@@ -3481,6 +3507,10 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *s3.Obje
 		o.bytes = aws.Int64Value(info.Size)
 		o.storageClass = stringClonePointer(info.StorageClass)
 		o.versionID = stringClonePointer(versionID)
+		// If is delete marker, show that metadata has been read as there is none to read
+		if info.Size == isDeleteMarker {
+			o.meta = map[string]string{}
+		}
 	} else if !o.fs.opt.NoHeadObject {
 		err := o.readMetaData(ctx) // reads info and meta, returning an error
 		if err != nil {
@@ -3778,7 +3808,7 @@ func (ls *versionsList) List(ctx context.Context) (resp *s3.ListObjectsV2Output,
 		//structs.SetFrom(obj, objVersion)
 		setFrom_s3Object_s3ObjectVersion(obj, objVersion)
 		// Adjust the file names
-		if !ls.usingVersionAt && !aws.BoolValue(objVersion.IsLatest) {
+		if !ls.usingVersionAt && (!aws.BoolValue(objVersion.IsLatest) || objVersion.Size == isDeleteMarker) {
 			if obj.Key != nil && objVersion.LastModified != nil {
 				*obj.Key = version.Add(*obj.Key, *objVersion.LastModified)
 			}
@@ -4046,6 +4076,7 @@ func (f *Fs) listDir(ctx context.Context, bucket, directory, prefix string, addB
 		addBucket:    addBucket,
 		withVersions: f.opt.Versions,
 		versionAt:    f.opt.VersionAt,
+		hidden:       f.opt.VersionDeleted,
 	}, func(remote string, object *s3.Object, versionID *string, isDirectory bool) error {
 		entry, err := f.itemToDirEntry(ctx, remote, object, versionID, isDirectory)
 		if err != nil {
@@ -4132,6 +4163,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
 			recurse:      true,
 			withVersions: f.opt.Versions,
 			versionAt:    f.opt.VersionAt,
+			hidden:       f.opt.VersionDeleted,
 		}, func(remote string, object *s3.Object, versionID *string, isDirectory bool) error {
 			entry, err := f.itemToDirEntry(ctx, remote, object, versionID, isDirectory)
 			if err != nil {
@@ -4898,6 +4930,7 @@ func (f *Fs) restoreStatus(ctx context.Context, all bool) (out []restoreStatusOu
 		recurse:       true,
 		withVersions:  f.opt.Versions,
 		versionAt:     f.opt.VersionAt,
+		hidden:        f.opt.VersionDeleted,
 		restoreStatus: true,
 	}, func(remote string, object *s3.Object, versionID *string, isDirectory bool) error {
 		entry, err := f.itemToDirEntry(ctx, remote, object, versionID, isDirectory)
diff --git a/backend/sharefile/sharefile.go b/backend/sharefile/sharefile.go
index 92e618010..a135f8705 100644
--- a/backend/sharefile/sharefile.go
+++ b/backend/sharefile/sharefile.go
@@ -1176,6 +1176,12 @@ func (f *Fs) DirCacheFlush() {
 	f.dirCache.ResetRoot()
 }
 
+// Shutdown shutdown the fs
+func (f *Fs) Shutdown(ctx context.Context) error {
+	f.tokenRenewer.Shutdown()
+	return nil
+}
+
 // Hashes returns the supported hash sets.
 func (f *Fs) Hashes() hash.Set {
 	return hash.Set(hash.MD5)
@@ -1466,6 +1472,7 @@ var (
 	_ fs.Copier   = (*Fs)(nil)
 	// _ fs.PutStreamer     = (*Fs)(nil)
 	_ fs.DirCacheFlusher = (*Fs)(nil)
+	_ fs.Shutdowner      = (*Fs)(nil)
 	_ fs.Object          = (*Object)(nil)
 	_ fs.IDer            = (*Object)(nil)
 )
diff --git a/backend/smb/connpool.go b/backend/smb/connpool.go
index a34e84f29..13a8c3042 100644
--- a/backend/smb/connpool.go
+++ b/backend/smb/connpool.go
@@ -40,7 +40,7 @@ func (f *Fs) dial(ctx context.Context, network, addr string) (*conn, error) {
 		},
 	}
 
-	session, err := d.DialContext(ctx, tconn)
+	session, err := d.DialConn(ctx, tconn, addr)
 	if err != nil {
 		return nil, err
 	}
diff --git a/backend/union/union.go b/backend/union/union.go
index eeaed8f31..5854afc11 100644
--- a/backend/union/union.go
+++ b/backend/union/union.go
@@ -877,6 +877,13 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 		opt:       *opt,
 		upstreams: usedUpstreams,
 	}
+	// Correct root if definitely pointing to a file
+	if fserr == fs.ErrorIsFile {
+		f.root = path.Dir(f.root)
+		if f.root == "." || f.root == "/" {
+			f.root = ""
+		}
+	}
 	err = upstream.Prepare(f.upstreams)
 	if err != nil {
 		return nil, err
diff --git a/bin/.ignore-emails b/bin/.ignore-emails
index 14cbc62fc..b45311099 100644
--- a/bin/.ignore-emails
+++ b/bin/.ignore-emails
@@ -10,3 +10,4 @@
 <stoesser@yay-digital.de>
 <services+github@simjo.st>
 <seb•ɑƬ•chezwam•ɖɵʈ•org>
+<allllaboutyou@gmail.com>
\ No newline at end of file
diff --git a/cmd/mountlib/check_linux.go b/cmd/mountlib/check_linux.go
index 81e3ccf37..58d839bf5 100644
--- a/cmd/mountlib/check_linux.go
+++ b/cmd/mountlib/check_linux.go
@@ -7,15 +7,10 @@ import (
 	"fmt"
 	"path/filepath"
 	"strings"
-	"time"
 
 	"github.com/moby/sys/mountinfo"
 )
 
-const (
-	pollInterval = 100 * time.Millisecond
-)
-
 // CheckMountEmpty checks if folder is not already a mountpoint.
 // On Linux we use the OS-specific /proc/self/mountinfo API so the check won't access the path.
 // Directories marked as "mounted" by autofs are considered not mounted.
@@ -80,19 +75,5 @@ func CheckMountReady(mountpoint string) error {
 	return fmt.Errorf(msg, mountpointAbs)
 }
 
-// WaitMountReady waits until mountpoint is mounted by rclone.
-func WaitMountReady(mountpoint string, timeout time.Duration) (err error) {
-	endTime := time.Now().Add(timeout)
-	for {
-		err = CheckMountReady(mountpoint)
-		delay := time.Until(endTime)
-		if err == nil || delay <= 0 {
-			break
-		}
-		if delay > pollInterval {
-			delay = pollInterval
-		}
-		time.Sleep(delay)
-	}
-	return
-}
+// CanCheckMountReady is set if CheckMountReady is functional
+var CanCheckMountReady = true
diff --git a/cmd/mountlib/check_other.go b/cmd/mountlib/check_other.go
index 328f83bca..760be4ec6 100644
--- a/cmd/mountlib/check_other.go
+++ b/cmd/mountlib/check_other.go
@@ -3,10 +3,6 @@
 
 package mountlib
 
-import (
-	"time"
-)
-
 // CheckMountEmpty checks if mountpoint folder is empty.
 // On non-Linux unixes we list directory to ensure that.
 func CheckMountEmpty(mountpoint string) error {
@@ -19,9 +15,5 @@ func CheckMountReady(mountpoint string) error {
 	return nil
 }
 
-// WaitMountReady should wait until mountpoint is mounted by rclone.
-// The check is implemented only for Linux so we just sleep a little.
-func WaitMountReady(mountpoint string, timeout time.Duration) error {
-	time.Sleep(timeout)
-	return nil
-}
+// CanCheckMountReady is set if CheckMountReady is functional
+var CanCheckMountReady = false
diff --git a/cmd/mountlib/mount.go b/cmd/mountlib/mount.go
index 4bc9e066a..34795be60 100644
--- a/cmd/mountlib/mount.go
+++ b/cmd/mountlib/mount.go
@@ -23,7 +23,7 @@ import (
 	"github.com/rclone/rclone/vfs/vfscommon"
 	"github.com/rclone/rclone/vfs/vfsflags"
 
-	sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify"
+	"github.com/coreos/go-systemd/v22/daemon"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )
@@ -156,6 +156,38 @@ func AddFlags(flagSet *pflag.FlagSet) {
 	flags.DurationVarP(flagSet, &Opt.DaemonWait, "daemon-wait", "", Opt.DaemonWait, "Time to wait for ready mount from daemon (maximum time on Linux, constant sleep time on OSX/BSD) (not supported on Windows)", "Mount")
 }
 
+const (
+	pollInterval = 100 * time.Millisecond
+)
+
+// WaitMountReady waits until mountpoint is mounted by rclone.
+//
+// If the mount daemon dies prematurely it will notice too.
+func WaitMountReady(mountpoint string, timeout time.Duration, daemon *os.Process) (err error) {
+	endTime := time.Now().Add(timeout)
+	for {
+		if CanCheckMountReady {
+			err = CheckMountReady(mountpoint)
+			if err == nil {
+				break
+			}
+		}
+		err = daemonize.Check(daemon)
+		if err != nil {
+			return err
+		}
+		delay := time.Until(endTime)
+		if delay <= 0 {
+			break
+		}
+		if delay > pollInterval {
+			delay = pollInterval
+		}
+		time.Sleep(delay)
+	}
+	return
+}
+
 // NewMountCommand makes a mount command with the given name and Mount function
 func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Command {
 	var commandDefinition = &cobra.Command{
@@ -190,10 +222,10 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
 			}
 
 			mnt := NewMountPoint(mount, args[1], cmd.NewFsDir(args), &Opt, &vfsflags.Opt)
-			daemon, err := mnt.Mount()
+			mountDaemon, err := mnt.Mount()
 
 			// Wait for foreground mount, if any...
-			if daemon == nil {
+			if mountDaemon == nil {
 				if err == nil {
 					err = mnt.Wait()
 				}
@@ -203,15 +235,15 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
 				return
 			}
 
-			// Wait for daemon, if any...
+			// Wait for mountDaemon, if any...
 			killOnce := sync.Once{}
 			killDaemon := func(reason string) {
 				killOnce.Do(func() {
-					if err := daemon.Signal(os.Interrupt); err != nil {
-						fs.Errorf(nil, "%s. Failed to terminate daemon pid %d: %v", reason, daemon.Pid, err)
+					if err := mountDaemon.Signal(os.Interrupt); err != nil {
+						fs.Errorf(nil, "%s. Failed to terminate daemon pid %d: %v", reason, mountDaemon.Pid, err)
 						return
 					}
-					fs.Debugf(nil, "%s. Terminating daemon pid %d", reason, daemon.Pid)
+					fs.Debugf(nil, "%s. Terminating daemon pid %d", reason, mountDaemon.Pid)
 				})
 			}
 
@@ -219,7 +251,7 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
 				handle := atexit.Register(func() {
 					killDaemon("Got interrupt")
 				})
-				err = WaitMountReady(mnt.MountPoint, Opt.DaemonWait)
+				err = WaitMountReady(mnt.MountPoint, Opt.DaemonWait, mountDaemon)
 				if err != nil {
 					killDaemon("Daemon timed out")
 				}
@@ -243,7 +275,7 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
 }
 
 // Mount the remote at mountpoint
-func (m *MountPoint) Mount() (daemon *os.Process, err error) {
+func (m *MountPoint) Mount() (mountDaemon *os.Process, err error) {
 
 	// Ensure sensible defaults
 	m.SetVolumeName(m.MountOpt.VolumeName)
@@ -251,9 +283,9 @@ func (m *MountPoint) Mount() (daemon *os.Process, err error) {
 
 	// Start background task if --daemon is specified
 	if m.MountOpt.Daemon {
-		daemon, err = daemonize.StartDaemon(os.Args)
-		if daemon != nil || err != nil {
-			return daemon, err
+		mountDaemon, err = daemonize.StartDaemon(os.Args)
+		if mountDaemon != nil || err != nil {
+			return mountDaemon, err
 		}
 	}
 
@@ -273,7 +305,7 @@ func (m *MountPoint) Wait() error {
 	var finaliseOnce sync.Once
 	finalise := func() {
 		finaliseOnce.Do(func() {
-			_ = sysdnotify.Stopping()
+			_, _ = daemon.SdNotify(false, daemon.SdNotifyStopping)
 			// Unmount only if directory was mounted by rclone, e.g. don't unmount autofs hooks.
 			if err := CheckMountReady(m.MountPoint); err != nil {
 				fs.Debugf(m.MountPoint, "Unmounted externally. Just exit now.")
@@ -290,7 +322,7 @@ func (m *MountPoint) Wait() error {
 	defer atexit.Unregister(fnHandle)
 
 	// Notify systemd
-	if err := sysdnotify.Ready(); err != nil {
+	if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
 		return fmt.Errorf("failed to notify systemd: %w", err)
 	}
 
diff --git a/cmd/nfsmount/nfsmount.go b/cmd/nfsmount/nfsmount.go
index 755197fa2..e515e995a 100644
--- a/cmd/nfsmount/nfsmount.go
+++ b/cmd/nfsmount/nfsmount.go
@@ -1,29 +1,38 @@
-//go:build darwin && !cmount
-// +build darwin,!cmount
+//go:build unix
+// +build unix
 
 // Package nfsmount implements mounting functionality using serve nfs command
 //
-// NFS mount is only needed for macOS since it has no
-// support for FUSE-based file systems
+// This can potentially work on all unix like systems which can mount NFS.
 package nfsmount
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"net"
 	"os/exec"
 	"runtime"
-	"strings"
 
 	"github.com/rclone/rclone/cmd/mountlib"
 	"github.com/rclone/rclone/cmd/serve/nfs"
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config/flags"
 	"github.com/rclone/rclone/vfs"
 )
 
+var (
+	sudo = false
+)
+
 func init() {
-	cmd := mountlib.NewMountCommand("mount", false, mount)
-	cmd.Aliases = append(cmd.Aliases, "nfsmount")
-	mountlib.AddRc("nfsmount", mount)
+	name := "nfsmount"
+	cmd := mountlib.NewMountCommand(name, false, mount)
+	cmd.Annotations["versionIntroduced"] = "v1.65"
+	cmd.Annotations["status"] = "Experimental"
+	mountlib.AddRc(name, mount)
+	cmdFlags := cmd.Flags()
+	flags.BoolVarP(cmdFlags, &sudo, "sudo", "", sudo, "Use sudo to run the mount command as root.", "")
 }
 
 func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (asyncerrors <-chan error, unmount func() error, err error) {
@@ -42,24 +51,46 @@ func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (asyncerrors
 		err = fmt.Errorf("cannot find port number in %s", s.Addr().String())
 		return
 	}
-	optionsString := strings.Join(opt.ExtraOptions, ",")
-	err = exec.Command("mount", fmt.Sprintf("-oport=%s,mountport=%s,%s", port, port, optionsString), "localhost:", mountpoint).Run()
+
+	// Options
+	options := []string{
+		"-o", fmt.Sprintf("port=%s", port),
+		"-o", fmt.Sprintf("mountport=%s", port),
+	}
+	for _, option := range opt.ExtraOptions {
+		options = append(options, "-o", option)
+	}
+	options = append(options, opt.ExtraFlags...)
+
+	cmd := []string{}
+	if sudo {
+		cmd = append(cmd, "sudo")
+	}
+	cmd = append(cmd, "mount")
+	cmd = append(cmd, options...)
+	cmd = append(cmd, "localhost:", mountpoint)
+	fs.Debugf(nil, "Running mount command: %q", cmd)
+
+	out, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput()
 	if err != nil {
-		err = fmt.Errorf("failed to mount NFS volume %e", err)
+		out = bytes.TrimSpace(out)
+		err = fmt.Errorf("%s: failed to mount NFS volume: %v", out, err)
 		return
 	}
 	asyncerrors = errChan
 	unmount = func() error {
 		var umountErr error
+		var out []byte
 		if runtime.GOOS == "darwin" {
-			umountErr = exec.Command("diskutil", "umount", "force", mountpoint).Run()
+			out, umountErr = exec.Command("diskutil", "umount", "force", mountpoint).CombinedOutput()
 		} else {
-			umountErr = exec.Command("umount", "-f", mountpoint).Run()
+			out, umountErr = exec.Command("umount", "-f", mountpoint).CombinedOutput()
 		}
 		shutdownErr := s.Shutdown()
 		VFS.Shutdown()
 		if umountErr != nil {
-			return fmt.Errorf("failed to umount the NFS volume %e", umountErr)
+			out = bytes.TrimSpace(out)
+			return fmt.Errorf("%s: failed to umount the NFS volume %e", out, umountErr)
 		} else if shutdownErr != nil {
 			return fmt.Errorf("failed to shutdown NFS server: %e", shutdownErr)
 		}
diff --git a/cmd/nfsmount/nfsmount_unsupported.go b/cmd/nfsmount/nfsmount_unsupported.go
index d24115837..394374e66 100644
--- a/cmd/nfsmount/nfsmount_unsupported.go
+++ b/cmd/nfsmount/nfsmount_unsupported.go
@@ -1,8 +1,8 @@
 // Build for nfsmount for unsupported platforms to stop go complaining
 // about "no buildable Go source files "
 
-//go:build !darwin || cmount
-// +build !darwin cmount
+//go:build !unix
+// +build !unix
 
-// Package nfsmount implements mount command using NFS, not needed on most platforms
+// Package nfsmount implements mount command using NFS.
 package nfsmount
diff --git a/cmd/serve/docker/driver.go b/cmd/serve/docker/driver.go
index dfdb190f9..278a24e31 100644
--- a/cmd/serve/docker/driver.go
+++ b/cmd/serve/docker/driver.go
@@ -12,8 +12,7 @@ import (
 	"sync"
 	"time"
 
-	sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify"
-
+	"github.com/coreos/go-systemd/v22/daemon"
 	"github.com/rclone/rclone/cmd/mountlib"
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config"
@@ -87,7 +86,7 @@ func NewDriver(ctx context.Context, root string, mntOpt *mountlib.Options, vfsOp
 	})
 
 	// notify systemd
-	if err := sysdnotify.Ready(); err != nil {
+	if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
 		return nil, fmt.Errorf("failed to notify systemd: %w", err)
 	}
 
@@ -100,7 +99,10 @@ func (drv *Driver) Exit() {
 	drv.mu.Lock()
 	defer drv.mu.Unlock()
 
-	reportErr(sysdnotify.Stopping())
+	reportErr(func() error {
+		_, err := daemon.SdNotify(false, daemon.SdNotifyStopping)
+		return err
+	}())
 	drv.monChan <- true // ask monitor to exit
 	for _, vol := range drv.volumes {
 		reportErr(vol.unmountAll())
diff --git a/cmd/serve/docker/systemd.go b/cmd/serve/docker/systemd.go
index 981964c1b..8a69f6b08 100644
--- a/cmd/serve/docker/systemd.go
+++ b/cmd/serve/docker/systemd.go
@@ -6,8 +6,8 @@ package docker
 import (
 	"os"
 
-	"github.com/coreos/go-systemd/activation"
-	"github.com/coreos/go-systemd/util"
+	"github.com/coreos/go-systemd/v22/activation"
+	"github.com/coreos/go-systemd/v22/util"
 )
 
 func systemdActivationFiles() []*os.File {
diff --git a/cmd/serve/nfs/nfs.go b/cmd/serve/nfs/nfs.go
index 3e4722cb6..7a67f679e 100644
--- a/cmd/serve/nfs/nfs.go
+++ b/cmd/serve/nfs/nfs.go
@@ -92,6 +92,7 @@ This feature is only available on Unix platforms.
 	Annotations: map[string]string{
 		"versionIntroduced": "v1.65",
 		"groups":            "Filter",
+		"status":            "Experimental",
 	},
 	Run: Run,
 }
diff --git a/docs/content/authors.md b/docs/content/authors.md
index 447fac7b8..44fe472d4 100644
--- a/docs/content/authors.md
+++ b/docs/content/authors.md
@@ -57,7 +57,7 @@ put them back in again.` >}}
   * Scott McGillivray <scott.mcgillivray@gmail.com>
   * Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
   * Lukas Loesche <lukas@mesosphere.io>
-  * emyarod <allllaboutyou@gmail.com>
+  * emyarod <emyarod@users.noreply.github.com>
   * T.C. Ferguson <tcf909@gmail.com>
   * Brandur <brandur@mutelight.org>
   * Dario Giovannetti <dev@dariogiovannetti.net>
@@ -805,3 +805,9 @@ put them back in again.` >}}
   * Alen Šiljak <dev@alensiljak.eu.org>
   * 你知道未来吗 <rkonfj@gmail.com>
   * Abhinav Dhiman <8640877+ahnv@users.noreply.github.com>
+  * halms <7513146+halms@users.noreply.github.com>
+  * ben-ba <benjamin.brauner@gmx.de>
+  * Eli Orzitzer <e_orz@yahoo.com>
+  * Anthony Metzidis <anthony.metzidis@gmail.com>
+  * emyarod <afw5059@gmail.com>
+  * keongalvin <keongalvin@gmail.com>
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 30b2a5fbb..a15cc5188 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -1614,7 +1614,7 @@ json.dump(o, sys.stdout, indent="\t")
 ```
 
 You can find this example (slightly expanded) in the rclone source code at
-[bin/test_metadata_mapper.py](https://github.com/rclone/rclone/blob/master/test_metadata_mapper.py).
+[bin/test_metadata_mapper.py](https://github.com/rclone/rclone/blob/master/bin/test_metadata_mapper.py).
 
 If you want to see the input to the metadata mapper and the output
 returned from it in the log you can use `-vv --dump mapper`.
@@ -1674,7 +1674,7 @@ use multiple threads to transfer the file (default 256M).
 
 Capable backends are marked in the
 [overview](/overview/#optional-features) as `MultithreadUpload`. (They
-need to implement either the `OpenWriterAt` or `OpenChunkedWriter`
+need to implement either the `OpenWriterAt` or `OpenChunkWriter`
 internal interfaces). These include include, `local`, `s3`,
 `azureblob`, `b2`, `oracleobjectstorage` and `smb` at the time of
 writing.
diff --git a/docs/content/install.sh b/docs/content/install.sh
index 302882d82..075e6deef 100755
--- a/docs/content/install.sh
+++ b/docs/content/install.sh
@@ -193,12 +193,12 @@ case "$OS" in
     exit 2
 esac
 
-#cleanup
-rm -rf "$tmp_dir"
-
 #update version variable post install
 version=$(rclone --version 2>>errors | head -n 1)
 
+#cleanup
+rm -rf "$tmp_dir"
+
 printf "\n${version} has successfully installed."
 printf '\nNow run "rclone config" for setup. Check https://rclone.org/docs/ for more details.\n\n'
 exit 0
diff --git a/docs/content/s3.md b/docs/content/s3.md
index aa16509ac..46b5873ed 100644
--- a/docs/content/s3.md
+++ b/docs/content/s3.md
@@ -590,6 +590,7 @@ permissions are required to be available on the bucket being written to:
 * `GetObject`
 * `PutObject`
 * `PutObjectACL`
+* `CreateBucket` (unless using [s3-no-check-bucket](#s3-no-check-bucket))
 
 When using the `lsd` subcommand, the `ListAllMyBuckets` permission is required.
 
@@ -631,6 +632,7 @@ Notes on above:
    that `USER_NAME` has been created.
 2. The Resource entry must include both resource ARNs, as one implies
    the bucket and the other implies the bucket's objects.
+3. When using [s3-no-check-bucket](#s3-no-check-bucket) and the bucket already exsits, the `"arn:aws:s3:::BUCKET_NAME"` doesn't have to be included.
 
 For reference, [here's an Ansible script](https://gist.github.com/ebridges/ebfc9042dd7c756cd101cfa807b7ae2b)
 that will generate one or more buckets that will work with `rclone sync`.
diff --git a/fs/log/log.go b/fs/log/log.go
index 9c66fdf02..bf2bfd74b 100644
--- a/fs/log/log.go
+++ b/fs/log/log.go
@@ -10,7 +10,6 @@ import (
 	"runtime"
 	"strings"
 
-	systemd "github.com/iguanesolutions/go-systemd/v5"
 	"github.com/rclone/rclone/fs"
 	"github.com/sirupsen/logrus"
 )
@@ -49,7 +48,7 @@ func fnName() string {
 
 // Trace debugs the entry and exit of the calling function
 //
-// It is designed to be used in a defer statement so it returns a
+// It is designed to be used in a defer statement, so it returns a
 // function that logs the exit parameters.
 //
 // Any pointers in the exit function will be dereferenced
@@ -141,7 +140,7 @@ func InitLogging() {
 	// Activate systemd logger support if systemd invocation ID is
 	// detected and output is going to stderr (not logging to a file or syslog)
 	if !Redirected() {
-		if _, usingSystemd := systemd.GetInvocationID(); usingSystemd {
+		if isJournalStream() {
 			Opt.LogSystemdSupport = true
 		}
 	}
diff --git a/fs/log/systemd.go b/fs/log/systemd.go
index 2e572790a..c09227b09 100644
--- a/fs/log/systemd.go
+++ b/fs/log/systemd.go
@@ -1,7 +1,7 @@
 // Systemd interface for non-Unix variants only
 
-//go:build windows || nacl || plan9
-// +build windows nacl plan9
+//go:build !unix
+// +build !unix
 
 package log
 
@@ -10,8 +10,12 @@ import (
 	"runtime"
 )
 
-// Enables systemd logs if configured or if auto detected
+// Enables systemd logs if configured or if auto-detected
 func startSystemdLog() bool {
 	log.Fatalf("--log-systemd not supported on %s platform", runtime.GOOS)
 	return false
 }
+
+func isJournalStream() bool {
+	return false
+}
diff --git a/fs/log/systemd_unix.go b/fs/log/systemd_unix.go
index a2aa8e51c..fa6700734 100644
--- a/fs/log/systemd_unix.go
+++ b/fs/log/systemd_unix.go
@@ -1,20 +1,21 @@
 // Systemd interface for Unix variants only
 
-//go:build !windows && !nacl && !plan9
-// +build !windows,!nacl,!plan9
+//go:build unix
+// +build unix
 
 package log
 
 import (
 	"fmt"
 	"log"
+	"strconv"
 	"strings"
 
-	sysdjournald "github.com/iguanesolutions/go-systemd/v5/journald"
+	"github.com/coreos/go-systemd/v22/journal"
 	"github.com/rclone/rclone/fs"
 )
 
-// Enables systemd logs if configured or if auto detected
+// Enables systemd logs if configured or if auto-detected
 func startSystemdLog() bool {
 	flagsStr := "," + Opt.Format + ","
 	var flags int
@@ -25,27 +26,36 @@ func startSystemdLog() bool {
 		flags |= log.Lshortfile
 	}
 	log.SetFlags(flags)
+	// TODO: Use the native journal.Print approach rather than a custom implementation
 	fs.LogPrint = func(level fs.LogLevel, text string) {
-		text = fmt.Sprintf("%s%-6s: %s", systemdLogPrefix(level), level, text)
+		text = fmt.Sprintf("<%s>%-6s: %s", systemdLogPrefix(level), level, text)
 		_ = log.Output(4, text)
 	}
 	return true
 }
 
-var logLevelToSystemdPrefix = []string{
-	fs.LogLevelEmergency: sysdjournald.EmergPrefix,
-	fs.LogLevelAlert:     sysdjournald.AlertPrefix,
-	fs.LogLevelCritical:  sysdjournald.CritPrefix,
-	fs.LogLevelError:     sysdjournald.ErrPrefix,
-	fs.LogLevelWarning:   sysdjournald.WarningPrefix,
-	fs.LogLevelNotice:    sysdjournald.NoticePrefix,
-	fs.LogLevelInfo:      sysdjournald.InfoPrefix,
-	fs.LogLevelDebug:     sysdjournald.DebugPrefix,
+var logLevelToSystemdPrefix = []journal.Priority{
+	fs.LogLevelEmergency: journal.PriEmerg,
+	fs.LogLevelAlert:     journal.PriAlert,
+	fs.LogLevelCritical:  journal.PriCrit,
+	fs.LogLevelError:     journal.PriErr,
+	fs.LogLevelWarning:   journal.PriWarning,
+	fs.LogLevelNotice:    journal.PriNotice,
+	fs.LogLevelInfo:      journal.PriInfo,
+	fs.LogLevelDebug:     journal.PriDebug,
 }
 
 func systemdLogPrefix(l fs.LogLevel) string {
 	if l >= fs.LogLevel(len(logLevelToSystemdPrefix)) {
 		return ""
 	}
-	return logLevelToSystemdPrefix[l]
+	return strconv.Itoa(int(logLevelToSystemdPrefix[l]))
+}
+
+func isJournalStream() bool {
+	if usingJournald, _ := journal.StderrIsJournalStream(); usingJournald {
+		return true
+	}
+
+	return false
 }
diff --git a/go.mod b/go.mod
index eb5d6ebfe..046a3433c 100644
--- a/go.mod
+++ b/go.mod
@@ -20,10 +20,10 @@ require (
 	github.com/atotto/clipboard v0.1.4
 	github.com/aws/aws-sdk-go v1.46.6
 	github.com/buengese/sgzip v0.1.1
-	github.com/cloudsoda/go-smb2 v0.0.0-20231106205947-b0758ecc4c67
+	github.com/cloudsoda/go-smb2 v0.0.0-20231124195312-f3ec8ae2c891
 	github.com/colinmarc/hdfs/v2 v2.4.0
 	github.com/coreos/go-semver v0.3.1
-	github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
+	github.com/coreos/go-systemd/v22 v22.5.0
 	github.com/dop251/scsu v0.0.0-20220106150536-84ac88021d00
 	github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
 	github.com/gabriel-vasile/mimetype v1.4.3
@@ -34,7 +34,6 @@ require (
 	github.com/hanwen/go-fuse/v2 v2.4.0
 	github.com/henrybear327/Proton-API-Bridge v1.0.0
 	github.com/henrybear327/go-proton-api v1.0.0
-	github.com/iguanesolutions/go-systemd/v5 v5.1.1
 	github.com/jcmturner/gokrb5/v8 v8.4.4
 	github.com/jlaffaye/ftp v0.2.0
 	github.com/josephspurrier/goversioninfo v1.4.0
diff --git a/go.sum b/go.sum
index 0196db684..f9b8ce299 100644
--- a/go.sum
+++ b/go.sum
@@ -133,15 +133,15 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM
 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
 github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
-github.com/cloudsoda/go-smb2 v0.0.0-20231106205947-b0758ecc4c67 h1:KzZU0EMkUm4vX/jPp5d/VttocDpocL/8QP0zyiI9Xiw=
-github.com/cloudsoda/go-smb2 v0.0.0-20231106205947-b0758ecc4c67/go.mod h1:xFxVVe3plxwhM+6BgTTPByEgG8hggo8+gtRUkbc5W8Q=
+github.com/cloudsoda/go-smb2 v0.0.0-20231124195312-f3ec8ae2c891 h1:nPP4suUiNage0vvyEBgfAnhTPwwXhNqtHmSuiCIQwKU=
+github.com/cloudsoda/go-smb2 v0.0.0-20231124195312-f3ec8ae2c891/go.mod h1:xFxVVe3plxwhM+6BgTTPByEgG8hggo8+gtRUkbc5W8Q=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/colinmarc/hdfs/v2 v2.4.0 h1:v6R8oBx/Wu9fHpdPoJJjpGSUxo8NhHIwrwsfhFvU9W0=
 github.com/colinmarc/hdfs/v2 v2.4.0/go.mod h1:0NAO+/3knbMx6+5pCv+Hcbaz4xn/Zzbn9+WIib2rKVI=
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
-github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
-github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -315,8 +315,6 @@ github.com/henrybear327/go-proton-api v1.0.0 h1:zYi/IbjLwFAW7ltCeqXneUGJey0TN//X
 github.com/henrybear327/go-proton-api v1.0.0/go.mod h1:w63MZuzufKcIZ93pwRgiOtxMXYafI8H74D77AxytOBc=
 github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/iguanesolutions/go-systemd/v5 v5.1.1 h1:Hs0Z16knPGCBFnKECrICPh+RQ89Sgy0xyzcalrHMKdw=
-github.com/iguanesolutions/go-systemd/v5 v5.1.1/go.mod h1:Quv57scs6S7T0rC6qyLfW20KU/P4p9hrbLPF+ILYrXY=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -395,7 +393,6 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
-github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
 github.com/minio/minio-go/v6 v6.0.46/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
@@ -693,7 +690,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -731,7 +727,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/lib/daemonize/daemon_other.go b/lib/daemonize/daemon_other.go
index a0781d453..a7d663d0e 100644
--- a/lib/daemonize/daemon_other.go
+++ b/lib/daemonize/daemon_other.go
@@ -1,5 +1,4 @@
-//go:build windows || plan9 || js
-// +build windows plan9 js
+//go:build !unix
 
 // Package daemonize provides daemonization stub for non-Unix platforms.
 package daemonize
@@ -10,7 +9,14 @@ import (
 	"runtime"
 )
 
+var errNotSupported = fmt.Errorf("daemon mode is not supported on the %s platform", runtime.GOOS)
+
 // StartDaemon runs background twin of current process.
 func StartDaemon(args []string) (*os.Process, error) {
-	return nil, fmt.Errorf("background mode is not supported on %s platform", runtime.GOOS)
+	return nil, errNotSupported
+}
+
+// Check returns non nil if the daemon process has died
+func Check(daemon *os.Process) error {
+	return errNotSupported
 }
diff --git a/lib/daemonize/daemon_unix.go b/lib/daemonize/daemon_unix.go
index 1fcb2c55c..6e1e0442e 100644
--- a/lib/daemonize/daemon_unix.go
+++ b/lib/daemonize/daemon_unix.go
@@ -1,15 +1,16 @@
-//go:build !windows && !plan9 && !js
-// +build !windows,!plan9,!js
+//go:build unix
 
 // Package daemonize provides daemonization interface for Unix platforms.
 package daemonize
 
 import (
+	"fmt"
 	"os"
 	"strings"
 	"syscall"
 
 	"github.com/rclone/rclone/fs"
+	"golang.org/x/sys/unix"
 )
 
 // StartDaemon runs background twin of current process.
@@ -108,3 +109,20 @@ func argsToEnv(origArgs, origEnv []string) (args, env []string) {
 	}
 	return
 }
+
+// Check returns non nil if the daemon process has died
+func Check(daemon *os.Process) error {
+	var status unix.WaitStatus
+	wpid, err := unix.Wait4(daemon.Pid, &status, unix.WNOHANG, nil)
+	// fs.Debugf(nil, "wait4 returned wpid=%d, err=%v, status=%d", wpid, err, status)
+	if err != nil {
+		return err
+	}
+	if wpid == 0 {
+		return nil
+	}
+	if status.Exited() {
+		return fmt.Errorf("daemon exited with error code %d", status.ExitStatus())
+	}
+	return nil
+}
diff --git a/lib/oauthutil/renew.go b/lib/oauthutil/renew.go
index 7f98491e8..e3652ee9c 100644
--- a/lib/oauthutil/renew.go
+++ b/lib/oauthutil/renew.go
@@ -1,6 +1,7 @@
 package oauthutil
 
 import (
+	"sync"
 	"sync/atomic"
 
 	"github.com/rclone/rclone/fs"
@@ -8,10 +9,12 @@ import (
 
 // Renew allows tokens to be renewed on expiry if uploads are in progress.
 type Renew struct {
-	name    string       // name to use in logs
-	ts      *TokenSource // token source that needs renewing
-	uploads atomic.Int32 // number of uploads in progress
-	run     func() error // a transaction to run to renew the token on
+	name     string       // name to use in logs
+	ts       *TokenSource // token source that needs renewing
+	uploads  atomic.Int32 // number of uploads in progress
+	run      func() error // a transaction to run to renew the token on
+	done     chan any     // channel to end the go routine
+	shutdown sync.Once
 }
 
 // NewRenew creates a new Renew struct and starts a background process
@@ -24,6 +27,7 @@ func NewRenew(name string, ts *TokenSource, run func() error) *Renew {
 		name: name,
 		ts:   ts,
 		run:  run,
+		done: make(chan any),
 	}
 	go r.renewOnExpiry()
 	return r
@@ -36,7 +40,11 @@ func NewRenew(name string, ts *TokenSource, run func() error) *Renew {
 func (r *Renew) renewOnExpiry() {
 	expiry := r.ts.OnExpiry()
 	for {
-		<-expiry
+		select {
+		case <-expiry:
+		case <-r.done:
+			return
+		}
 		uploads := r.uploads.Load()
 		if uploads != 0 {
 			fs.Debugf(r.name, "Token expired - %d uploads in progress - refreshing", uploads)
@@ -72,3 +80,15 @@ func (r *Renew) Invalidate() {
 func (r *Renew) Expire() error {
 	return r.ts.Expire()
 }
+
+// Shutdown stops the timer and no more renewal will take place.
+func (r *Renew) Shutdown() {
+	if r == nil {
+		return
+	}
+	// closing a channel can only be done once
+	r.shutdown.Do(func() {
+		r.ts.expiryTimer.Stop()
+		close(r.done)
+	})
+}
diff --git a/lib/systemd/notify.go b/lib/systemd/notify.go
index a185b71a6..8dccafb4f 100644
--- a/lib/systemd/notify.go
+++ b/lib/systemd/notify.go
@@ -1,10 +1,11 @@
 package systemd
 
 import (
+	"fmt"
 	"log"
 	"sync"
 
-	sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify"
+	"github.com/coreos/go-systemd/v22/daemon"
 	"github.com/rclone/rclone/lib/atexit"
 )
 
@@ -13,13 +14,13 @@ import (
 // stopping. This function will be called on exit if the service exits
 // on a signal.
 func Notify() func() {
-	if err := sysdnotify.Ready(); err != nil {
+	if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
 		log.Printf("failed to notify ready to systemd: %v", err)
 	}
 	var finaliseOnce sync.Once
 	finalise := func() {
 		finaliseOnce.Do(func() {
-			if err := sysdnotify.Stopping(); err != nil {
+			if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil {
 				log.Printf("failed to notify stopping to systemd: %v", err)
 			}
 		})
@@ -30,3 +31,10 @@ func Notify() func() {
 		finalise()
 	}
 }
+
+// UpdateStatus updates the systemd status
+func UpdateStatus(status string) error {
+	systemdStatus := fmt.Sprintf("STATUS=%s", status)
+	_, err := daemon.SdNotify(false, systemdStatus)
+	return err
+}
diff --git a/vfs/vfscache/cache.go b/vfs/vfscache/cache.go
index e5bb19c79..9bcfc90ee 100644
--- a/vfs/vfscache/cache.go
+++ b/vfs/vfscache/cache.go
@@ -14,7 +14,6 @@ import (
 	"sync"
 	"time"
 
-	sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify"
 	"github.com/rclone/rclone/fs"
 	fscache "github.com/rclone/rclone/fs/cache"
 	"github.com/rclone/rclone/fs/config"
@@ -25,6 +24,7 @@ import (
 	"github.com/rclone/rclone/lib/diskusage"
 	"github.com/rclone/rclone/lib/encoder"
 	"github.com/rclone/rclone/lib/file"
+	"github.com/rclone/rclone/lib/systemd"
 	"github.com/rclone/rclone/vfs/vfscache/writeback"
 	"github.com/rclone/rclone/vfs/vfscommon"
 )
@@ -814,7 +814,7 @@ func (c *Cache) clean(kicked bool) {
 	stats := fmt.Sprintf("objects %d (was %d) in use %d, to upload %d, uploading %d, total size %v (was %v)",
 		newItems, oldItems, totalInUse, uploadsQueued, uploadsInProgress, newUsed, oldUsed)
 	fs.Infof(nil, "vfs cache: cleaned: %s", stats)
-	if err = sysdnotify.Status(fmt.Sprintf("[%s] vfs cache: %s", time.Now().Format("15:04"), stats)); err != nil {
+	if err = systemd.UpdateStatus(fmt.Sprintf("[%s] vfs cache: %s", time.Now().Format("15:04"), stats)); err != nil {
 		fs.Errorf(nil, "vfs cache: updating systemd status with current stats failed: %s", err)
 	}
 }
diff --git a/vfs/vfsflags/vfsflags.go b/vfs/vfsflags/vfsflags.go
index 2f1adafde..83b593d29 100644
--- a/vfs/vfsflags/vfsflags.go
+++ b/vfs/vfsflags/vfsflags.go
@@ -22,7 +22,7 @@ func AddFlags(flagSet *pflag.FlagSet) {
 	flags.BoolVarP(flagSet, &Opt.NoChecksum, "no-checksum", "", Opt.NoChecksum, "Don't compare checksums on up/download", "VFS")
 	flags.BoolVarP(flagSet, &Opt.NoSeek, "no-seek", "", Opt.NoSeek, "Don't allow seeking in files", "VFS")
 	flags.DurationVarP(flagSet, &Opt.DirCacheTime, "dir-cache-time", "", Opt.DirCacheTime, "Time to cache directory entries for", "VFS")
-	flags.BoolVarP(flagSet, &Opt.Refresh, "vfs-refresh", "", Opt.Refresh, "Refreshes the directory cache recursively on start", "VFS")
+	flags.BoolVarP(flagSet, &Opt.Refresh, "vfs-refresh", "", Opt.Refresh, "Refreshes the directory cache recursively in the background on start", "VFS")
 	flags.DurationVarP(flagSet, &Opt.PollInterval, "poll-interval", "", Opt.PollInterval, "Time to wait between polling for changes, must be smaller than dir-cache-time and only on supported remotes (set 0 to disable)", "VFS")
 	flags.BoolVarP(flagSet, &Opt.ReadOnly, "read-only", "", Opt.ReadOnly, "Only allow read-only access", "VFS")
 	flags.FVarP(flagSet, &Opt.CacheMode, "vfs-cache-mode", "", "Cache mode off|minimal|writes|full", "VFS")


Test patch:
diff --git a/backend/s3/s3_test.go b/backend/s3/s3_test.go
index 3415bebf2..7e6e55360 100644
--- a/backend/s3/s3_test.go
+++ b/backend/s3/s3_test.go
@@ -2,6 +2,9 @@
 package s3
 
 import (
+	"context"
+	"net/http"
+	"strings"
 	"testing"
 
 	"github.com/rclone/rclone/fs"
@@ -9,6 +12,13 @@ import (
 	"github.com/rclone/rclone/fstest/fstests"
 )
 
+func SetupS3Test(t *testing.T) (context.Context, *Options, *http.Client) {
+	ctx, opt := context.Background(), new(Options)
+	opt.Provider = "AWS"
+	client := getClient(ctx, opt)
+	return ctx, opt, client
+}
+
 // TestIntegration runs integration tests against the remote
 func TestIntegration(t *testing.T) {
 	fstests.Run(t, &fstests.Opt{
@@ -39,6 +49,28 @@ func TestIntegration2(t *testing.T) {
 	})
 }
 
+func TestAWSDualStackOption(t *testing.T) {
+	{
+		// test enabled
+		ctx, opt, client := SetupS3Test(t)
+		opt.UseDualStack = true
+		s3Conn, _, _ := s3Connection(ctx, opt, client)
+		if !strings.Contains(s3Conn.Endpoint, "dualstack") {
+			t.Errorf("dualstack failed got: %s, wanted: dualstack", s3Conn.Endpoint)
+			t.Fail()
+		}
+	}
+	{
+		// test default case
+		ctx, opt, client := SetupS3Test(t)
+		s3Conn, _, _ := s3Connection(ctx, opt, client)
+		if strings.Contains(s3Conn.Endpoint, "dualstack") {
+			t.Errorf("dualstack failed got: %s, NOT wanted: dualstack", s3Conn.Endpoint)
+			t.Fail()
+		}
+	}
+}
+
 func (f *Fs) SetUploadChunkSize(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
 	return f.setUploadChunkSize(cs)
 }
diff --git a/cmd/selfupdate/selfupdate_test.go b/cmd/selfupdate/selfupdate_test.go
index 92d1d91c7..032c28bec 100644
--- a/cmd/selfupdate/selfupdate_test.go
+++ b/cmd/selfupdate/selfupdate_test.go
@@ -17,6 +17,7 @@ import (
 	_ "github.com/rclone/rclone/fstest" // needed to run under integration tests
 	"github.com/rclone/rclone/fstest/testy"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestGetVersion(t *testing.T) {
@@ -76,7 +77,7 @@ func TestInstallOnLinux(t *testing.T) {
 
 	// Must keep non-standard permissions
 	assert.NoError(t, os.Chmod(path, 0644))
-	assert.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
+	require.NoError(t, InstallUpdate(ctx, &Options{Beta: true, Output: path}))
 
 	info, err := os.Stat(path)
 	assert.NoError(t, err)
diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go
index 7ab03dd60..095a94d23 100644
--- a/fstest/fstests/fstests.go
+++ b/fstest/fstests/fstests.go
@@ -1675,6 +1675,24 @@ func Run(t *testing.T, opt *Opt) {
 				require.NotNil(t, fileRemote)
 				assert.Equal(t, fs.ErrorIsFile, err)
 
+				// Check Fs.Root returns the right thing
+				t.Run("FsRoot", func(t *testing.T) {
+					skipIfNotOk(t)
+					got := fileRemote.Root()
+					remoteDir := path.Dir(remoteName)
+					want := remoteDir
+					colon := strings.LastIndex(want, ":")
+					if colon >= 0 {
+						want = want[colon+1:]
+					}
+					if isLocalRemote {
+						// only check last path element on local
+						require.Equal(t, filepath.Base(remoteDir), filepath.Base(got))
+					} else {
+						require.Equal(t, want, got)
+					}
+				})
+
 				if strings.HasPrefix(remoteName, "TestChunker") && strings.Contains(remoteName, "Nometa") {
 					// TODO fix chunker and remove this bypass
 					t.Logf("Skip listing check -- chunker can't yet handle this tricky case")