Repository: rclone/rclone
Pull request #6129 (2022-04-27 10:52:18)

Jottacloud: Fix listing output of remote with special characters
#### What is the purpose of this change?

This fixes the failing integration test: TestIntegration/FsMkdir/FsPutFiles/FsIsFile

Previously there was a discrepancy:

```
rclone ls "remote:Archive/rclone-test-tozewob2qibivak0cucuxip9/hello? sausage/êé/Hello, 世界/ \" abc"
        0 Archive/Archive/rclone-test-tozewob2qibivak0cucuxip9/hello? sausage/êé/Hello, 世界/ " abc/z.txt

rclone ls "remote:Archive/rclone-test-tozewob2qibivak0cucuxip9/hello? sausage/êé/Hello, 世界/ \" abc" --disable ListR
        0 z.txt
```

The latter is correct, and with this fix both returns this result.

#### Was the change discussed in an issue or in the forum before?

No. Detected by integration test TestIntegration/FsMkdir/FsPutFiles/FsIsFile.

#### 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 04a11a549..784a236cc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,7 +25,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        job_name: ['linux', 'mac_amd64', 'mac_arm64', 'windows_amd64', 'windows_386', 'other_os', 'go1.16', 'go1.17']
+        job_name: ['linux', 'mac_amd64', 'mac_arm64', 'windows', 'other_os', 'go1.16', 'go1.17']
 
         include:
           - job_name: linux
@@ -55,23 +55,12 @@ jobs:
             build_flags: '-include "^darwin/arm64" -cgo -macos-arch arm64 -cgo-cflags=-I/usr/local/include -cgo-ldflags=-L/usr/local/lib'
             deploy: true
 
-          - job_name: windows_amd64
+          - job_name: windows
             os: windows-latest
             go: '1.18.x'
             gotags: cmount
-            build_flags: '-include "^windows/amd64" -cgo'
-            build_args: '-buildmode exe'
-            quicktest: true
-            racequicktest: true
-            deploy: true
-
-          - job_name: windows_386
-            os: windows-latest
-            go: '1.18.x'
-            gotags: cmount
-            goarch: '386'
-            cgo: '1'
-            build_flags: '-include "^windows/386" -cgo'
+            cgo: '0'
+            build_flags: '-include "^windows/"'
             build_args: '-buildmode exe'
             quicktest: true
             deploy: true
@@ -79,7 +68,7 @@ jobs:
           - job_name: other_os
             os: ubuntu-latest
             go: '1.18.x'
-            build_flags: '-exclude "^(windows/(386|amd64)|darwin/|linux/)"'
+            build_flags: '-exclude "^(windows/|darwin/|linux/)"'
             compile_all: true
             deploy: true
 
@@ -178,6 +167,11 @@ jobs:
         run: |
           make
 
+      - name: Rclone version
+        shell: bash
+        run: |
+          rclone version
+
       - name: Run tests
         shell: bash
         run: |
diff --git a/Makefile b/Makefile
index c155b2240..f96e1d92a 100644
--- a/Makefile
+++ b/Makefile
@@ -97,7 +97,7 @@ release_dep_linux:
 
 # Get the release dependencies we only install on Windows
 release_dep_windows:
-	GO111MODULE=off GOOS="" GOARCH="" go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
+	GOOS="" GOARCH="" go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
 
 # Update dependencies
 showupdates:
