Repository: rclone/rclone
Pull request #7706 (2024-03-29 12:01:28)

docs: Add R2 note about no_check_bucket
<!--
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?

Context: I work for Cloudflare.

Some time ago I added a note about `no_check_bucket` being required for R2 object-level tokens onto https://developers.cloudflare.com/r2/examples/rclone/ in https://github.com/cloudflare/cloudflare-docs/pull/11438

This is required for non-admin tokens due to rclone sending a PUT to buckets as part of the object upload flow.

This change adds the same note to the rclone docs as well, to reduce friction and confusion across the board.

P.S. Bonus: is this something that can be baked into rclone as a quirk or not? For now documentation will suffice.

#### Was the change discussed in an issue or in the forum before?

It was not, sorry! Let me know if that is needed.

#### 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/onedrive/onedrive.go b/backend/onedrive/onedrive.go
index 5ce26f6f9..c1da57a60 100644
--- a/backend/onedrive/onedrive.go
+++ b/backend/onedrive/onedrive.go
@@ -291,7 +291,7 @@ all onedrive types. If an SHA1 hash is desired then set this option
 accordingly.
 
 From July 2023 QuickXorHash will be the only available hash for
-both OneDrive for Business and OneDriver Personal.
+both OneDrive for Business and OneDrive Personal.
 
 This can be set to "none" to not use any hashes.
 
