Repository: rclone/rclone
Pull request #7174 (2023-07-25 21:03:04)

protondrive: fix a bug in parsing User metadata
<!--
Thank you very much for contributing code or documentation to rclone! Please
fill out the following questions to make it easier for us to review your
changes.

You do not need to check all the boxes below all at once, feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box.
-->

#### What is the purpose of this change?

To address the [issue](https://github.com/rclone/rclone/issues/6072#issuecomment-1648721049) reported here, by using the approach mentioned [here](https://github.com/rclone/rclone/issues/6072#issuecomment-1648985817)

<!--
Describe the changes here
-->

#### Was the change discussed in an issue or in the forum before?

<!--
Link issues and relevant forum posts here.
-->

#### Checklist

- [x] I have read the [contribution guidelines](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#submitting-a-new-feature-or-bug-fix).
- [x] I have added tests for all changes in this PR if appropriate.
- [x] I have added documentation for the changes if appropriate.
- [x] All commit messages are in [house style](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#commit-messages).
- [x] I'm done, this Pull Request is ready for review :-)



Patch:
diff --git a/backend/fichier/api.go b/backend/fichier/api.go
index d75869a25..c0e3843dc 100644
--- a/backend/fichier/api.go
+++ b/backend/fichier/api.go
@@ -408,6 +408,32 @@ func (f *Fs) moveFile(ctx context.Context, url string, folderID int, rename stri
 	return response, nil
 }
 
+func (f *Fs) moveDir(ctx context.Context, folderID int, newLeaf string, destinationFolderID int) (response *MoveDirResponse, err error) {
+	request := &MoveDirRequest{
+		FolderID:            folderID,
+		DestinationFolderID: destinationFolderID,
+		Rename:              newLeaf,
+		// DestinationUser:     destinationUser,
+	}
+
+	opts := rest.Opts{
+		Method: "POST",
+		Path:   "/folder/mv.cgi",
+	}
+
+	response = &MoveDirResponse{}
+	err = f.pacer.Call(func() (bool, error) {
+		resp, err := f.rest.CallJSON(ctx, &opts, request, response)
+		return shouldRetry(ctx, resp, err)
+	})
+
+	if err != nil {
+		return nil, fmt.Errorf("couldn't move dir: %w", err)
+	}
+
+	return response, nil
+}
+
 func (f *Fs) copyFile(ctx context.Context, url string, folderID int, rename string) (response *CopyFileResponse, err error) {
 	request := &CopyFileRequest{
 		URLs:     []string{url},
diff --git a/backend/fichier/fichier.go b/backend/fichier/fichier.go
index 25df3d971..4a2caf5bf 100644
--- a/backend/fichier/fichier.go
+++ b/backend/fichier/fichier.go
@@ -488,6 +488,51 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	return dstObj, nil
 }
 
+// DirMove moves src, srcRemote to this remote at dstRemote
+// using server-side move operations.
+//
+// Will only be called if src.Fs().Name() == f.Name()
+//
+// If it isn't possible then return fs.ErrorCantDirMove.
+//
+// If destination exists then return fs.ErrorDirExists.
+//
+// This is complicated by the fact that we can't use moveDir to move
+// to a different directory AND rename at the same time as it can
+// overwrite files in the source directory.
+func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
+	srcFs, ok := src.(*Fs)
+	if !ok {
+		fs.Debugf(srcFs, "Can't move directory - not same remote type")
+		return fs.ErrorCantDirMove
+	}
+
+	srcID, _, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
+	if err != nil {
+		return err
+	}
+	srcIDnumeric, err := strconv.Atoi(srcID)
+	if err != nil {
+		return err
+	}
+	dstDirectoryIDnumeric, err := strconv.Atoi(dstDirectoryID)
+	if err != nil {
+		return err
+	}
+
+	var resp *MoveDirResponse
+	resp, err = f.moveDir(ctx, srcIDnumeric, dstLeaf, dstDirectoryIDnumeric)
+	if err != nil {
+		return fmt.Errorf("couldn't rename leaf: %w", err)
+	}
+	if resp.Status != "OK" {
+		return fmt.Errorf("couldn't rename leaf: %s", resp.Message)
+	}
+
+	srcFs.dirCache.FlushDir(srcRemote)
+	return nil
+}
+
 // Copy src to this remote using server side move operations.
 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
 	srcObj, ok := src.(*Object)
@@ -561,6 +606,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration,
 var (
 	_ fs.Fs              = (*Fs)(nil)
 	_ fs.Mover           = (*Fs)(nil)
+	_ fs.DirMover        = (*Fs)(nil)
 	_ fs.Copier          = (*Fs)(nil)
 	_ fs.PublicLinker    = (*Fs)(nil)
 	_ fs.PutUncheckeder  = (*Fs)(nil)
diff --git a/backend/fichier/structs.go b/backend/fichier/structs.go
index aed910576..289aa64b7 100644
--- a/backend/fichier/structs.go
+++ b/backend/fichier/structs.go
@@ -70,6 +70,22 @@ type MoveFileResponse struct {
 	URLs    []string `json:"urls"`
 }
 
+// MoveDirRequest is the request structure of the corresponding request
+type MoveDirRequest struct {
+	FolderID            int    `json:"folder_id"`
+	DestinationFolderID int    `json:"destination_folder_id,omitempty"`
+	DestinationUser     string `json:"destination_user"`
+	Rename              string `json:"rename,omitempty"`
+}
+
+// MoveDirResponse is the response structure of the corresponding request
+type MoveDirResponse struct {
+	Status  string `json:"status"`
+	Message string `json:"message"`
+	OldName string `json:"old_name"`
+	NewName string `json:"new_name"`
+}
+
 // CopyFileRequest is the request structure of the corresponding request
 type CopyFileRequest struct {
 	URLs     []string `json:"urls"`
diff --git a/backend/protondrive/protondrive.go b/backend/protondrive/protondrive.go
index cac3a4c81..c1904eea2 100644
--- a/backend/protondrive/protondrive.go
+++ b/backend/protondrive/protondrive.go
@@ -737,8 +737,8 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
 		return nil, err
 	}
 
-	total := int64(user.MaxSpace)
-	used := int64(user.UsedSpace)
+	total := user.MaxSpace
+	used := user.UsedSpace
 	free := total - used
 
 	usage := &fs.Usage{
diff --git a/cmd/serve/webdav/webdav.go b/cmd/serve/webdav/webdav.go
index 0af46ec4d..ae8c4f2d1 100644
--- a/cmd/serve/webdav/webdav.go
+++ b/cmd/serve/webdav/webdav.go
@@ -6,8 +6,10 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
+	"mime"
 	"net/http"
 	"os"
+	"path"
 	"strconv"
 	"strings"
 	"time"
@@ -580,12 +582,14 @@ func (fi FileInfo) ContentType(ctx context.Context) (contentType string, err err
 		fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo)
 		return "application/octet-stream", nil
 	}
-	entry := node.DirEntry()
+	entry := node.DirEntry() // can be nil
 	switch x := entry.(type) {
 	case fs.Object:
 		return fs.MimeType(ctx, x), nil
 	case fs.Directory:
 		return "inode/directory", nil
+	case nil:
+		return mime.TypeByExtension(path.Ext(node.Name())), nil
 	}
 	fs.Errorf(fi, "Expecting fs.Object or fs.Directory, got %T", entry)
 	return "application/octet-stream", nil
diff --git a/docs/content/b2.md b/docs/content/b2.md
index b5e5552b5..9d2bd5acb 100644
--- a/docs/content/b2.md
+++ b/docs/content/b2.md
@@ -231,6 +231,19 @@ $ rclone -q --b2-versions ls b2:cleanup-test
         9 one.txt
 ```
 
+#### Versions naming caveat
+
+When using `--b2-versions` flag rclone is relying on the file name
+to work out whether the objects are versions or not. Versions' names
+are created by inserting timestamp between file name and its extension.
+```
+        9 file.txt
+        8 file-v2023-07-17-161032-000.txt
+       16 file-v2023-06-15-141003-000.txt
+```
+If there are real files present with the same names as versions, then
+behaviour of `--b2-versions` can be unpredictable.
+
 ### Data usage
 
 It is useful to know how many requests are sent to the server in different scenarios.
diff --git a/docs/content/dropbox.md b/docs/content/dropbox.md
index d7d785b5b..609393425 100644
--- a/docs/content/dropbox.md
+++ b/docs/content/dropbox.md
@@ -494,7 +494,7 @@ to be the same account as the Dropbox you want to access)
 
 2. Choose an API => Usually this should be `Dropbox API`
 
-3. Choose the type of access you want to use => `Full Dropbox` or `App Folder`
+3. Choose the type of access you want to use => `Full Dropbox` or `App Folder`. If you want to use Team Folders, `Full Dropbox` is required ([see here](https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/How-to-create-team-folder-inside-my-app-s-folder/m-p/601005/highlight/true#M27911)).
 
 4. Name your App. The app name is global, so you can't use `rclone` for example
 
diff --git a/docs/content/s3.md b/docs/content/s3.md
index 24b9b20f3..7b9e6369c 100644
--- a/docs/content/s3.md
+++ b/docs/content/s3.md
@@ -464,6 +464,19 @@ $ rclone -q --s3-versions ls s3:cleanup-test
         9 one.txt
 ```
 
+#### Versions naming caveat
+
+When using `--s3-versions` flag rclone is relying on the file name
+to work out whether the objects are versions or not. Versions' names
+are created by inserting timestamp between file name and its extension.
+```
+        9 file.txt
+        8 file-v2023-07-17-161032-000.txt
+       16 file-v2023-06-15-141003-000.txt
+```
+If there are real files present with the same names as versions, then
+behaviour of `--s3-versions` can be unpredictable.
+
 ### Cleanup
 
 If you run `rclone cleanup s3:bucket` then it will remove all pending
diff --git a/fs/rc/rc.go b/fs/rc/rc.go
index 3b150b5f4..b7db4e587 100644
--- a/fs/rc/rc.go
+++ b/fs/rc/rc.go
@@ -18,22 +18,21 @@ import (
 
 // Options contains options for the remote control server
 type Options struct {
-	HTTP                     libhttp.Config
-	Auth                     libhttp.AuthConfig
-	Template                 libhttp.TemplateConfig
-	Enabled                  bool   // set to enable the server
-	Serve                    bool   // set to serve files from remotes
-	Files                    string // set to enable serving files locally
-	NoAuth                   bool   // set to disable auth checks on AuthRequired methods
-	WebUI                    bool   // set to launch the web ui
-	WebGUIUpdate             bool   // set to check new update
-	WebGUIForceUpdate        bool   // set to force download new update
-	WebGUINoOpenBrowser      bool   // set to disable auto opening browser
-	WebGUIFetchURL           string // set the default url for fetching webgui
-	AccessControlAllowOrigin string // set the access control for CORS configuration
-	EnableMetrics            bool   // set to disable prometheus metrics on /metrics
-	JobExpireDuration        time.Duration
-	JobExpireInterval        time.Duration
+	HTTP                libhttp.Config
+	Auth                libhttp.AuthConfig
+	Template            libhttp.TemplateConfig
+	Enabled             bool   // set to enable the server
+	Serve               bool   // set to serve files from remotes
+	Files               string // set to enable serving files locally
+	NoAuth              bool   // set to disable auth checks on AuthRequired methods
+	WebUI               bool   // set to launch the web ui
+	WebGUIUpdate        bool   // set to check new update
+	WebGUIForceUpdate   bool   // set to force download new update
+	WebGUINoOpenBrowser bool   // set to disable auto opening browser
+	WebGUIFetchURL      string // set the default url for fetching webgui
+	EnableMetrics       bool   // set to disable prometheus metrics on /metrics
+	JobExpireDuration   time.Duration
+	JobExpireInterval   time.Duration
 }
 
 // DefaultOpt is the default values used for Options
diff --git a/fs/rc/rcflags/rcflags.go b/fs/rc/rcflags/rcflags.go
index f323806c8..7857c5189 100644
--- a/fs/rc/rcflags/rcflags.go
+++ b/fs/rc/rcflags/rcflags.go
@@ -27,7 +27,6 @@ func AddFlags(flagSet *pflag.FlagSet) {
 	flags.BoolVarP(flagSet, &Opt.WebGUIForceUpdate, "rc-web-gui-force-update", "", false, "Force update to latest version of web gui")
 	flags.BoolVarP(flagSet, &Opt.WebGUINoOpenBrowser, "rc-web-gui-no-open-browser", "", false, "Don't open the browser automatically")
 	flags.StringVarP(flagSet, &Opt.WebGUIFetchURL, "rc-web-fetch-url", "", "https://api.github.com/repos/rclone/rclone-webui-react/releases/latest", "URL to fetch the releases for webgui")
-	flags.StringVarP(flagSet, &Opt.AccessControlAllowOrigin, "rc-allow-origin", "", "", "Set the allowed origin for CORS")
 	flags.BoolVarP(flagSet, &Opt.EnableMetrics, "rc-enable-metrics", "", false, "Enable prometheus metrics on /metrics")
 	flags.DurationVarP(flagSet, &Opt.JobExpireDuration, "rc-job-expire-duration", "", Opt.JobExpireDuration, "Expire finished async jobs older than this value")
 	flags.DurationVarP(flagSet, &Opt.JobExpireInterval, "rc-job-expire-interval", "", Opt.JobExpireInterval, "Interval to check for expired async jobs")
diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go
index f353c3ea1..8afd871ce 100644
--- a/fs/rc/rcserver/rcserver.go
+++ b/fs/rc/rcserver/rcserver.go
@@ -15,7 +15,6 @@ import (
 	"regexp"
 	"sort"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/go-chi/chi/v5/middleware"
@@ -38,7 +37,6 @@ import (
 )
 
 var promHandler http.Handler
-var onlyOnceWarningAllowOrigin sync.Once
 
 func init() {
 	rcloneCollector := accounting.NewRcloneCollector(context.Background())
@@ -214,23 +212,6 @@ func writeError(path string, in rc.Params, w http.ResponseWriter, err error, sta
 func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
 	path := strings.TrimLeft(r.URL.Path, "/")
 
-	allowOrigin := rcflags.Opt.AccessControlAllowOrigin
-	if allowOrigin != "" {
-		onlyOnceWarningAllowOrigin.Do(func() {
-			if allowOrigin == "*" {
-				fs.Logf(nil, "Warning: Allow origin set to *. This can cause serious security problems.")
-			}
-		})
-		w.Header().Add("Access-Control-Allow-Origin", allowOrigin)
-	} else {
-		urls := s.server.URLs()
-		if len(urls) == 1 {
-			w.Header().Add("Access-Control-Allow-Origin", urls[0])
-		} else {
-			fs.Errorf(nil, "Warning, need exactly 1 URL for Access-Control-Allow-Origin, got %d %q", len(urls), urls)
-		}
-	}
-
 	// echo back access control headers client needs
 	//reqAccessHeaders := r.Header.Get("Access-Control-Request-Headers")
 	w.Header().Add("Access-Control-Request-Method", "POST, OPTIONS, GET, HEAD")
diff --git a/go.mod b/go.mod
index e1f33f9b5..8f3beff60 100644
--- a/go.mod
+++ b/go.mod
@@ -28,8 +28,8 @@ require (
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/google/uuid v1.3.0
 	github.com/hanwen/go-fuse/v2 v2.2.1-0.20230410213758-80c1c8221982
-	github.com/henrybear327/Proton-API-Bridge v0.0.0-20230723213541-aeabe60e01e9
-	github.com/henrybear327/go-proton-api v0.0.0-20230723213442-6a56030e2edc
+	github.com/henrybear327/Proton-API-Bridge v0.0.0-20230725204219-611ec433f83c
+	github.com/henrybear327/go-proton-api v0.0.0-20230725203741-316f1b3227a9
 	github.com/hirochachacha/go-smb2 v1.1.0
 	github.com/iguanesolutions/go-systemd/v5 v5.1.1
 	github.com/jcmturner/gokrb5/v8 v8.4.4
@@ -147,7 +147,7 @@ require (
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	github.com/zeebo/errs v1.3.0 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
+	golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 // indirect
 	google.golang.org/grpc v1.54.0 // indirect
diff --git a/go.sum b/go.sum
index 80a960289..7a2f07242 100644
--- a/go.sum
+++ b/go.sum
@@ -205,6 +205,7 @@ github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14j
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -220,6 +221,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -299,8 +301,12 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/henrybear327/Proton-API-Bridge v0.0.0-20230723213541-aeabe60e01e9 h1:ScjocJzxEgwul+yeyPCKpyD1lLemHECynj60U9axIi4=
 github.com/henrybear327/Proton-API-Bridge v0.0.0-20230723213541-aeabe60e01e9/go.mod h1:Snkc1HsqPbB3gQZ7LI+47bHXwHnIrYDYWB4wg34QIbQ=
+github.com/henrybear327/Proton-API-Bridge v0.0.0-20230725204219-611ec433f83c h1:dtM2iOEwFBbpV7jSAZ+EHL4LUWhm2rsofkZhFzTL2jM=
+github.com/henrybear327/Proton-API-Bridge v0.0.0-20230725204219-611ec433f83c/go.mod h1:iY633SZMUnQE1PCV7gBU7u+35TCDVkEqA0IhHHyBdOg=
 github.com/henrybear327/go-proton-api v0.0.0-20230723213442-6a56030e2edc h1:KivXKtS1s0BZiL+0CSPzT5dPcF+zEjysXXqkA/G1SH8=
 github.com/henrybear327/go-proton-api v0.0.0-20230723213442-6a56030e2edc/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
+github.com/henrybear327/go-proton-api v0.0.0-20230725203741-316f1b3227a9 h1:ITuA1x2yBCE4Wgae+ywZmezVuKOKhJ4nuDtColKE32c=
+github.com/henrybear327/go-proton-api v0.0.0-20230725203741-316f1b3227a9/go.mod h1:eHoT/G3lmdatxoY04LSQthG/YnPmu4aebNZe8h+yvBc=
 github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
 github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
 github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
@@ -603,6 +609,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
+golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -838,6 +846,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
diff --git a/lib/http/middleware.go b/lib/http/middleware.go
index 2aa319c81..6deba60f6 100644
--- a/lib/http/middleware.go
+++ b/lib/http/middleware.go
@@ -181,6 +181,13 @@ func MiddlewareCORS(allowOrigin string) Middleware {
 			w.Header().Add("Access-Control-Request-Method", "POST, OPTIONS, GET, HEAD")
 			w.Header().Add("Access-Control-Allow-Headers", "authorization, Content-Type")
 
+			if r.Method == "OPTIONS" {
+				w.WriteHeader(http.StatusOK)
+				return
+				// Because CORS preflight OPTIONS requests are not authenticated,
+				// and require a 200 OK response, we will return early here.
+			}
+
 			next.ServeHTTP(w, r)
 		})
 	}
diff --git a/lib/http/server.go b/lib/http/server.go
index 3c26d5cd5..3a43d37d9 100644
--- a/lib/http/server.go
+++ b/lib/http/server.go
@@ -109,6 +109,7 @@ type Config struct {
 	TLSKeyBody         []byte        // TLS PEM Private key body, ignores TLSKey
 	ClientCA           string        // Client certificate authority to verify clients with
 	MinTLSVersion      string        // MinTLSVersion contains the minimum TLS version that is acceptable.
+	AllowOrigin        string        // AllowOrigin sets the Access-Control-Allow-Origin header
 }
 
 // AddFlagsPrefix adds flags for the httplib
@@ -122,6 +123,7 @@ func (cfg *Config) AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string) {
 	flags.StringVarP(flagSet, &cfg.ClientCA, prefix+"client-ca", "", cfg.ClientCA, "Client certificate authority to verify clients with")
 	flags.StringVarP(flagSet, &cfg.BaseURL, prefix+"baseurl", "", cfg.BaseURL, "Prefix for URLs - leave blank for root")
 	flags.StringVarP(flagSet, &cfg.MinTLSVersion, prefix+"min-tls-version", "", cfg.MinTLSVersion, "Minimum TLS version that is acceptable")
+	flags.StringVarP(flagSet, &cfg.AllowOrigin, prefix+"allow-origin", "", cfg.AllowOrigin, "Origin which cross-domain request (CORS) can be executed from")
 }
 
 // AddHTTPFlagsPrefix adds flags for the httplib
@@ -236,6 +238,8 @@ func NewServer(ctx context.Context, options ...Option) (*Server, error) {
 		return nil, err
 	}
 
+	s.mux.Use(MiddlewareCORS(s.cfg.AllowOrigin))
+
 	s.initAuth()
 
 	for _, addr := range s.cfg.ListenAddr {


Test patch:
diff --git a/fs/rc/rcserver/rcserver_test.go b/fs/rc/rcserver/rcserver_test.go
index 278722297..05889ecc9 100644
--- a/fs/rc/rcserver/rcserver_test.go
+++ b/fs/rc/rcserver/rcserver_test.go
@@ -552,32 +552,6 @@ Unknown command
 	testServer(t, tests, &opt)
 }
 
-func TestMethods(t *testing.T) {
-	tests := []testRun{{
-		Name:     "options",
-		URL:      "",
-		Method:   "OPTIONS",
-		Status:   http.StatusOK,
-		Expected: "",
-		Headers: map[string]string{
-			"Access-Control-Allow-Origin":   "testURL",
-			"Access-Control-Request-Method": "POST, OPTIONS, GET, HEAD",
-			"Access-Control-Allow-Headers":  "authorization, Content-Type",
-		},
-	}, {
-		Name:   "bad",
-		URL:    "",
-		Method: "POTATO",
-		Status: http.StatusMethodNotAllowed,
-		Expected: `Method Not Allowed
-`,
-	}}
-	opt := newTestOpt()
-	opt.Serve = true
-	opt.Files = testFs
-	testServer(t, tests, &opt)
-}
-
 func TestMetrics(t *testing.T) {
 	stats := accounting.GlobalStats()
 	tests := makeMetricsTestCases(stats)
diff --git a/lib/http/middleware_test.go b/lib/http/middleware_test.go
index 24cc97afb..f8ac196b9 100644
--- a/lib/http/middleware_test.go
+++ b/lib/http/middleware_test.go
@@ -329,23 +329,22 @@ var _testCORSHeaderKeys = []string{
 
 func TestMiddlewareCORS(t *testing.T) {
 	servers := []struct {
-		name   string
-		http   Config
-		origin string
+		name string
+		http Config
 	}{
 		{
 			name: "EmptyOrigin",
 			http: Config{
-				ListenAddr: []string{"127.0.0.1:0"},
+				ListenAddr:  []string{"127.0.0.1:0"},
+				AllowOrigin: "",
 			},
-			origin: "",
 		},
 		{
 			name: "CustomOrigin",
 			http: Config{
-				ListenAddr: []string{"127.0.0.1:0"},
+				ListenAddr:  []string{"127.0.0.1:0"},
+				AllowOrigin: "http://test.rclone.org",
 			},
-			origin: "http://test.rclone.org",
 		},
 	}
 
@@ -357,8 +356,6 @@ func TestMiddlewareCORS(t *testing.T) {
 				require.NoError(t, s.Shutdown())
 			}()
 
-			s.Router().Use(MiddlewareCORS(ss.origin))
-
 			expected := []byte("data")
 			s.Router().Mount("/", testEchoHandler(expected))
 			s.Serve()
@@ -384,8 +381,69 @@ func TestMiddlewareCORS(t *testing.T) {
 			}
 
 			expectedOrigin := url
-			if ss.origin != "" {
-				expectedOrigin = ss.origin
+			if ss.http.AllowOrigin != "" {
+				expectedOrigin = ss.http.AllowOrigin
+			}
+			require.Equal(t, expectedOrigin, resp.Header.Get("Access-Control-Allow-Origin"), "allow origin should match")
+		})
+	}
+}
+
+func TestMiddlewareCORSWithAuth(t *testing.T) {
+	authServers := []struct {
+		name string
+		http Config
+		auth AuthConfig
+	}{
+		{
+			name: "ServerWithAuth",
+			http: Config{
+				ListenAddr:  []string{"127.0.0.1:0"},
+				AllowOrigin: "http://test.rclone.org",
+			},
+			auth: AuthConfig{
+				Realm:     "test",
+				BasicUser: "test_user",
+				BasicPass: "test_pass",
+			},
+		},
+	}
+
+	for _, ss := range authServers {
+		t.Run(ss.name, func(t *testing.T) {
+			s, err := NewServer(context.Background(), WithConfig(ss.http))
+			require.NoError(t, err)
+			defer func() {
+				require.NoError(t, s.Shutdown())
+			}()
+
+			expected := []byte("data")
+			s.Router().Mount("/", testEchoHandler(expected))
+			s.Serve()
+
+			url := testGetServerURL(t, s)
+
+			client := &http.Client{}
+			req, err := http.NewRequest("OPTIONS", url, nil)
+			require.NoError(t, err)
+
+			resp, err := client.Do(req)
+			require.NoError(t, err)
+			defer func() {
+				_ = resp.Body.Close()
+			}()
+
+			require.Equal(t, http.StatusOK, resp.StatusCode, "OPTIONS should return ok even if not authenticated")
+
+			testExpectRespBody(t, resp, []byte{})
+
+			for _, key := range _testCORSHeaderKeys {
+				require.Contains(t, resp.Header, key, "CORS headers should be sent even if not authenticated")
+			}
+
+			expectedOrigin := url
+			if ss.http.AllowOrigin != "" {
+				expectedOrigin = ss.http.AllowOrigin
 			}
 			require.Equal(t, expectedOrigin, resp.Header.Get("Access-Control-Allow-Origin"), "allow origin should match")
 		})
diff --git a/lib/http/server_test.go b/lib/http/server_test.go
index d3bf997d4..656c7702f 100644
--- a/lib/http/server_test.go
+++ b/lib/http/server_test.go
@@ -82,8 +82,6 @@ func TestNewServerUnix(t *testing.T) {
 
 	require.Empty(t, s.URLs(), "unix socket should not appear in URLs")
 
-	s.Router().Use(MiddlewareCORS(""))
-
 	expected := []byte("hello world")
 	s.Router().Mount("/", testEchoHandler(expected))
 	s.Serve()