diff --git a/README.md b/README.md
index 61a028606..0235323b5 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,8 @@ Rclone *("rsync for cloud storage")* is a command-line program to sync files and
   * Box [:page_facing_up:](https://rclone.org/box/)
   * Ceph [:page_facing_up:](https://rclone.org/s3/#ceph)
   * China Mobile Ecloud Elastic Object Storage (EOS) [:page_facing_up:](https://rclone.org/s3/#china-mobile-ecloud-eos)
+  * Cloudflare R2 [:page_facing_up:](https://rclone.org/s3/#cloudflare-r2)
+  * Arvan Cloud Object Storage (AOS) [:page_facing_up:](https://rclone.org/s3/#arvan-cloud-object-storage-aos)
   * Citrix ShareFile [:page_facing_up:](https://rclone.org/sharefile/)
   * DigitalOcean Spaces [:page_facing_up:](https://rclone.org/s3/#digitalocean-spaces)
   * Digi Storage [:page_facing_up:](https://rclone.org/koofr/#digi-storage)
@@ -42,6 +44,7 @@ Rclone *("rsync for cloud storage")* is a command-line program to sync files and
   * HDFS (Hadoop Distributed Filesystem) [:page_facing_up:](https://rclone.org/hdfs/)
   * HTTP [:page_facing_up:](https://rclone.org/http/)
   * Hubic [:page_facing_up:](https://rclone.org/hubic/)
+  * Internet Archive [:page_facing_up:](https://rclone.org/internetarchive/)
   * Jottacloud [:page_facing_up:](https://rclone.org/jottacloud/)
   * IBM COS S3 [:page_facing_up:](https://rclone.org/s3/#ibm-cos-s3)
   * Koofr [:page_facing_up:](https://rclone.org/koofr/)
diff --git a/backend/all/all.go b/backend/all/all.go
index b10cbae38..a93909eca 100644
--- a/backend/all/all.go
+++ b/backend/all/all.go
@@ -22,6 +22,7 @@ import (
 	_ "github.com/rclone/rclone/backend/hdfs"
 	_ "github.com/rclone/rclone/backend/http"
 	_ "github.com/rclone/rclone/backend/hubic"
+	_ "github.com/rclone/rclone/backend/internetarchive"
 	_ "github.com/rclone/rclone/backend/jottacloud"
 	_ "github.com/rclone/rclone/backend/koofr"
 	_ "github.com/rclone/rclone/backend/local"
diff --git a/backend/b2/b2.go b/backend/b2/b2.go
index d347c3c8e..ba3e03eb7 100644
--- a/backend/b2/b2.go
+++ b/backend/b2/b2.go
@@ -64,7 +64,8 @@ const (
 
 // Globals
 var (
-	errNotWithVersions = errors.New("can't modify or delete files in --b2-versions mode")
+	errNotWithVersions  = errors.New("can't modify or delete files in --b2-versions mode")
+	errNotWithVersionAt = errors.New("can't modify or delete files in --b2-version-at mode")
 )
 
 // Register with Fs
@@ -106,6 +107,11 @@ in the [b2 integrations checklist](https://www.backblaze.com/b2/docs/integration
 			Help:     "Include old versions in directory listings.\n\nNote that when using this no file write operations are permitted,\nso you can't upload files or delete them.",
 			Default:  false,
 			Advanced: true,
+		}, {
+			Name:     "version_at",
+			Help:     "Show file versions as they were at the specified time.\n\nNote that when using this no file write operations are permitted,\nso you can't upload files or delete them.",
+			Default:  fs.Time{},
+			Advanced: true,
 		}, {
 			Name:    "hard_delete",
 			Help:    "Permanently delete files on remote removal, otherwise hide files.",
@@ -211,6 +217,7 @@ type Options struct {
 	Endpoint                      string               `config:"endpoint"`
 	TestMode                      string               `config:"test_mode"`
 	Versions                      bool                 `config:"versions"`
+	VersionAt                     fs.Time              `config:"version_at"`
 	HardDelete                    bool                 `config:"hard_delete"`
 	UploadCutoff                  fs.SizeSuffix        `config:"upload_cutoff"`
 	CopyCutoff                    fs.SizeSuffix        `config:"copy_cutoff"`
@@ -696,9 +703,12 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
 		Method: "POST",
 		Path:   "/b2_list_file_names",
 	}
-	if hidden {
+	if hidden || f.opt.VersionAt.IsSet() {
 		opts.Path = "/b2_list_file_versions"
 	}
+
+	lastFileName := ""
+
 	for {
 		var response api.ListFileNamesResponse
 		err := f.pacer.Call(func() (bool, error) {
@@ -728,7 +738,21 @@ func (f *Fs) list(ctx context.Context, bucket, directory, prefix string, addBuck
 			if addBucket {
 				remote = path.Join(bucket, remote)
 			}
+
+			if f.opt.VersionAt.IsSet() {
+				if time.Time(file.UploadTimestamp).After(time.Time(f.opt.VersionAt)) {
+					// Ignore versions that were created after the specified time
+					continue
+				}
+
+				if file.Name == lastFileName {
+					// Ignore versions before the already returned version
+					continue
+				}
+			}
+
 			// Send object
+			lastFileName = file.Name
 			err = fn(remote, file, isDirectory)
 			if err != nil {
 				if err == errEndList {
@@ -1828,6 +1852,9 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 	if o.fs.opt.Versions {
 		return errNotWithVersions
 	}
+	if o.fs.opt.VersionAt.IsSet() {
+		return errNotWithVersionAt
+	}
 	size := src.Size()
 
 	bucket, bucketPath := o.split()
@@ -1983,6 +2010,9 @@ func (o *Object) Remove(ctx context.Context) error {
 	if o.fs.opt.Versions {
 		return errNotWithVersions
 	}
+	if o.fs.opt.VersionAt.IsSet() {
+		return errNotWithVersionAt
+	}
 	if o.fs.opt.HardDelete {
 		return o.fs.deleteByID(ctx, o.id, bucketPath)
 	}
diff --git a/backend/internetarchive/internetarchive.go b/backend/internetarchive/internetarchive.go
new file mode 100644
index 000000000..ce021e35f
--- /dev/null
+++ b/backend/internetarchive/internetarchive.go
@@ -0,0 +1,1109 @@
+// Package internetarchive provides an interface to Internet Archive's Item
+// via their native API than using S3-compatible endpoints.
+package internetarchive
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/ncw/swift/v2"
+	"github.com/rclone/rclone/fs"
+	"github.com/rclone/rclone/fs/config"
+	"github.com/rclone/rclone/fs/config/configmap"
+	"github.com/rclone/rclone/fs/config/configstruct"
+	"github.com/rclone/rclone/fs/fserrors"
+	"github.com/rclone/rclone/fs/fshttp"
+	"github.com/rclone/rclone/fs/hash"
+	"github.com/rclone/rclone/lib/bucket"
+	"github.com/rclone/rclone/lib/encoder"
+	"github.com/rclone/rclone/lib/pacer"
+	"github.com/rclone/rclone/lib/random"
+	"github.com/rclone/rclone/lib/rest"
+)
+
+// Register with Fs
+func init() {
+	fs.Register(&fs.RegInfo{
+		Name:        "internetarchive",
+		Description: "Internet Archive",
+		NewFs:       NewFs,
+		Options: []fs.Option{{
+			Name: "access_key_id",
+			Help: "IAS3 Access Key.\n\nLeave blank for anonymous access.\nYou can find one here: https://archive.org/account/s3.php",
+		}, {
+			Name: "secret_access_key",
+			Help: "IAS3 Secret Key (password).\n\nLeave blank for anonymous access.",
+		}, {
+			// their official client (https://github.com/jjjake/internetarchive) hardcodes following the two
+			Name:     "endpoint",
+			Help:     "IAS3 Endpoint.\n\nLeave blank for default value.",
+			Default:  "https://s3.us.archive.org",
+			Advanced: true,
+		}, {
+			Name:     "front_endpoint",
+			Help:     "Host of InternetArchive Frontend.\n\nLeave blank for default value.",
+			Default:  "https://archive.org",
+			Advanced: true,
+		}, {
+			Name: "disable_checksum",
+			Help: `Don't ask the server to test against MD5 checksum calculated by rclone.
+Normally rclone will calculate the MD5 checksum of the input before
+uploading it so it can ask the server to check the object against checksum.
+This is great for data integrity checking but can cause long delays for
+large files to start uploading.`,
+			Default:  true,
+			Advanced: true,
+		}, {
+			Name: "wait_archive",
+			Help: `Timeout for waiting the server's processing tasks (specifically archive and book_op) to finish.
+Only enable if you need to be guaranteed to be reflected after write operations.
+0 to disable waiting. No errors to be thrown in case of timeout.`,
+			Default:  fs.Duration(0),
+			Advanced: true,
+		}, {
+			Name:     config.ConfigEncoding,
+			Help:     config.ConfigEncodingHelp,
+			Advanced: true,
+			Default: encoder.EncodeZero |
+				encoder.EncodeSlash |
+				encoder.EncodeLtGt |
+				encoder.EncodeCrLf |
+				encoder.EncodeDel |
+				encoder.EncodeCtl |
+				encoder.EncodeInvalidUtf8 |
+				encoder.EncodeDot,
+		},
+		}})
+}
+
+// maximum size of an item. this is constant across all items
+const iaItemMaxSize int64 = 1099511627776
+
+// Options defines the configuration for this backend
+type Options struct {
+	AccessKeyID     string               `config:"access_key_id"`
+	SecretAccessKey string               `config:"secret_access_key"`
+	Endpoint        string               `config:"endpoint"`
+	FrontEndpoint   string               `config:"front_endpoint"`
+	DisableChecksum bool                 `config:"disable_checksum"`
+	WaitArchive     fs.Duration          `config:"wait_archive"`
+	Enc             encoder.MultiEncoder `config:"encoding"`
+}
+
+// Fs represents an IAS3 remote
+type Fs struct {
+	name     string       // name of this remote
+	root     string       // the path we are working on if any
+	opt      Options      // parsed config options
+	features *fs.Features // optional features
+	srv      *rest.Client // the connection to IAS3
+	front    *rest.Client // the connection to frontend
+	pacer    *fs.Pacer    // pacer for API calls
+	ctx      context.Context
+}
+
+// Object describes a file at IA
+type Object struct {
+	fs      *Fs       // reference to Fs
+	remote  string    // the remote path
+	modTime time.Time // last modified time
+	size    int64     // size of the file in bytes
+	md5     string    // md5 hash of the file presented by the server
+	sha1    string    // sha1 hash of the file presented by the server
+	crc32   string    // crc32 of the file presented by the server
+}
+
+// IAFile reprensents a subset of object in MetadataResponse.Files
+type IAFile struct {
+	Name string `json:"name"`
+	// Source     string `json:"source"`
+	Mtime       string          `json:"mtime"`
+	RcloneMtime json.RawMessage `json:"rclone-mtime"`
+	UpdateTrack json.RawMessage `json:"rclone-update-track"`
+	Size        string          `json:"size"`
+	Md5         string          `json:"md5"`
+	Crc32       string          `json:"crc32"`
+	Sha1        string          `json:"sha1"`
+}
+
+// MetadataResponse reprensents subset of the JSON object returned by (frontend)/metadata/
+type MetadataResponse struct {
+	Files    []IAFile `json:"files"`
+	ItemSize int64    `json:"item_size"`
+}
+
+// ModMetadataResponse represents response for amending metadata
+type ModMetadataResponse struct {
+	// https://archive.org/services/docs/api/md-write.html#example
+	Success bool   `json:"success"`
+	Error   string `json:"error"`
+}
+
+// Name of the remote (as passed into NewFs)
+func (f *Fs) Name() string {
+	return f.name
+}
+
+// Root of the remote (as passed into NewFs)
+func (f *Fs) Root() string {
+	return f.root
+}
+
+// String converts this Fs to a string
+func (f *Fs) String() string {
+	bucket, file := f.split("")
+	if bucket == "" {
+		return "Internet Archive root"
+	}
+	if file == "" {
+		return fmt.Sprintf("Internet Archive item %s", bucket)
+	}
+	return fmt.Sprintf("Internet Archive item %s path %s", bucket, file)
+}
+
+// Features returns the optional features of this Fs
+func (f *Fs) Features() *fs.Features {
+	return f.features
+}
+
+// Hashes returns type of hashes supported by IA
+func (f *Fs) Hashes() hash.Set {
+	return hash.NewHashSet(hash.MD5, hash.SHA1, hash.CRC32)
+}
+
+// Precision returns the precision of mtime that the server responds
+func (f *Fs) Precision() time.Duration {
+	if f.opt.WaitArchive == 0 {
+		return fs.ModTimeNotSupported
+	}
+	return time.Nanosecond
+}
+
+// retryErrorCodes is a slice of error codes that we will retry
+// See: https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
+var retryErrorCodes = []int{
+	429, // Too Many Requests
+	500, // Internal Server Error - "We encountered an internal error. Please try again."
+	503, // Service Unavailable/Slow Down - "Reduce your request rate"
+}
+
+// NewFs constructs an Fs from the path
+func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
+	// Parse config into Options struct
+	opt := new(Options)
+	err := configstruct.Set(m, opt)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse the endpoints
+	ep, err := url.Parse(opt.Endpoint)
+	if err != nil {
+		return nil, err
+	}
+	fe, err := url.Parse(opt.FrontEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	root = strings.Trim(root, "/")
+
+	f := &Fs{
+		name: name,
+		opt:  *opt,
+		ctx:  ctx,
+	}
+	f.setRoot(root)
+	f.features = (&fs.Features{
+		BucketBased: true,
+	}).Fill(ctx, f)
+
+	f.srv = rest.NewClient(fshttp.NewClient(ctx))
+	f.srv.SetRoot(ep.String())
+
+	f.front = rest.NewClient(fshttp.NewClient(ctx))
+	f.front.SetRoot(fe.String())
+
+	if opt.AccessKeyID != "" && opt.SecretAccessKey != "" {
+		auth := fmt.Sprintf("LOW %s:%s", opt.AccessKeyID, opt.SecretAccessKey)
+		f.srv.SetHeader("Authorization", auth)
+		f.front.SetHeader("Authorization", auth)
+	}
+
+	f.pacer = fs.NewPacer(ctx, pacer.NewS3(pacer.MinSleep(10*time.Millisecond)))
+
+	// test if the root exists as a file
+	_, err = f.NewObject(ctx, "/")
+	if err == nil {
+		f.setRoot(betterPathDir(root))
+		return f, fs.ErrorIsFile
+	}
+	return f, nil
+}
+
+// setRoot changes the root of the Fs
+func (f *Fs) setRoot(root string) {
+	f.root = strings.Trim(root, "/")
+}
+
+// Remote returns the remote path
+func (o *Object) Remote() string {
+	return o.remote
+}
+
+// ModTime is the last modified time (read-only)
+func (o *Object) ModTime(ctx context.Context) time.Time {
+	return o.modTime
+}
+
+// Size is the file length
+func (o *Object) Size() int64 {
+	return o.size
+}
+
+// Fs returns the parent Fs
+func (o *Object) Fs() fs.Info {
+	return o.fs
+}
+
+// Hash returns the hash value presented by IA
+func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) {
+	if ty == hash.MD5 {
+		return o.md5, nil
+	}
+	if ty == hash.SHA1 {
+		return o.sha1, nil
+	}
+	if ty == hash.CRC32 {
+		return o.crc32, nil
+	}
+	return "", hash.ErrUnsupported
+}
+
+// Storable returns if this object is storable
+func (o *Object) Storable() bool {
+	return true
+}
+
+// SetModTime sets modTime on a particular file
+func (o *Object) SetModTime(ctx context.Context, t time.Time) (err error) {
+	bucket, reqDir := o.split()
+	if bucket == "" {
+		return fs.ErrorCantSetModTime
+	}
+	if reqDir == "" {
+		return fs.ErrorCantSetModTime
+	}
+
+	// https://archive.org/services/docs/api/md-write.html
+	var patch = []interface{}{
+		// we should drop it first to clear all rclone-provided mtimes
+		struct {
+			Op   string `json:"op"`
+			Path string `json:"path"`
+		}{"remove", "/rclone-mtime"},
+		struct {
+			Op    string `json:"op"`
+			Path  string `json:"path"`
+			Value string `json:"value"`
+		}{"add", "/rclone-mtime", t.Format(time.RFC3339Nano)},
+	}
+	res, err := json.Marshal(patch)
+	if err != nil {
+		return err
+	}
+	params := url.Values{}
+	params.Add("-target", fmt.Sprintf("files/%s", reqDir))
+	params.Add("-patch", string(res))
+	body := []byte(params.Encode())
+	bodyLen := int64(len(body))
+
+	var resp *http.Response
+	var result ModMetadataResponse
+	// make a POST request to (frontend)/metadata/:item/
+	opts := rest.Opts{
+		Method:        "POST",
+		Path:          path.Join("/metadata/", bucket),
+		Body:          bytes.NewReader(body),
+		ContentLength: &bodyLen,
+		ContentType:   "application/x-www-form-urlencoded",
+	}
+
+	err = o.fs.pacer.Call(func() (bool, error) {
+		resp, err = o.fs.front.CallJSON(ctx, &opts, nil, &result)
+		return o.fs.shouldRetry(resp, err)
+	})
+	if err != nil {
+		return err
+	}
+
+	if result.Success {
+		o.modTime = t
+		return nil
+	}
+
+	return errors.New(result.Error)
+}
+
+// List files and directories in a directory
+func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
+	bucket, reqDir := f.split(dir)
+	if bucket == "" {
+		if reqDir != "" {
+			return nil, fs.ErrorListBucketRequired
+		}
+		return entries, nil
+	}
+	grandparent := f.opt.Enc.ToStandardPath(strings.Trim(path.Join(bucket, reqDir), "/") + "/")
+
+	allEntries, err := f.listAllUnconstrained(ctx, bucket)
+	if err != nil {
+		return entries, err
+	}
+	for _, ent := range allEntries {
+		obj, ok := ent.(*Object)
+		if ok && strings.HasPrefix(obj.remote, grandparent) {
+			path := trimPathPrefix(obj.remote, grandparent, f.opt.Enc)
+			if !strings.Contains(path, "/") {
+				obj.remote = trimPathPrefix(obj.remote, f.root, f.opt.Enc)
+				entries = append(entries, obj)
+			}
+		}
+		dire, ok := ent.(*fs.Dir)
+		if ok && strings.HasPrefix(dire.Remote(), grandparent) {
+			path := trimPathPrefix(dire.Remote(), grandparent, f.opt.Enc)
+			if !strings.Contains(path, "/") {
+				dire.SetRemote(trimPathPrefix(dire.Remote(), f.root, f.opt.Enc))
+				entries = append(entries, dire)
+			}
+		}
+	}
+
+	return entries, nil
+}
+
+// Mkdir can't be performed on IA like git repositories
+func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
+	return nil
+}
+
+// Rmdir as well, unless we're asked for recursive deletion
+func (f *Fs) Rmdir(ctx context.Context, dir string) error {
+	return nil
+}
+
+// NewObject finds the Object at remote.  If it can't be found
+// it returns the error fs.ErrorObjectNotFound.
+func (f *Fs) NewObject(ctx context.Context, remote string) (ret fs.Object, err error) {
+	bucket, filepath := f.split(remote)
+	filepath = strings.Trim(filepath, "/")
+	if bucket == "" {
+		if filepath != "" {
+			return nil, fs.ErrorListBucketRequired
+		}
+		return nil, fs.ErrorIsDir
+	}
+
+	grandparent := f.opt.Enc.ToStandardPath(strings.Trim(path.Join(bucket, filepath), "/"))
+
+	allEntries, err := f.listAllUnconstrained(ctx, bucket)
+	if err != nil {
+		return nil, err
+	}
+	for _, ent := range allEntries {
+		obj, ok := ent.(*Object)
+		if ok && obj.remote == grandparent {
+			obj.remote = trimPathPrefix(obj.remote, f.root, f.opt.Enc)
+			return obj, nil
+		}
+	}
+
+	return nil, fs.ErrorObjectNotFound
+}
+
+// Put uploads a file
+func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
+	o := &Object{
+		fs:      f,
+		remote:  src.Remote(),
+		modTime: src.ModTime(ctx),
+		size:    src.Size(),
+	}
+
+	err := o.Update(ctx, in, src, options...)
+	if err == nil {
+		return o, nil
+	}
+
+	return nil, err
+}
+
+// PublicLink generates a public link to the remote path (usually readable by anyone)
+func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
+	if strings.HasSuffix(remote, "/") {
+		return "", fs.ErrorCantShareDirectories
+	}
+	if _, err := f.NewObject(ctx, remote); err != nil {
+		return "", err
+	}
+	bucket, bucketPath := f.split(remote)
+	return path.Join(f.opt.FrontEndpoint, "/download/", bucket, bucketPath), nil
+}
+
+// Copy src to this remote using server-side copy operations.
+//
+// This is stored with the remote path given
+//
+// It returns the destination Object and a possible error
+//
+// Will only be called if src.Fs().Name() == f.Name()
+//
+// If it isn't possible then return fs.ErrorCantCopy
+func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (_ fs.Object, err error) {
+	dstBucket, dstPath := f.split(remote)
+	srcObj, ok := src.(*Object)
+	if !ok {
+		fs.Debugf(src, "Can't copy - not same remote type")
+		return nil, fs.ErrorCantCopy
+	}
+	srcBucket, srcPath := srcObj.split()
+
+	if dstBucket == srcBucket && dstPath == srcPath {
+		// https://github.com/jjjake/internetarchive/blob/2456376533251df9d05e0a14d796ec1ced4959f5/internetarchive/cli/ia_copy.py#L68
+		fs.Debugf(src, "Can't copy - the source and destination files cannot be the same!")
+		return nil, fs.ErrorCantCopy
+	}
+
+	updateTracker := random.String(32)
+	headers := map[string]string{
+		"x-archive-auto-make-bucket": "1",
+		"x-archive-queue-derive":     "0",
+		"x-archive-keep-old-version": "0",
+		"x-amz-copy-source":          quotePath(path.Join("/", srcBucket, srcPath)),
+		"x-amz-metadata-directive":   "COPY",
+		"x-archive-filemeta-sha1":    srcObj.sha1,
+		"x-archive-filemeta-md5":     srcObj.md5,
+		"x-archive-filemeta-crc32":   srcObj.crc32,
+		"x-archive-filemeta-size":    fmt.Sprint(srcObj.size),
+		// add this too for sure
+		"x-archive-filemeta-rclone-mtime":        srcObj.modTime.Format(time.RFC3339Nano),
+		"x-archive-filemeta-rclone-update-track": updateTracker,
+	}
+
+	// make a PUT request at (IAS3)/:item/:path without body
+	var resp *http.Response
+	opts := rest.Opts{
+		Method:       "PUT",
+		Path:         "/" + url.PathEscape(path.Join(dstBucket, dstPath)),
+		ExtraHeaders: headers,
+	}
+
+	err = f.pacer.Call(func() (bool, error) {
+		resp, err = f.srv.Call(ctx, &opts)
+		return f.shouldRetry(resp, err)
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// we can't update/find metadata here as IA will also
+	// queue server-side copy as well as upload/delete.
+	return f.waitFileUpload(ctx, trimPathPrefix(path.Join(dstBucket, dstPath), f.root, f.opt.Enc), updateTracker, srcObj.size)
+}
+
+// ListR lists the objects and directories of the Fs starting
+// from dir recursively into out.
+//
+// dir should be "" to start from the root, and should not
+// have trailing slashes.
+//
+// This should return ErrDirNotFound if the directory isn't
+// found.
+//
+// It should call callback for each tranche of entries read.
+// These need not be returned in any particular order.  If
+// callback returns an error then the listing will stop
+// immediately.
+//
+// Don't implement this unless you have a more efficient way
+// of listing recursively than doing a directory traversal.
+func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
+	var allEntries, entries fs.DirEntries
+	bucket, reqDir := f.split(dir)
+	if bucket == "" {
+		if reqDir != "" {
+			return fs.ErrorListBucketRequired
+		}
+		return callback(entries)
+	}
+	grandparent := f.opt.Enc.ToStandardPath(strings.Trim(path.Join(bucket, reqDir), "/") + "/")
+
+	allEntries, err = f.listAllUnconstrained(ctx, bucket)
+	if err != nil {
+		return err
+	}
+	for _, ent := range allEntries {
+		obj, ok := ent.(*Object)
+		if ok && strings.HasPrefix(obj.remote, grandparent) {
+			obj.remote = trimPathPrefix(obj.remote, f.root, f.opt.Enc)
+			entries = append(entries, obj)
+		}
+		dire, ok := ent.(*fs.Dir)
+		if ok && strings.HasPrefix(dire.Remote(), grandparent) {
+			dire.SetRemote(trimPathPrefix(dire.Remote(), f.root, f.opt.Enc))
+			entries = append(entries, dire)
+		}
+	}
+
+	return callback(entries)
+}
+
+// CleanUp removes all files inside history/
+func (f *Fs) CleanUp(ctx context.Context) (err error) {
+	bucket, _ := f.split("/")
+	if bucket == "" {
+		return fs.ErrorListBucketRequired
+	}
+	entries, err := f.listAllUnconstrained(ctx, bucket)
+	if err != nil {
+		return err
+	}
+
+	for _, ent := range entries {
+		obj, ok := ent.(*Object)
+		if ok && strings.HasPrefix(obj.remote, bucket+"/history/") {
+			err = obj.Remove(ctx)
+			if err != nil {
+				return err
+			}
+		}
+		// we can fully ignore directories, as they're just virtual entries to
+		// comply with rclone's requirement
+	}
+
+	return nil
+}
+
+// About returns things about remaining and used spaces
+func (f *Fs) About(ctx context.Context) (_ *fs.Usage, err error) {
+	bucket, _ := f.split("/")
+	if bucket == "" {
+		return nil, fs.ErrorListBucketRequired
+	}
+
+	result, err := f.requestMetadata(ctx, bucket)
+	if err != nil {
+		return nil, err
+	}
+
+	// perform low-level operation here since it's ridiculous to make 2 same requests
+	var historySize int64
+	for _, ent := range result.Files {
+		if strings.HasPrefix(ent.Name, "history/") {
+			size := parseSize(ent.Size)
+			if size < 0 {
+				// parse error can be ignored since it's not fatal
+				continue
+			}
+			historySize += size
+		}
+	}
+
+	usage := &fs.Usage{
+		Total:   fs.NewUsageValue(iaItemMaxSize),
+		Free:    fs.NewUsageValue(iaItemMaxSize - result.ItemSize),
+		Used:    fs.NewUsageValue(result.ItemSize),
+		Trashed: fs.NewUsageValue(historySize), // bytes in trash
+	}
+	return usage, nil
+}
+
+// Open an object for read
+func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
+	var optionsFixed []fs.OpenOption
+	for _, opt := range options {
+		if optRange, ok := opt.(*fs.RangeOption); ok {
+			// Ignore range option if file is empty
+			if o.Size() == 0 && optRange.Start == 0 && optRange.End > 0 {
+				continue
+			}
+		}
+		optionsFixed = append(optionsFixed, opt)
+	}
+
+	var resp *http.Response
+	// make a GET request to (frontend)/download/:item/:path
+	opts := rest.Opts{
+		Method:  "GET",
+		Path:    path.Join("/download/", o.fs.root, o.fs.opt.Enc.FromStandardPath(o.remote)),
+		Options: optionsFixed,
+	}
+	err = o.fs.pacer.Call(func() (bool, error) {
+		resp, err = o.fs.front.Call(ctx, &opts)
+		return o.fs.shouldRetry(resp, err)
+	})
+	if err != nil {
+		return nil, err
+	}
+	return resp.Body, nil
+}
+
+// Update the Object from in with modTime and size
+func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
+	bucket, bucketPath := o.split()
+	modTime := src.ModTime(ctx)
+	size := src.Size()
+	updateTracker := random.String(32)
+
+	// Set the mtime in the metadata
+	// internetarchive backend builds at header level as IAS3 has extension outside X-Amz-
+	headers := map[string]string{
+		// https://github.com/jjjake/internetarchive/blob/2456376533251df9d05e0a14d796ec1ced4959f5/internetarchive/iarequest.py#L158
+		"x-amz-filemeta-rclone-mtime":        modTime.Format(time.RFC3339Nano),
+		"x-amz-filemeta-rclone-update-track": updateTracker,
+
+		// we add some more headers for intuitive actions
+		"x-amz-auto-make-bucket":     "1",    // create an item if does not exist, do nothing if already
+		"x-archive-auto-make-bucket": "1",    // same as above in IAS3 original way
+		"x-archive-keep-old-version": "0",    // do not keep old versions (a.k.a. trashes in other clouds)
+		"x-archive-meta-mediatype":   "data", // mark media type of the uploading file as "data"
+		"x-archive-queue-derive":     "0",    // skip derivation process (e.g. encoding to smaller files, OCR on PDFs)
+		"x-archive-cascade-delete":   "1",    // enable "cascate delete" (delete all derived files in addition to the file itself)
+	}
+	if size >= 0 {
+		headers["Content-Length"] = fmt.Sprintf("%d", size)
+		headers["x-archive-size-hint"] = fmt.Sprintf("%d", size)
+	}
+
+	// read the md5sum if available
+	var md5sumHex string
+	if !o.fs.opt.DisableChecksum {
+		md5sumHex, err = src.Hash(ctx, hash.MD5)
+		if err == nil && matchMd5.MatchString(md5sumHex) {
+			// Set the md5sum in header on the object if
+			// the user wants it
+			// https://github.com/jjjake/internetarchive/blob/245637653/internetarchive/item.py#L969
+			headers["Content-MD5"] = md5sumHex
+		}
+	}
+
+	// make a PUT request at (IAS3)/encoded(:item/:path)
+	var resp *http.Response
+	opts := rest.Opts{
+		Method:        "PUT",
+		Path:          "/" + url.PathEscape(path.Join(bucket, bucketPath)),
+		Body:          in,
+		ContentLength: &size,
+		ExtraHeaders:  headers,
+	}
+
+	err = o.fs.pacer.Call(func() (bool, error) {
+		resp, err = o.fs.srv.Call(ctx, &opts)
+		return o.fs.shouldRetry(resp, err)
+	})
+
+	// we can't update/find metadata here as IA will "ingest" uploaded file(s)
+	// upon uploads. (you can find its progress at https://archive.org/history/ItemNameHere )
+	// or we have to wait for finish? (needs polling (frontend)/metadata/:item or scraping (frontend)/history/:item)
+	var newObj *Object
+	if err == nil {
+		newObj, err = o.fs.waitFileUpload(ctx, o.remote, updateTracker, size)
+	} else {
+		newObj = &Object{}
+	}
+	o.crc32 = newObj.crc32
+	o.md5 = newObj.md5
+	o.sha1 = newObj.sha1
+	o.modTime = newObj.modTime
+	o.size = newObj.size
+	return err
+}
+
+// Remove an object
+func (o *Object) Remove(ctx context.Context) (err error) {
+	bucket, bucketPath := o.split()
+
+	// make a DELETE request at (IAS3)/:item/:path
+	var resp *http.Response
+	opts := rest.Opts{
+		Method: "DELETE",
+		Path:   "/" + url.PathEscape(path.Join(bucket, bucketPath)),
+	}
+
+	err = o.fs.pacer.Call(func() (bool, error) {
+		resp, err = o.fs.srv.Call(ctx, &opts)
+		return o.fs.shouldRetry(resp, err)
+	})
+
+	// deleting files can take bit longer as
+	// it'll be processed on same queue as uploads
+	if err == nil {
+		err = o.fs.waitDelete(ctx, bucket, bucketPath)
+	}
+	return err
+}
+
+// String converts this Fs to a string
+func (o *Object) String() string {
+	if o == nil {
+		return "<nil>"
+	}
+	return o.remote
+}
+
+func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
+	if resp != nil {
+		for _, e := range retryErrorCodes {
+			if resp.StatusCode == e {
+				return true, err
+			}
+		}
+	}
+	// Ok, not an awserr, check for generic failure conditions
+	return fserrors.ShouldRetry(err), err
+}
+
+var matchMd5 = regexp.MustCompile(`^[0-9a-f]{32}$`)
+
+// split returns bucket and bucketPath from the rootRelativePath
+// relative to f.root
+func (f *Fs) split(rootRelativePath string) (bucketName, bucketPath string) {
+	bucketName, bucketPath = bucket.Split(path.Join(f.root, rootRelativePath))
+	return f.opt.Enc.FromStandardName(bucketName), f.opt.Enc.FromStandardPath(bucketPath)
+}
+
+// split returns bucket and bucketPath from the object
+func (o *Object) split() (bucket, bucketPath string) {
+	return o.fs.split(o.remote)
+}
+
+func (f *Fs) requestMetadata(ctx context.Context, bucket string) (result MetadataResponse, err error) {
+	var resp *http.Response
+	// make a GET request to (frontend)/metadata/:item/
+	opts := rest.Opts{
+		Method: "GET",
+		Path:   path.Join("/metadata/", bucket),
+	}
+
+	err = f.pacer.Call(func() (bool, error) {
+		resp, err = f.front.CallJSON(ctx, &opts, nil, &result)
+		return f.shouldRetry(resp, err)
+	})
+
+	return result, err
+}
+
+// list up all files/directories without any filters
+func (f *Fs) listAllUnconstrained(ctx context.Context, bucket string) (entries fs.DirEntries, err error) {
+	result, err := f.requestMetadata(ctx, bucket)
+	if err != nil {
+		return nil, err
+	}
+
+	knownDirs := map[string]time.Time{
+		"": time.Unix(0, 0),
+	}
+	for _, file := range result.Files {
+		dir := strings.Trim(betterPathDir(file.Name), "/")
+		nameWithBucket := path.Join(bucket, file.Name)
+
+		mtimeTime := file.parseMtime()
+
+		// populate children directories
+		child := dir
+		for {
+			if _, ok := knownDirs[child]; ok {
+				break
+			}
+			// directory
+			d := fs.NewDir(f.opt.Enc.ToStandardPath(path.Join(bucket, child)), mtimeTime)
+			entries = append(entries, d)
+
+			knownDirs[child] = mtimeTime
+			child = strings.Trim(betterPathDir(child), "/")
+		}
+		if _, ok := knownDirs[betterPathDir(file.Name)]; !ok {
+			continue
+		}
+
+		size := parseSize(file.Size)
+
+		o := makeValidObject(f, f.opt.Enc.ToStandardPath(nameWithBucket), file, mtimeTime, size)
+		entries = append(entries, o)
+	}
+
+	return entries, nil
+}
+
+func (f *Fs) waitFileUpload(ctx context.Context, reqPath, tracker string, newSize int64) (ret *Object, err error) {
+	bucket, bucketPath := f.split(reqPath)
+
+	ret = &Object{
+		fs:      f,
+		remote:  trimPathPrefix(path.Join(bucket, bucketPath), f.root, f.opt.Enc),
+		modTime: time.Unix(0, 0),
+		size:    -1,
+	}
+
+	if f.opt.WaitArchive == 0 {
+		// user doesn't want to poll, let's not
+		ret2, err := f.NewObject(ctx, reqPath)
+		if err == nil {
+			ret2, ok := ret2.(*Object)
+			if ok {
+				ret = ret2
+				ret.crc32 = ""
+				ret.md5 = ""
+				ret.sha1 = ""
+				ret.size = -1
+			}
+		}
+		return ret, nil
+	}
+
+	retC := make(chan struct {
+		*Object
+		error
+	}, 1)
+	go func() {
+		isFirstTime := true
+		existed := false
+		for {
+			if !isFirstTime {
+				// depending on the queue, it takes time
+				time.Sleep(10 * time.Second)
+			}
+			metadata, err := f.requestMetadata(ctx, bucket)
+			if err != nil {
+				retC <- struct {
+					*Object
+					error
+				}{ret, err}
+				return
+			}
+
+			var iaFile *IAFile
+			for _, f := range metadata.Files {
+				if f.Name == bucketPath {
+					iaFile = &f
+					break
+				}
+			}
+			if isFirstTime {
+				isFirstTime = false
+				existed = iaFile != nil
+			}
+			if iaFile == nil {
+				continue
+			}
+			if !existed && !isFirstTime {
+				// fast path: file wasn't exited before
+				retC <- struct {
+					*Object
+					error
+				}{makeValidObject2(f, *iaFile, bucket), nil}
+				return
+			}
+
+			fileTrackers, _ := listOrString(iaFile.UpdateTrack)
+			trackerMatch := false
+			for _, v := range fileTrackers {
+				if v == tracker {
+					trackerMatch = true
+					break
+				}
+			}
+			if !trackerMatch {
+				continue
+			}
+			if !compareSize(parseSize(iaFile.Size), newSize) {
+				continue
+			}
+
+			// voila!
+			retC <- struct {
+				*Object
+				error
+			}{makeValidObject2(f, *iaFile, bucket), nil}
+			return
+		}
+	}()
+
+	select {
+	case res := <-retC:
+		return res.Object, res.error
+	case <-time.After(time.Duration(f.opt.WaitArchive)):
+		return ret, nil
+	}
+}
+
+func (f *Fs) waitDelete(ctx context.Context, bucket, bucketPath string) (err error) {
+	if f.opt.WaitArchive == 0 {
+		// user doesn't want to poll, let's not
+		return nil
+	}
+
+	retC := make(chan error, 1)
+	go func() {
+		for {
+			metadata, err := f.requestMetadata(ctx, bucket)
+			if err != nil {
+				retC <- err
+				return
+			}
+
+			found := false
+			for _, f := range metadata.Files {
+				if f.Name == bucketPath {
+					found = true
+					break
+				}
+			}
+
+			if !found {
+				retC <- nil
+				return
+			}
+
+			// depending on the queue, it takes time
+			time.Sleep(10 * time.Second)
+		}
+	}()
+
+	select {
+	case res := <-retC:
+		return res
+	case <-time.After(time.Duration(f.opt.WaitArchive)):
+		return nil
+	}
+}
+
+func makeValidObject(f *Fs, remote string, file IAFile, mtime time.Time, size int64) *Object {
+	return &Object{
+		fs:      f,
+		remote:  remote,
+		modTime: mtime,
+		size:    size,
+		md5:     file.Md5,
+		crc32:   file.Crc32,
+		sha1:    file.Sha1,
+	}
+}
+
+func makeValidObject2(f *Fs, file IAFile, bucket string) *Object {
+	mtimeTime := file.parseMtime()
+
+	size := parseSize(file.Size)
+
+	return makeValidObject(f, trimPathPrefix(path.Join(bucket, file.Name), f.root, f.opt.Enc), file, mtimeTime, size)
+}
+
+func listOrString(jm json.RawMessage) (rmArray []string, err error) {
+	// rclone-metadata can be an array or string
+	// try to deserialize it as array first
+	err = json.Unmarshal(jm, &rmArray)
+	if err != nil {
+		// if not, it's a string
+		dst := new(string)
+		err = json.Unmarshal(jm, dst)
+		if err == nil {
+			rmArray = []string{*dst}
+		}
+	}
+	return
+}
+
+func (file IAFile) parseMtime() (mtime time.Time) {
+	// method 1: use metadata added by rclone
+	rmArray, err := listOrString(file.RcloneMtime)
+	// let's take the first value we can deserialize
+	for _, value := range rmArray {
+		mtime, err = time.Parse(time.RFC3339Nano, value)
+		if err == nil {
+			break
+		}
+	}
+	if err != nil {
+		// method 2: use metadata added by IAS3
+		mtime, err = swift.FloatStringToTime(file.Mtime)
+	}
+	if err != nil {
+		// metadata files don't have some of the fields
+		mtime = time.Unix(0, 0)
+	}
+	return mtime
+}
+
+func compareSize(a, b int64) bool {
+	if a < 0 || b < 0 {
+		// we won't compare if any of them is not known
+		return true
+	}
+	return a == b
+}
+
+func parseSize(str string) int64 {
+	size, err := strconv.ParseInt(str, 10, 64)
+	if err != nil {
+		size = -1
+	}
+	return size
+}
+
+func betterPathDir(p string) string {
+	d := path.Dir(p)
+	if d == "." {
+		return ""
+	}
+	return d
+}
+
+func betterPathClean(p string) string {
+	d := path.Clean(p)
+	if d == "." {
+		return ""
+	}
+	return d
+}
+
+func trimPathPrefix(s, prefix string, enc encoder.MultiEncoder) string {
+	// we need to clean the paths to make tests pass!
+	s = betterPathClean(s)
+	prefix = betterPathClean(prefix)
+	if s == prefix || s == prefix+"/" {
+		return ""
+	}
+	prefix = enc.ToStandardPath(strings.TrimRight(prefix, "/"))
+	return enc.ToStandardPath(strings.TrimPrefix(s, prefix+"/"))
+}
+
+// mimicks urllib.parse.quote() on Python; exclude / from url.PathEscape
+func quotePath(s string) string {
+	seg := strings.Split(s, "/")
+	newValues := []string{}
+	for _, v := range seg {
+		newValues = append(newValues, url.PathEscape(v))
+	}
+	return strings.Join(newValues, "/")
+}
+
+var (
+	_ fs.Fs           = &Fs{}
+	_ fs.Copier       = &Fs{}
+	_ fs.ListRer      = &Fs{}
+	_ fs.CleanUpper   = &Fs{}
+	_ fs.PublicLinker = &Fs{}
+	_ fs.Abouter      = &Fs{}
+	_ fs.Object       = &Object{}
+)
diff --git a/backend/jottacloud/jottacloud.go b/backend/jottacloud/jottacloud.go
index 62491dea1..5d3838254 100644
--- a/backend/jottacloud/jottacloud.go
+++ b/backend/jottacloud/jottacloud.go
@@ -932,7 +932,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
 	return entries, nil
 }
 
-func parseListRStream(ctx context.Context, r io.Reader, trimPrefix string, filesystem *Fs, callback func(fs.DirEntry) error) error {
+func parseListRStream(ctx context.Context, r io.Reader, filesystem *Fs, callback func(fs.DirEntry) error) error {
 
 	type stats struct {
 		Folders int `xml:"folders"`
@@ -968,8 +968,12 @@ func parseListRStream(ctx context.Context, r io.Reader, trimPrefix string, files
 		})
 	}
 
+	// liststream paths are /mountpoint/root/path
+	// so the returned paths should have /mountpoint/root/ trimmed
+	// as the caller is expecting path.
+	pathPrefix := filesystem.opt.Enc.FromStandardPath(path.Join("/", filesystem.opt.Mountpoint, filesystem.root))
 	trimPathPrefix := func(p string) string {
-		p = strings.TrimPrefix(p, trimPrefix)
+		p = strings.TrimPrefix(p, pathPrefix)
 		p = strings.TrimPrefix(p, "/")
 		return p
 	}
@@ -1052,11 +1056,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
 			return shouldRetry(ctx, resp, err)
 		}
 
-		// liststream paths are /mountpoint/root/path
-		// so the returned paths should have /mountpoint/root/ trimmed
-		// as the caller is expecting path.
-		trimPrefix := path.Join("/", f.opt.Mountpoint, f.root)
-		err = parseListRStream(ctx, resp.Body, trimPrefix, f, func(d fs.DirEntry) error {
+		err = parseListRStream(ctx, resp.Body, f, func(d fs.DirEntry) error {
 			if d.Remote() == dir {
 				return nil
 			}
diff --git a/backend/s3/s3.go b/backend/s3/s3.go
index 461e48ee9..d8ba92724 100644
--- a/backend/s3/s3.go
+++ b/backend/s3/s3.go
@@ -32,6 +32,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/endpoints"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/session"
+	v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
 	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/ncw/swift/v2"
 	"github.com/rclone/rclone/fs"
@@ -58,7 +59,7 @@ import (
 func init() {
 	fs.Register(&fs.RegInfo{
 		Name:        "s3",
-		Description: "Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, China Mobile, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, Netease, RackCorp, Scaleway, SeaweedFS, StackPath, Storj, Tencent COS and Wasabi",
+		Description: "Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, China Mobile, Cloudflare, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, Netease, RackCorp, Scaleway, SeaweedFS, StackPath, Storj, Tencent COS and Wasabi",
 		NewFs:       NewFs,
 		CommandHelp: commandHelp,
 		Options: []fs.Option{{
@@ -78,6 +79,12 @@ func init() {
 			}, {
 				Value: "ChinaMobile",
 				Help:  "China Mobile Ecloud Elastic Object Storage (EOS)",
+			}, {
+				Value: "Cloudflare",
+				Help:  "Cloudflare R2 Storage",
+			}, {
+				Value: "ArvanCloud",
+				Help:  "Arvan Cloud Object Storage (AOS)",
 			}, {
 				Value: "DigitalOcean",
 				Help:  "Digital Ocean Spaces",
@@ -294,10 +301,18 @@ func init() {
 				Value: "fr-par",
 				Help:  "Paris, France",
 			}},
+		}, {
+			Name:     "region",
+			Help:     "Region to connect to.",
+			Provider: "Cloudflare",
+			Examples: []fs.OptionExample{{
+				Value: "auto",
+				Help:  "R2 buckets are automatically distributed across Cloudflare's data centers for low latency.",
+			}},
 		}, {
 			Name:     "region",
 			Help:     "Region to connect to.\n\nLeave blank if you are using an S3 clone and you don't have a region.",
-			Provider: "!AWS,Alibaba,ChinaMobile,RackCorp,Scaleway,Storj,TencentCOS",
+			Provider: "!AWS,Alibaba,ChinaMobile,Cloudflare,ArvanCloud,RackCorp,Scaleway,Storj,TencentCOS",
 			Examples: []fs.OptionExample{{
 				Value: "",
 				Help:  "Use this if unsure.\nWill use v4 signatures and an empty region.",
@@ -405,6 +420,18 @@ func init() {
 				Value: "eos-anhui-1.cmecloud.cn",
 				Help:  "Anhui China (Huainan)",
 			}},
+		}, {
+			// ArvanCloud endpoints: https://www.arvancloud.com/en/products/cloud-storage
+			Name:     "endpoint",
+			Help:     "Endpoint for Arvan Cloud Object Storage (AOS) API.",
+			Provider: "ArvanCloud",
+			Examples: []fs.OptionExample{{
+				Value: "s3.ir-thr-at1.arvanstorage.com",
+				Help:  "The default endpoint - a good choice if you are unsure.\nTehran Iran (Asiatech)",
+			}, {
+				Value: "s3.ir-tbz-sh1.arvanstorage.com",
+				Help:  "Tabriz Iran (Shahriar)",
+			}},
 		}, {
 			Name:     "endpoint",
 			Help:     "Endpoint for IBM COS S3 API.\n\nSpecify if using an IBM COS On Premise.",
@@ -845,7 +872,7 @@ func init() {
 		}, {
 			Name:     "endpoint",
 			Help:     "Endpoint for S3 API.\n\nRequired when using an S3 clone.",
-			Provider: "!AWS,IBMCOS,TencentCOS,Alibaba,ChinaMobile,Scaleway,StackPath,Storj,RackCorp",
+			Provider: "!AWS,IBMCOS,TencentCOS,Alibaba,ChinaMobile,ArvanCloud,Scaleway,StackPath,Storj,RackCorp",
 			Examples: []fs.OptionExample{{
 				Value:    "objects-us-east-1.dream.io",
 				Help:     "Dream Objects endpoint",
@@ -898,6 +925,10 @@ func init() {
 				Value:    "s3.ap-northeast-2.wasabisys.com",
 				Help:     "Wasabi AP Northeast 2 (Osaka) endpoint",
 				Provider: "Wasabi",
+			}, {
+				Value:    "s3.ir-thr-at1.arvanstorage.com",
+				Help:     "ArvanCloud Tehran Iran (Asiatech) endpoint",
+				Provider: "ArvanCloud",
 			}},
 		}, {
 			Name:     "location_constraint",
@@ -1074,6 +1105,17 @@ func init() {
 				Value: "anhui1",
 				Help:  "Anhui China (Huainan)",
 			}},
+		}, {
+			Name:     "location_constraint",
+			Help:     "Location constraint - must match endpoint.\n\nUsed when creating buckets only.",
+			Provider: "ArvanCloud",
+			Examples: []fs.OptionExample{{
+				Value: "ir-thr-at1",
+				Help:  "Tehran Iran (Asiatech)",
+			}, {
+				Value: "ir-tbz-sh1",
+				Help:  "Tabriz Iran (Shahriar)",
+			}},
 		}, {
 			Name:     "location_constraint",
 			Help:     "Location constraint - must match endpoint when using IBM Cloud Public.\n\nFor on-prem COS, do not make a selection from this list, hit enter.",
@@ -1240,7 +1282,7 @@ func init() {
 		}, {
 			Name:     "location_constraint",
 			Help:     "Location constraint - must be set to match the Region.\n\nLeave blank if not sure. Used when creating buckets only.",
-			Provider: "!AWS,IBMCOS,Alibaba,ChinaMobile,RackCorp,Scaleway,StackPath,Storj,TencentCOS",
+			Provider: "!AWS,IBMCOS,Alibaba,ChinaMobile,Cloudflare,ArvanCloud,RackCorp,Scaleway,StackPath,Storj,TencentCOS",
 		}, {
 			Name: "acl",
 			Help: `Canned ACL used when creating buckets and storing or copying objects.
@@ -1452,6 +1494,15 @@ If you leave it blank, this is calculated automatically from the sse_customer_ke
 				Value: "STANDARD_IA",
 				Help:  "Infrequent access storage mode",
 			}},
+		}, {
+			// Mapping from here: https://www.arvancloud.com/en/products/cloud-storage
+			Name:     "storage_class",
+			Help:     "The storage class to use when storing new objects in ArvanCloud.",
+			Provider: "ArvanCloud",
+			Examples: []fs.OptionExample{{
+				Value: "STANDARD",
+				Help:  "Standard storage class",
+			}},
 		}, {
 			// Mapping from here: https://intl.cloud.tencent.com/document/product/436/30925
 			Name:     "storage_class",
@@ -1784,6 +1835,20 @@ This should be true, false or left unset to use the default for the provider.
 `,
 			Default:  fs.Tristate{},
 			Advanced: true,
+		}, {
+			Name: "use_presigned_request",
+			Help: `Whether to use a presigned request or PutObject for single part uploads
+
+If this is false rclone will use PutObject from the AWS SDK to upload
+an object.
+
+Versions of rclone < 1.59 use presigned requests to upload a single
+part object and setting this flag to true will re-enable that
+functionality. This shouldn't be necessary except in exceptional
+circumstances or for testing.
+`,
+			Default:  false,
+			Advanced: true,
 		},
 		}})
 }
@@ -1849,6 +1914,7 @@ type Options struct {
 	DisableHTTP2          bool                 `config:"disable_http2"`
 	DownloadURL           string               `config:"download_url"`
 	UseMultipartEtag      fs.Tristate          `config:"use_multipart_etag"`
+	UsePresignedRequest   bool                 `config:"use_presigned_request"`
 }
 
 // Fs represents a remote s3 server
@@ -1860,6 +1926,7 @@ type Fs struct {
 	ctx           context.Context  // global context for reading config
 	features      *fs.Features     // optional features
 	c             *s3.S3           // the connection to the s3 server
+	cu            *s3.S3           // unsigned connection to the s3 server for PutObject
 	ses           *session.Session // the s3 session
 	rootBucket    string           // bucket part of root (if any)
 	rootDirectory string           // directory part of root (if any)
@@ -1992,7 +2059,11 @@ func getClient(ctx context.Context, opt *Options) *http.Client {
 }
 
 // s3Connection makes a connection to s3
-func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S3, *session.Session, error) {
+//
+// If unsignedBody is set then the connection is configured for
+// unsigned bodies which is necessary for PutObject if we don't want
+// it to seek
+func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S3, *s3.S3, *session.Session, error) {
 	ci := fs.GetConfig(ctx)
 	// Make the auth
 	v := credentials.Value{
@@ -2009,7 +2080,7 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
 	// start a new AWS session
 	awsSession, err := session.NewSession()
 	if err != nil {
-		return nil, nil, fmt.Errorf("NewSession: %w", err)
+		return nil, nil, nil, fmt.Errorf("NewSession: %w", err)
 	}
 
 	// first provider to supply a credential set "wins"
@@ -2049,9 +2120,9 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
 		// if no access key/secret and iam is explicitly disabled then fall back to anon interaction
 		cred = credentials.AnonymousCredentials
 	case v.AccessKeyID == "":
-		return nil, nil, errors.New("access_key_id not found")
+		return nil, nil, nil, errors.New("access_key_id not found")
 	case v.SecretAccessKey == "":
-		return nil, nil, errors.New("secret_access_key not found")
+		return nil, nil, nil, errors.New("secret_access_key not found")
 	}
 
 	if opt.Region == "" {
@@ -2090,25 +2161,36 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (*s3.S
 		// (from the shared config file) if the passed-in Options.Config.Credentials is nil.
 		awsSessionOpts.Config.Credentials = nil
 	}
+	// Setting this stops PutObject reading the body twice and seeking
+	// We add our own Content-MD5 for data protection
+	awsSessionOpts.Config.S3DisableContentMD5Validation = aws.Bool(true)
 	ses, err := session.NewSessionWithOptions(awsSessionOpts)
 	if err != nil {
-		return nil, nil, err
-	}
-	c := s3.New(ses)
-	if opt.V2Auth || opt.Region == "other-v2-signature" {
-		fs.Debugf(nil, "Using v2 auth")
-		signer := func(req *request.Request) {
-			// Ignore AnonymousCredentials object
-			if req.Config.Credentials == credentials.AnonymousCredentials {
-				return
+		return nil, nil, nil, err
+	}
+	newC := func(unsignedBody bool) *s3.S3 {
+		c := s3.New(ses)
+		if opt.V2Auth || opt.Region == "other-v2-signature" {
+			fs.Debugf(nil, "Using v2 auth")
+			signer := func(req *request.Request) {
+				// Ignore AnonymousCredentials object
+				if req.Config.Credentials == credentials.AnonymousCredentials {
+					return
+				}
+				sign(v.AccessKeyID, v.SecretAccessKey, req.HTTPRequest)
 			}
-			sign(v.AccessKeyID, v.SecretAccessKey, req.HTTPRequest)
+			c.Handlers.Sign.Clear()
+			c.Handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
+			c.Handlers.Sign.PushBack(signer)
+		} else if unsignedBody {
+			// If the body is unsigned then tell the signer that we aren't signing the payload
+			c.Handlers.Sign.Clear()
+			c.Handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
+			c.Handlers.Sign.PushBackNamed(v4.BuildNamedHandler("v4.SignRequestHandler.WithUnsignedPayload", v4.WithUnsignedPayload))
 		}
-		c.Handlers.Sign.Clear()
-		c.Handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
-		c.Handlers.Sign.PushBack(signer)
+		return c
 	}
-	return c, ses, nil
+	return newC(false), newC(true), ses, nil
 }
 
 func checkUploadChunkSize(cs fs.SizeSuffix) error {
@@ -2167,6 +2249,13 @@ func setQuirks(opt *Options) {
 		listObjectsV2 = false
 		virtualHostStyle = false
 		urlEncodeListings = false
+	case "Cloudflare":
+		virtualHostStyle = false
+		useMultipartEtag = false // currently multipart Etags are random
+	case "ArvanCloud":
+		listObjectsV2 = false
+		virtualHostStyle = false
+		urlEncodeListings = false
 	case "DigitalOcean":
 		urlEncodeListings = false
 	case "Dreamhost":
@@ -2286,7 +2375,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 		opt.SSECustomerKeyMD5 = base64.StdEncoding.EncodeToString(md5sumBinary[:])
 	}
 	srv := getClient(ctx, opt)
-	c, ses, err := s3Connection(ctx, opt, srv)
+	c, cu, ses, err := s3Connection(ctx, opt, srv)
 	if err != nil {
 		return nil, err
 	}
@@ -2304,6 +2393,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 		ci:      ci,
 		ctx:     ctx,
 		c:       c,
+		cu:      cu,
 		ses:     ses,
 		pacer:   pc,
 		cache:   bucket.NewCache(),
@@ -2427,11 +2517,12 @@ func (f *Fs) updateRegionForBucket(ctx context.Context, bucket string) error {
 	// Make a new session with the new region
 	oldRegion := f.opt.Region
 	f.opt.Region = region
-	c, ses, err := s3Connection(f.ctx, &f.opt, f.srv)
+	c, cu, ses, err := s3Connection(f.ctx, &f.opt, f.srv)
 	if err != nil {
 		return fmt.Errorf("creating new session failed: %w", err)
 	}
 	f.c = c
+	f.cu = cu
 	f.ses = ses
 
 	fs.Logf(f, "Switched region to %q from %q", region, oldRegion)
@@ -3928,6 +4019,117 @@ func (o *Object) uploadMultipart(ctx context.Context, req *s3.PutObjectInput, si
 	return etag, nil
 }
 
+// unWrapAwsError unwraps AWS errors, looking for a non AWS error
+//
+// It returns true if one was found and the error, or false and the
+// error passed in.
+func unWrapAwsError(err error) (found bool, outErr error) {
+	if awsErr, ok := err.(awserr.Error); ok {
+		var origErrs []error
+		if batchErr, ok := awsErr.(awserr.BatchError); ok {
+			origErrs = batchErr.OrigErrs()
+		} else {
+			origErrs = []error{awsErr.OrigErr()}
+		}
+		for _, origErr := range origErrs {
+			found, newErr := unWrapAwsError(origErr)
+			if found {
+				return found, newErr
+			}
+		}
+		return false, err
+	}
+	return true, err
+}
+
+// Upload a single part using PutObject
+func (o *Object) uploadSinglepartPutObject(ctx context.Context, req *s3.PutObjectInput, size int64, in io.Reader) (etag string, lastModified time.Time, err error) {
+	req.Body = readers.NewFakeSeeker(in, size)
+	var resp *s3.PutObjectOutput
+	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
+		resp, err = o.fs.cu.PutObject(req)
+		return o.fs.shouldRetry(ctx, err)
+	})
+	if err != nil {
+		// Return the underlying error if we have a Serialization error if possible
+		//
+		// Serialization errors are synthesized locally in the SDK (not returned from the
+		// server). We'll get one if the SDK attempts a retry, however the FakeSeeker will
+		// remember the previous error from Read and return that.
+		if do, ok := err.(awserr.Error); ok && do.Code() == request.ErrCodeSerialization {
+			if found, newErr := unWrapAwsError(err); found {
+				err = newErr
+			}
+		}
+		return etag, lastModified, err
+	}
+	lastModified = time.Now()
+	etag = aws.StringValue(resp.ETag)
+	return etag, lastModified, nil
+}
+
+// Upload a single part using a presigned request
+func (o *Object) uploadSinglepartPresignedRequest(ctx context.Context, req *s3.PutObjectInput, size int64, in io.Reader) (etag string, lastModified time.Time, err error) {
+	// Create the request
+	putObj, _ := o.fs.c.PutObjectRequest(req)
+
+	// Sign it so we can upload using a presigned request.
+	//
+	// Note the SDK didn't used to support streaming to
+	// PutObject so we used this work-around.
+	url, headers, err := putObj.PresignRequest(15 * time.Minute)
+	if err != nil {
+		return etag, lastModified, fmt.Errorf("s3 upload: sign request: %w", err)
+	}
+
+	if o.fs.opt.V2Auth && headers == nil {
+		headers = putObj.HTTPRequest.Header
+	}
+
+	// Set request to nil if empty so as not to make chunked encoding
+	if size == 0 {
+		in = nil
+	}
+
+	// create the vanilla http request
+	httpReq, err := http.NewRequestWithContext(ctx, "PUT", url, in)
+	if err != nil {
+		return etag, lastModified, fmt.Errorf("s3 upload: new request: %w", err)
+	}
+
+	// set the headers we signed and the length
+	httpReq.Header = headers
+	httpReq.ContentLength = size
+
+	var resp *http.Response
+	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
+		var err error
+		resp, err = o.fs.srv.Do(httpReq)
+		if err != nil {
+			return o.fs.shouldRetry(ctx, err)
+		}
+		body, err := rest.ReadBody(resp)
+		if err != nil {
+			return o.fs.shouldRetry(ctx, err)
+		}
+		if resp.StatusCode >= 200 && resp.StatusCode < 299 {
+			return false, nil
+		}
+		err = fmt.Errorf("s3 upload: %s: %s", resp.Status, body)
+		return fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
+	})
+	if err != nil {
+		return etag, lastModified, err
+	}
+	if resp != nil {
+		if date, err := http.ParseTime(resp.Header.Get("Date")); err != nil {
+			lastModified = date
+		}
+		etag = resp.Header.Get("Etag")
+	}
+	return etag, lastModified, nil
+}
+
 // Update the Object from in with modTime and size
 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
 	bucket, bucketPath := o.split()
@@ -3979,6 +4181,9 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 		ContentType: &mimeType,
 		Metadata:    metadata,
 	}
+	if size >= 0 {
+		req.ContentLength = &size
+	}
 	if md5sumBase64 != "" {
 		req.ContentMD5 = &md5sumBase64
 	}
@@ -4033,66 +4238,21 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 		}
 	}
 
-	var resp *http.Response // response from PUT
-	var wantETag string     // Multipart upload Etag to check
+	var wantETag string        // Multipart upload Etag to check
+	var gotEtag string         // Etag we got from the upload
+	var lastModified time.Time // Time we got from the upload
 	if multipart {
 		wantETag, err = o.uploadMultipart(ctx, &req, size, in)
-		if err != nil {
-			return err
-		}
 	} else {
-
-		// Create the request
-		putObj, _ := o.fs.c.PutObjectRequest(&req)
-
-		// Sign it so we can upload using a presigned request.
-		//
-		// Note the SDK doesn't currently support streaming to
-		// PutObject so we'll use this work-around.
-		url, headers, err := putObj.PresignRequest(15 * time.Minute)
-		if err != nil {
-			return fmt.Errorf("s3 upload: sign request: %w", err)
-		}
-
-		if o.fs.opt.V2Auth && headers == nil {
-			headers = putObj.HTTPRequest.Header
-		}
-
-		// Set request to nil if empty so as not to make chunked encoding
-		if size == 0 {
-			in = nil
-		}
-
-		// create the vanilla http request
-		httpReq, err := http.NewRequestWithContext(ctx, "PUT", url, in)
-		if err != nil {
-			return fmt.Errorf("s3 upload: new request: %w", err)
-		}
-
-		// set the headers we signed and the length
-		httpReq.Header = headers
-		httpReq.ContentLength = size
-
-		err = o.fs.pacer.CallNoRetry(func() (bool, error) {
-			var err error
-			resp, err = o.fs.srv.Do(httpReq)
-			if err != nil {
-				return o.fs.shouldRetry(ctx, err)
-			}
-			body, err := rest.ReadBody(resp)
-			if err != nil {
-				return o.fs.shouldRetry(ctx, err)
-			}
-			if resp.StatusCode >= 200 && resp.StatusCode < 299 {
-				return false, nil
-			}
-			err = fmt.Errorf("s3 upload: %s: %s", resp.Status, body)
-			return fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
-		})
-		if err != nil {
-			return err
+		if o.fs.opt.UsePresignedRequest {
+			gotEtag, lastModified, err = o.uploadSinglepartPresignedRequest(ctx, &req, size, in)
+		} else {
+			gotEtag, lastModified, err = o.uploadSinglepartPutObject(ctx, &req, size, in)
 		}
 	}
+	if err != nil {
+		return err
+	}
 
 	// User requested we don't HEAD the object after uploading it
 	// so make up the object as best we can assuming it got
@@ -4105,11 +4265,11 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
 		o.mimeType = aws.StringValue(req.ContentType)
 		o.storageClass = aws.StringValue(req.StorageClass)
 		// If we have done a single part PUT request then we can read these
-		if resp != nil {
-			if date, err := http.ParseTime(resp.Header.Get("Date")); err == nil {
-				o.lastModified = date
-			}
-			o.setMD5FromEtag(resp.Header.Get("Etag"))
+		if gotEtag != "" {
+			o.setMD5FromEtag(gotEtag)
+		}
+		if !o.lastModified.IsZero() {
+			o.lastModified = lastModified
 		}
 		return nil
 	}
diff --git a/bin/cross-compile.go b/bin/cross-compile.go
index 2d10dec88..405f1b79c 100644
--- a/bin/cross-compile.go
+++ b/bin/cross-compile.go
@@ -265,9 +265,12 @@ func buildWindowsResourceSyso(goarch string, versionTag string) string {
 		"-o",
 		sysoPath,
 	}
-	if goarch == "amd64" {
+	if strings.Contains(goarch, "64") {
 		args = append(args, "-64") // Make the syso a 64-bit coff file
 	}
+	if strings.Contains(goarch, "arm") {
+		args = append(args, "-arm") // Make the syso an arm binary
+	}
 	args = append(args, jsonPath)
 	err = runEnv(args, nil)
 	if err != nil {
diff --git a/bin/make_manual.py b/bin/make_manual.py
index 474d49b9d..0afa5f2b5 100755
--- a/bin/make_manual.py
+++ b/bin/make_manual.py
@@ -48,6 +48,7 @@ docs = [
     "hdfs.md",
     "http.md",
     "hubic.md",
+    "internetarchive.md",
     "jottacloud.md",
     "koofr.md",
     "mailru.md",
diff --git a/cmd/cmount/fs.go b/cmd/cmount/fs.go
index 9e034da55..553712b16 100644
--- a/cmd/cmount/fs.go
+++ b/cmd/cmount/fs.go
@@ -1,7 +1,6 @@
-//go:build cmount && cgo && (linux || darwin || freebsd || windows)
+//go:build cmount && ((linux && cgo) || (darwin && cgo) || (freebsd && cgo) || windows)
 // +build cmount
-// +build cgo
-// +build linux darwin freebsd windows
+// +build linux,cgo darwin,cgo freebsd,cgo windows
 
 package cmount
 
@@ -13,12 +12,12 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/winfsp/cgofuse/fuse"
 	"github.com/rclone/rclone/cmd/mountlib"
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/fserrors"
 	"github.com/rclone/rclone/fs/log"
 	"github.com/rclone/rclone/vfs"
+	"github.com/winfsp/cgofuse/fuse"
 )
 
 const fhUnset = ^uint64(0)
diff --git a/cmd/cmount/mount.go b/cmd/cmount/mount.go
index df92b792b..873adcaf0 100644
--- a/cmd/cmount/mount.go
+++ b/cmd/cmount/mount.go
@@ -2,10 +2,9 @@
 //
 // This uses the cgo based cgofuse library
 
-//go:build cmount && cgo && (linux || darwin || freebsd || windows)
+//go:build cmount && ((linux && cgo) || (darwin && cgo) || (freebsd && cgo) || windows)
 // +build cmount
-// +build cgo
-// +build linux darwin freebsd windows
+// +build linux,cgo darwin,cgo freebsd,cgo windows
 
 package cmount
 
@@ -18,12 +17,12 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/winfsp/cgofuse/fuse"
 	"github.com/rclone/rclone/cmd/mountlib"
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/lib/atexit"
 	"github.com/rclone/rclone/lib/buildinfo"
 	"github.com/rclone/rclone/vfs"
+	"github.com/winfsp/cgofuse/fuse"
 )
 
 func init() {
diff --git a/cmd/cmount/mount_unsupported.go b/cmd/cmount/mount_unsupported.go
index 74e441804..3b6761beb 100644
--- a/cmd/cmount/mount_unsupported.go
+++ b/cmd/cmount/mount_unsupported.go
@@ -1,7 +1,10 @@
 // Build for cmount for unsupported platforms to stop go complaining
 // about "no buildable Go source files "
 
-//go:build (!linux && !darwin && !freebsd && !windows) || !brew || !cgo || !cmount
-// +build !linux,!darwin,!freebsd,!windows !brew !cgo !cmount
+//go:build !((linux && cgo && cmount) || (darwin && cgo && cmount) || (freebsd && cgo && cmount) || (windows && cmount))
+// +build !linux !cgo !cmount
+// +build !darwin !cgo !cmount
+// +build !freebsd !cgo !cmount
+// +build !windows !cmount
 
 package cmount
diff --git a/cmd/cmount/mountpoint_windows.go b/cmd/cmount/mountpoint_windows.go
index 8a06c88b4..c58dbc0f3 100644
--- a/cmd/cmount/mountpoint_windows.go
+++ b/cmd/cmount/mountpoint_windows.go
@@ -1,5 +1,5 @@
-//go:build cmount && cgo && windows
-// +build cmount,cgo,windows
+//go:build cmount && windows
+// +build cmount,windows
 
 package cmount
 
diff --git a/docs/content/_index.md b/docs/content/_index.md
index 083ad49c2..167c0749d 100644
--- a/docs/content/_index.md
+++ b/docs/content/_index.md
@@ -113,8 +113,10 @@ WebDAV or S3, that work out of the box.)
 {{< provider name="Box" home="https://www.box.com/" config="/box/" >}}
 {{< provider name="Ceph" home="http://ceph.com/" config="/s3/#ceph" >}}
 {{< provider name="China Mobile Ecloud Elastic Object Storage (EOS)" home="https://ecloud.10086.cn/home/product-introduction/eos/" config="/s3/#china-mobile-ecloud-eos" >}}
+{{< provider name="Arvan Cloud Object Storage (AOS)" home="https://www.arvancloud.com/en/products/cloud-storage" config="/s3/#arvan-cloud-object-storage-aos" >}}
 {{< provider name="Citrix ShareFile" home="http://sharefile.com/" config="/sharefile/" >}}
 {{< provider name="C14" home="https://www.online.net/en/storage/c14-cold-storage" config="/s3/#scaleway" >}}
+{{< provider name="Cloudflare R2" home="https://blog.cloudflare.com/r2-open-beta/" config="/s3/#cloudflare-r2" >}}
 {{< provider name="DigitalOcean Spaces" home="https://www.digitalocean.com/products/object-storage/" config="/s3/#digitalocean-spaces" >}}
 {{< provider name="Digi Storage" home="https://storage.rcs-rds.ro/" config="/koofr/#digi-storage" >}}
 {{< provider name="Dreamhost" home="https://www.dreamhost.com/cloud/storage/" config="/s3/#dreamhost" >}}
@@ -127,6 +129,7 @@ WebDAV or S3, that work out of the box.)
 {{< provider name="HDFS" home="https://hadoop.apache.org/" config="/hdfs/" >}}
 {{< provider name="HTTP" home="https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol" config="/http/" >}}
 {{< provider name="Hubic" home="https://hubic.com/" config="/hubic/" >}}
+{{< provider name="Internet Archive" home="https://archive.org/" config="/internetarchive/" >}}
 {{< provider name="Jottacloud" home="https://www.jottacloud.com/en/" config="/jottacloud/" >}}
 {{< provider name="IBM COS S3" home="http://www.ibm.com/cloud/object-storage" config="/s3/#ibm-cos-s3" >}}
 {{< provider name="Koofr" home="https://koofr.eu/" config="/koofr/" >}}
diff --git a/docs/content/authors.md b/docs/content/authors.md
index 8a0e1f44a..e41176809 100644
--- a/docs/content/authors.md
+++ b/docs/content/authors.md
@@ -583,3 +583,13 @@ put them back in again.` >}}
   * rafma0 <int.main@gmail.com>
   * Adrien Rey-Jarthon <jobs@adrienjarthon.com>
   * Nick Gooding <73336146+nickgooding@users.noreply.github.com>
+  * Leroy van Logchem <lr.vanlogchem@gmail.com>
+  * Zsolt Ero <zsolt.ero@gmail.com>
+  * Lesmiscore <nao20010128@gmail.com>
+  * ehsantdy <ehsan.tadayon@arvancloud.com>
+  * SwazRGB <65694696+swazrgb@users.noreply.github.com>
+  * Mateusz Puczyński <mati6095@gmail.com>
+  * Michael C Tiernan - MIT-Research Computing Project <mtiernan@mit.edu>
+  * Kaspian <34658474+KaspianDev@users.noreply.github.com>
+  * Werner <EvilOlaf@users.noreply.github.com>
+  * Hugal31 <hugo.laloge@gmail.com>
diff --git a/docs/content/b2.md b/docs/content/b2.md
index b2d44d2ad..04f3375b7 100644
--- a/docs/content/b2.md
+++ b/docs/content/b2.md
@@ -173,6 +173,11 @@ the file instead of hiding it.
 Old versions of files, where available, are visible using the 
 `--b2-versions` flag.
 
+It is also possible to view a bucket as it was at a certain point in time,
+using the `--b2-version-at` flag. This will show the file versions as they
+were at that time, showing files that have been deleted afterwards, and
+hiding files that were created since.
+
 If you wish to remove all the old versions then you can use the
 `rclone cleanup remote:bucket` command which will delete all the old
 versions of files, leaving the current ones intact.  You can also
diff --git a/docs/content/bisync.md b/docs/content/bisync.md
index 7107e3253..8ab466527 100644
--- a/docs/content/bisync.md
+++ b/docs/content/bisync.md
@@ -700,7 +700,7 @@ consider using the flag
 
 Google docs exist as virtual files on Google Drive and cannot be transferred
 to other filesystems natively. While it is possible to export a Google doc to
-a normal file (with `.xlsx` extension, for example), it's not possible
+a normal file (with `.xlsx` extension, for example), it is not possible
 to import a normal file back into a Google document.
 
 Bisync's handling of Google Doc files is to flag them in the run log output
diff --git a/docs/content/changelog.md b/docs/content/changelog.md
index e983dddff..fab1db463 100644
--- a/docs/content/changelog.md
+++ b/docs/content/changelog.md
@@ -5,6 +5,48 @@ description: "Rclone Changelog"
 
 # Changelog
 
+## v1.58.1 - 2022-04-29
+
+[See commits](https://github.com/rclone/rclone/compare/v1.58.0...v1.58.1)
+
+* Bug Fixes
+    * build: Update github.com/billziss-gh to github.com/winfsp (Nick Craig-Wood)
+    * filter: Fix timezone of `--min-age`/`-max-age` from UTC to local as documented (Nick Craig-Wood)
+    * rc/js: Correct RC method names (Sơn Trần-Nguyễn)
+    * docs
+        * Fix some links to command pages (albertony)
+        * Add `--multi-thread-streams` note to `--transfers`. (Zsolt Ero)
+* Mount
+    * Fix `--devname` and fusermount: unknown option 'fsname' when mounting via rc (Nick Craig-Wood)
+* VFS
+    * Remove wording which suggests VFS is only for mounting (Nick Craig-Wood)
+* Dropbox
+    * Fix retries of multipart uploads with incorrect_offset error (Nick Craig-Wood)
+* Google Cloud Storage
+    * Use the s3 pacer to speed up transactions (Nick Craig-Wood)
+    * pacer: Default the Google pacer to a burst of 100 to fix gcs pacing (Nick Craig-Wood)
+* Jottacloud
+    * Fix scope in token request (albertony)
+* Netstorage
+    * Fix unescaped HTML in documentation (Nick Craig-Wood)
+    * Make levels of headings consistent (Nick Craig-Wood)
+    * Add support contacts to netstorage doc (Nil Alexandrov)
+* Onedrive
+    * Note that sharepoint also changes web files (.html, .aspx) (GH)
+* Putio
+    * Handle rate limit errors (Berkan Teber)
+    * Fix multithread download and other ranged requests (rafma0)
+* S3
+    * Add ChinaMobile EOS to provider list (GuoXingbin)
+    * Sync providers in config description with providers (Nick Craig-Wood)
+* SFTP
+    * Fix OpenSSH 8.8+ RSA keys incompatibility (KARBOWSKI Piotr)
+    * Note that Scaleway C14 is deprecating SFTP in favor of S3 (Adrien Rey-Jarthon)
+* Storj
+    * Fix bucket creation on Move (Nick Craig-Wood)
+* WebDAV
+    * Don't override Referer if user sets it (Nick Craig-Wood)
+
 ## v1.58.0 - 2022-03-18
 
 [See commits](https://github.com/rclone/rclone/compare/v1.57.0...v1.58.0)
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 6d3d29b62..8a072e67a 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -50,6 +50,7 @@ See the following for detailed instructions for
   * [HDFS](/hdfs/)
   * [HTTP](/http/)
   * [Hubic](/hubic/)
+  * [Internet Archive](/internetarchive/)
   * [Jottacloud](/jottacloud/)
   * [Koofr](/koofr/)
   * [Mail.ru Cloud](/mailru/)
@@ -1793,6 +1794,8 @@ of timeouts or bigger if you have lots of bandwidth and a fast remote.
 
 The default is to run 4 file transfers in parallel.
 
+Look at --multi-thread-streams if you would like to control single file transfers.
+
 ### -u, --update ###
 
 This forces rclone to skip any files which exist on the destination
diff --git a/docs/content/faq.md b/docs/content/faq.md
index f6f31a1b6..ecd240882 100644
--- a/docs/content/faq.md
+++ b/docs/content/faq.md
@@ -31,7 +31,7 @@ the node running rclone would need to have lots of bandwidth.
 
 The syncs would be incremental (on a file by file basis).
 
-Eg
+e.g.
 
     rclone sync -i drive:Folder s3:bucket
 
diff --git a/docs/content/install.md b/docs/content/install.md
index 52e63f534..eccea9543 100644
--- a/docs/content/install.md
+++ b/docs/content/install.md
@@ -26,11 +26,11 @@ using the [rclone selfupdate](/commands/rclone_selfupdate/) command.
 
 To install rclone on Linux/macOS/BSD systems, run:
 
-    curl https://rclone.org/install.sh | sudo bash
+    sudo -v ; curl https://rclone.org/install.sh | sudo bash
 
 For beta installation, run:
 
-    curl https://rclone.org/install.sh | sudo bash -s beta
+    sudo -v ; curl https://rclone.org/install.sh | sudo bash -s beta
 
 Note that this script checks the version of rclone installed first and
 won't re-download if not needed.
diff --git a/docs/content/install.sh b/docs/content/install.sh
index 3294c1563..8a8242fee 100755
--- a/docs/content/install.sh
+++ b/docs/content/install.sh
@@ -12,7 +12,7 @@ set -e
 #when adding a tool to the list make sure to also add its corresponding command further in the script
 unzip_tools_list=('unzip' '7z' 'busybox')
 
-usage() { echo "Usage: curl https://rclone.org/install.sh | sudo bash [-s beta]" 1>&2; exit 1; }
+usage() { echo "Usage: sudo -v ; curl https://rclone.org/install.sh | sudo bash [-s beta]" 1>&2; exit 1; }
 
 #check for beta flag
 if [ -n "$1" ] && [ "$1" != "beta" ]; then
@@ -81,6 +81,8 @@ case $OS in
     ;;  
   Darwin)
     OS='osx'
+    binTgtDir=/usr/local/bin
+    man1TgtDir=/usr/local/share/man/man1
     ;;
   SunOS)
     OS='solaris'
@@ -171,12 +173,14 @@ case "$OS" in
     ;;
   'osx')
     #binary
-    mkdir -p /usr/local/bin
-    cp rclone /usr/local/bin/rclone.new
-    mv /usr/local/bin/rclone.new /usr/local/bin/rclone
+    mkdir -m 0555 -p ${binTgtDir}
+    cp rclone ${binTgtDir}/rclone.new
+    mv ${binTgtDir}/rclone.new ${binTgtDir}/rclone
+    chmod a=x ${binTgtDir}/rclone
     #manual
-    mkdir -p /usr/local/share/man/man1
-    cp rclone.1 /usr/local/share/man/man1/    
+    mkdir -m 0555 -p ${man1TgtDir}
+    cp rclone.1 ${man1TgtDir}    
+    chmod a=r ${man1TgtDir}/rclone.1
     ;;
   *)
     echo 'OS not supported'
diff --git a/docs/content/internetarchive.md b/docs/content/internetarchive.md
new file mode 100644
index 000000000..d0e937fdf
--- /dev/null
+++ b/docs/content/internetarchive.md
@@ -0,0 +1,222 @@
+---
+title: "Internet Archive"
+description: "Rclone docs for Internet Archive"
+---
+
+# {{< icon "fa fa-archive" >}} Internet Archive
+
+The Internet Archive backend utilizes Items on [archive.org](https://archive.org/)
+
+Refer to [IAS3 API documentation](https://archive.org/services/docs/api/ias3.html) for the API this backend uses.
+
+Paths are specified as `remote:bucket` (or `remote:` for the `lsd`
+command.)  You may put subdirectories in too, e.g. `remote:item/path/to/dir`.
+
+Once you have made a remote (see the provider specific section above)
+you can use it like this:
+
+Unlike S3, listing up all items uploaded by you isn't supported.
+
+Make a new item
+
+    rclone mkdir remote:item
+
+List the contents of a item
+
+    rclone ls remote:item
+
+Sync `/home/local/directory` to the remote item, deleting any excess
+files in the item.
+
+    rclone sync -i /home/local/directory remote:item
+
+## Notes
+Because of Internet Archive's architecture, it enqueues write operations (and extra post-processings) in a per-item queue. You can check item's queue at https://catalogd.archive.org/history/item-name-here . Because of that, all uploads/deletes will not show up immediately and takes some time to be available.
+The per-item queue is enqueued to an another queue, Item Deriver Queue. [You can check the status of Item Deriver Queue here.](https://catalogd.archive.org/catalog.php?whereami=1) This queue has a limit, and it may block you from uploading, or even deleting. You should avoid uploading a lot of small files for better behavior.
+
+You can optionally wait for the server's processing to finish, by setting non-zero value to `wait_archive` key.
+By making it wait, rclone can do normal file comparison.
+Make sure to set a large enough value (e.g. `30m0s` for smaller files) as it can take a long time depending on server's queue.
+
+## Configuration
+
+Here is an example of making an internetarchive configuration.
+Most applies to the other providers as well, any differences are described [below](#providers).
+
+First run
+
+    rclone config
+
+This will guide you through an interactive setup process.
+
+```
+No remotes found, make a new one?
+n) New remote
+s) Set configuration password
+q) Quit config
+n/s/q> n
+name> remote
+Option Storage.
+Type of storage to configure.
+Choose a number from below, or type in your own value.
+XX / InternetArchive Items
+   \ (internetarchive)
+Storage> internetarchive
+Option access_key_id.
+IAS3 Access Key.
+Leave blank for anonymous access.
+You can find one here: https://archive.org/account/s3.php
+Enter a value. Press Enter to leave empty.
+access_key_id> XXXX
+Option secret_access_key.
+IAS3 Secret Key (password).
+Leave blank for anonymous access.
+Enter a value. Press Enter to leave empty.
+secret_access_key> XXXX
+Edit advanced config?
+y) Yes
+n) No (default)
+y/n> y
+Option endpoint.
+IAS3 Endpoint.
+Leave blank for default value.
+Enter a string value. Press Enter for the default (https://s3.us.archive.org).
+endpoint> 
+Option front_endpoint.
+Host of InternetArchive Frontend.
+Leave blank for default value.
+Enter a string value. Press Enter for the default (https://archive.org).
+front_endpoint> 
+Option disable_checksum.
+Don't store MD5 checksum with object metadata.
+Normally rclone will calculate the MD5 checksum of the input before
+uploading it so it can ask the server to check the object against checksum.
+This is great for data integrity checking but can cause long delays for
+large files to start uploading.
+Enter a boolean value (true or false). Press Enter for the default (true).
+disable_checksum> true
+Option encoding.
+The encoding for the backend.
+See the [encoding section in the overview](/overview/#encoding) for more info.
+Enter a encoder.MultiEncoder value. Press Enter for the default (Slash,Question,Hash,Percent,Del,Ctl,InvalidUtf8,Dot).
+encoding> 
+Edit advanced config?
+y) Yes
+n) No (default)
+y/n> n
+--------------------
+[remote]
+type = internetarchive
+access_key_id = XXXX
+secret_access_key = XXXX
+--------------------
+y) Yes this is OK (default)
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+```
+
+{{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/internetarchive/internetarchive.go then run make backenddocs" >}}
+### Standard options
+
+Here are the standard options specific to internetarchive (Internet Archive).
+
+#### --internetarchive-access-key-id
+
+IAS3 Access Key.
+
+Leave blank for anonymous access.
+You can find one here: https://archive.org/account/s3.php
+
+Properties:
+
+- Config:      access_key_id
+- Env Var:     RCLONE_INTERNETARCHIVE_ACCESS_KEY_ID
+- Type:        string
+- Required:    false
+
+#### --internetarchive-secret-access-key
+
+IAS3 Secret Key (password).
+
+Leave blank for anonymous access.
+
+Properties:
+
+- Config:      secret_access_key
+- Env Var:     RCLONE_INTERNETARCHIVE_SECRET_ACCESS_KEY
+- Type:        string
+- Required:    false
+
+### Advanced options
+
+Here are the advanced options specific to internetarchive (Internet Archive).
+
+#### --internetarchive-endpoint
+
+IAS3 Endpoint.
+
+Leave blank for default value.
+
+Properties:
+
+- Config:      endpoint
+- Env Var:     RCLONE_INTERNETARCHIVE_ENDPOINT
+- Type:        string
+- Default:     "https://s3.us.archive.org"
+
+#### --internetarchive-front-endpoint
+
+Host of InternetArchive Frontend.
+
+Leave blank for default value.
+
+Properties:
+
+- Config:      front_endpoint
+- Env Var:     RCLONE_INTERNETARCHIVE_FRONT_ENDPOINT
+- Type:        string
+- Default:     "https://archive.org"
+
+#### --internetarchive-disable-checksum
+
+Don't ask the server to test against MD5 checksum calculated by rclone.
+Normally rclone will calculate the MD5 checksum of the input before
+uploading it so it can ask the server to check the object against checksum.
+This is great for data integrity checking but can cause long delays for
+large files to start uploading.
+
+Properties:
+
+- Config:      disable_checksum
+- Env Var:     RCLONE_INTERNETARCHIVE_DISABLE_CHECKSUM
+- Type:        bool
+- Default:     true
+
+#### --internetarchive-wait-archive
+
+Timeout for waiting the server's processing tasks (specifically archive and book_op) to finish.
+Only enable if you need to be guaranteed to be reflected after write operations.
+0 to disable waiting. No errors to be thrown in case of timeout.
+
+Properties:
+
+- Config:      wait_archive
+- Env Var:     RCLONE_INTERNETARCHIVE_WAIT_ARCHIVE
+- Type:        Duration
+- Default:     0s
+
+#### --internetarchive-encoding
+
+The encoding for the backend.
+
+See the [encoding section in the overview](/overview/#encoding) for more info.
+
+Properties:
+
+- Config:      encoding
+- Env Var:     RCLONE_INTERNETARCHIVE_ENCODING
+- Type:        MultiEncoder
+- Default:     Slash,LtGt,CrLf,Del,Ctl,InvalidUtf8,Dot
+
+{{< rem autogenerated options stop >}}
diff --git a/docs/content/overview.md b/docs/content/overview.md
index c86b791b8..73974b65b 100644
--- a/docs/content/overview.md
+++ b/docs/content/overview.md
@@ -32,6 +32,7 @@ Here is an overview of the major features of each cloud storage system.
 | HDFS                         | -           | Yes     | No               | No              | -         |
 | HTTP                         | -           | No      | No               | No              | R         |
 | Hubic                        | MD5         | Yes     | No               | No              | R/W       |
+| Internet Archive             | MD5, SHA1, CRC32 | Yes | No              | No              | -         |
 | Jottacloud                   | MD5         | Yes     | Yes              | No              | R         |
 | Koofr                        | MD5         | No      | Yes              | No              | -         |
 | Mail.ru Cloud                | Mailru ⁶    | Yes     | Yes              | No              | -         |
@@ -427,6 +428,7 @@ upon backend-specific capabilities.
 | HDFS                         | Yes   | No   | Yes  | Yes     | No      | No    | Yes          | No           | Yes   | Yes      |
 | HTTP                         | No    | No   | No   | No      | No      | No    | No           | No           | No    | Yes      |
 | Hubic                        | Yes † | Yes  | No   | No      | No      | Yes   | Yes          | No           | Yes   | No       |
+| Internet Archive             | No    | Yes  | No   | No      | Yes     | Yes   | No           | Yes          | Yes   | No       |
 | Jottacloud                   | Yes   | Yes  | Yes  | Yes     | Yes     | Yes   | No           | Yes          | Yes   | Yes      |
 | Mail.ru Cloud                | Yes   | Yes  | Yes  | Yes     | Yes     | No    | No           | Yes          | Yes   | Yes      |
 | Mega                         | Yes   | No   | Yes  | Yes     | Yes     | No    | No           | Yes          | Yes   | Yes      |
diff --git a/docs/content/s3.md b/docs/content/s3.md
index 400bb3059..aaf76804d 100644
--- a/docs/content/s3.md
+++ b/docs/content/s3.md
@@ -12,6 +12,8 @@ The S3 backend can be used with a number of different providers:
 {{< provider name="Alibaba Cloud (Aliyun) Object Storage System (OSS)" home="https://www.alibabacloud.com/product/oss/" config="/s3/#alibaba-oss" >}}
 {{< provider name="Ceph" home="http://ceph.com/" config="/s3/#ceph" >}}
 {{< provider name="China Mobile Ecloud Elastic Object Storage (EOS)" home="https://ecloud.10086.cn/home/product-introduction/eos/" config="/s3/#china-mobile-ecloud-eos" >}}
+{{< provider name="Cloudflare R2" home="https://blog.cloudflare.com/r2-open-beta/" config="/s3/#cloudflare-r2" >}}
+{{< provider name="Arvan Cloud Object Storage (AOS)" home="https://www.arvancloud.com/en/products/cloud-storage" config="/s3/#arvan-cloud-object-storage-aos" >}}
 {{< provider name="DigitalOcean Spaces" home="https://www.digitalocean.com/products/object-storage/" config="/s3/#digitalocean-spaces" >}}
 {{< provider name="Dreamhost" home="https://www.dreamhost.com/cloud/storage/" config="/s3/#dreamhost" >}}
 {{< provider name="IBM COS S3" home="http://www.ibm.com/cloud/object-storage" config="/s3/#ibm-cos-s3" >}}
@@ -70,7 +72,7 @@ name> remote
 Type of storage to configure.
 Choose a number from below, or type in your own value
 [snip]
-XX / Amazon S3 Compliant Storage Providers including AWS, Ceph, ChinaMobile, Dreamhost, IBM COS, Minio, and Tencent COS
+XX / Amazon S3 Compliant Storage Providers including AWS, Ceph, ChinaMobile, ArvanCloud, Dreamhost, IBM COS, Minio, and Tencent COS
    \ "s3"
 [snip]
 Storage> s3
@@ -567,7 +569,7 @@ A simple solution is to set the `--s3-upload-cutoff 0` and force all the files t
 {{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/s3/s3.go then run make backenddocs" >}}
 ### Standard options
 
-Here are the standard options specific to s3 (Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, RackCorp, SeaweedFS, and Tencent COS).
+Here are the standard options specific to s3 (Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, RackCorp, SeaweedFS, and Tencent COS).
 
 #### --s3-provider
 
@@ -588,6 +590,8 @@ Properties:
         - Ceph Object Storage
     - "ChinaMobile"
         - China Mobile Ecloud Elastic Object Storage (EOS)
+    - "ArvanCloud"
+        - Arvan Cloud Object Storage (AOS)
     - "DigitalOcean"
         - Digital Ocean Spaces
     - "Dreamhost"
@@ -829,7 +833,7 @@ Properties:
 
 - Config:      region
 - Env Var:     RCLONE_S3_REGION
-- Provider:    !AWS,Alibaba,ChinaMobile,RackCorp,Scaleway,Storj,TencentCOS
+- Provider:    !AWS,Alibaba,ChinaMobile,ArvanCloud,RackCorp,Scaleway,Storj,TencentCOS
 - Type:        string
 - Required:    false
 - Examples:
@@ -926,6 +930,24 @@ Endpoint for China Mobile Ecloud Elastic Object Storage (EOS) API.
     - "eos-anhui-1.cmecloud.cn"
         - Anhui China (Huainan)
 
+#### --s3-endpoint
+
+Endpoint for Arvan Cloud Object Storage (AOS) API.
+
+- Config:      endpoint
+- Env Var:     RCLONE_S3_ENDPOINT
+- Provider:    ArvanCloud
+- Type:        string
+- Required:    false
+- Examples:
+    - "s3.ir-thr-at1.arvanstorage.com"
+        - The default endpoint - a good choice if you are unsure.
+        - Tehran Iran (Asiatech)
+    - "s3.ir-tbz-sh1.arvanstorage.com"
+        - Tabriz Iran (Shahriar)
+
+
+
 #### --s3-endpoint
 
 Endpoint for IBM COS S3 API.
@@ -1295,7 +1317,7 @@ Properties:
 
 - Config:      endpoint
 - Env Var:     RCLONE_S3_ENDPOINT
-- Provider:    !AWS,IBMCOS,TencentCOS,Alibaba,ChinaMobile,Scaleway,StackPath,Storj,RackCorp
+- Provider:    !AWS,IBMCOS,TencentCOS,Alibaba,ChinaMobile,ArvanCloud,Scaleway,StackPath,Storj,RackCorp
 - Type:        string
 - Required:    false
 - Examples:
@@ -1325,6 +1347,8 @@ Properties:
         - Wasabi AP Northeast 1 (Tokyo) endpoint
     - "s3.ap-northeast-2.wasabisys.com"
         - Wasabi AP Northeast 2 (Osaka) endpoint
+    - "s3.ir-thr-at1.arvanstorage.com"
+        - ArvanCloud Tehran Iran (Asiatech) endpoint
 
 #### --s3-location-constraint
 
@@ -1532,7 +1556,7 @@ Properties:
 
 - Config:      location_constraint
 - Env Var:     RCLONE_S3_LOCATION_CONSTRAINT
-- Provider:    !AWS,IBMCOS,Alibaba,ChinaMobile,RackCorp,Scaleway,StackPath,Storj,TencentCOS
+- Provider:    !AWS,IBMCOS,Alibaba,ChinaMobile,ArvanCloud,RackCorp,Scaleway,StackPath,Storj,TencentCOS
 - Type:        string
 - Required:    false
 
@@ -1604,7 +1628,7 @@ Properties:
 
 - Config:      server_side_encryption
 - Env Var:     RCLONE_S3_SERVER_SIDE_ENCRYPTION
-- Provider:    AWS,Ceph,ChinaMobile,Minio
+- Provider:    AWS,Ceph,ChinaMobile,ArvanCloud,Minio
 - Type:        string
 - Required:    false
 - Examples:
@@ -1705,6 +1729,19 @@ The storage class to use when storing new objects in ChinaMobile.
 
 #### --s3-storage-class
 
+The storage class to use when storing new objects in ArvanCloud.
+
+- Config:      storage_class
+- Env Var:     RCLONE_S3_STORAGE_CLASS
+- Provider:    ArvanCloud
+- Type:        string
+- Required:    false
+- Examples:
+    - "STANDARD"
+        - Standard storage class
+
+#### --s3-storage-class
+
 The storage class to use when storing new objects in Tencent COS.
 
 Properties:
@@ -1747,7 +1784,7 @@ Properties:
 
 ### Advanced options
 
-Here are the advanced options specific to s3 (Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, RackCorp, SeaweedFS, and Tencent COS).
+Here are the advanced options specific to s3 (Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, RackCorp, SeaweedFS, and Tencent COS).
 
 #### --s3-bucket-acl
 
@@ -1799,7 +1836,7 @@ Properties:
 
 - Config:      sse_customer_algorithm
 - Env Var:     RCLONE_S3_SSE_CUSTOMER_ALGORITHM
-- Provider:    AWS,Ceph,ChinaMobile,Minio
+- Provider:    AWS,Ceph,ChinaMobile,ArvanCloud,Minio
 - Type:        string
 - Required:    false
 - Examples:
@@ -1816,7 +1853,7 @@ Properties:
 
 - Config:      sse_customer_key
 - Env Var:     RCLONE_S3_SSE_CUSTOMER_KEY
-- Provider:    AWS,Ceph,ChinaMobile,Minio
+- Provider:    AWS,Ceph,ChinaMobile,ArvanCloud,Minio
 - Type:        string
 - Required:    false
 - Examples:
@@ -1834,7 +1871,7 @@ Properties:
 
 - Config:      sse_customer_key_md5
 - Env Var:     RCLONE_S3_SSE_CUSTOMER_KEY_MD5
-- Provider:    AWS,Ceph,ChinaMobile,Minio
+- Provider:    AWS,Ceph,ChinaMobile,ArvanCloud,Minio
 - Type:        string
 - Required:    false
 - Examples:
@@ -2443,12 +2480,19 @@ You will be able to list and copy data but not upload it.
 This is the provider used as main example and described in the [configuration](#configuration) section above.
 
 ### AWS Snowball Edge
-[AWS Snowball](https://aws.amazon.com/snowball/) is a hardware appliance used for transferring
-bulk data back to AWS. Its main software interface is S3 object storage.
 
-To use rclone with AWS Snowball Edge devices, configure as standard for an 'S3 Compatible Service'
-be sure to set `upload_cutoff = 0` otherwise you will run into authentication header issues as
-the snowball device does not support query parameter based authentication.
+[AWS Snowball](https://aws.amazon.com/snowball/) is a hardware
+appliance used for transferring bulk data back to AWS. Its main
+software interface is S3 object storage.
+
+To use rclone with AWS Snowball Edge devices, configure as standard
+for an 'S3 Compatible Service'.
+
+If using rclone pre v1.59 be sure to set `upload_cutoff = 0` otherwise
+you will run into authentication header issues as the snowball device
+does not support query parameter based authentication.
+
+With rclone v1.59 or later setting `upload_cutoff` should not be necessary.
 
 eg.
 ```
@@ -2487,10 +2531,11 @@ server_side_encryption =
 storage_class =
 ```
 
-If you are using an older version of CEPH, e.g. 10.2.x Jewel, then you
-may need to supply the parameter `--s3-upload-cutoff 0` or put this in
-the config file as `upload_cutoff 0` to work around a bug which causes
-uploading of small files to fail.
+If you are using an older version of CEPH (e.g. 10.2.x Jewel) and a
+version of rclone before v1.59 then you may need to supply the
+parameter `--s3-upload-cutoff 0` or put this in the config file as
+`upload_cutoff 0` to work around a bug which causes uploading of small
+files to fail.
 
 Note also that Ceph sometimes puts `/` in the passwords it gives
 users.  If you read the secret access key using the command line tools
@@ -2517,6 +2562,115 @@ removed).
 Because this is a json dump, it is encoding the `/` as `\/`, so if you
 use the secret key as `xxxxxx/xxxx`  it will work fine.
 
+### Cloudflare R2 {#cloudflare-r2}
+
+[Cloudflare R2](https://blog.cloudflare.com/r2-open-beta/) Storage
+allows developers to store large amounts of unstructured data without
+the costly egress bandwidth fees associated with typical cloud storage
+services.
+
+Here is an example of making a Cloudflare R2 configuration. First run:
+
+    rclone config
+
+This will guide you through an interactive setup process.
+
+```
+No remotes found, make a new one?
+n) New remote
+s) Set configuration password
+q) Quit config
+n/s/q> n
+name> r2
+Option Storage.
+Type of storage to configure.
+Choose a number from below, or type in your own value.
+...
+XX / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, China Mobile, Cloudflare, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, Netease, RackCorp, Scaleway, SeaweedFS, StackPath, Storj, Tencent COS and Wasabi
+   \ (s3)
+...
+Storage> s3
+Option provider.
+Choose your S3 provider.
+Choose a number from below, or type in your own value.
+Press Enter to leave empty.
+...
+XX / Cloudflare R2 Storage
+   \ (Cloudflare)
+...
+provider> Cloudflare
+Option env_auth.
+Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars).
+Only applies if access_key_id and secret_access_key is blank.
+Choose a number from below, or type in your own boolean value (true or false).
+Press Enter for the default (false).
+ 1 / Enter AWS credentials in the next step.
+   \ (false)
+ 2 / Get AWS credentials from the environment (env vars or IAM).
+   \ (true)
+env_auth> 1
+Option access_key_id.
+AWS Access Key ID.
+Leave blank for anonymous access or runtime credentials.
+Enter a value. Press Enter to leave empty.
+access_key_id> ACCESS_KEY
+Option secret_access_key.
+AWS Secret Access Key (password).
+Leave blank for anonymous access or runtime credentials.
+Enter a value. Press Enter to leave empty.
+secret_access_key> SECRET_ACCESS_KEY
+Option region.
+Region to connect to.
+Choose a number from below, or type in your own value.
+Press Enter to leave empty.
+ 1 / R2 buckets are automatically distributed across Cloudflare's data centers for low latency.
+   \ (auto)
+region> 1
+Option endpoint.
+Endpoint for S3 API.
+Required when using an S3 clone.
+Enter a value. Press Enter to leave empty.
+endpoint> https://ACCOUNT_ID.r2.cloudflarestorage.com
+Option acl.
+Canned ACL used when creating buckets and storing or copying objects.
+This ACL is used for creating objects and if bucket_acl isn't set, for creating buckets too.
+For more info visit https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
+Note that this ACL is applied when server-side copying objects as S3
+doesn't copy the ACL from the source but rather writes a fresh one.
+Choose a number from below, or type in your own value.
+Press Enter to leave empty.
+   / Owner gets FULL_CONTROL.
+ 1 | No one else has access rights (default).
+   \ (private)
+...
+acl> 1
+Edit advanced config?
+y) Yes
+n) No (default)
+y/n> n
+--------------------
+y) Yes this is OK (default)
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+```
+
+This will leave your config looking something like:
+
+```
+[r2]
+type = s3
+provider = Cloudflare
+access_key_id = ACCESS_KEY
+secret_access_key = SECRET_ACCESS_KEY
+region = auto
+endpoint = https://ACCOUNT_ID.r2.cloudflarestorage.com
+acl = private
+```
+
+Now run `rclone lsf r2:` to see your buckets and `rclone lsf
+r2:bucket` to look within a bucket.
+
 ### Dreamhost
 
 Dreamhost [DreamObjects](https://www.dreamhost.com/cloud/storage/) is
@@ -2615,7 +2769,7 @@ Choose a number from below, or type in your own value
    	\ "alias"
  	2 / Amazon Drive
    	\ "amazon cloud drive"
- 	3 / Amazon S3 Complaint Storage Providers (Dreamhost, Ceph, ChinaMobile, Minio, IBM COS)
+ 	3 / Amazon S3 Complaint Storage Providers (Dreamhost, Ceph, ChinaMobile, ArvanCloud, Minio, IBM COS)
    	\ "s3"
  	4 / Backblaze B2
    	\ "b2"
@@ -2900,7 +3054,7 @@ Choose `s3` backend
 Type of storage to configure.
 Choose a number from below, or type in your own value.
 [snip]
-XX / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, RackCorp, SeaweedFS, and Tencent COS
+XX / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Lyve Cloud, Minio, RackCorp, SeaweedFS, and Tencent COS
    \ (s3)
 [snip]
 Storage> s3
@@ -3087,7 +3241,7 @@ name> wasabi
 Type of storage to configure.
 Choose a number from below, or type in your own value
 [snip]
-XX / Amazon S3 (also Dreamhost, Ceph, ChinaMobile, Minio)
+XX / Amazon S3 (also Dreamhost, Ceph, ChinaMobile, ArvanCloud, Minio)
    \ "s3"
 [snip]
 Storage> s3
@@ -3201,7 +3355,7 @@ Type of storage to configure.
 Enter a string value. Press Enter for the default ("").
 Choose a number from below, or type in your own value
 [snip]
- 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, Digital Ocean, Dreamhost, IBM COS, Minio, and Tencent COS
+ 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Minio, and Tencent COS
    \ "s3"
 [snip]
 Storage> s3
@@ -3541,6 +3695,114 @@ d) Delete this remote
 y/e/d> y
 ```
 
+### ArvanCloud
+
+[ArvanCloud](https://www.arvancloud.com/en/products/cloud-storage) ArvanCloud Object Storage goes beyond the limited traditional file storage. 
+It gives you access to backup and archived files and allows sharing. 
+Files like profile image in the app, images sent by users or scanned documents can be stored securely and easily in our Object Storage service.
+
+ArvanCloud provides an S3 interface which can be configured for use with
+rclone like this.
+
+```
+No remotes found, make a new one?
+n) New remote
+s) Set configuration password
+n/s> n
+name> ArvanCloud
+Type of storage to configure.
+Choose a number from below, or type in your own value
+[snip]
+XX / Amazon S3 (also Dreamhost, Ceph, ChinaMobile, ArvanCloud, Minio)
+   \ "s3"
+[snip]
+Storage> s3
+Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars). Only applies if access_key_id and secret_access_key is blank.
+Choose a number from below, or type in your own value
+ 1 / Enter AWS credentials in the next step
+   \ "false"
+ 2 / Get AWS credentials from the environment (env vars or IAM)
+   \ "true"
+env_auth> 1
+AWS Access Key ID - leave blank for anonymous access or runtime credentials.
+access_key_id> YOURACCESSKEY
+AWS Secret Access Key (password) - leave blank for anonymous access or runtime credentials.
+secret_access_key> YOURSECRETACCESSKEY
+Region to connect to.
+Choose a number from below, or type in your own value
+   / The default endpoint - a good choice if you are unsure.
+ 1 | US Region, Northern Virginia, or Pacific Northwest.
+   | Leave location constraint empty.
+   \ "us-east-1"
+[snip]
+region> 
+Endpoint for S3 API.
+Leave blank if using ArvanCloud to use the default endpoint for the region.
+Specify if using an S3 clone such as Ceph.
+endpoint> s3.arvanstorage.com
+Location constraint - must be set to match the Region. Used when creating buckets only.
+Choose a number from below, or type in your own value
+ 1 / Empty for Iran-Tehran Region.
+   \ ""
+[snip]
+location_constraint>
+Canned ACL used when creating buckets and/or storing objects in S3.
+For more info visit https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
+Choose a number from below, or type in your own value
+ 1 / Owner gets FULL_CONTROL. No one else has access rights (default).
+   \ "private"
+[snip]
+acl>
+The server-side encryption algorithm used when storing this object in S3.
+Choose a number from below, or type in your own value
+ 1 / None
+   \ ""
+ 2 / AES256
+   \ "AES256"
+server_side_encryption>
+The storage class to use when storing objects in S3.
+Choose a number from below, or type in your own value
+ 1 / Default
+   \ ""
+ 2 / Standard storage class
+   \ "STANDARD"
+storage_class>
+Remote config
+--------------------
+[ArvanCloud]
+env_auth = false
+access_key_id = YOURACCESSKEY
+secret_access_key = YOURSECRETACCESSKEY
+region = ir-thr-at1
+endpoint = s3.arvanstorage.com
+location_constraint =
+acl =
+server_side_encryption =
+storage_class =
+--------------------
+y) Yes this is OK
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+```
+
+This will leave the config file looking like this.
+
+```
+[ArvanCloud]
+type = s3
+provider = ArvanCloud
+env_auth = false
+access_key_id = YOURACCESSKEY
+secret_access_key = YOURSECRETACCESSKEY
+region =
+endpoint = s3.arvanstorage.com
+location_constraint =
+acl =
+server_side_encryption =
+storage_class =
+```
+
 ### Tencent COS {#tencent-cos}
 
 [Tencent Cloud Object Storage (COS)](https://intl.cloud.tencent.com/product/cos) is a distributed storage service offered by Tencent Cloud for unstructured data. It is secure, stable, massive, convenient, low-delay and low-cost.
@@ -3574,7 +3836,7 @@ Choose a number from below, or type in your own value
    \ "alias"
  3 / Amazon Drive
    \ "amazon cloud drive"
- 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, Digital Ocean, Dreamhost, IBM COS, Minio, and Tencent COS
+ 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, Ceph, ChinaMobile, ArvanCloud, Digital Ocean, Dreamhost, IBM COS, Minio, and Tencent COS
    \ "s3"
 [snip]
 Storage> s3
diff --git a/docs/layouts/chrome/navbar.html b/docs/layouts/chrome/navbar.html
index fe532b45a..552b976ca 100644
--- a/docs/layouts/chrome/navbar.html
+++ b/docs/layouts/chrome/navbar.html
@@ -73,6 +73,7 @@
           <a class="dropdown-item" href="/hdfs/"><i class="fa fa-globe"></i> HDFS (Hadoop Distributed Filesystem)</a>
           <a class="dropdown-item" href="/http/"><i class="fa fa-globe"></i> HTTP</a>
           <a class="dropdown-item" href="/hubic/"><i class="fa fa-space-shuttle"></i> Hubic</a>
+          <a class="dropdown-item" href="/internetarchive/"><i class="fa fa-archive"></i> Internet Archive</a>
           <a class="dropdown-item" href="/jottacloud/"><i class="fa fa-cloud"></i> Jottacloud</a>
           <a class="dropdown-item" href="/koofr/"><i class="fa fa-suitcase"></i> Koofr</a>
           <a class="dropdown-item" href="/mailru/"><i class="fa fa-at"></i> Mail.ru Cloud</a>
diff --git a/fs/mount_helper.go b/fs/mount_helper.go
index 884b1f67c..f45b85895 100644
--- a/fs/mount_helper.go
+++ b/fs/mount_helper.go
@@ -208,7 +208,7 @@ func parseHelperOptionString(optString string) (opts []string, err error) {
 				if len(param) == 0 {
 					return nil, errHelperEmptyOption
 				}
-				if param[0] == '-' || param[0] == '_' {
+				if param[0] == '-' {
 					return nil, errHelperOptionName
 				}
 				prev = i + 1
diff --git a/fs/open_options.go b/fs/open_options.go
index 8d997118a..84e43045c 100644
--- a/fs/open_options.go
+++ b/fs/open_options.go
@@ -138,8 +138,14 @@ func (o *RangeOption) Decode(size int64) (offset, limit int64) {
 // absolute fetch using the size passed in and makes sure the range does
 // not exceed filesize. Some remotes (e.g. Onedrive, Box) don't support
 // range requests which index from the end.
+//
+// It also adjusts any SeekOption~s, turning them into absolute
+// RangeOption~s instead.
 func FixRangeOption(options []OpenOption, size int64) {
-	if size == 0 {
+	if size < 0 {
+		// Can't do anything for unknown length objects
+		return
+	} else if size == 0 {
 		// if size 0 then remove RangeOption~s
 		// replacing with a NullOptions~s which won't be rendered
 		for i := range options {
@@ -150,18 +156,21 @@ func FixRangeOption(options []OpenOption, size int64) {
 		}
 		return
 	}
-	for i := range options {
-		option := options[i]
-		if x, ok := option.(*RangeOption); ok {
+	for i, option := range options {
+		switch x := option.(type) {
+		case *RangeOption:
 			// If start is < 0 then fetch from the end
 			if x.Start < 0 {
 				x = &RangeOption{Start: size - x.End, End: -1}
 				options[i] = x
 			}
-			if x.End > size {
+			// If end is too big or undefined, fetch to the end
+			if x.End > size || x.End < 0 {
 				x = &RangeOption{Start: x.Start, End: size - 1}
 				options[i] = x
 			}
+		case *SeekOption:
+			options[i] = &RangeOption{Start: x.Offset, End: size - 1}
 		}
 	}
 }
diff --git a/fs/parseduration.go b/fs/parseduration.go
index 3184f6e1b..23e749446 100644
--- a/fs/parseduration.go
+++ b/fs/parseduration.go
@@ -76,18 +76,28 @@ var timeFormats = []string{
 	"2006-01-02",
 }
 
-// parse the age as time before the epoch in various date formats
-func parseDurationDates(age string, epoch time.Time) (t time.Duration, err error) {
+// parse the date as time in various date formats
+func parseTimeDates(date string) (t time.Time, err error) {
 	var instant time.Time
 	for _, timeFormat := range timeFormats {
-		instant, err = time.ParseInLocation(timeFormat, age, time.Local)
+		instant, err = time.ParseInLocation(timeFormat, date, time.Local)
 		if err == nil {
-			return epoch.Sub(instant), nil
+			return instant, nil
 		}
 	}
 	return t, err
 }
 
+// parse the age as time before the epoch in various date formats
+func parseDurationDates(age string, epoch time.Time) (d time.Duration, err error) {
+	instant, err := parseTimeDates(age)
+	if err != nil {
+		return d, err
+	}
+
+	return epoch.Sub(instant), nil
+}
+
 // parseDurationFromNow parses a duration string. Allows ParseDuration to match the time
 // package and easier testing within the fs package.
 func parseDurationFromNow(age string, getNow func() time.Time) (d time.Duration, err error) {
diff --git a/fs/parsetime.go b/fs/parsetime.go
new file mode 100644
index 000000000..145cfa0de
--- /dev/null
+++ b/fs/parsetime.go
@@ -0,0 +1,91 @@
+package fs
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+// Time is a time.Time with some more parsing options
+type Time time.Time
+
+// For overriding in unittests.
+var (
+	timeNowFunc = time.Now
+)
+
+// Turn Time into a string
+func (t Time) String() string {
+	if !t.IsSet() {
+		return "off"
+	}
+	return time.Time(t).Format(time.RFC3339Nano)
+}
+
+// IsSet returns if the time is not zero
+func (t Time) IsSet() bool {
+	return !time.Time(t).IsZero()
+}
+
+// ParseTime parses a time or duration string as a Time.
+func ParseTime(date string) (t time.Time, err error) {
+	if date == "off" {
+		return time.Time{}, nil
+	}
+
+	now := timeNowFunc()
+
+	// Attempt to parse as a text time
+	t, err = parseTimeDates(date)
+	if err == nil {
+		return t, nil
+	}
+
+	// Attempt to parse as a time.Duration offset from now
+	d, err := time.ParseDuration(date)
+	if err == nil {
+		return now.Add(-d), nil
+	}
+
+	d, err = parseDurationSuffixes(date)
+	if err == nil {
+		return now.Add(-d), nil
+	}
+
+	return t, err
+}
+
+// Set a Time
+func (t *Time) Set(s string) error {
+	parsedTime, err := ParseTime(s)
+	if err != nil {
+		return err
+	}
+	*t = Time(parsedTime)
+	return nil
+}
+
+// Type of the value
+func (t Time) Type() string {
+	return "Time"
+}
+
+// UnmarshalJSON makes sure the value can be parsed as a string in JSON
+func (t *Time) UnmarshalJSON(in []byte) error {
+	var s string
+	err := json.Unmarshal(in, &s)
+	if err != nil {
+		return err
+	}
+
+	return t.Set(s)
+}
+
+// Scan implements the fmt.Scanner interface
+func (t *Time) Scan(s fmt.ScanState, ch rune) error {
+	token, err := s.Token(true, nil)
+	if err != nil {
+		return err
+	}
+	return t.Set(string(token))
+}
diff --git a/go.mod b/go.mod
index cf8d5c41b..5051c7c64 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/rclone/rclone
 
-go 1.18
+go 1.16
 
 require (
 	bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05
@@ -17,7 +17,6 @@ require (
 	github.com/artyom/mtab v1.0.0
 	github.com/atotto/clipboard v0.1.4
 	github.com/aws/aws-sdk-go v1.43.30
-	github.com/winfsp/cgofuse v1.5.0
 	github.com/buengese/sgzip v0.1.1
 	github.com/colinmarc/hdfs/v2 v2.3.0
 	github.com/coreos/go-semver v0.3.0
@@ -54,6 +53,7 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.7.1
 	github.com/t3rm1n4l/go-mega v0.0.0-20200416171014-ffad7fcb44b8
+	github.com/winfsp/cgofuse v1.5.1-0.20220421173602-ce7e5a65cac7
 	github.com/xanzy/ssh-agent v0.3.1
 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
 	github.com/yunify/qingstor-sdk-go/v3 v3.2.0
@@ -71,67 +71,12 @@ require (
 	storj.io/uplink v1.8.1
 )
 
-require (
-	cloud.google.com/go/compute v1.5.0 // indirect
-	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
-	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
-	github.com/Azure/go-autorest/logger v0.2.1 // indirect
-	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
-	github.com/anacrolix/log v0.10.1-0.20220126091220-5c1b6f3af59c // indirect
-	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/calebcase/tmpfile v1.0.3 // indirect
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/go-ole/go-ole v1.2.6 // indirect
-	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/googleapis/gax-go/v2 v2.2.0 // indirect
-	github.com/hashicorp/go-uuid v1.0.2 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
-	github.com/jcmturner/aescts/v2 v2.0.0 // indirect
-	github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
-	github.com/jcmturner/gofork v1.0.0 // indirect
-	github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
-	github.com/jcmturner/rpc/v2 v2.0.3 // indirect
-	github.com/jmespath/go-jmespath v0.4.0 // indirect
-	github.com/kr/fs v0.1.0 // indirect
-	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
-	github.com/mattn/go-ieproxy v0.0.1 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
-	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
-	github.com/onsi/gomega v1.13.0 // indirect
-	github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 // indirect
-	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
-	github.com/prometheus/client_model v0.2.0 // indirect
-	github.com/prometheus/common v0.32.1 // indirect
-	github.com/prometheus/procfs v0.7.3 // indirect
-	github.com/rivo/uniseg v0.2.0 // indirect
-	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-	github.com/spacemonkeygo/monkit/v3 v3.0.17 // indirect
-	github.com/tklauser/go-sysconf v0.3.10 // indirect
-	github.com/tklauser/numcpus v0.4.0 // indirect
-	github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect
-	github.com/yusufpapurcu/wmi v1.2.2 // indirect
-	github.com/zeebo/errs v1.2.2 // indirect
-	go.opencensus.io v0.23.0 // indirect
-	golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 // indirect
-	golang.org/x/mod v0.4.2 // indirect
-	golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect
-	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
-	google.golang.org/grpc v1.45.0 // indirect
-	google.golang.org/protobuf v1.27.1 // indirect
-	storj.io/common v0.0.0-20220317162831-b0b4a044a95f // indirect
-	storj.io/drpc v0.0.29 // indirect
-)
-
 require (
 	github.com/Microsoft/go-winio v0.5.1 // indirect
 	github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
+	golang.org/x/mobile v0.0.0-20220414153400-ce6a79cf6a13
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index 4ebd7f6bf..16b400095 100644
--- a/go.sum
+++ b/go.sum
@@ -122,8 +122,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs=
-github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
 github.com/buengese/sgzip v0.1.1 h1:ry+T8l1mlmiWEsDrH/YHZnCVWD2S3im1KLsyO+8ZmTU=
@@ -454,7 +452,6 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
 github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
 github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 h1:XeOYlK9W1uCmhjJSsY78Mcuh7MVkNjTzmHx1yBzizSU=
 github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14/go.mod h1:jVblp62SafmidSkvWrXyxAme3gaTfEtWwRPGz5cpvHg=
@@ -591,8 +588,8 @@ github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMI
 github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k=
 github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc=
 github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/winfsp/cgofuse v1.5.0 h1:MsBP7Mi/LiJf/7/F3O/7HjjR009ds6KCdqXzKpZSWxI=
-github.com/winfsp/cgofuse v1.5.0/go.mod h1:h3awhoUOcn2VYVKCwDaYxSLlZwnyK+A8KaDoLUp2lbU=
+github.com/winfsp/cgofuse v1.5.1-0.20220421173602-ce7e5a65cac7 h1:Lvi511+8ZWFr9YWbvXkxudGSjcrjM1NVPDIPeWusvDk=
+github.com/winfsp/cgofuse v1.5.1-0.20220421173602-ce7e5a65cac7/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I=
 github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
 github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
@@ -679,8 +676,8 @@ golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPI
 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mobile v0.0.0-20220325161704-447654d348e3 h1:ZDL7hDvJEQEcHVkoZawKmRUgbqn1pOIzb8EinBh5csU=
-golang.org/x/mobile v0.0.0-20220325161704-447654d348e3/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
+golang.org/x/mobile v0.0.0-20220414153400-ce6a79cf6a13 h1:C4eR3yV9J3RkP8WKkcQzpcPyr/foOcUtxW72DvJEOlI=
+golang.org/x/mobile v0.0.0-20220414153400-ce6a79cf6a13/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -689,7 +686,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -881,8 +877,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -943,7 +937,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 h1:YuekqPskqwCCPM79F1X5Dhv4ezTCj+Ki1oNwiafxkA0=
 golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/lib/readers/fakeseeker.go b/lib/readers/fakeseeker.go
new file mode 100644
index 000000000..844fc0422
--- /dev/null
+++ b/lib/readers/fakeseeker.go
@@ -0,0 +1,72 @@
+package readers
+
+import (
+	"errors"
+	"fmt"
+	"io"
+)
+
+// FakeSeeker adapts an io.Seeker into an io.ReadSeeker
+type FakeSeeker struct {
+	in      io.Reader
+	readErr error
+	length  int64
+	offset  int64
+	read    bool
+}
+
+// NewFakeSeeker creates a fake io.ReadSeeker from an io.Reader
+//
+// This can be seeked before reading to discover the length passed in.
+func NewFakeSeeker(in io.Reader, length int64) io.ReadSeeker {
+	if rs, ok := in.(io.ReadSeeker); ok {
+		return rs
+	}
+	return &FakeSeeker{
+		in:     in,
+		length: length,
+	}
+}
+
+// Seek the stream - possible only before reading
+func (r *FakeSeeker) Seek(offset int64, whence int) (abs int64, err error) {
+	if r.readErr != nil {
+		return 0, r.readErr
+	}
+	if r.read {
+		return 0, fmt.Errorf("FakeSeeker: can't Seek(%d, %d) after reading", offset, whence)
+	}
+	switch whence {
+	case io.SeekStart:
+		abs = offset
+	case io.SeekCurrent:
+		abs = r.offset + offset
+	case io.SeekEnd:
+		abs = r.length + offset
+	default:
+		return 0, errors.New("FakeSeeker: invalid whence")
+	}
+	if abs < 0 {
+		return 0, errors.New("FakeSeeker: negative position")
+	}
+	r.offset = abs
+	return abs, nil
+}
+
+// Read data from the stream. Will give an error if seeked.
+func (r *FakeSeeker) Read(p []byte) (n int, err error) {
+	if r.readErr != nil {
+		return 0, r.readErr
+	}
+	if !r.read && r.offset != 0 {
+		return 0, errors.New("FakeSeeker: not at start: can't read")
+	}
+	n, err = r.in.Read(p)
+	if n != 0 {
+		r.read = true
+	}
+	if err != nil {
+		r.readErr = err
+	}
+	return n, err
+}
diff --git a/librclone/gomobile/gomobile.go b/librclone/gomobile/gomobile.go
index 90bfd033e..cc899e43c 100644
--- a/librclone/gomobile/gomobile.go
+++ b/librclone/gomobile/gomobile.go
@@ -6,6 +6,8 @@ import (
 
 	_ "github.com/rclone/rclone/backend/all" // import all backends
 	_ "github.com/rclone/rclone/lib/plugin"  // import plugins
+
+	_ "golang.org/x/mobile/event/key" // make go.mod add this as a dependency
 )
 
 // RcloneInitialize initializes rclone as a library


Test patch:
diff --git a/backend/internetarchive/internetarchive_test.go b/backend/internetarchive/internetarchive_test.go
new file mode 100644
index 000000000..2faa6f484
--- /dev/null
+++ b/backend/internetarchive/internetarchive_test.go
@@ -0,0 +1,17 @@
+// Test internetarchive filesystem interface
+package internetarchive_test
+
+import (
+	"testing"
+
+	"github.com/rclone/rclone/backend/internetarchive"
+	"github.com/rclone/rclone/fstest/fstests"
+)
+
+// TestIntegration runs integration tests against the remote
+func TestIntegration(t *testing.T) {
+	fstests.Run(t, &fstests.Opt{
+		RemoteName: "TestIA:lesmi-rclone-test/",
+		NilObject:  (*internetarchive.Object)(nil),
+	})
+}
diff --git a/cmd/cmount/mount_test.go b/cmd/cmount/mount_test.go
index f652c69ed..542c35b1b 100644
--- a/cmd/cmount/mount_test.go
+++ b/cmd/cmount/mount_test.go
@@ -1,7 +1,6 @@
-//go:build cmount && cgo && (linux || darwin || freebsd || windows) && (!race || !windows)
+//go:build cmount && ((linux && cgo) || (darwin && cgo) || (freebsd && cgo) || windows) && (!race || !windows)
 // +build cmount
-// +build cgo
-// +build linux darwin freebsd windows
+// +build linux,cgo darwin,cgo freebsd,cgo windows
 // +build !race !windows
 
 // FIXME this doesn't work with the race detector under Windows either
diff --git a/fs/mount_helper_test.go b/fs/mount_helper_test.go
index 00c4fba79..3235b1195 100644
--- a/fs/mount_helper_test.go
+++ b/fs/mount_helper_test.go
@@ -20,7 +20,7 @@ func TestMountHelperArgs(t *testing.T) {
 		src: []string{},
 		dst: []string{"mount", "--daemon"},
 	}, {
-		src: []string{"-o", `x-systemd.automount,vvv,env.HTTPS_PROXY="a b;c,d?EF",ro,rw,args2env`},
+		src: []string{"-o", `x-systemd.automount,vvv,env.HTTPS_PROXY="a b;c,d?EF",ro,rw,args2env,_netdev`},
 		dst: []string{"mount", "--read-only", "--verbose=3", "--daemon"},
 		env: "HTTPS_PROXY=a b;c,d?EF",
 	}}
diff --git a/fs/open_options_test.go b/fs/open_options_test.go
index 52fc42ddb..829f45c19 100644
--- a/fs/open_options_test.go
+++ b/fs/open_options_test.go
@@ -149,6 +149,16 @@ func TestFixRangeOptions(t *testing.T) {
 			in:   []OpenOption{},
 			want: []OpenOption{},
 		},
+		{
+			name: "Unknown size -1",
+			in: []OpenOption{
+				&RangeOption{Start: 1, End: -1},
+			},
+			want: []OpenOption{
+				&RangeOption{Start: 1, End: -1},
+			},
+			size: -1,
+		},
 		{
 			name: "Fetch a range with size=0",
 			in: []OpenOption{
@@ -183,7 +193,7 @@ func TestFixRangeOptions(t *testing.T) {
 				&RangeOption{Start: 1, End: -1},
 			},
 			want: []OpenOption{
-				&RangeOption{Start: 1, End: -1},
+				&RangeOption{Start: 1, End: 99},
 			},
 			size: 100,
 		},
@@ -193,7 +203,7 @@ func TestFixRangeOptions(t *testing.T) {
 				&RangeOption{Start: -1, End: 10},
 			},
 			want: []OpenOption{
-				&RangeOption{Start: 90, End: -1},
+				&RangeOption{Start: 90, End: 99},
 			},
 			size: 100,
 		},
@@ -207,6 +217,20 @@ func TestFixRangeOptions(t *testing.T) {
 			},
 			size: 100,
 		},
+		{
+			name: "SeekOption",
+			in: []OpenOption{
+				&HTTPOption{Key: "a", Value: "1"},
+				&SeekOption{Offset: 10},
+				&HTTPOption{Key: "b", Value: "2"},
+			},
+			want: []OpenOption{
+				&HTTPOption{Key: "a", Value: "1"},
+				&RangeOption{Start: 10, End: 99},
+				&HTTPOption{Key: "b", Value: "2"},
+			},
+			size: 100,
+		},
 	} {
 		FixRangeOption(test.in, test.size)
 		assert.Equal(t, test.want, test.in, test.name)
diff --git a/fs/parsetime_test.go b/fs/parsetime_test.go
new file mode 100644
index 000000000..5784b971a
--- /dev/null
+++ b/fs/parsetime_test.go
@@ -0,0 +1,147 @@
+package fs
+
+import (
+	"encoding/json"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// Check it satisfies the interface
+var _ flagger = (*Time)(nil)
+
+func TestParseTime(t *testing.T) {
+	now := time.Date(2020, 9, 5, 8, 15, 5, 250, time.UTC)
+	oldTimeNowFunc := timeNowFunc
+	timeNowFunc = func() time.Time { return now }
+	defer func() { timeNowFunc = oldTimeNowFunc }()
+
+	for _, test := range []struct {
+		in   string
+		want time.Time
+		err  bool
+	}{
+		{"", time.Time{}, true},
+		{"1ms", now.Add(-time.Millisecond), false},
+		{"1s", now.Add(-time.Second), false},
+		{"1", now.Add(-time.Second), false},
+		{"1m", now.Add(-time.Minute), false},
+		{"1.5m", now.Add(-(3 * time.Minute) / 2), false},
+		{"1h", now.Add(-time.Hour), false},
+		{"1d", now.Add(-time.Hour * 24), false},
+		{"1w", now.Add(-time.Hour * 24 * 7), false},
+		{"1M", now.Add(-time.Hour * 24 * 30), false},
+		{"1y", now.Add(-time.Hour * 24 * 365), false},
+		{"1.5y", now.Add(-time.Hour * 24 * 365 * 3 / 2), false},
+		{"-1.5y", now.Add(time.Hour * 24 * 365 * 3 / 2), false},
+		{"-1s", now.Add(time.Second), false},
+		{"-1", now.Add(time.Second), false},
+		{"0", now, false},
+		{"100", now.Add(-100 * time.Second), false},
+		{"-100", now.Add(100 * time.Second), false},
+		{"1.s", now.Add(-time.Second), false},
+		{"1x", time.Time{}, true},
+		{"-1x", time.Time{}, true},
+		{"off", time.Time{}, false},
+		{"1h2m3s", now.Add(-(time.Hour + 2*time.Minute + 3*time.Second)), false},
+		{"2001-02-03", time.Date(2001, 2, 3, 0, 0, 0, 0, time.Local), false},
+		{"2001-02-03 10:11:12", time.Date(2001, 2, 3, 10, 11, 12, 0, time.Local), false},
+		{"2001-08-03 10:11:12", time.Date(2001, 8, 3, 10, 11, 12, 0, time.Local), false},
+		{"2001-02-03T10:11:12", time.Date(2001, 2, 3, 10, 11, 12, 0, time.Local), false},
+		{"2001-02-03T10:11:12.123Z", time.Date(2001, 2, 3, 10, 11, 12, 123000000, time.UTC), false},
+		{"2001-02-03T10:11:12.123+00:00", time.Date(2001, 2, 3, 10, 11, 12, 123000000, time.UTC), false},
+	} {
+		parsedTime, err := ParseTime(test.in)
+		if test.err {
+			require.Error(t, err, test.in)
+		} else {
+			require.NoError(t, err, test.in)
+		}
+		assert.True(t, test.want.Equal(parsedTime), "%v should be parsed as %v instead of %v", test.in, test.want, parsedTime)
+	}
+}
+
+func TestTimeString(t *testing.T) {
+	now := time.Date(2020, 9, 5, 8, 15, 5, 250, time.UTC)
+	oldTimeNowFunc := timeNowFunc
+	timeNowFunc = func() time.Time { return now }
+	defer func() { timeNowFunc = oldTimeNowFunc }()
+
+	for _, test := range []struct {
+		in   time.Time
+		want string
+	}{
+		{now, "2020-09-05T08:15:05.00000025Z"},
+		{time.Date(2021, 8, 5, 8, 15, 5, 0, time.UTC), "2021-08-05T08:15:05Z"},
+		{time.Time{}, "off"},
+	} {
+		got := Time(test.in).String()
+		assert.Equal(t, test.want, got)
+		// Test the reverse
+		reverse, err := ParseTime(test.want)
+		assert.NoError(t, err)
+		assert.Equal(t, test.in, reverse)
+	}
+}
+
+func TestTimeScan(t *testing.T) {
+	now := time.Date(2020, 9, 5, 8, 15, 5, 250, time.UTC)
+	oldTimeNowFunc := timeNowFunc
+	timeNowFunc = func() time.Time { return now }
+	defer func() { timeNowFunc = oldTimeNowFunc }()
+
+	var v1, v2, v3, v4, v5 Time
+	n, err := fmt.Sscan(" 17m -12h 0 off 2022-03-26T17:48:19Z ", &v1, &v2, &v3, &v4, &v5)
+	require.NoError(t, err)
+	assert.Equal(t, 5, n)
+	assert.Equal(t, Time(now.Add(-17*time.Minute)), v1)
+	assert.Equal(t, Time(now.Add(12*time.Hour)), v2)
+	assert.Equal(t, Time(now), v3)
+	assert.Equal(t, Time(time.Time{}), v4)
+	assert.Equal(t, Time(time.Date(2022, 03, 26, 17, 48, 19, 0, time.UTC)), v5)
+}
+
+func TestParseTimeUnmarshalJSON(t *testing.T) {
+	now := time.Date(2020, 9, 5, 8, 15, 5, 250, time.UTC)
+	oldTimeNowFunc := timeNowFunc
+	timeNowFunc = func() time.Time { return now }
+	defer func() { timeNowFunc = oldTimeNowFunc }()
+
+	for _, test := range []struct {
+		in   string
+		want time.Time
+		err  bool
+	}{
+		{`""`, time.Time{}, true},
+		{"0", time.Time{}, true},
+		{"1", time.Time{}, true},
+		{"1", time.Time{}, true},
+		{`"2022-03-26T17:48:19Z"`, time.Date(2022, 03, 26, 17, 48, 19, 0, time.UTC), false},
+		{`"0"`, now, false},
+		{`"1ms"`, now.Add(-time.Millisecond), false},
+		{`"1s"`, now.Add(-time.Second), false},
+		{`"1"`, now.Add(-time.Second), false},
+		{`"1m"`, now.Add(-time.Minute), false},
+		{`"1h"`, now.Add(-time.Hour), false},
+		{`"-1h"`, now.Add(time.Hour), false},
+		{`"1d"`, now.Add(-time.Hour * 24), false},
+		{`"1w"`, now.Add(-time.Hour * 24 * 7), false},
+		{`"1M"`, now.Add(-time.Hour * 24 * 30), false},
+		{`"1y"`, now.Add(-time.Hour * 24 * 365), false},
+		{`"off"`, time.Time{}, false},
+		{`"error"`, time.Time{}, true},
+		{"error", time.Time{}, true},
+	} {
+		var parsedTime Time
+		err := json.Unmarshal([]byte(test.in), &parsedTime)
+		if test.err {
+			require.Error(t, err, test.in)
+		} else {
+			require.NoError(t, err, test.in)
+		}
+		assert.Equal(t, Time(test.want), parsedTime, test.in)
+	}
+}
diff --git a/fstest/test_all/config.yaml b/fstest/test_all/config.yaml
index 81afe169e..1b6bb9e1d 100644
--- a/fstest/test_all/config.yaml
+++ b/fstest/test_all/config.yaml
@@ -133,6 +133,9 @@ backends:
  - backend:  "hubic"
    remote:   "TestHubic:"
    fastlist: false
+ - backend:  "internetarchive"
+   remote:   "TestIA:rclone-integration-test"
+   fastlist: true
  - backend:  "jottacloud"
    remote:   "TestJottacloud:"
    fastlist: true
@@ -201,6 +204,11 @@ backends:
  - backend:  "s3"
    remote:   "TestS3Alibaba:"
    fastlist: true
+ - backend:  "s3"
+   remote:   "TestS3R2:"
+   fastlist: true
+   ignore:
+     - TestIntegration/FsMkdir/FsPutFiles/SetTier
  - backend:  "sftp"
    remote:   "TestSFTPOpenssh:"
    fastlist: false
diff --git a/lib/readers/fakeseeker_test.go b/lib/readers/fakeseeker_test.go
new file mode 100644
index 000000000..0702b7cbb
--- /dev/null
+++ b/lib/readers/fakeseeker_test.go
@@ -0,0 +1,87 @@
+package readers
+
+import (
+	"bytes"
+	"io"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// Check interface
+var _ io.ReadSeeker = &FakeSeeker{}
+
+func TestFakeSeeker(t *testing.T) {
+	// Test that passing in an io.ReadSeeker just passes it through
+	bufReader := bytes.NewReader([]byte{1})
+	r := NewFakeSeeker(bufReader, 5)
+	assert.Equal(t, r, bufReader)
+
+	in := bytes.NewBufferString("hello")
+	buf := make([]byte, 16)
+	r = NewFakeSeeker(in, 5)
+	assert.NotEqual(t, r, in)
+
+	// check the seek offset is as passed in
+	checkPos := func(pos int64) {
+		abs, err := r.Seek(0, io.SeekCurrent)
+		require.NoError(t, err)
+		assert.Equal(t, pos, abs)
+	}
+
+	// Test some seeking
+	checkPos(0)
+
+	abs, err := r.Seek(2, io.SeekStart)
+	require.NoError(t, err)
+	assert.Equal(t, int64(2), abs)
+	checkPos(2)
+
+	abs, err = r.Seek(-1, io.SeekEnd)
+	require.NoError(t, err)
+	assert.Equal(t, int64(4), abs)
+	checkPos(4)
+
+	// Check can't read if not at start
+	_, err = r.Read(buf)
+	require.ErrorContains(t, err, "not at start")
+
+	// Seek back to start
+	abs, err = r.Seek(-4, io.SeekCurrent)
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), abs)
+	checkPos(0)
+
+	_, err = r.Seek(42, 17)
+	require.ErrorContains(t, err, "invalid whence")
+
+	_, err = r.Seek(-1, io.SeekStart)
+	require.ErrorContains(t, err, "negative position")
+
+	// Test reading now seeked back to the start
+	n, err := r.Read(buf)
+	require.NoError(t, err)
+	assert.Equal(t, 5, n)
+	assert.Equal(t, []byte("hello"), buf[:5])
+
+	// Seeking should give an error now
+	_, err = r.Seek(-1, io.SeekEnd)
+	require.ErrorContains(t, err, "after reading")
+}
+
+func TestFakeSeekerError(t *testing.T) {
+	in := bytes.NewBufferString("hello")
+	r := NewFakeSeeker(in, 5)
+	assert.NotEqual(t, r, in)
+
+	buf, err := io.ReadAll(r)
+	require.NoError(t, err)
+	assert.Equal(t, []byte("hello"), buf)
+
+	_, err = r.Read(buf)
+	assert.Equal(t, io.EOF, err)
+
+	_, err = r.Seek(0, io.SeekStart)
+	assert.Equal(t, io.EOF, err)
+}