Repository: rclone/rclone (language: go)

Issue:
bisync: modtime of latest/updated file gets changed matching modtime of the previous revision
#### What is the problem you are having with rclone?

Using `bisync`, where the remote is Synology NAS SFTP, the modification time of file downloaded file is sometimes not updated properly on one of the nodes and eventually the "old" modtime gets propagated to all the nodes.

Note that the content / checksum of the file is correctly propagated as expected. The confusion comes from the fact that the modtime of the file ends up being the date of the previous revision of the file, and that is propagated to all the nodes.

After spending some time on this, I got back to a working behavior by changing the pattern to prevent pulling any conflict files and removing rclone generated conflict files on all my nodes. In other words, the bisync pattern I used was pulling conflict files wich would themselves conflict with other conflict files from other nodes. I didn't care much about those files as I could confirm the main file(s) were ok.

However, my understanding of the problem is that those conflicts would somehow interfere with the bisync operation in a way that made the moditfication time not be updated while the content of the file was.

I understand that bisync is in beta and I'm happy to report that the feature is useful to me.

```
# on amd64 Linux (Ubuntu)
$ stat ~/REDACTED
  File: /home/XXX/REDACTED
  Size: 98584     	Blocks: 200        IO Block: 4096   regular file
Device: 252,1	Inode: 16664833    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/REDACTED)   Gid: ( 1000/REDACTED)
Access: 2025-02-03 01:50:02.254891607 -0500
Modify: 2025-01-31 23:57:40.000000000 -0500
Change: 2025-02-03 01:35:01.522276874 -0500
 Birth: 2025-02-03 01:35:01.503276785 -0500
```

```
# in termux (Android)
$ stat ~/REDACTED
  File: /data/data/com.termux/files/home/XXX/REDACTED
  Size: 98584     	Blocks: 200        IO Block: 4096   regular file
Device: 0,201	Inode: 83456       Links: 1
Access: (0660/-rw-rw----)  Uid: (10275/ u0_a275)   Gid: ( 1023/media_rw)
Context: u:object_r:fuse:s0
Access: 2025-02-03 00:38:52.000000000 -0500
Modify: 2025-01-31 23:57:40.483101109 -0500
Change: 2025-02-03 01:23:57.860059979 -0500
 Birth: -
```

#### What is your rclone version (output from `rclone version`)

```
rclone v1.69.0-termux
- os/version: unknown
- os/kernel: 5.15.123-android13-8-00033-g39606e3309fc-ab10974680 (aarch64)
- os/type: android
- os/arch: arm64 (ARMv8 compatible)
- go/version: go1.23.4
- go/linking: dynamic
- go/tags: noselfupdate
```

Note: I also use amd64 Linux clients. The client above was the one I notice that did not set proper modtime to the downloaded file. That bogus modtime somehow got propagated back.

#### Which OS you are using and how many bits (e.g. Windows 7, 64 bit)

arm64 Android, amd64 Linux

#### Which cloud storage system are you using? (e.g. Google Drive)

SFTP (Synology NAS)

#### The command you were trying to run (e.g. `rclone copy /tmp remote:tmp`)

```
rclone bisync REDACTED:PATH /data/data/com.termux/files/home/REDACTED --include /REDACTED* --compare size,modtime,checksum --resilient --recover --max-lock 2m --conflict-resolve newer --verbose
```
config:
```
[REDACTED]
type = sftp
host = REDACTED
user = REDACTED
port = REDACTED
pass = REDACTED
path_override = /volume1/
description = REDACTED
shell_type = unix
md5sum_command = none
sha1sum_command = none
```

#### A log from the command with the `-vv` flag (e.g. output from `rclone -vv copy /tmp remote:tmp`)

I think I provided above insightful information on the problem. If you feel however you still need my help to reproduce the issue, I will take the time top reproduce the issue, this time with the log level you request as it seemed I could reproduce  the issue systematically.

#### How to use GitHub

* Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are affected by the same issue.
* Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
* Subscribe to receive notifications on status change and new comments.



Discussion / hints:
Hey! Thanks for reporting this. I am wondering if it might be the same issue someone else reported on the forum, which I am currently testing a fix for. Would you be able to test it and see if it solves your problem?