diff --git a/backend/s3/s3.go b/backend/s3/s3.go
index 2b0b0ff17..a17446f9f 100644
--- a/backend/s3/s3.go
+++ b/backend/s3/s3.go
@@ -289,6 +289,9 @@ func init() {
 			}, {
 				Value: "sa-east-1",
 				Help:  "South America (Sao Paulo) Region.\nNeeds location constraint sa-east-1.",
+			}, {
+				Value: "il-central-1",
+				Help:  "Israel (Tel Aviv) Region.\nNeeds location constraint il-central-1.",
 			}, {
 				Value: "me-south-1",
 				Help:  "Middle East (Bahrain) Region.\nNeeds location constraint me-south-1.",
@@ -1453,6 +1456,9 @@ func init() {
 			}, {
 				Value: "sa-east-1",
 				Help:  "South America (Sao Paulo) Region",
+			}, {
+				Value: "il-central-1",
+				Help:  "Israel (Tel Aviv) Region",
 			}, {
 				Value: "me-south-1",
 				Help:  "Middle East (Bahrain) Region",
@@ -3190,8 +3196,8 @@ func setQuirks(opt *Options) {
 		if opt.MaxUploadParts > 1000 {
 			opt.MaxUploadParts = 1000
 		}
-		urlEncodeListings = false
-		useAlreadyExists = false // untested
+		urlEncodeListings = true
+		useAlreadyExists = true
 	case "SeaweedFS":
 		listObjectsV2 = false // untested
 		virtualHostStyle = false
diff --git a/backend/ulozto/ulozto.go b/backend/ulozto/ulozto.go
index f1364c6f9..eef719d4f 100644
--- a/backend/ulozto/ulozto.go
+++ b/backend/ulozto/ulozto.go
@@ -22,6 +22,7 @@ import (
 	"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/config/obscure"
 	"github.com/rclone/rclone/fs/fserrors"
 	"github.com/rclone/rclone/fs/fshttp"
 	"github.com/rclone/rclone/fs/hash"
@@ -59,8 +60,8 @@ func init() {
 			{
 				Name:    "app_token",
 				Default: "",
-				Help: "The application token identifying the app. An app API key can be either found in the API " +
-					"doc https://uloz.to/upload-resumable-api-beta or obtained from customer service.",
+				Help: `The application token identifying the app. An app API key can be either found in the API
+doc https://uloz.to/upload-resumable-api-beta or obtained from customer service.`,
 				Sensitive: true,
 			},
 			{
@@ -70,16 +71,16 @@ func init() {
 				Sensitive: true,
 			},
 			{
-				Name:      "password",
-				Default:   "",
-				Help:      "The password for the user.",
-				Sensitive: true,
+				Name:       "password",
+				Default:    "",
+				Help:       "The password for the user.",
+				IsPassword: true,
 			},
 			{
 				Name: "root_folder_slug",
-				Help: "If set, rclone will use this folder as the root folder for all operations. For example, " +
-					"if the slug identifies 'foo/bar/', 'ulozto:baz' is equivalent to 'ulozto:foo/bar/baz' without " +
-					"any root slug set.",
+				Help: `If set, rclone will use this folder as the root folder for all operations. For example,
+if the slug identifies 'foo/bar/', 'ulozto:baz' is equivalent to 'ulozto:foo/bar/baz' without
+any root slug set.`,
 				Default:   "",
 				Advanced:  true,
 				Sensitive: true,
@@ -237,9 +238,13 @@ func (f *Fs) authenticate(ctx context.Context) (response *api.AuthenticateRespon
 		Path:   "/v6/session",
 	}
 
+	clearPassword, err := obscure.Reveal(f.opt.Password)
+	if err != nil {
+		return nil, err
+	}
 	authRequest := api.AuthenticateRequest{
 		Login:    f.opt.Username,
-		Password: f.opt.Password,
+		Password: clearPassword,
 	}
 
 	err = f.pacer.Call(func() (bool, error) {
diff --git a/cmd/bisync/resolve.go b/cmd/bisync/resolve.go
index e9c57a7d0..534c81db7 100644
--- a/cmd/bisync/resolve.go
+++ b/cmd/bisync/resolve.go
@@ -120,14 +120,16 @@ func (b *bisyncRun) setResolveDefaults(ctx context.Context) error {
 	return nil
 }
 
-type renames map[string]renamesInfo // [originalName]newName (remember the originalName may have an alias)
-// the newName may be the same as the old name (if winner), but should not be blank, unless we're deleting.
-// the oldNames may not match each other, if we're normalizing case or unicode
-// all names should be "remotes" (relative names, without base path)
-type renamesInfo struct {
-	path1 namePair
-	path2 namePair
-}
+type (
+	renames map[string]renamesInfo // [originalName]newName (remember the originalName may have an alias)
+	// the newName may be the same as the old name (if winner), but should not be blank, unless we're deleting.
+	// the oldNames may not match each other, if we're normalizing case or unicode
+	// all names should be "remotes" (relative names, without base path)
+	renamesInfo struct {
+		path1 namePair
+		path2 namePair
+	}
+)
 type namePair struct {
 	oldName string
 	newName string
@@ -395,26 +397,26 @@ func (b *bisyncRun) resolveNewerOlder(t1, t2 time.Time, remote1, remote2 string,
 	}
 	if t1.After(t2) {
 		if prefer == PreferNewer {
-			fs.Infof(remote1, "Path1 is newer. Path1: %v, Path2: %v, Difference: %s", t1, t2, t1.Sub(t2))
+			fs.Infof(remote1, "Path1 is newer. Path1: %v, Path2: %v, Difference: %s", t1.Local(), t2.Local(), t1.Sub(t2))
 			return 1
 		} else if prefer == PreferOlder {
-			fs.Infof(remote1, "Path2 is older. Path1: %v, Path2: %v, Difference: %s", t1, t2, t1.Sub(t2))
+			fs.Infof(remote1, "Path2 is older. Path1: %v, Path2: %v, Difference: %s", t1.Local(), t2.Local(), t1.Sub(t2))
 			return 2
 		}
 	} else if t1.Before(t2) {
 		if prefer == PreferNewer {
-			fs.Infof(remote1, "Path2 is newer. Path1: %v, Path2: %v, Difference: %s", t1, t2, t2.Sub(t1))
+			fs.Infof(remote1, "Path2 is newer. Path1: %v, Path2: %v, Difference: %s", t1.Local(), t2.Local(), t2.Sub(t1))
 			return 2
 		} else if prefer == PreferOlder {
-			fs.Infof(remote1, "Path1 is older. Path1: %v, Path2: %v, Difference: %s", t1, t2, t2.Sub(t1))
+			fs.Infof(remote1, "Path1 is older. Path1: %v, Path2: %v, Difference: %s", t1.Local(), t2.Local(), t2.Sub(t1))
 			return 1
 		}
 	}
 	if t1.Equal(t2) {
-		fs.Infof(remote1, "Winner cannot be determined as times are equal. Path1: %v, Path2: %v, Difference: %s", t1, t2, t2.Sub(t1))
+		fs.Infof(remote1, "Winner cannot be determined as times are equal. Path1: %v, Path2: %v, Difference: %s", t1.Local(), t2.Local(), t2.Sub(t1))
 		return 0
 	}
-	fs.Errorf(remote1, "Winner cannot be determined. Path1: %v, Path2: %v", t1, t2) // shouldn't happen unless prefer is of wrong type
+	fs.Errorf(remote1, "Winner cannot be determined. Path1: %v, Path2: %v", t1.Local(), t2.Local()) // shouldn't happen unless prefer is of wrong type
 	return 0
 }
 
diff --git a/docs/content/authors.md b/docs/content/authors.md
index 29018e7c1..f3930bc05 100644
--- a/docs/content/authors.md
+++ b/docs/content/authors.md
@@ -836,3 +836,5 @@ put them back in again.` >}}
   * Vitaly <9034218+gvitali@users.noreply.github.com>
   * iotmaestro <iotmaestro@proton.me>
   * psychopatt <66741203+psychopatt@users.noreply.github.com>
+  * Alex Garel <alex@garel.org>
+  * Warrentheo <warrentheo@hotmail.com>
diff --git a/docs/content/s3.md b/docs/content/s3.md
index eb9089406..dde37558d 100644
--- a/docs/content/s3.md
+++ b/docs/content/s3.md
@@ -2377,6 +2377,8 @@ acl = private
 Now run `rclone lsf r2:` to see your buckets and `rclone lsf
 r2:bucket` to look within a bucket.
 
+For R2 tokens with the "Object Read & Write" permission, you may also need to add `no_check_bucket = true` for object uploads to work correctly.
+
 ### Dreamhost
 
 Dreamhost [DreamObjects](https://www.dreamhost.com/cloud/storage/) is
@@ -3359,10 +3361,11 @@ endpoint = s3.nl-ams.scw.cloud
 access_key_id = SCWXXXXXXXXXXXXXX
 secret_access_key = 1111111-2222-3333-44444-55555555555555
 region = nl-ams
-location_constraint =
+location_constraint = nl-ams
 acl = private
-server_side_encryption =
-storage_class =
+upload_cutoff = 5M
+chunk_size = 5M
+copy_cutoff = 5M
 ```
 
 [C14 Cold Storage](https://www.online.net/en/storage/c14-cold-storage) is the low-cost S3 Glacier alternative from Scaleway and it works the same way as on S3 by accepting the "GLACIER" `storage_class`.
diff --git a/docs/content/ulozto.md b/docs/content/ulozto.md
index cc7bc7d25..e98da5418 100644
--- a/docs/content/ulozto.md
+++ b/docs/content/ulozto.md
@@ -10,7 +10,7 @@ Paths are specified as `remote:path`
 
 Paths may be as deep as required, e.g. `remote:directory/subdirectory`.
 
-The initial setup for Box involves filling in the user credentials. 
+The initial setup for Uloz.to involves filling in the user credentials. 
 `rclone config` walks you through it.
 
 ## Configuration
@@ -49,8 +49,15 @@ username> user
 
 Option password.
 The password for the user.
-Enter a value. Press Enter to leave empty.
-password> secretPassword
+Choose an alternative below. Press Enter for the default (n).
+y) Yes, type in my own password
+g) Generate random password
+n) No, leave this optional password blank (default)
+y/g/n> y
+Enter the password:
+password:
+Confirm the password:
+password:
 
 Edit advanced config?
 y) Yes
@@ -79,8 +86,9 @@ To copy a local folder to a Uloz.to folder called backup:
     rclone copy /home/source remote:backup
 
 ### User credentials
+
 The only reliable method is to authenticate the user using 
-username and password. Uloz.to offers anb API key as well, but 
+username and password. Uloz.to offers an API key as well, but 
 it's reserved for the use of Uloz.to's in-house application
 and using it in different circumstances is unreliable. 
 
@@ -114,7 +122,7 @@ as they can't be used in JSON strings.
 All files are currently uploaded using a single HTTP request, so 
 for uploading large files a stable connection is necessary.  Rclone will
 upload up to `--transfers` chunks at the same time (shared among all 
-uploads).  
+uploads).
 
 ### Deleting files
 
@@ -136,7 +144,7 @@ to a specific folder hierarchy.
 
 In order to do this you will have to find the `Folder slug` of the
 folder you wish to use as root.  This will be the last segment
-of the URL when you open the relevant folder in the Box web
+of the URL when you open the relevant folder in the Uloz.to web
 interface.
 
 For example, for exploring a folder with URL 
@@ -154,7 +162,8 @@ Here are the Standard options specific to ulozto (Uloz.to).
 
 #### --ulozto-app-token
 
-The application token identifying the app. An app API key can be either found in the API doc https://uloz.to/upload-resumable-api-beta or obtained from customer service.
+The application token identifying the app. An app API key can be either found in the API
+doc https://uloz.to/upload-resumable-api-beta or obtained from customer service.
 
 Properties:
 
@@ -178,6 +187,8 @@ Properties:
 
 The password for the user.
 
+**NB** Input to this must be obscured - see [rclone obscure](/commands/rclone_obscure/).
+
 Properties:
 
 - Config:      password
@@ -191,7 +202,9 @@ Here are the Advanced options specific to ulozto (Uloz.to).
 
 #### --ulozto-root-folder-slug
 
-If set, rclone will use this folder as the root folder for all operations. For example, if the slug identifies 'foo/bar/', 'ulozto:baz' is equivalent to 'ulozto:foo/bar/baz' without any root slug set.
+If set, rclone will use this folder as the root folder for all operations. For example,
+if the slug identifies 'foo/bar/', 'ulozto:baz' is equivalent to 'ulozto:foo/bar/baz' without
+any root slug set.
 
 Properties:
 
@@ -224,6 +237,17 @@ Properties:
 - Type:        Encoding
 - Default:     Slash,BackSlash,Del,Ctl,InvalidUtf8,Dot
 
+#### --ulozto-description
+
+Description of the remote
+
+Properties:
+
+- Config:      description
+- Env Var:     RCLONE_ULOZTO_DESCRIPTION
+- Type:        string
+- Required:    false
+
 {{< rem autogenerated options stop >}}
 
 ## Limitations
@@ -245,4 +269,4 @@ exposed in the API.  Backends without this capability cannot determine
 free space for an rclone mount or use policy `mfs` (most free space) 
 as a member of an rclone union remote.
 
-See [List of backends that do not support rclone about](https://rclone.org/overview/#optional-features) and [rclone about](https://rclone.org/commands/rclone_about/)
\ No newline at end of file
+See [List of backends that do not support rclone about](https://rclone.org/overview/#optional-features) and [rclone about](https://rclone.org/commands/rclone_about/)
diff --git a/fs/config/ui.go b/fs/config/ui.go
index c58fb92e4..694548b71 100644
--- a/fs/config/ui.go
+++ b/fs/config/ui.go
@@ -495,7 +495,7 @@ func ChooseOption(o *fs.Option, name string) string {
 		case uint, byte, uint16, uint32, uint64:
 			what = "unsigned integer"
 		default:
-			what = fmt.Sprintf("%T value", o.Default)
+			what = fmt.Sprintf("value of type %s", o.Type())
 		}
 	}
 	var in string
diff --git a/fs/operations/copy.go b/fs/operations/copy.go
index e04b8a9e4..45fb3f64a 100644
--- a/fs/operations/copy.go
+++ b/fs/operations/copy.go
@@ -98,9 +98,9 @@ func (c *copy) checkPartial() (remoteForCopy string, inplace bool, err error) {
 	// Avoid making the leaf name longer if it's already lengthy to avoid
 	// trouble with file name length limits.
 	suffix := "." + random.String(8) + c.ci.PartialSuffix
-	base := path.Base(c.remoteForCopy)
+	base := path.Base(remoteForCopy)
 	if len(base) > 100 {
-		remoteForCopy = TruncateString(c.remoteForCopy, len(c.remoteForCopy)-len(suffix)) + suffix
+		remoteForCopy = TruncateString(remoteForCopy, len(remoteForCopy)-len(suffix)) + suffix
 	} else {
 		remoteForCopy += suffix
 	}
diff --git a/fs/registry.go b/fs/registry.go
index ddabfd6df..39caca9e3 100644
--- a/fs/registry.go
+++ b/fs/registry.go
@@ -22,7 +22,7 @@ var Registry []*RegInfo
 // optDescription is a basic description option
 var optDescription = Option{
 	Name:     "description",
-	Help:     "Description of the remote",
+	Help:     "Description of the remote.",
 	Advanced: true,
 }
 


Test patch:
diff --git a/cmd/bisync/bisync_test.go b/cmd/bisync/bisync_test.go
index ac4e34429..b0ede2b41 100644
--- a/cmd/bisync/bisync_test.go
+++ b/cmd/bisync/bisync_test.go
@@ -66,7 +66,7 @@ var initDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, bisync.TZ)
 // go test ./cmd/bisync -remote local -golden
 // go test ./cmd/bisync -remote local -case extended_filenames
 // go run ./fstest/test_all -run '^TestBisync.*$' -timeout 3h -verbose -maxtries 5
-// go run ./fstest/test_all -remotes local,TestCrypt:,TestDrive:,TestOneDrive:,TestOneDriveBusiness:,TestDropbox:,TestCryptDrive:,TestOpenDrive:,TestChunker:,:memory:,TestCryptNoEncryption:,TestCombine:DirA,TestFTPRclone:,TestWebdavRclone:,TestS3Rclone:,TestSFTPRclone:,TestSFTPRcloneSSH: -run '^TestBisync.*$' -timeout 3h -verbose -maxtries 5
+// go run ./fstest/test_all -remotes local,TestCrypt:,TestDrive:,TestOneDrive:,TestOneDriveBusiness:,TestDropbox:,TestCryptDrive:,TestOpenDrive:,TestChunker:,:memory:,TestCryptNoEncryption:,TestCombine:DirA,TestFTPRclone:,TestWebdavRclone:,TestS3Rclone:,TestSFTPRclone:,TestSFTPRcloneSSH:,TestNextcloud:,TestChunkerNometaLocal:,TestChunkerChunk3bLocal:,TestChunkerLocal:,TestChunkerChunk3bNometaLocal:,TestStorj: -run '^TestBisync.*$' -timeout 3h -verbose -maxtries 5
 // go test -timeout 3h -run '^TestBisync.*$' github.com/rclone/rclone/cmd/bisync -remote TestDrive:Bisync -v
 // go test -timeout 3h -run '^TestBisyncRemoteRemote/basic$' github.com/rclone/rclone/cmd/bisync -remote TestDropbox:Bisync -v
 
@@ -93,6 +93,8 @@ var logReplacements = []string{
 	`^INFO  : .*?: Crypt detected! Using cryptcheck instead of check. \(Use --size-only or --ignore-checksum to disable\)$`, dropMe,
 	// ignore drive info messages
 	`^NOTICE:.*?Files of unknown size \(such as Google Docs\) do not sync reliably with --checksum or --size-only\. Consider using modtime instead \(the default\) or --drive-skip-gdocs.*?$`, dropMe,
+	// ignore cache backend cache expired messages
+	`^INFO  : .*cache expired.*$`, dropMe,
 	// ignore differences in backend features
 	`^.*?"HashType1":.*?$`, dropMe,
 	`^.*?"HashType2":.*?$`, dropMe,
@@ -254,7 +256,7 @@ func testBisync(t *testing.T, path1, path2 string) {
 
 	baseDir, err := os.Getwd()
 	require.NoError(t, err, "get current directory")
-	randName := "bisync." + time.Now().Format("150405-") + random.String(5)
+	randName := "bisync-" + time.Now().Format("150405-") + random.String(5) // AzureBlob doesn't like dots
 	tempDir := filepath.Join(os.TempDir(), randName)
 	workDir := filepath.Join(tempDir, "workdir")
 
@@ -319,7 +321,8 @@ func testBisync(t *testing.T, path1, path2 string) {
 		testCase = strings.ReplaceAll(testCase, "-", "_")
 		testCase = strings.TrimPrefix(testCase, "test_")
 		t.Run(testCase, func(childTest *testing.T) {
-			b.runTestCase(ctx, childTest, testCase)
+			bCopy := *b
+			bCopy.runTestCase(ctx, childTest, testCase)
 		})
 	}
 }
@@ -366,13 +369,58 @@ func (b *bisyncTest) runTestCase(ctx context.Context, t *testing.T, testCase str
 	})
 	require.NoError(b.t, err, "jamming initial dates")
 
+	// copy to a new unique initdir and datadir so concurrent tests don't interfere with each other
+	ctxNoDsStore, _ := ctxNoDsStore(ctx, b.t)
+	makeUnique := func(label, oldPath string) (newPath string) {
+		newPath = oldPath
+		info, err := os.Stat(oldPath)
+		if err == nil && info.IsDir() { // datadir is optional
+			oldFs, err := cache.Get(ctx, oldPath)
+			require.NoError(b.t, err)
+			newPath = b.tempDir + "/" + label + "/" + "test_" + b.testCase + "-" + random.String(8)
+			newFs, err := cache.Get(ctx, newPath)
+			require.NoError(b.t, err)
+			require.NoError(b.t, sync.CopyDir(ctxNoDsStore, newFs, oldFs, true), "setting up "+label)
+		}
+		return newPath
+	}
+	b.initDir = makeUnique("initdir", b.initDir)
+	b.dataDir = makeUnique("datadir", b.dataDir)
+
 	// Prepare initial content
 	b.cleanupCase(ctx)
+	fstest.CheckListingWithPrecision(b.t, b.fs1, []fstest.Item{}, []string{}, b.fs1.Precision()) // verify starting from empty
+	fstest.CheckListingWithPrecision(b.t, b.fs2, []fstest.Item{}, []string{}, b.fs2.Precision())
 	initFs, err := cache.Get(ctx, b.initDir)
 	require.NoError(b.t, err)
-	ctxNoDsStore, _ := ctxNoDsStore(ctx, b.t)
-	require.NoError(b.t, sync.CopyDir(ctxNoDsStore, b.fs1, initFs, true), "setting up path1")
-	require.NoError(b.t, sync.CopyDir(ctxNoDsStore, b.fs2, initFs, true), "setting up path2")
+
+	// verify pre-test equality (garbage in, garbage out!)
+	srcObjs, srcDirs, err := walk.GetAll(ctxNoDsStore, initFs, "", false, -1)
+	assert.NoError(b.t, err)
+	items := []fstest.Item{}
+	for _, obj := range srcObjs {
+		require.False(b.t, strings.Contains(obj.Remote(), ".partial"))
+		rc, err := operations.Open(ctxNoDsStore, obj)
+		assert.NoError(b.t, err)
+		bytes := make([]byte, obj.Size())
+		_, err = rc.Read(bytes)
+		assert.NoError(b.t, err)
+		assert.NoError(b.t, rc.Close())
+		item := fstest.NewItem(norm.NFC.String(obj.Remote()), string(bytes), obj.ModTime(ctxNoDsStore))
+		items = append(items, item)
+	}
+	dirs := []string{}
+	for _, dir := range srcDirs {
+		dirs = append(dirs, norm.NFC.String(dir.Remote()))
+	}
+	log.Printf("checking initFs %s", initFs)
+	fstest.CheckListingWithPrecision(b.t, initFs, items, dirs, initFs.Precision())
+	checkError(b.t, sync.CopyDir(ctxNoDsStore, b.fs1, initFs, true), "setting up path1")
+	log.Printf("checking Path1 %s", b.fs1)
+	fstest.CheckListingWithPrecision(b.t, b.fs1, items, dirs, b.fs1.Precision())
+	checkError(b.t, sync.CopyDir(ctxNoDsStore, b.fs2, initFs, true), "setting up path2")
+	log.Printf("checking path2 %s", b.fs2)
+	fstest.CheckListingWithPrecision(b.t, b.fs2, items, dirs, b.fs2.Precision())
 
 	// Create log file
 	b.mkdir(b.workDir)
@@ -479,12 +527,12 @@ func (b *bisyncTest) runTestCase(ctx context.Context, t *testing.T, testCase str
 // if a local path is provided, it's ignored (the test will run under system temp)
 func (b *bisyncTest) makeTempRemote(ctx context.Context, remote, subdir string) (f, parent fs.Fs, path, canon string) {
 	var err error
-	if bilib.IsLocalPath(remote) && !strings.HasPrefix(remote, ":") {
+	if bilib.IsLocalPath(remote) && !strings.HasPrefix(remote, ":") && !strings.Contains(remote, ",") {
 		if remote != "" && remote != "local" {
 			b.t.Fatalf(`Missing ":" in remote %q. Use "local" to test with local filesystem.`, remote)
 		}
 		parent, err = cache.Get(ctx, b.tempDir)
-		require.NoError(b.t, err, "parsing %s", b.tempDir)
+		checkError(b.t, err, "parsing local tempdir %s", b.tempDir)
 
 		path = filepath.Join(b.tempDir, b.testCase)
 		canon = bilib.CanonicalPath(path) + "_"
@@ -496,7 +544,8 @@ func (b *bisyncTest) makeTempRemote(ctx context.Context, remote, subdir string)
 		}
 		remote += b.randName
 		parent, err = cache.Get(ctx, remote)
-		require.NoError(b.t, err, "parsing %s", remote)
+		checkError(b.t, err, "parsing remote %s", remote)
+		checkError(b.t, operations.Mkdir(ctx, parent, subdir), "Mkdir "+subdir) // ensure dir exists (storj seems to need this)
 
 		path = remote + "/" + b.testCase
 		canon = bilib.CanonicalPath(path) + "_"
@@ -504,7 +553,7 @@ func (b *bisyncTest) makeTempRemote(ctx context.Context, remote, subdir string)
 	}
 
 	f, err = cache.Get(ctx, path)
-	require.NoError(b.t, err, "parsing %s/%s", remote, subdir)
+	checkError(b.t, err, "parsing remote/subdir %s/%s", remote, subdir)
 	path = bilib.FsPath(f) // Make it canonical
 	return
 }
@@ -885,9 +934,11 @@ func (b *bisyncTest) checkPreReqs(ctx context.Context, opt *bisync.Options) (con
 
 	if b.testCase == "normalization" || b.testCase == "extended_char_paths" || b.testCase == "extended_filenames" {
 		// test whether remote is capable of running test
-		const chars = "ě_{chr:81}{chr:fe}{spc}áñhࢺ_測試Русский_ěáñ👸🏼🧝🏾‍♀️💆🏿‍♂️🐨🤙🏼🤮🧑🏻‍🔧🧑‍🔬éö"
+		const chars = "ě{chr:81}{chr:fe}{spc}áñhࢺ_測試Рускийěáñ👸🏼🧝🏾‍♀️💆🏿‍♂️🐨🤙🏼🤮🧑🏻‍🔧🧑‍🔬éö"
 		testfilename1 := splitLine(norm.NFD.String(norm.NFC.String(chars)))[0]
 		testfilename2 := splitLine(norm.NFC.String(norm.NFD.String(chars)))[0]
+		tempDir, err := cache.Get(ctx, b.tempDir)
+		require.NoError(b.t, err)
 		preTest := func(f fs.Fs, testfilename string) string {
 			in := bytes.NewBufferString(testfilename)
 			objinfo := object.NewStaticObjectInfo(testfilename, initDate, int64(len(testfilename)), true, nil, nil)
@@ -902,8 +953,21 @@ func (b *bisyncTest) checkPreReqs(ctx context.Context, opt *bisync.Options) (con
 				// we can still deal with this as long as both remotes auto-convert the same way.
 				b.t.Logf("Warning: this remote seems to auto-convert special characters (testcase: %s) (expected: \n%s (%s) actual: \n%s (%s))\n (fs: %s) \n%v", b.testCase, testfilename, detectEncoding(testfilename), entries[0].Remote(), detectEncoding(entries[0].Remote()), f, diffStr)
 			}
-			err = obj.Remove(ctx)
+			// test whether we can fix-case
+			ctxFixCase, ci := fs.AddConfig(ctx)
+			ci.FixCase = true
+			transformedName := strings.ToLower(obj.Remote())
+			src, err := tempDir.Put(ctx, in, object.NewStaticObjectInfo(transformedName, initDate, int64(len(transformedName)), true, nil, nil)) // local
 			require.NoError(b.t, err)
+			upperObj, err := operations.Copy(ctxFixCase, f, nil, transformedName, src)
+			if err != nil || upperObj.Remote() != transformedName {
+				b.t.Skipf("Fs is incapable of running test as can't fix-case, skipping: %s (expected: \n%s (%s) actual: \n%s (%v))\n (fs: %s) \n", b.testCase, transformedName, detectEncoding(transformedName), upperObj.Remote(), err, f)
+			}
+			require.NoError(b.t, src.Remove(ctx))
+			require.NoError(b.t, obj.Remove(ctx))
+			if obj != nil {
+				require.NoError(b.t, upperObj.Remove(ctx))
+			}
 			return entries[0].Remote()
 		}
 		got1 := preTest(b.fs1, testfilename1)
@@ -1444,6 +1508,9 @@ func (b *bisyncTest) mangleResult(dir, file string, golden bool) string {
 	lines := strings.Split(s, eol)
 	pathReplacer := b.newReplacer(true)
 
+	if b.fs1.Hashes() == hash.Set(hash.None) || b.fs2.Hashes() == hash.Set(hash.None) {
+		logReplacements = append(logReplacements, `^.*{hashtype} differ.*$`, dropMe)
+	}
 	rep := logReplacements
 	if b.testCase == "dry_run" {
 		rep = append(rep, dryrunReplacements...)
@@ -1799,7 +1866,16 @@ func ctxNoDsStore(ctx context.Context, t *testing.T) (context.Context, *filter.F
 	ctxNoDsStore, fi := filter.AddConfig(ctx)
 	err := fi.AddRule("- .DS_Store")
 	require.NoError(t, err)
+	err = fi.AddRule("- *.partial")
+	require.NoError(t, err)
 	err = fi.AddRule("+ **")
 	require.NoError(t, err)
 	return ctxNoDsStore, fi
 }
+
+func checkError(t *testing.T, err error, msgAndArgs ...interface{}) {
+	if errors.Is(err, fs.ErrorCantUploadEmptyFiles) {
+		t.Skipf("Skip test because remote cannot upload empty files")
+	}
+	assert.NoError(t, err, msgAndArgs...)
+}
diff --git a/cmd/bisync/testdata/bisync_vscode_debuggers_launch.json b/cmd/bisync/testdata/bisync_vscode_debuggers_launch.json
index 8cd1eb198..cdf9c83ba 100644
--- a/cmd/bisync/testdata/bisync_vscode_debuggers_launch.json
+++ b/cmd/bisync/testdata/bisync_vscode_debuggers_launch.json
@@ -58537,6 +58537,654 @@
 			"program": "./cmd/bisync",
 			"args": ["-remote", "TestQuatrix:", "-remote2", "TestQuatrix:", "-case", "test_volatile", "-no-cleanup"]
 		},
+		{
+			"name": "Test TestUlozto: test_all_changed LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_all_changed", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_all_changed RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_all_changed", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_all_changed RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_all_changed", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_backupdir LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_backupdir", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_backupdir RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_backupdir", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_backupdir RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_backupdir", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_basic LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_basic", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_basic RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_basic", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_basic RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_basic", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_changes LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_changes", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_changes RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_changes", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_changes RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_changes", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_access LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_check_access", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_access RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_check_access", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_access RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_check_access", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_access_filters LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_check_access_filters", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_access_filters RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_check_access_filters", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_access_filters RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_check_access_filters", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_filename LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_check_filename", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_filename RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_check_filename", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_filename RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_check_filename", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_sync LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_check_sync", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_sync RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_check_sync", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_check_sync RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_check_sync", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_compare_all LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_compare_all", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_compare_all RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_compare_all", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_compare_all RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_compare_all", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_createemptysrcdirs LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_createemptysrcdirs", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_createemptysrcdirs RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_createemptysrcdirs", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_createemptysrcdirs RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_createemptysrcdirs", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_dry_run LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_dry_run", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_dry_run RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_dry_run", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_dry_run RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_dry_run", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_equal LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_equal", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_equal RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_equal", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_equal RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_equal", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_extended_char_paths LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_extended_char_paths", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_extended_char_paths RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_extended_char_paths", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_extended_char_paths RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_extended_char_paths", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_extended_filenames LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_extended_filenames", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_extended_filenames RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_extended_filenames", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_extended_filenames RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_extended_filenames", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_filters LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_filters", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_filters RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_filters", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_filters RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_filters", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_filtersfile_checks LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_filtersfile_checks", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_filtersfile_checks RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_filtersfile_checks", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_filtersfile_checks RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_filtersfile_checks", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_ignorelistingchecksum LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_ignorelistingchecksum", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_ignorelistingchecksum RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_ignorelistingchecksum", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_ignorelistingchecksum RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_ignorelistingchecksum", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_max_delete_path1 LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_max_delete_path1", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_max_delete_path1 RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_max_delete_path1", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_max_delete_path1 RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_max_delete_path1", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_max_delete_path2_force LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_max_delete_path2_force", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_max_delete_path2_force RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_max_delete_path2_force", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_max_delete_path2_force RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_max_delete_path2_force", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_nomodtime LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_nomodtime", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_nomodtime RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_nomodtime", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_nomodtime RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_nomodtime", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_normalization LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_normalization", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_normalization RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_normalization", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_normalization RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_normalization", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_rclone_args LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_rclone_args", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_rclone_args RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_rclone_args", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_rclone_args RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_rclone_args", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resolve LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_resolve", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resolve RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_resolve", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resolve RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_resolve", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resync LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_resync", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resync RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_resync", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resync RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_resync", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resync_modes LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_resync_modes", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resync_modes RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_resync_modes", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_resync_modes RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_resync_modes", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_rmdirs LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_rmdirs", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_rmdirs RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_rmdirs", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_rmdirs RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_rmdirs", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_volatile LocalRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "local", "-remote2", "TestUlozto:", "-case", "test_volatile", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_volatile RemoteLocal",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "local", "-case", "test_volatile", "-no-cleanup"]
+		},
+		{
+			"name": "Test TestUlozto: test_volatile RemoteRemote",
+			"type": "go",
+			"request": "launch",
+			"mode": "test",
+			"program": "./cmd/bisync",
+			"args": ["-remote", "TestUlozto:", "-remote2", "TestUlozto:", "-case", "test_volatile", "-no-cleanup"]
+		},
 
     ]
 }
\ No newline at end of file
diff --git a/fs/operations/copy_test.go b/fs/operations/copy_test.go
index f309b0060..73aa5e05a 100644
--- a/fs/operations/copy_test.go
+++ b/fs/operations/copy_test.go
@@ -5,6 +5,9 @@ import (
 	"crypto/rand"
 	"errors"
 	"fmt"
+	"os"
+	"path"
+	"sort"
 	"strings"
 	"testing"
 
@@ -121,6 +124,46 @@ func TestCopyFile(t *testing.T) {
 	r.CheckRemoteItems(t, file2)
 }
 
+// Find the longest file name for writing to local
+func maxLengthFileName(t *testing.T, r *fstest.Run) string {
+	require.NoError(t, r.Flocal.Mkdir(context.Background(), "")) // create the root
+	const maxLen = 16 * 1024
+	name := strings.Repeat("A", maxLen)
+	i := sort.Search(len(name), func(i int) (fail bool) {
+		filePath := path.Join(r.LocalName, name[:i])
+		err := os.WriteFile(filePath, []byte{0}, 0777)
+		if err != nil {
+			return true
+		}
+		err = os.Remove(filePath)
+		if err != nil {
+			t.Logf("Failed to remove test file: %v", err)
+		}
+		return false
+	})
+	return name[:i-1]
+}
+
+// Check we can copy a file of maximum name length
+func TestCopyLongFile(t *testing.T) {
+	ctx := context.Background()
+	r := fstest.NewRun(t)
+	if !r.Fremote.Features().IsLocal {
+		t.Skip("Test only runs on local")
+	}
+
+	// Find the maximum length of file we can write
+	name := maxLengthFileName(t, r)
+	t.Logf("Max length of file name is %d", len(name))
+	file1 := r.WriteFile(name, "file1 contents", t1)
+	r.CheckLocalItems(t, file1)
+
+	err := operations.CopyFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
+	require.NoError(t, err)
+	r.CheckLocalItems(t, file1)
+	r.CheckRemoteItems(t, file1)
+}
+
 func TestCopyFileBackupDir(t *testing.T) {
 	ctx := context.Background()
 	ctx, ci := fs.AddConfig(ctx)