Repository: syncthing/syncthing (language: go)

Issue:
[Android] Renaming file uppercase causes both files to be deleted
**What happened**
I renamed test.txt to TEST.txt and both files were deleted.

**What operating system, operating system version and version of Syncthing you are running**
jpc device:
v1.18.2, Linux (64-bit Intel/AMD)

**The same for other connected devices, where relevant**
jphone device:
Syncthing Version: v1.18.2
Syncthing-Android Version: 1.18.2

**Screenshot if the issue concerns something visible in the GUI**
![syncth](https://user-images.githubusercontent.com/39860768/134816763-6503c2e2-37fe-4449-8bd1-677a5a64cf62.png)


Discussion / hints:
Please post the config.xml from both sides. This should not happen with reasonable/default settings...
**Linux** device here
```
<configuration version="35">
    <folder id="reixg-dsqph" label="cours" path="/home/jerome/cours" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
        <filesystemType>basic</filesystemType>
        <device id="CEDTNQ2-ZZVWOVG-FOY6BLN-J3TCDIC-STHMPIK-ECCMC5E-XOTDOBW-64RS3QZ" introducedBy="">
            <encryptionPassword></encryptionPassword>
        </device>
        <device id="57WOA3J-U66ZC5U-OR4MGTE-2Z4YONC-LOSYK7M-ZADGN7J-5WI72UB-LPDIQAX" introducedBy="">
            <encryptionPassword></encryptionPassword>
        </device>
        <device id="7EFK7KM-U7DETJ7-QTHI7UD-5PVHEAD-JPZGN5J-LNPR3IS-73IWI37-QEZ6SA4" introducedBy="">
            <encryptionPassword></encryptionPassword>
        </device>
        <minDiskFree unit="%">1</minDiskFree>
        <versioning>
            <cleanupIntervalS>0</cleanupIntervalS>
            <fsPath></fsPath>
            <fsType>basic</fsType>
        </versioning>
        <copiers>0</copiers>
        <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
        <hashers>0</hashers>
        <order>random</order>
        <ignoreDelete>false</ignoreDelete>
        <scanProgressIntervalS>0</scanProgressIntervalS>
        <pullerPauseS>0</pullerPauseS>
        <maxConflicts>10</maxConflicts>
        <disableSparseFiles>false</disableSparseFiles>
        <disableTempIndexes>false</disableTempIndexes>
        <paused>false</paused>
        <weakHashThresholdPct>25</weakHashThresholdPct>
        <markerName>.stfolder</markerName>
        <copyOwnershipFromParent>false</copyOwnershipFromParent>
        <modTimeWindowS>0</modTimeWindowS>
        <maxConcurrentWrites>2</maxConcurrentWrites>
        <disableFsync>false</disableFsync>
        <blockPullOrder>standard</blockPullOrder>
        <copyRangeMethod>standard</copyRangeMethod>
        <caseSensitiveFS>false</caseSensitiveFS>
        <junctionsAsDirs>false</junctionsAsDirs>
    </folder>
    <device id="CEDTNQ2-ZZVWOVG-FOY6BLN-J3TCDIC-STHMPIK-ECCMC5E-XOTDOBW-64RS3QZ" name="jpc" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
        <address>dynamic</address>
        <paused>false</paused>
        <autoAcceptFolders>false</autoAcceptFolders>
        <maxSendKbps>0</maxSendKbps>
        <maxRecvKbps>0</maxRecvKbps>
        <maxRequestKiB>0</maxRequestKiB>
        <untrusted>false</untrusted>
        <remoteGUIPort>0</remoteGUIPort>
    </device>
    <device id="57WOA3J-U66ZC5U-OR4MGTE-2Z4YONC-LOSYK7M-ZADGN7J-5WI72UB-LPDIQAX" name="phone" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
        <address>dynamic</address>
        <paused>false</paused>
        <autoAcceptFolders>false</autoAcceptFolders>
        <maxSendKbps>0</maxSendKbps>
        <maxRecvKbps>0</maxRecvKbps>
        <maxRequestKiB>0</maxRequestKiB>
        <untrusted>false</untrusted>
        <remoteGUIPort>0</remoteGUIPort>
    </device>
    <gui enabled="true" tls="false" debugging="false">
        <address>127.0.0.1:8384</address>
        <apikey>womMUXyd26cfzxSfQEvw9jkmANRJVFwu</apikey>
        <theme>black</theme>
    </gui>
    <ldap></ldap>
    <options>
        <listenAddress>default</listenAddress>
        <globalAnnounceServer>default</globalAnnounceServer>
        <globalAnnounceEnabled>true</globalAnnounceEnabled>
        <localAnnounceEnabled>true</localAnnounceEnabled>
        <localAnnouncePort>21027</localAnnouncePort>
        <localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
        <maxSendKbps>0</maxSendKbps>
        <maxRecvKbps>0</maxRecvKbps>
        <reconnectionIntervalS>60</reconnectionIntervalS>
        <relaysEnabled>true</relaysEnabled>
        <relayReconnectIntervalM>10</relayReconnectIntervalM>
        <startBrowser>true</startBrowser>
        <natEnabled>true</natEnabled>
        <natLeaseMinutes>60</natLeaseMinutes>
        <natRenewalMinutes>30</natRenewalMinutes>
        <natTimeoutSeconds>10</natTimeoutSeconds>
        <urAccepted>-1</urAccepted>
        <urSeen>3</urSeen>
        <urUniqueID></urUniqueID>
        <urURL>https://data.syncthing.net/newdata</urURL>
        <urPostInsecurely>false</urPostInsecurely>
        <urInitialDelayS>1800</urInitialDelayS>
        <restartOnWakeup>true</restartOnWakeup>
        <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
        <upgradeToPreReleases>false</upgradeToPreReleases>
        <keepTemporariesH>24</keepTemporariesH>
        <cacheIgnoredFiles>false</cacheIgnoredFiles>
        <progressUpdateIntervalS>5</progressUpdateIntervalS>
        <limitBandwidthInLan>false</limitBandwidthInLan>
        <minHomeDiskFree unit="%">1</minHomeDiskFree>
        <releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL>
        <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
        <tempIndexMinBlocks>10</tempIndexMinBlocks>
        <trafficClass>0</trafficClass>
        <setLowPriority>true</setLowPriority>
        <maxFolderConcurrency>0</maxFolderConcurrency>
        <crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL>
        <crashReportingEnabled>true</crashReportingEnabled>
        <stunKeepaliveStartS>180</stunKeepaliveStartS>
        <stunKeepaliveMinS>20</stunKeepaliveMinS>
        <stunServer>default</stunServer>
        <databaseTuning>auto</databaseTuning>
        <maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB>
        <announceLANAddresses>true</announceLANAddresses>
        <sendFullIndexOnUpgrade>false</sendFullIndexOnUpgrade>
        <connectionLimitEnough>0</connectionLimitEnough>
        <connectionLimitMax>0</connectionLimitMax>
        <insecureAllowOldTLSVersions>false</insecureAllowOldTLSVersions>
    </options>
    <defaults>
        <folder id="" label="" path="~" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
            <filesystemType>basic</filesystemType>
            <device id="CEDTNQ2-ZZVWOVG-FOY6BLN-J3TCDIC-STHMPIK-ECCMC5E-XOTDOBW-64RS3QZ" introducedBy="">
                <encryptionPassword></encryptionPassword>
            </device>
            <minDiskFree unit="%">1</minDiskFree>
            <versioning>
                <cleanupIntervalS>3600</cleanupIntervalS>
                <fsPath></fsPath>
                <fsType>basic</fsType>
            </versioning>
            <copiers>0</copiers>
            <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
            <hashers>0</hashers>
            <order>random</order>
            <ignoreDelete>false</ignoreDelete>
            <scanProgressIntervalS>0</scanProgressIntervalS>
            <pullerPauseS>0</pullerPauseS>
            <maxConflicts>10</maxConflicts>
            <disableSparseFiles>false</disableSparseFiles>
            <disableTempIndexes>false</disableTempIndexes>
            <paused>false</paused>
            <weakHashThresholdPct>25</weakHashThresholdPct>
            <markerName>.stfolder</markerName>
            <copyOwnershipFromParent>false</copyOwnershipFromParent>
            <modTimeWindowS>0</modTimeWindowS>
            <maxConcurrentWrites>2</maxConcurrentWrites>
            <disableFsync>false</disableFsync>
            <blockPullOrder>standard</blockPullOrder>
            <copyRangeMethod>standard</copyRangeMethod>
            <caseSensitiveFS>false</caseSensitiveFS>
            <junctionsAsDirs>false</junctionsAsDirs>
        </folder>
        <device id="" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
            <address>dynamic</address>
            <paused>false</paused>
            <autoAcceptFolders>false</autoAcceptFolders>
            <maxSendKbps>0</maxSendKbps>
            <maxRecvKbps>0</maxRecvKbps>
            <maxRequestKiB>0</maxRequestKiB>
            <untrusted>false</untrusted>
            <remoteGUIPort>0</remoteGUIPort>
        </device>
    </defaults>
</configuration>
```

**Android** device

```
<?xml version="1.0" encoding="UTF-8"?><configuration version="35">
    <folder id="reixg-dsqph" label="cours" path="/storage/emulated/0/cours" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" ignorePerms="true" autoNormalize="true">
        <filesystemType>basic</filesystemType>
        <device id="CEDTNQ2-ZZVWOVG-FOY6BLN-J3TCDIC-STHMPIK-ECCMC5E-XOTDOBW-64RS3QZ" introducedBy="">
            <encryptionPassword/>
        </device>
        <device id="57WOA3J-U66ZC5U-OR4MGTE-2Z4YONC-LOSYK7M-ZADGN7J-5WI72UB-LPDIQAX" introducedBy="">
            <encryptionPassword/>
        </device>
        <device id="7EFK7KM-U7DETJ7-QTHI7UD-5PVHEAD-JPZGN5J-LNPR3IS-73IWI37-QEZ6SA4" introducedBy="">
            <encryptionPassword/>
        </device>
        <minDiskFree unit="%">1</minDiskFree>
        <versioning>
            <cleanupIntervalS>3600</cleanupIntervalS>
            <fsPath/>
            <fsType>basic</fsType>
        </versioning>
        <copiers>0</copiers>
        <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
        <hashers>1</hashers>
        <order>random</order>
        <ignoreDelete>false</ignoreDelete>
        <scanProgressIntervalS>0</scanProgressIntervalS>
        <pullerPauseS>0</pullerPauseS>
        <maxConflicts>10</maxConflicts>
        <disableSparseFiles>false</disableSparseFiles>
        <disableTempIndexes>false</disableTempIndexes>
        <paused>false</paused>
        <weakHashThresholdPct>25</weakHashThresholdPct>
        <markerName>.stfolder</markerName>
        <copyOwnershipFromParent>false</copyOwnershipFromParent>
        <modTimeWindowS>0</modTimeWindowS>
        <maxConcurrentWrites>2</maxConcurrentWrites>
        <disableFsync>false</disableFsync>
        <blockPullOrder>standard</blockPullOrder>
        <copyRangeMethod>standard</copyRangeMethod>
        <caseSensitiveFS>false</caseSensitiveFS>
        <junctionsAsDirs>false</junctionsAsDirs>
    </folder>
    <device id="CEDTNQ2-ZZVWOVG-FOY6BLN-J3TCDIC-STHMPIK-ECCMC5E-XOTDOBW-64RS3QZ" name="desktop" compression="metadata" introducer="true" skipIntroductionRemovals="false" introducedBy="">
        <address>dynamic</address>
        <paused>false</paused>
        <autoAcceptFolders>false</autoAcceptFolders>
        <maxSendKbps>0</maxSendKbps>
        <maxRecvKbps>0</maxRecvKbps>
        <ignoredFolder time="2021-09-23T23:40:54+02:00" id="47nzj-1cntd" label="OpenCamera"/>
        <maxRequestKiB>0</maxRequestKiB>
        <untrusted>false</untrusted>
        <remoteGUIPort>0</remoteGUIPort>
    </device>
    <device id="57WOA3J-U66ZC5U-OR4MGTE-2Z4YONC-LOSYK7M-ZADGN7J-5WI72UB-LPDIQAX" name="SM-A520F" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
        <address>dynamic</address>
        <paused>false</paused>
        <autoAcceptFolders>false</autoAcceptFolders>
        <maxSendKbps>0</maxSendKbps>
        <maxRecvKbps>0</maxRecvKbps>
        <maxRequestKiB>0</maxRequestKiB>
        <untrusted>false</untrusted>
        <remoteGUIPort>0</remoteGUIPort>
    </device>
    <gui enabled="true" tls="true" debugging="false">
        <address>127.0.0.1:8384</address>
        <user>syncthing</user>
        <password>$2a$04$a3RiNTq693qAn05JOMZ5rO72S4pF6XRFdaoaxxfhANUUq55BNTjqe</password>
        <apikey>pFLd6YJTHZrSryLPY2e5wyLZMmATwdcW</apikey>
        <theme>default</theme>
    </gui>
    <ldap/>
    <options>
        <listenAddress>default</listenAddress>
        <globalAnnounceServer>default</globalAnnounceServer>
        <globalAnnounceEnabled>true</globalAnnounceEnabled>
        <localAnnounceEnabled>true</localAnnounceEnabled>
        <localAnnouncePort>21027</localAnnouncePort>
        <localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
        <maxSendKbps>0</maxSendKbps>
        <maxRecvKbps>0</maxRecvKbps>
        <reconnectionIntervalS>60</reconnectionIntervalS>
        <relaysEnabled>true</relaysEnabled>
        <relayReconnectIntervalM>10</relayReconnectIntervalM>
        <startBrowser>true</startBrowser>
        <natEnabled>true</natEnabled>
        <natLeaseMinutes>60</natLeaseMinutes>
        <natRenewalMinutes>30</natRenewalMinutes>
        <natTimeoutSeconds>10</natTimeoutSeconds>
        <urAccepted>-1</urAccepted>
        <urSeen>0</urSeen>
        <urUniqueID/>
        <urURL>https://data.syncthing.net/newdata</urURL>
        <urPostInsecurely>false</urPostInsecurely>
        <urInitialDelayS>1800</urInitialDelayS>
        <restartOnWakeup>true</restartOnWakeup>
        <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
        <upgradeToPreReleases>false</upgradeToPreReleases>
        <keepTemporariesH>24</keepTemporariesH>
        <cacheIgnoredFiles>false</cacheIgnoredFiles>
        <progressUpdateIntervalS>5</progressUpdateIntervalS>
        <limitBandwidthInLan>false</limitBandwidthInLan>
        <minHomeDiskFree unit="%">1</minHomeDiskFree>
        <releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL>
        <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
        <tempIndexMinBlocks>10</tempIndexMinBlocks>
        <trafficClass>0</trafficClass>
        <setLowPriority>true</setLowPriority>
        <maxFolderConcurrency>0</maxFolderConcurrency>
        <crashReportingURL>https://crash.syncthing.net/newcrash</crashReportingURL>
        <crashReportingEnabled>true</crashReportingEnabled>
        <stunKeepaliveStartS>180</stunKeepaliveStartS>
        <stunKeepaliveMinS>20</stunKeepaliveMinS>
        <stunServer>default</stunServer>
        <databaseTuning>auto</databaseTuning>
        <maxConcurrentIncomingRequestKiB>0</maxConcurrentIncomingRequestKiB>
        <announceLANAddresses>true</announceLANAddresses>
        <sendFullIndexOnUpgrade>false</sendFullIndexOnUpgrade>
        <connectionLimitEnough>0</connectionLimitEnough>
        <connectionLimitMax>0</connectionLimitMax>
        <insecureAllowOldTLSVersions>false</insecureAllowOldTLSVersions>
    <weakHashSelectionMethod>never</weakHashSelectionMethod></options>
    <remoteIgnoredDevice time="2021-08-28T18:59:13+02:00" id="ILWYUGX-D6M2TL4-3OLORSY-XI6VVC6-I4QGPZK-X3LX7RQ-MBJCTU4-APLBOAL" name="thinkpad" address="192.168.1.27:22000"/>
    <defaults>
        <folder id="" label="" path="~" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" ignorePerms="true" autoNormalize="true">
            <filesystemType>basic</filesystemType>
            <device id="57WOA3J-U66ZC5U-OR4MGTE-2Z4YONC-LOSYK7M-ZADGN7J-5WI72UB-LPDIQAX" introducedBy="">
                <encryptionPassword/>
            </device>
            <minDiskFree unit="%">1</minDiskFree>
            <versioning>
                <cleanupIntervalS>3600</cleanupIntervalS>
                <fsPath/>
                <fsType>basic</fsType>
            </versioning>
            <copiers>0</copiers>
            <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
            <hashers>1</hashers>
            <order>random</order>
            <ignoreDelete>false</ignoreDelete>
            <scanProgressIntervalS>0</scanProgressIntervalS>
            <pullerPauseS>0</pullerPauseS>
            <maxConflicts>10</maxConflicts>
            <disableSparseFiles>false</disableSparseFiles>
            <disableTempIndexes>false</disableTempIndexes>
            <paused>false</paused>
            <weakHashThresholdPct>25</weakHashThresholdPct>
            <markerName>.stfolder</markerName>
            <copyOwnershipFromParent>false</copyOwnershipFromParent>
            <modTimeWindowS>0</modTimeWindowS>
            <maxConcurrentWrites>2</maxConcurrentWrites>
            <disableFsync>false</disableFsync>
            <blockPullOrder>standard</blockPullOrder>
            <copyRangeMethod>standard</copyRangeMethod>
            <caseSensitiveFS>false</caseSensitiveFS>
            <junctionsAsDirs>false</junctionsAsDirs>
        </folder>
        <device id="" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
            <address>dynamic</address>
            <paused>false</paused>
            <autoAcceptFolders>false</autoAcceptFolders>
            <maxSendKbps>0</maxSendKbps>
            <maxRecvKbps>0</maxRecvKbps>
            <maxRequestKiB>0</maxRequestKiB>
            <untrusted>false</untrusted>
            <remoteGUIPort>0</remoteGUIPort>
        </device>
    </defaults>
</configuration>
```
That looks precisely as it should, and we have code to prevent exactly what happened to you, yet here we are. Given the devices involved I'm inclined to suspect that this is a question of Android file system caches lying to us somehow. It could probably be reproduced by someone with an Android inclination and narrowed down closer.
I can confirm that something seems to be acting up on Android. When I try to rename the file from `test.txt` to `TEST.txt` on Windows, and then sync it with Android, I get an error there. However, in my case the file isn't deleted though. The error stays like that for a minute or so, and then just goes away.

![image](https://user-images.githubusercontent.com/5626656/134900422-25b88698-5d87-44ad-b740-672a81f68069.png)
![image](https://user-images.githubusercontent.com/5626656/134900426-3dbefdfd-9baf-4750-bf7d-b84894590acf.png)

Here is a log from the Android device with `db,model` enabled.

```
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(test.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 taking rename shortcut test.txt -> TEST.txt
2021-09-27 20:35:17 rename shortcut for TEST.txt failed: file modified but not rescanned; will try again later
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 mgieg-z3mzc Get(test.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 Deleting file test.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for test.txt: delete file: file modified but not rescanned; will try again later
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 2 on try 2
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(test.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 taking rename shortcut test.txt -> TEST.txt
2021-09-27 20:35:17 rename shortcut for TEST.txt failed: file modified but not rescanned; will try again later
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 mgieg-z3mzc Get(test.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 Deleting file test.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for test.txt: delete file: file modified but not rescanned; will try again later
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 2 on try 3
2021-09-27 20:35:17 Puller (folder "xxx" (mgieg-z3mzc), item "TEST.txt"): syncing: file does not exist
2021-09-27 20:35:17 Puller (folder "xxx" (mgieg-z3mzc), item "test.txt"): syncing: delete file: file modified but not rescanned; will try again later
2021-09-27 20:35:17 "xxx" (mgieg-z3mzc): Failed to sync 2 items
2021-09-27 20:35:17 Folder "xxx" (mgieg-z3mzc) isn't making sync progress - retrying in 1m0s.
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 scheduling scan after pulling for test.txt
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 progress emitter: bytes completed for mgieg-z3mzc: 0
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 Running something due to request
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 scanning
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 model@0x520830a0 Completion(D4DZUGP, "mgieg-z3mzc"): map[completion:100 globalBytes:40602799 globalItems:88 needBytes:0 needDeletes:0 needItems:0 sequence:1006]
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 mgieg-z3mzc WithPrefixedHaveTruncated(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4, "test.txt")
2021-09-27 20:35:17 marking file as deleted File{Name:"test.txt", Sequence:520, Permissions:0644, ModTime:2021-09-27 20:35:17.425344379 +0900 KST, Version:{[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:true, BlockSize:131072, Blocks:[], BlocksHash:}
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(test.txt)
2021-09-27 20:35:17 mgieg-z3mzc Update(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4, [1])
2021-09-27 20:35:17 removing sequence; folder="mgieg-z3mzc" sequence=520 test.txt
2021-09-27 20:35:17 insert (local); folder="mgieg-z3mzc" File{Name:"test.txt", Sequence:521, Permissions:0644, ModTime:2021-09-27 20:35:17.425344379 +0900 KST, Version:{[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:true, BlockSize:131072, Blocks:[], BlocksHash:}
2021-09-27 20:35:17 update global; folder="mgieg-z3mzc" device=7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4 file="test.txt" version={[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]} invalid=false
2021-09-27 20:35:17 new global for "test.txt" after update: {{Version:{[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]}, Deleted:true, Devices:{7777777}, Invalid:{}}, {Version:{[{D4DZUGP 1632742508} {6I7GNCY 1632742217}]}, Deleted:true, Devices:{D4DZUGP}, Invalid:{}}}
2021-09-27 20:35:17 local need delete; folder="mgieg-z3mzc", name="test.txt"
2021-09-27 20:35:17 adding sequence; folder="mgieg-z3mzc" sequence=521 test.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 finished scanning, detected 1 changes
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 mgieg-z3mzc WithHaveSequence(521)
2021-09-27 20:35:17 indexHandler@0x5256e3c0 for mgieg-z3mzc to D4DZUGP at [fe80::5e3c:27ff:fe37:988d%wlan0]:22000-[fe80::9868:3753:49ff:a06%wlan0]:22000/tcp-server/TLS1.3-TLS_CHACHA20_POLY1305_SHA256: Sending 1 files (<86 bytes)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 pulling
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 1
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 2
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 3
2021-09-27 20:35:17 Puller (folder "xxx" (mgieg-z3mzc), item "TEST.txt"): syncing: file does not exist
2021-09-27 20:35:17 "xxx" (mgieg-z3mzc): Failed to sync 1 items
2021-09-27 20:35:17 Folder "xxx" (mgieg-z3mzc) isn't making sync progress - retrying in 1m0s.
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 progress emitter: bytes completed for mgieg-z3mzc: 0
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 model@0x520830a0 Completion(D4DZUGP, "mgieg-z3mzc"): map[completion:95 globalBytes:40602799 globalItems:88 needBytes:0 needDeletes:1 needItems:0 sequence:1006]
2021-09-27 20:35:17 Index update (in): D4DZUGP / "mgieg-z3mzc": 1 files
2021-09-27 20:35:17 mgieg-z3mzc Update(D4DZUGP, [1])
2021-09-27 20:35:17 insert (remote); folder="mgieg-z3mzc" device=D4DZUGP File{Name:"test.txt", Sequence:1007, Permissions:0644, ModTime:2021-09-27 20:35:17.425344379 +0900 KST, Version:{[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:true, BlockSize:131072, Blocks:[], BlocksHash:}
2021-09-27 20:35:17 update global; folder="mgieg-z3mzc" device=D4DZUGP file="test.txt" version={[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]} invalid=false
2021-09-27 20:35:17 new global for "test.txt" after update: {{Version:{[{D4DZUGP 1632742375} {6I7GNCY 1632742517}]}, Deleted:true, Devices:{7777777, D4DZUGP}, Invalid:{}}}
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 pulling
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 1
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 2
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:35:17 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:35:17 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:35:17 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:35:17 progress emitter: disabled, skip registering
2021-09-27 20:35:17 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 new error for TEST.txt: file does not exist
2021-09-27 20:35:17 progress emitter: disabled, skip deregistering
2021-09-27 20:35:17 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 3
2021-09-27 20:35:17 Puller (folder "xxx" (mgieg-z3mzc), item "TEST.txt"): syncing: file does not exist
2021-09-27 20:35:17 "xxx" (mgieg-z3mzc): Failed to sync 1 items
2021-09-27 20:35:17 Folder "xxx" (mgieg-z3mzc) isn't making sync progress - retrying in 1m0s.
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 progress emitter: bytes completed for mgieg-z3mzc: 0
2021-09-27 20:35:17 mgieg-z3mzc Snapshot()
2021-09-27 20:35:17 model@0x520830a0 Completion(D4DZUGP, "mgieg-z3mzc"): map[completion:100 globalBytes:40602799 globalItems:88 needBytes:0 needDeletes:0 needItems:0 sequence:1007]
2021-09-27 20:36:24 mgieg-z3mzc Snapshot()
2021-09-27 20:36:24 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 pulling
2021-09-27 20:36:24 mgieg-z3mzc Snapshot()
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:36:24 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:36:24 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:36:24 mgieg-z3mzc GetGlobal(TEST.txt)
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 parent not missing TEST.txt
2021-09-27 20:36:24 mgieg-z3mzc Availability(TEST.txt)
2021-09-27 20:36:24 mgieg-z3mzc Get(TEST.txt)
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 need file TEST.txt; copy 0, reused 1
2021-09-27 20:36:24 progress emitter: disabled, skip registering
2021-09-27 20:36:24 not weak hashing TEST.txt. file did not contain any weak hashes
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 closing TEST.txt
2021-09-27 20:36:24 progress emitter: disabled, skip deregistering
2021-09-27 20:36:24 mgieg-z3mzc Update(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4, [1])
2021-09-27 20:36:24 removing sequence; folder="mgieg-z3mzc" sequence=519 TEST.txt
2021-09-27 20:36:24 insert (local); folder="mgieg-z3mzc" File{Name:"TEST.txt", Sequence:522, Permissions:0644, ModTime:2021-09-27 20:27:30.425344379 +0900 KST, Version:{[{D4DZUGP 1632742508} {6I7GNCY 1632742383}]}, VersionHash:, Length:1, Deleted:false, Invalid:false, LocalFlags:0x0, NoPermissions:true, BlockSize:131072, Blocks:[Block{0/1/720907/01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b}], BlocksHash:8882be321f134bfc1645b17f523da98cad094e4b17b6aad35776c36f35663ee1}
2021-09-27 20:36:24 update global; folder="mgieg-z3mzc" device=7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4 file="TEST.txt" version={[{D4DZUGP 1632742508} {6I7GNCY 1632742383}]} invalid=false
2021-09-27 20:36:24 new global for "TEST.txt" after update: {{Version:{[{D4DZUGP 1632742508} {6I7GNCY 1632742383}]}, Deleted:false, Devices:{D4DZUGP, 7777777}, Invalid:{}}}
2021-09-27 20:36:24 local need delete; folder="mgieg-z3mzc", name="TEST.txt"
2021-09-27 20:36:24 adding sequence; folder="mgieg-z3mzc" sequence=522 TEST.txt
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 changed 1 on try 1
2021-09-27 20:36:24 mgieg-z3mzc Snapshot()
2021-09-27 20:36:24 mgieg-z3mzc Snapshot()
2021-09-27 20:36:24 mgieg-z3mzc WithHaveSequence(522)
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 copiers: 2 pullerPendingKiB: 32768
2021-09-27 20:36:24 mgieg-z3mzc WithNeed(7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4)
2021-09-27 20:36:24 indexHandler@0x5256e3c0 for mgieg-z3mzc to D4DZUGP at [fe80::5e3c:27ff:fe37:988d%wlan0]:22000-[fe80::9868:3753:49ff:a06%wlan0]:22000/tcp-server/TLS1.3-TLS_CHACHA20_POLY1305_SHA256: Sending 1 files (<163 bytes)
2021-09-27 20:36:24 sendreceive/mgieg-z3mzc@0x523a65a0 changed 0 on try 2
2021-09-27 20:36:24 mgieg-z3mzc Snapshot()
2021-09-27 20:36:24 progress emitter: bytes completed for mgieg-z3mzc: 0
2021-09-27 20:36:24 mgieg-z3mzc Snapshot()
2021-09-27 20:36:24 model@0x520830a0 Completion(D4DZUGP, "mgieg-z3mzc"): map[completion:100 globalBytes:40602799 globalItems:88 needBytes:0 needDeletes:0 needItems:0 sequence:1007]
```
I can reproduce it on Linux<->Android.
> I can reproduce it on Linux<->Android.

If you can, it would be helpful if you could enable `db` and `model` debug logging on both sides, then proceed with the reproduction, then upload the logs here. You can enable the two in the Web GUI (which on Android can be accessed from the left slide-out menu).

On my side, as mentioned above, with Android and Windows, there is some funkiness, but none of the files end up being deleted.
- [computer.log](https://github.com/syncthing/syncthing/files/7498475/computer.log)
- [mobile.log](https://github.com/syncthing/syncthing/files/7498476/mobile.log)
This is terrifying. Relevant bits from the mobile:

```
2021-11-08 16:18:51 sendreceive/mo9mp-yxlxk@0x4000b16a80 taking rename shortcut Projects/syncthing/Test.txt -> Projects/syncthing/test.txt
2021-11-08 16:18:51 insert (local); folder="mo9mp-yxlxk" File{Name:"Projects/syncthing/test.txt", Sequence:10166824, Permissions:0644, ModTime:2021-11-08 16:18:21.505086179 +0000 UTC, Version:{[{2PZEQBA 1636388331}]}, VersionHash:, Length:0, Deleted:false, Invalid:false, LocalFlags:0x0, NoPermissions:false, BlockSize:131072, Blocks:[Block{0/0/0/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855}], BlocksHash:d3bfe98e8f1ce891614854569a20f5f4508d6ab4025b872b7b3a8282ef75b12a}
2021-11-08 16:18:51 insert (local); folder="mo9mp-yxlxk" File{Name:"Projects/syncthing/Test.txt", Sequence:10166825, Permissions:0644, ModTime:2021-11-08 16:18:51.505086179 +0000 UTC, Version:{[{2PZEQBA 1636388331}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:false, BlockSize:131072, Blocks:[], BlocksHash:}
2021-11-08 16:18:51 insert (local); folder="mo9mp-yxlxk" File{Name:"Projects/syncthing/Test.txt", Sequence:10166825, Permissions:0644, ModTime:2021-11-08 16:18:51.505086179 +0000 UTC, Version:{[{2PZEQBA 1636388331}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:false, BlockSize:131072, Blocks:[], BlocksHash:}
2021-11-08 16:19:06 marking file as deleted File{Name:"Projects/syncthing/test.txt", Sequence:10166824, Permissions:0644, ModTime:2021-11-08 16:19:06.505086179 +0000 UTC, Version:{[{QR54VZV 1636388346} {2PZEQBA 1636388331}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:false, BlockSize:131072, Blocks:[], BlocksHash:}
2021-11-08 16:19:06 insert (local); folder="mo9mp-yxlxk" File{Name:"Projects/syncthing/test.txt", Sequence:10166826, Permissions:0644, ModTime:2021-11-08 16:19:06.505086179 +0000 UTC, Version:{[{QR54VZV 1636388346} {2PZEQBA 1636388331}]}, VersionHash:, Length:0, Deleted:true, Invalid:false, LocalFlags:0x0, NoPermissions:false, BlockSize:131072, Blocks:[], BlocksHash:}
```

Basically this means it successfully renamed from Test.txt to test.txt at around 16:18:51, and then at 2021-11-08 16:19:06, i.e. a lot later, android is saying `test.txt` isn't there anymore.  
Could you please also enable `fs` debug logging on the mobile and repro again, to confirm what exactly is happening at the filesystem layer.

And what android version and what brand are you using?
If it can help, I began to see issues since I upgraded my phone to Android 11. I'm running Lineage OS 18.1
[mobile-filesystem-log.txt](https://github.com/syncthing/syncthing/files/7504967/mobile-filesystem-log.txt)

- Xiaomi Redmi Note 8T
- Android 11 RKQ1.201004.002
- Security update 2021-10-01
This is not limited to case-only renames.

https://forum.syncthing.net/t/renaming-caused-file-deletion-bug/17591/2

Fix patch:
diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go
index 214343db696..9a733b9e1f2 100644
--- a/lib/config/folderconfiguration.go
+++ b/lib/config/folderconfiguration.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/shirou/gopsutil/v3/disk"
 
+	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/util"
@@ -42,24 +43,29 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
 	return c
 }
 
-func (f FolderConfiguration) Filesystem() fs.Filesystem {
+// Filesystem creates a filesystem for the path and options of this folder.
+// The fset parameter may be nil, in which case no mtime handling on top of
+// the fileystem is provided.
+func (f FolderConfiguration) Filesystem(fset *db.FileSet) fs.Filesystem {
 	// This is intentionally not a pointer method, because things like
-	// cfg.Folders["default"].Filesystem() should be valid.
-	var opts []fs.Option
+	// cfg.Folders["default"].Filesystem(nil) should be valid.
+	opts := make([]fs.Option, 0, 3)
 	if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs {
 		opts = append(opts, new(fs.OptionJunctionsAsDirs))
 	}
-	filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
 	if !f.CaseSensitiveFS {
-		filesystem = fs.NewCaseFilesystem(filesystem)
+		opts = append(opts, new(fs.OptionDetectCaseConflicts))
 	}
-	return filesystem
+	if fset != nil {
+		opts = append(opts, fset.MtimeOption())
+	}
+	return fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
 }
 
 func (f FolderConfiguration) ModTimeWindow() time.Duration {
 	dur := time.Duration(f.RawModTimeWindowS) * time.Second
 	if f.RawModTimeWindowS < 1 && runtime.GOOS == "android" {
-		if usage, err := disk.Usage(f.Filesystem().URI()); err != nil {
+		if usage, err := disk.Usage(f.Filesystem(nil).URI()); err != nil {
 			dur = 2 * time.Second
 			l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: err == "%v"`, f.Path, err)
 		} else if strings.HasPrefix(strings.ToLower(usage.Fstype), "ext2") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext3") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext4") {
@@ -89,7 +95,7 @@ func (f *FolderConfiguration) CreateMarker() error {
 		// begin with.
 		permBits = 0700
 	}
-	fs := f.Filesystem()
+	fs := f.Filesystem(nil)
 	err := fs.Mkdir(DefaultMarkerName, permBits)
 	if err != nil {
 		return err
@@ -106,7 +112,7 @@ func (f *FolderConfiguration) CreateMarker() error {
 
 // CheckPath returns nil if the folder root exists and contains the marker file
 func (f *FolderConfiguration) CheckPath() error {
-	fi, err := f.Filesystem().Stat(".")
+	fi, err := f.Filesystem(nil).Stat(".")
 	if err != nil {
 		if !fs.IsNotExist(err) {
 			return err
@@ -124,7 +130,7 @@ func (f *FolderConfiguration) CheckPath() error {
 		return ErrPathNotDirectory
 	}
 
-	_, err = f.Filesystem().Stat(f.MarkerName)
+	_, err = f.Filesystem(nil).Stat(f.MarkerName)
 	if err != nil {
 		if !fs.IsNotExist(err) {
 			return err
@@ -145,7 +151,7 @@ func (f *FolderConfiguration) CreateRoot() (err error) {
 		permBits = 0700
 	}
 
-	filesystem := f.Filesystem()
+	filesystem := f.Filesystem(nil)
 
 	if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
 		err = filesystem.MkdirAll(".", permBits)
@@ -256,7 +262,7 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
 	if val <= 0 {
 		return nil
 	}
-	fs := f.Filesystem()
+	fs := f.Filesystem(nil)
 	usage, err := fs.Usage(".")
 	if err != nil {
 		return nil
diff --git a/lib/config/migrations.go b/lib/config/migrations.go
index f018d936f1e..29ee0addc32 100644
--- a/lib/config/migrations.go
+++ b/lib/config/migrations.go
@@ -199,7 +199,7 @@ func migrateToConfigV23(cfg *Configuration) {
 	// marker name in later versions.
 
 	for i := range cfg.Folders {
-		fs := cfg.Folders[i].Filesystem()
+		fs := cfg.Folders[i].Filesystem(nil)
 		// Invalid config posted, or tests.
 		if fs == nil {
 			continue
@@ -235,18 +235,18 @@ func migrateToConfigV21(cfg *Configuration) {
 		switch folder.Versioning.Type {
 		case "simple", "trashcan":
 			// Clean out symlinks in the known place
-			cleanSymlinks(folder.Filesystem(), ".stversions")
+			cleanSymlinks(folder.Filesystem(nil), ".stversions")
 		case "staggered":
 			versionDir := folder.Versioning.Params["versionsPath"]
 			if versionDir == "" {
 				// default place
-				cleanSymlinks(folder.Filesystem(), ".stversions")
+				cleanSymlinks(folder.Filesystem(nil), ".stversions")
 			} else if filepath.IsAbs(versionDir) {
 				// absolute
 				cleanSymlinks(fs.NewFilesystem(fs.FilesystemTypeBasic, versionDir), ".")
 			} else {
 				// relative to folder
-				cleanSymlinks(folder.Filesystem(), versionDir)
+				cleanSymlinks(folder.Filesystem(nil), versionDir)
 			}
 		}
 	}
diff --git a/lib/db/set.go b/lib/db/set.go
index 836a5111474..178800fd162 100644
--- a/lib/db/set.go
+++ b/lib/db/set.go
@@ -403,8 +403,8 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
 	}
 }
 
-func (s *FileSet) MtimeFS(filesystem fs.Filesystem) fs.Filesystem {
-	opStr := fmt.Sprintf("%s MtimeFS()", s.folder)
+func (s *FileSet) MtimeOption() fs.Option {
+	opStr := fmt.Sprintf("%s MtimeOption()", s.folder)
 	l.Debugf(opStr)
 	prefix, err := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
 	if backend.IsClosed(err) {
@@ -413,7 +413,7 @@ func (s *FileSet) MtimeFS(filesystem fs.Filesystem) fs.Filesystem {
 		fatalError(err, opStr, s.db)
 	}
 	kv := NewNamespacedKV(s.db, string(prefix))
-	return fs.NewMtimeFS(filesystem, kv)
+	return fs.NewMtimeOption(kv)
 }
 
 func (s *FileSet) ListDevices() []protocol.DeviceID {
diff --git a/lib/fs/basicfs.go b/lib/fs/basicfs.go
index 28392e70390..9ace634a824 100644
--- a/lib/fs/basicfs.go
+++ b/lib/fs/basicfs.go
@@ -27,12 +27,13 @@ var (
 
 type OptionJunctionsAsDirs struct{}
 
-func (o *OptionJunctionsAsDirs) apply(fs Filesystem) {
+func (o *OptionJunctionsAsDirs) apply(fs Filesystem) Filesystem {
 	if basic, ok := fs.(*BasicFilesystem); !ok {
 		l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
 	} else {
 		basic.junctionsAsDirs = true
 	}
+	return fs
 }
 
 func (o *OptionJunctionsAsDirs) String() string {
diff --git a/lib/fs/casefs.go b/lib/fs/casefs.go
index ffbd371e867..71bef00d861 100644
--- a/lib/fs/casefs.go
+++ b/lib/fs/casefs.go
@@ -123,21 +123,27 @@ func (r *caseFilesystemRegistry) cleaner() {
 
 var globalCaseFilesystemRegistry = caseFilesystemRegistry{fss: make(map[fskey]*caseFilesystem)}
 
-// caseFilesystem is a BasicFilesystem with additional checks to make a
-// potentially case insensitive underlying FS behave like it's case-sensitive.
-type caseFilesystem struct {
-	Filesystem
-	realCaser
-}
-
-// NewCaseFilesystem ensures that the given, potentially case-insensitive filesystem
+// OptionDetectCaseConflicts ensures that the potentially case-insensitive filesystem
 // behaves like a case-sensitive filesystem. Meaning that it takes into account
 // the real casing of a path and returns ErrCaseConflict if the given path differs
 // from the real path. It is safe to use with any filesystem, i.e. also a
 // case-sensitive one. However it will add some overhead and thus shouldn't be
 // used if the filesystem is known to already behave case-sensitively.
-func NewCaseFilesystem(fs Filesystem) Filesystem {
-	return wrapFilesystem(fs, globalCaseFilesystemRegistry.get)
+type OptionDetectCaseConflicts struct{}
+
+func (o *OptionDetectCaseConflicts) apply(fs Filesystem) Filesystem {
+	return globalCaseFilesystemRegistry.get(fs)
+}
+
+func (o *OptionDetectCaseConflicts) String() string {
+	return "detectCaseConflicts"
+}
+
+// caseFilesystem is a BasicFilesystem with additional checks to make a
+// potentially case insensitive underlying FS behave like it's case-sensitive.
+type caseFilesystem struct {
+	Filesystem
+	realCaser
 }
 
 func (f *caseFilesystem) Chmod(name string, mode FileMode) error {
diff --git a/lib/fs/filesystem.go b/lib/fs/filesystem.go
index d2af30518fd..d834f9950a4 100644
--- a/lib/fs/filesystem.go
+++ b/lib/fs/filesystem.go
@@ -202,10 +202,29 @@ var IsPathSeparator = os.IsPathSeparator
 // representation of those must be part of the returned string.
 type Option interface {
 	String() string
-	apply(Filesystem)
+	apply(Filesystem) Filesystem
 }
 
 func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
+	var caseOpt Option
+	var mtimeOpt Option
+	i := 0
+	for _, opt := range opts {
+		if caseOpt != nil && mtimeOpt != nil {
+			break
+		}
+		switch opt.(type) {
+		case *OptionDetectCaseConflicts:
+			caseOpt = opt
+		case *optionMtime:
+			mtimeOpt = opt
+		default:
+			opts[i] = opt
+			i++
+		}
+	}
+	opts = opts[:i]
+
 	var fs Filesystem
 	switch fsType {
 	case FilesystemTypeBasic:
@@ -221,6 +240,17 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
 		}
 	}
 
+	// Case handling is the innermost, as any filesystem calls by wrappers should be case-resolved
+	if caseOpt != nil {
+		fs = caseOpt.apply(fs)
+	}
+
+	// mtime handling should happen inside walking, as filesystem calls while
+	// walking should be mtime-resolved too
+	if mtimeOpt != nil {
+		fs = mtimeOpt.apply(fs)
+	}
+
 	if l.ShouldDebug("walkfs") {
 		return NewWalkFilesystem(&logFilesystem{fs})
 	}
@@ -289,21 +319,6 @@ func Canonicalize(file string) (string, error) {
 	return file, nil
 }
 
-// wrapFilesystem should always be used when wrapping a Filesystem.
-// It ensures proper wrapping order, which right now means:
-// `logFilesystem` needs to be the outermost wrapper for caller lookup.
-func wrapFilesystem(fs Filesystem, wrapFn func(Filesystem) Filesystem) Filesystem {
-	logFs, ok := fs.(*logFilesystem)
-	if ok {
-		fs = logFs.Filesystem
-	}
-	fs = wrapFn(fs)
-	if ok {
-		fs = &logFilesystem{fs}
-	}
-	return fs
-}
-
 // unwrapFilesystem removes "wrapping" filesystems to expose the filesystem of the requested wrapperType, if it exists.
 func unwrapFilesystem(fs Filesystem, wrapperType filesystemWrapperType) (Filesystem, bool) {
 	var ok bool
diff --git a/lib/fs/mtimefs.go b/lib/fs/mtimefs.go
index 5113fc0b7df..832b872c934 100644
--- a/lib/fs/mtimefs.go
+++ b/lib/fs/mtimefs.go
@@ -33,20 +33,34 @@ func WithCaseInsensitivity(v bool) MtimeFSOption {
 	}
 }
 
-// NewMtimeFS returns a filesystem with nanosecond mtime precision, regardless
-// of what shenanigans the underlying filesystem gets up to.
-func NewMtimeFS(fs Filesystem, db database, options ...MtimeFSOption) Filesystem {
-	return wrapFilesystem(fs, func(underlying Filesystem) Filesystem {
-		f := &mtimeFS{
-			Filesystem: underlying,
-			chtimes:    underlying.Chtimes, // for mocking it out in the tests
-			db:         db,
-		}
-		for _, opt := range options {
-			opt(f)
-		}
-		return f
-	})
+type optionMtime struct {
+	db      database
+	options []MtimeFSOption
+}
+
+// NewMtimeOption makes any filesystem provide nanosecond mtime precision,
+// regardless of what shenanigans the underlying filesystem gets up to.
+func NewMtimeOption(db database, options ...MtimeFSOption) Option {
+	return &optionMtime{
+		db:      db,
+		options: options,
+	}
+}
+
+func (o *optionMtime) apply(fs Filesystem) Filesystem {
+	f := &mtimeFS{
+		Filesystem: fs,
+		chtimes:    fs.Chtimes, // for mocking it out in the tests
+		db:         o.db,
+	}
+	for _, opt := range o.options {
+		opt(f)
+	}
+	return f
+}
+
+func (_ *optionMtime) String() string {
+	return "mtime"
 }
 
 func (f *mtimeFS) Chtimes(name string, atime, mtime time.Time) error {
@@ -104,25 +118,6 @@ func (f *mtimeFS) Lstat(name string) (FileInfo, error) {
 	return info, nil
 }
 
-func (f *mtimeFS) Walk(root string, walkFn WalkFunc) error {
-	return f.Filesystem.Walk(root, func(path string, info FileInfo, err error) error {
-		if info != nil {
-			mtimeMapping, loadErr := f.load(path)
-			if loadErr != nil && err == nil {
-				// The iterator gets to deal with the error
-				err = loadErr
-			}
-			if mtimeMapping.Real == info.ModTime() {
-				info = mtimeFileInfo{
-					FileInfo: info,
-					mtime:    mtimeMapping.Virtual,
-				}
-			}
-		}
-		return walkFn(path, info, err)
-	})
-}
-
 func (f *mtimeFS) Create(name string) (File, error) {
 	fd, err := f.Filesystem.Create(name)
 	if err != nil {
diff --git a/lib/model/folder.go b/lib/model/folder.go
index a318a64eec1..01680012660 100644
--- a/lib/model/folder.go
+++ b/lib/model/folder.go
@@ -108,7 +108,7 @@ func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg conf
 		shortID:       model.shortID,
 		fset:          fset,
 		ignores:       ignores,
-		mtimefs:       fset.MtimeFS(cfg.Filesystem()),
+		mtimefs:       cfg.Filesystem(fset),
 		modTimeWindow: cfg.ModTimeWindow(),
 		done:          make(chan struct{}),
 
@@ -1001,7 +1001,7 @@ func (f *folder) monitorWatch(ctx context.Context) {
 	for {
 		select {
 		case <-failTimer.C:
-			eventChan, errChan, err = f.Filesystem().Watch(".", f.ignores, ctx, f.IgnorePerms)
+			eventChan, errChan, err = f.mtimefs.Watch(".", f.ignores, ctx, f.IgnorePerms)
 			// We do this once per minute initially increased to
 			// max one hour in case of repeat failures.
 			f.scanOnWatchErr()
diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go
index af57b7b0ff5..e8f0af524a0 100644
--- a/lib/model/folder_sendrecv.go
+++ b/lib/model/folder_sendrecv.go
@@ -1264,7 +1264,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
 	// Hope that it's usually in the same folder, so start with that one.
 	folders := []string{f.folderID}
 	for folder, cfg := range f.model.cfg.Folders() {
-		folderFilesystems[folder] = cfg.Filesystem()
+		folderFilesystems[folder] = cfg.Filesystem(nil)
 		if folder != f.folderID {
 			folders = append(folders, folder)
 		}
diff --git a/lib/model/model.go b/lib/model/model.go
index 819bb371857..fdceb1e69e1 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -334,7 +334,7 @@ func (m *model) StartDeadlockDetector(timeout time.Duration) {
 
 // Need to hold lock on m.fmut when calling this.
 func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, fset *db.FileSet, cacheIgnoredFiles bool) {
-	ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(cacheIgnoredFiles))
+	ignores := ignore.New(cfg.Filesystem(nil), ignore.WithCache(cacheIgnoredFiles))
 	if cfg.Type != config.FolderTypeReceiveEncrypted {
 		if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
 			l.Warnln("Loading ignores:", err)
@@ -398,7 +398,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
 	}
 
 	// These are our metadata files, and they should always be hidden.
-	ffs := cfg.Filesystem()
+	ffs := cfg.Filesystem(nil)
 	_ = ffs.Hide(config.DefaultMarkerName)
 	_ = ffs.Hide(".stversions")
 	_ = ffs.Hide(".stignore")
@@ -430,7 +430,7 @@ func (m *model) warnAboutOverwritingProtectedFiles(cfg config.FolderConfiguratio
 	}
 
 	// This is a bit of a hack.
-	ffs := cfg.Filesystem()
+	ffs := cfg.Filesystem(nil)
 	if ffs.Type() != fs.FilesystemTypeBasic {
 		return
 	}
@@ -480,7 +480,7 @@ func (m *model) removeFolder(cfg config.FolderConfiguration) {
 	if isPathUnique {
 		// Remove (if empty and removable) or move away (if non-empty or
 		// otherwise not removable) Syncthing-specific marker files.
-		fs := cfg.Filesystem()
+		fs := cfg.Filesystem(nil)
 		if err := fs.Remove(config.DefaultMarkerName); err != nil {
 			moved := config.DefaultMarkerName + time.Now().Format(".removed-20060102-150405")
 			_ = fs.Rename(config.DefaultMarkerName, moved)
@@ -1841,7 +1841,7 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, blockNo
 	// Grab the FS after limiting, as it causes I/O and we want to minimize
 	// the race time between the symlink check and the read.
 
-	folderFs := folderCfg.Filesystem()
+	folderFs := folderCfg.Filesystem(nil)
 
 	if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil {
 		l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
@@ -2007,7 +2007,7 @@ func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, er
 	if !ok {
 		return fs.MtimeMapping{}, ErrFolderMissing
 	}
-	return fs.GetMtimeMapping(ffs.MtimeFS(fcfg.Filesystem()), file)
+	return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file)
 }
 
 // Connection returns the current connection for device, and a boolean whether a connection was found.
@@ -2041,7 +2041,7 @@ func (m *model) LoadIgnores(folder string) ([]string, []string, error) {
 	}
 
 	if !ignoresOk {
-		ignores = ignore.New(cfg.Filesystem())
+		ignores = ignore.New(cfg.Filesystem(nil))
 	}
 
 	err := ignores.Load(".stignore")
@@ -2096,7 +2096,7 @@ func (m *model) setIgnores(cfg config.FolderConfiguration, content []string) err
 		return err
 	}
 
-	if err := ignore.WriteIgnores(cfg.Filesystem(), ".stignore", content); err != nil {
+	if err := ignore.WriteIgnores(cfg.Filesystem(nil), ".stignore", content); err != nil {
 		l.Warnln("Saving .stignore:", err)
 		return err
 	}
@@ -3229,7 +3229,7 @@ type storedEncryptionToken struct {
 }
 
 func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
-	fd, err := cfg.Filesystem().Open(encryptionTokenPath(cfg))
+	fd, err := cfg.Filesystem(nil).Open(encryptionTokenPath(cfg))
 	if err != nil {
 		return nil, err
 	}
@@ -3243,7 +3243,7 @@ func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
 
 func writeEncryptionToken(token []byte, cfg config.FolderConfiguration) error {
 	tokenName := encryptionTokenPath(cfg)
-	fd, err := cfg.Filesystem().OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0666)
+	fd, err := cfg.Filesystem(nil).OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0666)
 	if err != nil {
 		return err
 	}
diff --git a/lib/versioner/external.go b/lib/versioner/external.go
index b39d4044b15..2319077e4b5 100644
--- a/lib/versioner/external.go
+++ b/lib/versioner/external.go
@@ -41,7 +41,7 @@ func newExternal(cfg config.FolderConfiguration) Versioner {
 
 	s := external{
 		command:    command,
-		filesystem: cfg.Filesystem(),
+		filesystem: cfg.Filesystem(nil),
 	}
 
 	l.Debugf("instantiated %#v", s)
diff --git a/lib/versioner/simple.go b/lib/versioner/simple.go
index 06ab00b2730..e4865116a01 100644
--- a/lib/versioner/simple.go
+++ b/lib/versioner/simple.go
@@ -40,7 +40,7 @@ func newSimple(cfg config.FolderConfiguration) Versioner {
 	s := simple{
 		keep:            keep,
 		cleanoutDays:    cleanoutDays,
-		folderFs:        cfg.Filesystem(),
+		folderFs:        cfg.Filesystem(nil),
 		versionsFs:      versionerFsFromFolderCfg(cfg),
 		copyRangeMethod: cfg.CopyRangeMethod,
 	}
diff --git a/lib/versioner/staggered.go b/lib/versioner/staggered.go
index 25ec71e4b3f..2a341590e19 100644
--- a/lib/versioner/staggered.go
+++ b/lib/versioner/staggered.go
@@ -44,7 +44,7 @@ func newStaggered(cfg config.FolderConfiguration) Versioner {
 	versionsFs := versionerFsFromFolderCfg(cfg)
 
 	s := &staggered{
-		folderFs:   cfg.Filesystem(),
+		folderFs:   cfg.Filesystem(nil),
 		versionsFs: versionsFs,
 		interval: [4]interval{
 			{30, 60 * 60},                     // first hour -> 30 sec between versions
diff --git a/lib/versioner/trashcan.go b/lib/versioner/trashcan.go
index 2e8110259a2..a72d7b42e47 100644
--- a/lib/versioner/trashcan.go
+++ b/lib/versioner/trashcan.go
@@ -33,7 +33,7 @@ func newTrashcan(cfg config.FolderConfiguration) Versioner {
 	// On error we default to 0, "do not clean out the trash can"
 
 	s := &trashcan{
-		folderFs:        cfg.Filesystem(),
+		folderFs:        cfg.Filesystem(nil),
 		versionsFs:      versionerFsFromFolderCfg(cfg),
 		cleanoutDays:    cleanoutDays,
 		copyRangeMethod: cfg.CopyRangeMethod,
diff --git a/lib/versioner/util.go b/lib/versioner/util.go
index 1ee4ae2921d..3dd80857619 100644
--- a/lib/versioner/util.go
+++ b/lib/versioner/util.go
@@ -256,7 +256,7 @@ func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath str
 }
 
 func versionerFsFromFolderCfg(cfg config.FolderConfiguration) (versionsFs fs.Filesystem) {
-	folderFs := cfg.Filesystem()
+	folderFs := cfg.Filesystem(nil)
 	if cfg.Versioning.FSPath == "" {
 		versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), ".stversions"))
 	} else if cfg.Versioning.FSType == fs.FilesystemTypeBasic && !filepath.IsAbs(cfg.Versioning.FSPath) {


Test patch:
diff --git a/lib/config/config_test.go b/lib/config/config_test.go
index a029dce0e74..628099f5165 100644
--- a/lib/config/config_test.go
+++ b/lib/config/config_test.go
@@ -457,7 +457,7 @@ func TestIssue1262(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	actual := cfg.Folders()["test"].Filesystem().URI()
+	actual := cfg.Folders()["test"].Filesystem(nil).URI()
 	expected := `e:\`
 
 	if actual != expected {
@@ -494,7 +494,7 @@ func TestFolderPath(t *testing.T) {
 		Path: "~/tmp",
 	}
 
-	realPath := folder.Filesystem().URI()
+	realPath := folder.Filesystem(nil).URI()
 	if !filepath.IsAbs(realPath) {
 		t.Error(realPath, "should be absolute")
 	}
diff --git a/lib/fs/casefs_test.go b/lib/fs/casefs_test.go
index 9c5915db858..b819197f0af 100644
--- a/lib/fs/casefs_test.go
+++ b/lib/fs/casefs_test.go
@@ -34,8 +34,12 @@ func TestRealCase(t *testing.T) {
 	})
 }
 
+func newCaseFilesystem(fsys Filesystem) *caseFilesystem {
+	return globalCaseFilesystemRegistry.get(fsys).(*caseFilesystem)
+}
+
 func testRealCase(t *testing.T, fsys Filesystem) {
-	testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
+	testFs := newCaseFilesystem(fsys)
 	comps := []string{"Foo", "bar", "BAZ", "bAs"}
 	path := filepath.Join(comps...)
 	testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0777)
@@ -86,7 +90,7 @@ func TestRealCaseSensitive(t *testing.T) {
 }
 
 func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
-	testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
+	testFs := newCaseFilesystem(fsys)
 
 	names := make([]string, 2)
 	names[0] = "foo"
@@ -139,7 +143,7 @@ func testCaseFSStat(t *testing.T, fsys Filesystem) {
 		sensitive = false
 	}
 
-	testFs := NewCaseFilesystem(fsys)
+	testFs := newCaseFilesystem(fsys)
 	_, err = testFs.Stat("FOO")
 	if sensitive {
 		if IsNotExist(err) {
diff --git a/lib/fs/mtimefs_test.go b/lib/fs/mtimefs_test.go
index bf14f514ef2..6c294bd24aa 100644
--- a/lib/fs/mtimefs_test.go
+++ b/lib/fs/mtimefs_test.go
@@ -26,7 +26,7 @@ func TestMtimeFS(t *testing.T) {
 	// a random time with nanosecond precision
 	testTime := time.Unix(1234567890, 123456789)
 
-	mtimefs := newMtimeFS(newBasicFilesystem("."), make(mapStore))
+	mtimefs := newMtimeFS(".", make(mapStore))
 
 	// Do one Chtimes call that will go through to the normal filesystem
 	mtimefs.chtimes = os.Chtimes
@@ -88,8 +88,8 @@ func TestMtimeFSWalk(t *testing.T) {
 	}
 	defer func() { _ = os.RemoveAll(dir) }()
 
-	underlying := NewFilesystem(FilesystemTypeBasic, dir)
-	mtimefs := newMtimeFS(underlying, make(mapStore))
+	mtimefs, walkFs := newMtimeFSWithWalk(dir, make(mapStore))
+	underlying := mtimefs.Filesystem
 	mtimefs.chtimes = failChtimes
 
 	if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0644); err != nil {
@@ -120,7 +120,7 @@ func TestMtimeFSWalk(t *testing.T) {
 	}
 
 	found := false
-	_ = mtimefs.Walk("", func(path string, info FileInfo, err error) error {
+	_ = walkFs.Walk("", func(path string, info FileInfo, err error) error {
 		if path == "file" {
 			found = true
 			if !info.ModTime().Equal(newTime) {
@@ -142,8 +142,8 @@ func TestMtimeFSOpen(t *testing.T) {
 	}
 	defer func() { _ = os.RemoveAll(dir) }()
 
-	underlying := NewFilesystem(FilesystemTypeBasic, dir)
-	mtimefs := newMtimeFS(underlying, make(mapStore))
+	mtimefs := newMtimeFS(dir, make(mapStore))
+	underlying := mtimefs.Filesystem
 	mtimefs.chtimes = failChtimes
 
 	if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0644); err != nil {
@@ -222,12 +222,12 @@ func TestMtimeFSInsensitive(t *testing.T) {
 
 	// The test should fail with a case sensitive mtimefs
 	t.Run("with case sensitive mtimefs", func(t *testing.T) {
-		theTest(t, newMtimeFS(newBasicFilesystem("."), make(mapStore)), false)
+		theTest(t, newMtimeFS(".", make(mapStore)), false)
 	})
 
 	// And succeed with a case insensitive one.
 	t.Run("with case insensitive mtimefs", func(t *testing.T) {
-		theTest(t, newMtimeFS(newBasicFilesystem("."), make(mapStore), WithCaseInsensitivity(true)), true)
+		theTest(t, newMtimeFS(".", make(mapStore), WithCaseInsensitivity(true)), true)
 	})
 }
 
@@ -261,6 +261,12 @@ func evilChtimes(name string, mtime, atime time.Time) error {
 	return os.Chtimes(name, mtime.Add(300*time.Hour).Truncate(time.Hour), atime.Add(300*time.Hour).Truncate(time.Hour))
 }
 
-func newMtimeFS(fs Filesystem, db database, options ...MtimeFSOption) *mtimeFS {
-	return NewMtimeFS(fs, db, options...).(*mtimeFS)
+func newMtimeFS(path string, db database, options ...MtimeFSOption) *mtimeFS {
+	mtimefs, _ := newMtimeFSWithWalk(path, db, options...)
+	return mtimefs
+}
+
+func newMtimeFSWithWalk(path string, db database, options ...MtimeFSOption) (*mtimeFS, *walkFilesystem) {
+	wfs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, options...)).(*walkFilesystem)
+	return wfs.Filesystem.(*mtimeFS), wfs
 }
diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go
index 740a31202c1..0e3dbb3f720 100644
--- a/lib/model/folder_recvonly_test.go
+++ b/lib/model/folder_recvonly_test.go
@@ -28,7 +28,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -110,7 +110,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -200,7 +200,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -270,7 +270,7 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -335,7 +335,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -425,7 +425,7 @@ func TestRecvOnlyRevertOwnID(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
diff --git a/lib/model/folder_sendrecv_test.go b/lib/model/folder_sendrecv_test.go
index 4999e99b5d0..0e87bccafc5 100644
--- a/lib/model/folder_sendrecv_test.go
+++ b/lib/model/folder_sendrecv_test.go
@@ -109,7 +109,7 @@ func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testMode
 func cleanupSRFolder(f *sendReceiveFolder, m *testModel, wrapperCancel context.CancelFunc) {
 	wrapperCancel()
 	os.Remove(m.cfg.ConfigPath())
-	os.RemoveAll(f.Filesystem().URI())
+	os.RemoveAll(f.Filesystem(nil).URI())
 }
 
 // Layout of the files: (indexes from the above array)
@@ -172,7 +172,7 @@ func TestHandleFileWithTemp(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
 	defer cleanupSRFolder(f, m, wcfgCancel)
 
-	if _, err := prepareTmpFile(f.Filesystem()); err != nil {
+	if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
 		t.Fatal(err)
 	}
 
@@ -230,7 +230,7 @@ func TestCopierFinder(t *testing.T) {
 
 			defer cleanupSRFolder(f, m, wcfgCancel)
 
-			if _, err := prepareTmpFile(f.Filesystem()); err != nil {
+			if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
 				t.Fatal(err)
 			}
 
@@ -290,7 +290,7 @@ func TestCopierFinder(t *testing.T) {
 			}
 
 			// Verify that the fetched blocks have actually been written to the temp file
-			blks, err := scanner.HashFile(context.TODO(), f.Filesystem(), tempFile, protocol.MinBlockSize, nil, false)
+			blks, err := scanner.HashFile(context.TODO(), f.Filesystem(nil), tempFile, protocol.MinBlockSize, nil, false)
 			if err != nil {
 				t.Log(err)
 			}
@@ -308,7 +308,7 @@ func TestWeakHash(t *testing.T) {
 	// Setup the model/pull environment
 	model, fo, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(fo, model, wcfgCancel)
-	ffs := fo.Filesystem()
+	ffs := fo.Filesystem(nil)
 
 	tempFile := fs.TempName("weakhash")
 	var shift int64 = 10
@@ -673,7 +673,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 func TestIssue3164(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	tmpDir := ffs.URI()
 
 	ignDir := filepath.Join("issue3164", "oktodelete")
@@ -764,7 +764,7 @@ func TestDiffEmpty(t *testing.T) {
 func TestDeleteIgnorePerms(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	f.IgnorePerms = true
 
 	name := "deleteIgnorePerms"
@@ -806,7 +806,7 @@ func TestCopyOwner(t *testing.T) {
 	f.folder.FolderConfiguration.CopyOwnershipFromParent = true
 
 	f.fset = newFileSet(t, f.ID, m.db)
-	f.mtimefs = f.fset.MtimeFS(f.Filesystem())
+	f.mtimefs = f.Filesystem(f.fset)
 
 	// Create a parent dir with a certain owner/group.
 
@@ -905,7 +905,7 @@ func TestCopyOwner(t *testing.T) {
 func TestSRConflictReplaceFileByDir(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 
@@ -937,7 +937,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
 func TestSRConflictReplaceFileByLink(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 
@@ -970,7 +970,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
 func TestDeleteBehindSymlink(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	destDir := createTmpDir()
 	defer os.RemoveAll(destDir)
@@ -1063,7 +1063,7 @@ func TestPullCtxCancel(t *testing.T) {
 func TestPullDeleteUnscannedDir(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	dir := "foobar"
 	must(t, ffs.MkdirAll(dir, 0777))
@@ -1092,7 +1092,7 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
 func TestPullCaseOnlyPerformFinish(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 	contents := []byte("contents")
@@ -1153,7 +1153,7 @@ func TestPullCaseOnlySymlink(t *testing.T) {
 func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 	if dir {
diff --git a/lib/model/model_test.go b/lib/model/model_test.go
index eac0b69fece..e6f8ee49e8d 100644
--- a/lib/model/model_test.go
+++ b/lib/model/model_test.go
@@ -222,7 +222,7 @@ func BenchmarkIndex_100(b *testing.B) {
 func benchmarkIndex(b *testing.B, nfiles int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	files := genFiles(nfiles)
 	must(b, m.Index(device1, fcfg.ID, files))
@@ -249,7 +249,7 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
 func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	files := genFiles(nfiles)
 	ufiles := genFiles(nufiles)
@@ -1517,7 +1517,7 @@ func TestIgnores(t *testing.T) {
 
 	// Invalid path, treated like no patterns at all.
 	fcfg := config.FolderConfiguration{ID: "fresh", Path: "XXX"}
-	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
+	ignores := ignore.New(fcfg.Filesystem(nil), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
 	m.fmut.Lock()
 	m.folderCfgs[fcfg.ID] = fcfg
 	m.folderIgnores[fcfg.ID] = ignores
@@ -1709,7 +1709,7 @@ func TestRWScanRecovery(t *testing.T) {
 func TestGlobalDirectoryTree(t *testing.T) {
 	m, _, fcfg, wCancel := setupModelWithConnection(t)
 	defer wCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	b := func(isfile bool, path ...string) protocol.FileInfo {
 		typ := protocol.FileInfoTypeDirectory
@@ -2012,7 +2012,7 @@ func BenchmarkTree_100_10(b *testing.B) {
 func benchmarkTree(b *testing.B, n1, n2 int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	m.ScanFolder(fcfg.ID)
 	files := genDeepFiles(n1, n2)
@@ -2477,7 +2477,7 @@ func TestIssue2571(t *testing.T) {
 
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer os.RemoveAll(testFs.URI())
 
 	for _, dir := range []string{"toLink", "linkTarget"} {
@@ -2516,7 +2516,7 @@ func TestIssue4573(t *testing.T) {
 
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer os.RemoveAll(testFs.URI())
 
 	must(t, testFs.MkdirAll("inaccessible", 0755))
@@ -2546,7 +2546,7 @@ func TestIssue4573(t *testing.T) {
 func TestInternalScan(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer os.RemoveAll(testFs.URI())
 
 	testCases := map[string]func(protocol.FileInfo) bool{
@@ -2644,7 +2644,7 @@ func TestCustomMarkerName(t *testing.T) {
 func TestRemoveDirWithContent(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	tfs.MkdirAll("dirwith", 0755)
@@ -2706,7 +2706,7 @@ func TestIssue4475(t *testing.T) {
 	m, conn, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
 	defer cleanupModel(m)
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 
 	// Scenario: Dir is deleted locally and before syncing/index exchange
 	// happens, a file is create in that dir on the remote.
@@ -2769,7 +2769,7 @@ func TestVersionRestore(t *testing.T) {
 	fcfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", fs.FilesystemTypeBasic, dir)
 	fcfg.Versioning.Type = "simple"
 	fcfg.FSWatcherEnabled = false
-	filesystem := fcfg.Filesystem()
+	filesystem := fcfg.Filesystem(nil)
 
 	rawConfig := config.Configuration{
 		Folders: []config.FolderConfiguration{fcfg},
@@ -3005,7 +3005,7 @@ func TestIssue4094(t *testing.T) {
 		t.Fatalf("failed setting ignores: %v", err)
 	}
 
-	if _, err := fcfg.Filesystem().Lstat(".stignore"); err != nil {
+	if _, err := fcfg.Filesystem(nil).Lstat(".stignore"); err != nil {
 		t.Fatalf("failed stating .stignore: %v", err)
 	}
 }
@@ -3037,7 +3037,7 @@ func TestIssue4903(t *testing.T) {
 		t.Fatalf("expected path missing error, got: %v", err)
 	}
 
-	if _, err := fcfg.Filesystem().Lstat("."); !fs.IsNotExist(err) {
+	if _, err := fcfg.Filesystem(nil).Lstat("."); !fs.IsNotExist(err) {
 		t.Fatalf("Expected missing path error, got: %v", err)
 	}
 }
@@ -3067,7 +3067,7 @@ func TestParentOfUnignored(t *testing.T) {
 	m, cancel := newState(t, defaultCfg)
 	defer cleanupModel(m)
 	defer cancel()
-	defer defaultFolderConfig.Filesystem().Remove(".stignore")
+	defer defaultFolderConfig.Filesystem(nil).Remove(".stignore")
 
 	m.SetIgnores("default", []string{"!quux", "*"})
 
@@ -3184,7 +3184,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
 	m := setupModel(t, w)
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	br := &testutils.BlockingRW{}
 	nw := &testutils.NoopRW{}
@@ -3220,7 +3220,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 func TestModTimeWindow(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	fcfg.RawModTimeWindowS = 2
 	setFolder(t, w, fcfg)
 	m := setupModel(t, w)
@@ -3277,7 +3277,7 @@ func TestModTimeWindow(t *testing.T) {
 func TestDevicePause(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	sub := m.evLogger.Subscribe(events.DevicePaused)
 	defer sub.Unsubscribe()
@@ -3304,7 +3304,7 @@ func TestDevicePause(t *testing.T) {
 func TestDeviceWasSeen(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	m.deviceWasSeen(device1)
 
@@ -3399,7 +3399,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 
 	numFiles := 20
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	for i := 0; i < numFiles; i++ {
 		v := fmt.Sprintf("%d", i)
 		writeFile(t, ffs, v, []byte(v))
@@ -3468,7 +3468,7 @@ func TestRenameSameFile(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	writeFile(t, ffs, "file", []byte("file"))
 
 	m.ScanFolders()
@@ -3519,7 +3519,7 @@ func TestRenameEmptyFile(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 
 	writeFile(t, ffs, "file", []byte("data"))
 	writeFile(t, ffs, "empty", nil)
@@ -3596,7 +3596,7 @@ func TestBlockListMap(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	writeFile(t, ffs, "one", []byte("content"))
 	writeFile(t, ffs, "two", []byte("content"))
 	writeFile(t, ffs, "three", []byte("content"))
@@ -3664,7 +3664,7 @@ func TestScanRenameCaseOnly(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	name := "foo"
 	writeFile(t, ffs, name, []byte("contents"))
 
@@ -3782,7 +3782,7 @@ func TestAddFolderCompletion(t *testing.T) {
 
 func TestScanDeletedROChangedOnSR(t *testing.T) {
 	m, _, fcfg, wCancel := setupModelWithConnection(t)
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	defer wCancel()
 	defer cleanupModelAndRemoveDir(m, ffs.URI())
 	fcfg.Type = config.FolderTypeReceiveOnly
@@ -3886,7 +3886,7 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
 func TestIssue6961(t *testing.T) {
 	wcfg, fcfg, wcfgCancel := tmpDefaultWrapper()
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	waiter, err := wcfg.Modify(func(cfg *config.Configuration) {
 		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device2, "device2"))
 		fcfg.Type = config.FolderTypeReceiveOnly
@@ -3956,7 +3956,7 @@ func TestIssue6961(t *testing.T) {
 func TestCompletionEmptyGlobal(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 	files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID.Short()), Sequence: 1}}
 	m.fmut.Lock()
 	m.folderFiles[fcfg.ID].Update(protocol.LocalDeviceID, files)
@@ -4157,7 +4157,7 @@ func TestCCFolderNotRunning(t *testing.T) {
 	// Create the folder, but don't start it.
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	m := newModel(t, w, myID, "syncthing", "dev", nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -4264,7 +4264,7 @@ func TestDeletedNotLocallyChangedReceiveEncrypted(t *testing.T) {
 
 func deletedNotLocallyChanged(t *testing.T, ft config.FolderType) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	fcfg.Type = ft
 	setFolder(t, w, fcfg)
 	defer wCancel()
diff --git a/lib/model/requests_test.go b/lib/model/requests_test.go
index 76868748a2f..2721901a420 100644
--- a/lib/model/requests_test.go
+++ b/lib/model/requests_test.go
@@ -32,7 +32,7 @@ func TestRequestSimple(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	// We listen for incoming index updates and trigger when we see one for
@@ -79,7 +79,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -122,7 +122,7 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected names.
@@ -181,7 +181,7 @@ func TestRequestCreateTmpSymlink(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -224,7 +224,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
 	defer func() {
-		os.RemoveAll(fcfg.Filesystem().URI())
+		os.RemoveAll(fcfg.Filesystem(nil).URI())
 		os.Remove(w.ConfigPath())
 	}()
 
@@ -301,7 +301,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 	w, wCancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
 	defer wCancel()
 	fcfg := testFolderConfigTmp()
-	fss := fcfg.Filesystem()
+	fss := fcfg.Filesystem(nil)
 	fcfg.Type = ft
 	setFolder(t, w, fcfg)
 	m := setupModel(t, w)
@@ -430,7 +430,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 func TestIssue4841(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
@@ -478,7 +478,7 @@ func TestIssue4841(t *testing.T) {
 func TestRescanIfHaveInvalidContent(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	payload := []byte("hello")
@@ -544,7 +544,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
 func TestParentDeletion(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, testFs.URI())
 
 	parent := "foo"
@@ -623,7 +623,7 @@ func TestRequestSymlinkWindows(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
@@ -691,7 +691,7 @@ func equalContents(path string, contents []byte) error {
 func TestRequestRemoteRenameChanged(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -825,7 +825,7 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
 func TestRequestRemoteRenameConflict(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
@@ -916,7 +916,7 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
 func TestRequestDeleteChanged(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	done := make(chan struct{})
@@ -984,7 +984,7 @@ func TestRequestDeleteChanged(t *testing.T) {
 func TestNeedFolderFiles(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
@@ -1032,7 +1032,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
 	m := setupModel(t, w)
-	fss := fcfg.Filesystem()
+	fss := fcfg.Filesystem(nil)
 	tmpDir := fss.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
@@ -1127,7 +1127,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 func TestRequestLastFileProgress(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	done := make(chan struct{})
@@ -1162,7 +1162,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	indexChan := make(chan []protocol.FileInfo)
@@ -1275,7 +1275,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	dir1 := "foo"
 	dir2 := "bar"
 
@@ -1342,7 +1342,7 @@ func TestRequestReceiveEncrypted(t *testing.T) {
 
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	fcfg.Type = config.FolderTypeReceiveEncrypted
 	setFolder(t, w, fcfg)
 
@@ -1450,7 +1450,7 @@ func TestRequestGlobalInvalidToValid(t *testing.T) {
 	must(t, err)
 	waiter.Wait()
 	addFakeConn(m, device2, fcfg.ID)
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	indexChan := make(chan []protocol.FileInfo, 1)
diff --git a/lib/model/testutils_test.go b/lib/model/testutils_test.go
index e1660783977..954d64b0032 100644
--- a/lib/model/testutils_test.go
+++ b/lib/model/testutils_test.go
@@ -40,7 +40,7 @@ func init() {
 	defaultCfgWrapper, defaultCfgWrapperCancel = createTmpWrapper(config.New(myID))
 
 	defaultFolderConfig = testFolderConfig("testdata")
-	defaultFs = defaultFolderConfig.Filesystem()
+	defaultFs = defaultFolderConfig.Filesystem(nil)
 
 	waiter, _ := defaultCfgWrapper.Modify(func(cfg *config.Configuration) {
 		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device1, "device1"))
@@ -308,7 +308,7 @@ func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderCon
 	t.Helper()
 	m.removeFolder(fcfg)
 	fset := newFileSet(t, fcfg.ID, m.db)
-	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
+	ignores := ignore.New(fcfg.Filesystem(nil), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
 	m.fmut.Lock()
 	m.addAndStartFolderLockedWithIgnores(fcfg, fset, ignores)
 	m.fmut.Unlock()
diff --git a/lib/scanner/walk_test.go b/lib/scanner/walk_test.go
index 4edad2b4778..c673accf4dc 100644
--- a/lib/scanner/walk_test.go
+++ b/lib/scanner/walk_test.go
@@ -38,10 +38,14 @@ type testfile struct {
 
 type testfileList []testfile
 
+const (
+	testFsType     = fs.FilesystemTypeBasic
+	testFsLocation = "testdata"
+)
+
 var (
-	testFs     fs.Filesystem
-	testFsType = fs.FilesystemTypeBasic
-	testdata   = testfileList{
+	testFs   fs.Filesystem
+	testdata = testfileList{
 		{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
 		{"dir1", 128, ""},
 		{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
@@ -58,7 +62,7 @@ func init() {
 	// potentially taking down the box...
 	rdebug.SetMaxStack(10 * 1 << 20)
 
-	testFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
+	testFs = fs.NewFilesystem(testFsType, testFsLocation)
 }
 
 func TestWalkSub(t *testing.T) {
@@ -258,7 +262,7 @@ func TestNormalizationDarwinCaseFS(t *testing.T) {
 		return
 	}
 
-	testFs := fs.NewCaseFilesystem(testFs)
+	testFs := fs.NewFilesystem(testFsType, testFsLocation, new(fs.OptionDetectCaseConflicts))
 
 	testFs.RemoveAll("normalization")
 	defer testFs.RemoveAll("normalization")
diff --git a/lib/versioner/simple_test.go b/lib/versioner/simple_test.go
index 12dc2c81ac2..0e2f3346795 100644
--- a/lib/versioner/simple_test.go
+++ b/lib/versioner/simple_test.go
@@ -70,7 +70,7 @@ func TestSimpleVersioningVersionCount(t *testing.T) {
 			},
 		},
 	}
-	fs := cfg.Filesystem()
+	fs := cfg.Filesystem(nil)
 
 	v := newSimple(cfg)
 
diff --git a/lib/versioner/trashcan_test.go b/lib/versioner/trashcan_test.go
index 891c64ecf3f..e0c947be9f5 100644
--- a/lib/versioner/trashcan_test.go
+++ b/lib/versioner/trashcan_test.go
@@ -38,7 +38,7 @@ func TestTrashcanArchiveRestoreSwitcharoo(t *testing.T) {
 			FSPath: tmpDir2,
 		},
 	}
-	folderFs := cfg.Filesystem()
+	folderFs := cfg.Filesystem(nil)
 
 	versionsFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir2)