https://beta.rclone.org/branch/test-saltysyncs/v1.70.0-beta.8570.2341934d8.test-saltysyncs/

Source: https://github.com/rclone/rclone/commit/2341934d8ee396649745b266050f48b725a100de

> I understand that bisync is in beta and I'm happy to report that the feature is useful to me.

Glad to hear that 🙂 
Thanks for the quick response! 
..but I got unrelated issues with my sftp setup so I'm in the process of migrating to the S3 api (minio) - if - as it seems - the fix is not related to sftp then I'll be able to test it but my config will definitly have changed. I'll keep you posted.
Thanks. The fix is not specific to sftp, but your particular issue could be... so if possible, it would be ideal to rule that out by first confirming that you can reproduce the issue with your new config, and if so, then testing whether the fix solves it.
I just finished migrating the backend and I'm now using S3/minio. I can confirm the modtime keeps acting up.
```
Access: 2025-02-04 19:50:07.236072574 -0500
Modify: 2025-02-03 10:15:06.000000000 -0500
Change: 2025-02-04 19:50:07.225072558 -0500
 Birth: 2025-02-03 12:35:08.819823780 -0500
```

As can be seen, that file got updated Today, but the modtime reverted to yesterday in the bisync process.
I should be able to test the fix. I'll be back
BTW, no conflict files Today, so its probably not related to conflict files.
Well, I'm not a termux dev. So I can not easily test on termux as any termux app it seems need specific patching and compiling. I've installed the beta on my linux boxes but I'm not clear if this will be conclusive. My initial observation was that the issue originated from a termux node and back propagated to the other nodes. 
Update! We have a winner!
With the assumption from prior observations that the pb occured while pulling an update, and the fact that I could not update the termux nodes to 1.70 beta myself, I came up with that test :
So I disabled all but one of my termux nodes and initiated a change from that node. 
My termux nodes are running 1.69, my arm64/linux nodes are running v1.70.0-beta.8570.2341934d8.test-saltysyncs.
The update went as expected with the correct modtime on the linux nodes. 
Then I enabled another termux node running 1.69. and sure enough, the update occured, the modtime went backwards an propagated to the Linux nodes.

Please release 1.70 soon !!


@TheWhiteBug is there any chance you could do a `git bisect` to narrow down which change from v1.69.0 to the current beta fixes the problem? If we can work that out then we can put that in v1.69.1 which is due fairly soon.
> [@TheWhiteBug](https://github.com/TheWhiteBug) is there any chance you could do a `git bisect` to narrow down which change from v1.69.0 to the current beta fixes the problem? If we can work that out then we can put that in v1.69.1 which is due fairly soon.

@TheWhiteBug is talking about this fix: https://github.com/rclone/rclone/commit/2341934d8ee396649745b266050f48b725a100de which is on a dev branch, not `master`. 

It is not quite ready for merging yet as it needs some cleanup and I also wanted to add a test (ironically, recreating this issue will be harder than fixing it!)
@nielash wrote:
> [@TheWhiteBug](https://github.com/TheWhiteBug) is talking about this fix: [2341934](https://github.com/rclone/rclone/commit/2341934d8ee396649745b266050f48b725a100de) which is on a dev branch, not `master`.
> 
> It is not quite ready for merging yet as it needs some cleanup and I also wanted to add a test (ironically, recreating this issue will be harder than fixing it!)

Ah, thanks for clearing that up. I'd like to make the point release some time before 15th Feb so if it is ready by then I'll stick it in v1.69.1


Fix patch:
diff --git a/cmd/bisync/deltas.go b/cmd/bisync/deltas.go
index 1cb783d56ce3a..dde42e41413c3 100644
--- a/cmd/bisync/deltas.go
+++ b/cmd/bisync/deltas.go
@@ -286,7 +286,7 @@ func (b *bisyncRun) findDeltas(fctx context.Context, f fs.Fs, oldListing string,
 }
 
 // applyDeltas
-func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (changes1, changes2 bool, results2to1, results1to2 []Results, queues queues, err error) {
+func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (results2to1, results1to2 []Results, queues queues, err error) {
 	path1 := bilib.FsPath(b.fs1)
 	path2 := bilib.FsPath(b.fs2)
 
@@ -367,7 +367,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 		}
 	}
 
-	//if there are potential conflicts to check, check them all here (outside the loop) in one fell swoop
+	// if there are potential conflicts to check, check them all here (outside the loop) in one fell swoop
 	matches, err := b.checkconflicts(ctxCheck, filterCheck, b.fs1, b.fs2)
 
 	for _, file := range ds1.sort() {
@@ -392,7 +392,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 			} else if d2.is(deltaOther) {
 				b.indent("!WARNING", file, "New or changed in both paths")
 
-				//if files are identical, leave them alone instead of renaming
+				// if files are identical, leave them alone instead of renaming
 				if (dirs1.has(file) || dirs1.has(alias)) && (dirs2.has(file) || dirs2.has(alias)) {
 					fs.Infof(nil, "This is a directory, not a file. Skipping equality check and will not rename: %s", file)
 					ls1.getPut(file, skippedDirs1)
@@ -486,7 +486,6 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 
 	// Do the batch operation
 	if copy2to1.NotEmpty() && !b.InGracefulShutdown {
-		changes1 = true
 		b.indent("Path2", "Path1", "Do queued copies to")
 		ctx = b.setBackupDir(ctx, 1)
 		results2to1, err = b.fastCopy(ctx, b.fs2, b.fs1, copy2to1, "copy2to1")
@@ -498,12 +497,11 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 			return
 		}
 
-		//copy empty dirs from path2 to path1 (if --create-empty-src-dirs)
+		// copy empty dirs from path2 to path1 (if --create-empty-src-dirs)
 		b.syncEmptyDirs(ctx, b.fs1, copy2to1, dirs2, &results2to1, "make")
 	}
 
 	if copy1to2.NotEmpty() && !b.InGracefulShutdown {
-		changes2 = true
 		b.indent("Path1", "Path2", "Do queued copies to")
 		ctx = b.setBackupDir(ctx, 2)
 		results1to2, err = b.fastCopy(ctx, b.fs1, b.fs2, copy1to2, "copy1to2")
@@ -515,7 +513,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 			return
 		}
 
-		//copy empty dirs from path1 to path2 (if --create-empty-src-dirs)
+		// copy empty dirs from path1 to path2 (if --create-empty-src-dirs)
 		b.syncEmptyDirs(ctx, b.fs2, copy1to2, dirs1, &results1to2, "make")
 	}
 
@@ -523,7 +521,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 		if err = b.saveQueue(delete1, "delete1"); err != nil {
 			return
 		}
-		//propagate deletions of empty dirs from path2 to path1 (if --create-empty-src-dirs)
+		// propagate deletions of empty dirs from path2 to path1 (if --create-empty-src-dirs)
 		b.syncEmptyDirs(ctx, b.fs1, delete1, dirs1, &results2to1, "remove")
 	}
 
@@ -531,7 +529,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change
 		if err = b.saveQueue(delete2, "delete2"); err != nil {
 			return
 		}
-		//propagate deletions of empty dirs from path1 to path2 (if --create-empty-src-dirs)
+		// propagate deletions of empty dirs from path1 to path2 (if --create-empty-src-dirs)
 		b.syncEmptyDirs(ctx, b.fs2, delete2, dirs2, &results1to2, "remove")
 	}
 
diff --git a/cmd/bisync/operations.go b/cmd/bisync/operations.go
index 6a1d9d1faf6aa..75b36e8b0b331 100644
--- a/cmd/bisync/operations.go
+++ b/cmd/bisync/operations.go
@@ -359,8 +359,6 @@ func (b *bisyncRun) runLocked(octx context.Context) (err error) {
 
 	// Determine and apply changes to Path1 and Path2
 	noChanges := ds1.empty() && ds2.empty()
-	changes1 := false // 2to1
-	changes2 := false // 1to2
 	results2to1 := []Results{}
 	results1to2 := []Results{}
 
@@ -370,7 +368,7 @@ func (b *bisyncRun) runLocked(octx context.Context) (err error) {
 		fs.Infof(nil, "No changes found")
 	} else {
 		fs.Infof(nil, "Applying changes")
-		changes1, changes2, results2to1, results1to2, queues, err = b.applyDeltas(octx, ds1, ds2)
+		results2to1, results1to2, queues, err = b.applyDeltas(octx, ds1, ds2)
 		if err != nil {
 			if b.InGracefulShutdown && (err == context.Canceled || err == accounting.ErrorMaxTransferLimitReachedGraceful || strings.Contains(err.Error(), "context canceled")) {
 				fs.Infof(nil, "Ignoring sync error due to Graceful Shutdown: %v", err)
@@ -395,21 +393,11 @@ func (b *bisyncRun) runLocked(octx context.Context) (err error) {
 	}
 	b.saveOldListings()
 	// save new listings
-	// NOTE: "changes" in this case does not mean this run vs. last run, it means start of this run vs. end of this run.
-	// i.e. whether we can use the March lst-new as this side's lst without modifying it.
 	if noChanges {
 		b.replaceCurrentListings()
 	} else {
-		if changes1 || b.InGracefulShutdown { // 2to1
-			err1 = b.modifyListing(fctx, b.fs2, b.fs1, results2to1, queues, false)
-		} else {
-			err1 = bilib.CopyFileIfExists(b.newListing1, b.listing1)
-		}
-		if changes2 || b.InGracefulShutdown { // 1to2
-			err2 = b.modifyListing(fctx, b.fs1, b.fs2, results1to2, queues, true)
-		} else {
-			err2 = bilib.CopyFileIfExists(b.newListing2, b.listing2)
-		}
+		err1 = b.modifyListing(fctx, b.fs2, b.fs1, results2to1, queues, false) // 2to1
+		err2 = b.modifyListing(fctx, b.fs1, b.fs2, results1to2, queues, true)  // 1to2
 	}
 	if b.DebugName != "" {
 		l1, _ := b.loadListing(b.listing1)
diff --git a/docs/content/bisync.md b/docs/content/bisync.md
index 458f92eab7246..3aa0bae540efb 100644
--- a/docs/content/bisync.md
+++ b/docs/content/bisync.md
@@ -1815,6 +1815,9 @@ about _Unison_ and synchronization in general.
 
 ## Changelog
 
+### `v1.69.1`
+* Fixed an issue causing listings to not capture concurrent modifications under certain conditions
+
 ### `v1.68`
 * Fixed an issue affecting backends that round modtimes to a lower precision.
 


Test patch:
diff --git a/cmd/bisync/bisync_test.go b/cmd/bisync/bisync_test.go
index da89c3e20e10c..f154d4d2871a2 100644
--- a/cmd/bisync/bisync_test.go
+++ b/cmd/bisync/bisync_test.go
@@ -746,6 +746,16 @@ func (b *bisyncTest) runTestStep(ctx context.Context, line string) (err error) {
 	case "test-func":
 		b.TestFn = testFunc
 		return
+	case "concurrent-func":
+		b.TestFn = func() {
+			src := filepath.Join(b.dataDir, "file7.txt")
+			dst := "file1.txt"
+			err := b.copyFile(ctx, src, b.replaceHex(b.path2), dst)
+			if err != nil {
+				fs.Errorf(src, "error copying file: %v", err)
+			}
+		}
+		return
 	case "fix-names":
 		// in case the local os converted any filenames
 		ci.NoUnicodeNormalization = true
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.copy2to1.que b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.copy2to1.que
new file mode 100644
index 0000000000000..029f3e190ed2c
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.copy2to1.que
@@ -0,0 +1,1 @@
+"file1.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst
new file mode 100644
index 0000000000000..021dde2dc7589
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst
@@ -0,0 +1,10 @@
+# bisync listing v1 from test
+-      109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST"
+-       19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-new b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-new
new file mode 100644
index 0000000000000..021dde2dc7589
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-new
@@ -0,0 +1,10 @@
+# bisync listing v1 from test
+-      109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST"
+-       19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-old b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-old
new file mode 100644
index 0000000000000..021dde2dc7589
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-old
@@ -0,0 +1,10 @@
+# bisync listing v1 from test
+-      109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST"
+-       19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst
new file mode 100644
index 0000000000000..021dde2dc7589
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst
@@ -0,0 +1,10 @@
+# bisync listing v1 from test
+-      109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST"
+-       19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-new b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-new
new file mode 100644
index 0000000000000..021dde2dc7589
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-new
@@ -0,0 +1,10 @@
+# bisync listing v1 from test
+-      109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST"
+-       19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-old b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-old
new file mode 100644
index 0000000000000..021dde2dc7589
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-old
@@ -0,0 +1,10 @@
+# bisync listing v1 from test
+-      109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST"
+-       19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt"
+-        0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt"
diff --git a/cmd/bisync/testdata/test_concurrent/golden/test.log b/cmd/bisync/testdata/test_concurrent/golden/test.log
new file mode 100644
index 0000000000000..35f894aeda860
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/golden/test.log
@@ -0,0 +1,73 @@
+[36m(01)  :[0m [34mtest concurrent[0m
+
+[36m(02)  :[0m [34mtest initial bisync[0m
+[36m(03)  :[0m [34mbisync resync[0m
+INFO  : [2mSetting --ignore-listing-checksum as neither --checksum nor --compare checksum are set.[0m
+INFO  : Bisyncing with Comparison Settings:
+{
+"Modtime": true,
+"Size": true,
+"Checksum": false,
+"NoSlowHash": false,
+"SlowHashSyncOnly": false,
+"DownloadHash": false
+}
+INFO  : Synching Path1 "{path1/}" with Path2 "{path2/}"
+INFO  : Copying Path2 files to Path1
+INFO  : - [34mPath2[0m    [35mResync is copying files to[0m         - [36mPath1[0m
+INFO  : - [36mPath1[0m    [35mResync is copying files to[0m         - [36mPath2[0m
+INFO  : Resync updating listings
+INFO  : Validating listings for Path1 "{path1/}" vs Path2 "{path2/}"
+INFO  : [32mBisync successful[0m
+
+[36m(04)  :[0m [34mtest changed on one path - file1[0m
+[36m(05)  :[0m [34mtouch-glob 2001-01-02 {datadir/} file5R.txt[0m
+[36m(06)  :[0m [34mtouch-glob 2023-08-26 {datadir/} file7.txt[0m
+[36m(07)  :[0m [34mcopy-as {datadir/}file5R.txt {path2/} file1.txt[0m
+
+[36m(08)  :[0m [34mtest bisync with file changed during[0m
+[36m(09)  :[0m [34mconcurrent-func[0m
+[36m(10)  :[0m [34mbisync[0m
+INFO  : [2mSetting --ignore-listing-checksum as neither --checksum nor --compare checksum are set.[0m
+INFO  : Bisyncing with Comparison Settings:
+{
+"Modtime": true,
+"Size": true,
+"Checksum": false,
+"NoSlowHash": false,
+"SlowHashSyncOnly": false,
+"DownloadHash": false
+}
+INFO  : Synching Path1 "{path1/}" with Path2 "{path2/}"
+INFO  : Building Path1 and Path2 listings
+INFO  : Path1 checking for diffs
+INFO  : Path2 checking for diffs
+INFO  : - [34mPath2[0m    [35m[33mFile changed: [35msize (larger)[0m, [35mtime (newer)[0m[0m[0m - [36mfile1.txt[0m
+INFO  : Path2:    1 changes: [32m   0 new[0m, [33m   1 modified[0m, [31m   0 deleted[0m
+INFO  : ([33mModified[0m: [36m   1 newer[0m, [34m   0 older[0m, [36m   1 larger[0m, [34m   0 smaller[0m)
+INFO  : Applying changes
+INFO  : - [34mPath2[0m    [35m[32mQueue copy to[0m Path1[0m       - [36m{path1/}file1.txt[0m
+INFO  : - [34mPath2[0m    [35mDo queued copies to[0m                - [36mPath1[0m
+INFO  : Updating listings
+INFO  : Validating listings for Path1 "{path1/}" vs Path2 "{path2/}"
+INFO  : [32mBisync successful[0m
+
+[36m(11)  :[0m [34mbisync[0m
+INFO  : [2mSetting --ignore-listing-checksum as neither --checksum nor --compare checksum are set.[0m
+INFO  : Bisyncing with Comparison Settings:
+{
+"Modtime": true,
+"Size": true,
+"Checksum": false,
+"NoSlowHash": false,
+"SlowHashSyncOnly": false,
+"DownloadHash": false
+}
+INFO  : Synching Path1 "{path1/}" with Path2 "{path2/}"
+INFO  : Building Path1 and Path2 listings
+INFO  : Path1 checking for diffs
+INFO  : Path2 checking for diffs
+INFO  : No changes found
+INFO  : Updating listings
+INFO  : Validating listings for Path1 "{path1/}" vs Path2 "{path2/}"
+INFO  : [32mBisync successful[0m
diff --git a/cmd/bisync/testdata/test_concurrent/initial/RCLONE_TEST b/cmd/bisync/testdata/test_concurrent/initial/RCLONE_TEST
new file mode 100644
index 0000000000000..d8ca97c2a4dbe
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/initial/RCLONE_TEST
@@ -0,0 +1,1 @@
+This file is used for testing the health of rclone accesses to the local/remote file system.  Do not delete.
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file1.txt b/cmd/bisync/testdata/test_concurrent/initial/file1.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file2.txt b/cmd/bisync/testdata/test_concurrent/initial/file2.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file3.txt b/cmd/bisync/testdata/test_concurrent/initial/file3.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file4.txt b/cmd/bisync/testdata/test_concurrent/initial/file4.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file5.txt b/cmd/bisync/testdata/test_concurrent/initial/file5.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file6.txt b/cmd/bisync/testdata/test_concurrent/initial/file6.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file7.txt b/cmd/bisync/testdata/test_concurrent/initial/file7.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/initial/file8.txt b/cmd/bisync/testdata/test_concurrent/initial/file8.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/dummy.txt b/cmd/bisync/testdata/test_concurrent/modfiles/dummy.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file1.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file1.txt
new file mode 100644
index 0000000000000..464147f09c0c8
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file1.txt
@@ -0,0 +1,1 @@
+This file is newer
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file10.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file10.txt
new file mode 100644
index 0000000000000..464147f09c0c8
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file10.txt
@@ -0,0 +1,1 @@
+This file is newer
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file11.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file11.txt
new file mode 100644
index 0000000000000..464147f09c0c8
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file11.txt
@@ -0,0 +1,1 @@
+This file is newer
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file2.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file2.txt
new file mode 100644
index 0000000000000..0fd70321acd6b
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file2.txt
@@ -0,0 +1,1 @@
+Newer version
\ No newline at end of file
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file5L.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file5L.txt
new file mode 100644
index 0000000000000..43ceff1dbe273
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file5L.txt
@@ -0,0 +1,1 @@
+This file is newer and not equal to 5R
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file5R.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file5R.txt
new file mode 100644
index 0000000000000..a928fcf1382e0
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file5R.txt
@@ -0,0 +1,1 @@
+This file is newer and not equal to 5L
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file6.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file6.txt
new file mode 100644
index 0000000000000..464147f09c0c8
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file6.txt
@@ -0,0 +1,1 @@
+This file is newer
diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file7.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file7.txt
new file mode 100644
index 0000000000000..464147f09c0c8
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/modfiles/file7.txt
@@ -0,0 +1,1 @@
+This file is newer
diff --git a/cmd/bisync/testdata/test_concurrent/scenario.txt b/cmd/bisync/testdata/test_concurrent/scenario.txt
new file mode 100644
index 0000000000000..19ca9fcc2608f
--- /dev/null
+++ b/cmd/bisync/testdata/test_concurrent/scenario.txt
@@ -0,0 +1,15 @@
+test concurrent
+
+test initial bisync
+bisync resync
+
+test changed on one path - file1
+touch-glob 2001-01-02 {datadir/} file5R.txt
+touch-glob 2023-08-26 {datadir/} file7.txt
+copy-as {datadir/}file5R.txt {path2/} file1.txt
+
+test bisync with file changed during
+concurrent-func
+bisync
+
+bisync
\ No newline at end of file