[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1111236: trixie-pu: package nextcloud-desktop/3.16.7~deb13u1



 Control: tags -1 - moreinfo
Hey,
 
> Apologies for the delay in replying.
> 
> The debdiff that you have attached appears to be between the current
> stable package and the version in unstable, not the proposed backport
> to stable. Please provide a debdiff representing the actual proposed
> stable update.

ACK - it is a diff between stable and the version in unstable, because when I 
created the request stable was in freeze, so I asked for a pu. It is still 
true that the version in unstable and in that request would be the same.

I built the current package today via

sbuild -d trixie nextcloud-desktop_3.16.7-1.dsc

( no further changes are needed to ship this version to trixie except the 
version).

I will update the version to ~deb13u1 if needed.

> > Unfortunately the diff on the translations make the diff quite big :(
> 
> Feel free to filter the diff when attaching it to the bug report to
> exclude the translations, so long as you make it clear exactly what
> filtering has been applied.

I created a debdiff for 3.16.7-1~deb13u1 without the translations aka filtering 
the changes in

nextcloud.client-desktop/*translation.desktop
translations/*

As the version exists now for quite a while in unstable - I can say, no 
bugreport was open against this version from users in unstable/testing. 
Upstream hasn't shipped any new minor release for the 3.16 branch, so 3.16.7 
is still the last minor release from upstream.

Regards,

hefee

diff -Nru nextcloud-desktop-3.16.4/admin/linux/build-appimage.sh nextcloud-desktop-3.16.7/admin/linux/build-appimage.sh
--- nextcloud-desktop-3.16.4/admin/linux/build-appimage.sh	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/admin/linux/build-appimage.sh	2025-07-28 10:08:26.000000000 +0200
@@ -49,9 +49,9 @@
 
 [ -d usr/lib/x86_64-linux-gnu ] && mv usr/lib/x86_64-linux-gnu/* usr/lib/
 
-mkdir usr/plugins
-mv usr/lib64/*sync_vfs_suffix.so usr/plugins || mv usr/lib/*sync_vfs_suffix.so usr/plugins
-mv usr/lib64/*sync_vfs_xattr.so usr/plugins  || mv usr/lib/*sync_vfs_xattr.so usr/plugins
+mkdir -p AppDir/usr/plugins
+mv usr/lib64/*sync_vfs_suffix.so AppDir/usr/plugins || mv usr/lib/*sync_vfs_suffix.so AppDir/usr/plugins
+mv usr/lib64/*sync_vfs_xattr.so  AppDir/usr/plugins || mv usr/lib/*sync_vfs_xattr.so  AppDir/usr/plugins
 
 rm -rf usr/lib/cmake
 rm -rf usr/include
@@ -63,6 +63,10 @@
 rm -rf usr/share/nautilus-python/
 rm -rf usr/share/nemo-python/
 
+# The client-specific data dir also contains the translations, we want to have those in the AppImage.
+mkdir -p AppDir/usr/share
+mv usr/share/${EXECUTABLE_NAME} AppDir/usr/share/${EXECUTABLE_NAME}
+
 # Move sync exclude to right location
 mv /app/etc/*/sync-exclude.lst usr/bin/
 rm -rf etc
@@ -97,7 +101,7 @@
 
 # Workaround issue #103 and #7231
 export APPIMAGETOOL=appimagetool-x86_64.AppImage
-wget -O ${APPIMAGETOOL} --ca-directory=/etc/ssl/certs -c https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
+wget -O ${APPIMAGETOOL} --ca-directory=/etc/ssl/certs -c https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
 chmod a+x ${APPIMAGETOOL}
 rm -rf ./squashfs-root
 ./${APPIMAGETOOL} --appimage-extract
diff -Nru nextcloud-desktop-3.16.4/admin/linux/debian/drone-build.sh nextcloud-desktop-3.16.7/admin/linux/debian/drone-build.sh
--- nextcloud-desktop-3.16.4/admin/linux/debian/drone-build.sh	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/admin/linux/debian/drone-build.sh	2025-07-28 10:08:26.000000000 +0200
@@ -18,7 +18,7 @@
     UBUNTU_DISTRIBUTIONS="bionic focal jammy kinetic"
     DEBIAN_DISTRIBUTIONS="buster stretch testing"
 else
-    UBUNTU_DISTRIBUTIONS="jammy noble oracular plucky"
+    UBUNTU_DISTRIBUTIONS="jammy noble plucky questing"
     DEBIAN_DISTRIBUTIONS="bullseye bookworm testing"
 fi
 
diff -Nru nextcloud-desktop-3.16.4/admin/osx/CMakeLists.txt nextcloud-desktop-3.16.7/admin/osx/CMakeLists.txt
--- nextcloud-desktop-3.16.4/admin/osx/CMakeLists.txt	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/admin/osx/CMakeLists.txt	2025-07-28 10:08:26.000000000 +0200
@@ -11,6 +11,7 @@
 
 find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS Core REQUIRED)
 configure_file(create_mac.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/create_mac.sh)
+configure_file(macosx.entitlements.cmake ${CMAKE_CURRENT_BINARY_DIR}/macosx.entitlements)
 configure_file(macosx.pkgproj.cmake ${CMAKE_CURRENT_BINARY_DIR}/macosx.pkgproj)
 configure_file(pre_install.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/pre_install.sh)
 configure_file(post_install.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/post_install.sh)
diff -Nru nextcloud-desktop-3.16.4/admin/osx/mac-crafter/Sources/main.swift nextcloud-desktop-3.16.7/admin/osx/mac-crafter/Sources/main.swift
--- nextcloud-desktop-3.16.4/admin/osx/mac-crafter/Sources/main.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/admin/osx/mac-crafter/Sources/main.swift	2025-07-28 10:08:26.000000000 +0200
@@ -43,7 +43,7 @@
     @Option(name: [.long], help: "Brew installation script URL.")
     var brewInstallShUrl = "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh";
 
-    @Option(name: [.long], help: "CraftMaster git url.")
+    @Option(name: [.long], help: "CraftMaster Git URL.")
     var craftMasterGitUrl = "https://invent.kde.org/packaging/craftmaster.git";
 
     @Option(name: [.long], help: "Nextcloud Desktop Client craft blueprint git url.")
@@ -244,7 +244,12 @@
         let clientAppDir = "\(clientBuildDir)/image-\(buildType)-master/\(appName).app"
         if let codeSignIdentity {
             print("Code-signing Nextcloud Desktop Client libraries and frameworks...")
-            try codesignClientAppBundle(at: clientAppDir, withCodeSignIdentity: codeSignIdentity)
+            let entitlementsPath = "\(clientBuildDir)/work/build/admin/osx/macosx.entitlements"
+            try codesignClientAppBundle(
+                at: clientAppDir,
+                withCodeSignIdentity: codeSignIdentity,
+                usingEntitlements: entitlementsPath
+            )
         }
 
         print("Placing Nextcloud Desktop Client in \(productPath)...")
@@ -286,11 +291,19 @@
     @Option(name: [.short, .long], help: "Code signing identity for desktop client and libs.")
     var codeSignIdentity: String
 
+    @Option(name: [.short, .long], help: "Entitlements to apply to the app bundle.")
+    var entitlementsPath: String?
+
     mutating func run() throws {
         let absolutePath = appBundlePath.hasPrefix("/")
             ? appBundlePath
             : "\(FileManager.default.currentDirectoryPath)/\(appBundlePath)"
-        try codesignClientAppBundle(at: absolutePath, withCodeSignIdentity: codeSignIdentity)
+
+        try codesignClientAppBundle(
+            at: absolutePath,
+            withCodeSignIdentity: codeSignIdentity,
+            usingEntitlements: entitlementsPath
+        )
     }
 }
 
diff -Nru nextcloud-desktop-3.16.4/admin/osx/mac-crafter/Sources/Utils/Codesign.swift nextcloud-desktop-3.16.7/admin/osx/mac-crafter/Sources/Utils/Codesign.swift
--- nextcloud-desktop-3.16.4/admin/osx/mac-crafter/Sources/Utils/Codesign.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/admin/osx/mac-crafter/Sources/Utils/Codesign.swift	2025-07-28 10:08:26.000000000 +0200
@@ -55,9 +55,17 @@
 func codesign(identity: String, path: String, options: String = defaultCodesignOptions) throws {
     print("Code-signing \(path)...")
     let command = "codesign -s \"\(identity)\" \(options) \"\(path)\""
-    guard shell(command) == 0 else {
-        throw CodeSigningError.failedToCodeSign("Failed to code-sign \(path).")
+    for _ in 1...5 {
+        guard shell(command) == 0 else {
+            print("Code-signing failed, retrying ...")
+            continue
+        }
+
+        // code signing was successful
+        return
     }
+
+    throw CodeSigningError.failedToCodeSign("Failed to code-sign \(path).")
 }
 
 func recursivelyCodesign(
@@ -99,7 +107,9 @@
 }
 
 func codesignClientAppBundle(
-    at clientAppDir: String, withCodeSignIdentity codeSignIdentity: String
+    at clientAppDir: String,
+    withCodeSignIdentity codeSignIdentity: String,
+    usingEntitlements entitlementsPath: String? = nil
 ) throws {
     print("Code-signing Nextcloud Desktop Client libraries, frameworks and plugins...")
 
@@ -189,5 +199,13 @@
     let mainExecutableName = String(appName.dropLast(".app".count))
     let mainExecutablePath = "\(binariesDir)/\(mainExecutableName)"
     try recursivelyCodesign(path: binariesDir, identity: codeSignIdentity, skip: [mainExecutablePath])
-    try codesign(identity: codeSignIdentity, path: mainExecutablePath)
+
+    var mainExecutableCodesignOptions = defaultCodesignOptions
+    if let entitlementsPath {
+        mainExecutableCodesignOptions =
+            "--timestamp --force --verbose=4 --options runtime --entitlements \"\(entitlementsPath)\""
+    }
+    try codesign(
+        identity: codeSignIdentity, path: mainExecutablePath, options: mainExecutableCodesignOptions
+    )
 }
diff -Nru nextcloud-desktop-3.16.4/admin/osx/macosx.entitlements.cmake nextcloud-desktop-3.16.7/admin/osx/macosx.entitlements.cmake
--- nextcloud-desktop-3.16.4/admin/osx/macosx.entitlements.cmake	1970-01-01 01:00:00.000000000 +0100
+++ nextcloud-desktop-3.16.7/admin/osx/macosx.entitlements.cmake	2025-07-28 10:08:26.000000000 +0200
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd";>
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>@SOCKETAPI_TEAM_IDENTIFIER_PREFIX@@APPLICATION_REV_DOMAIN@</string>
+	</array>
+</dict>
+</plist>
diff -Nru nextcloud-desktop-3.16.4/admin/win/msi/Nextcloud.wxs nextcloud-desktop-3.16.7/admin/win/msi/Nextcloud.wxs
--- nextcloud-desktop-3.16.4/admin/win/msi/Nextcloud.wxs	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/admin/win/msi/Nextcloud.wxs	2025-07-28 10:08:26.000000000 +0200
@@ -44,8 +44,10 @@
         https://www.firegiant.com/wix/tutorial/upgrades-and-modularization/replacing-ourselves/
         https://www.joyofsetup.com/2010/01/16/major-upgrades-now-easier-than-ever/
     -->
-    <MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
-    <Property Id="REINSTALLMODE" Value="amus" />
+    <MajorUpgrade Schedule="afterInstallExecute" AllowDowngrades="yes" />
+    <Property Id="REINSTALLMODE" Value="dmus" />
+    <Property Id="MSIRMSHUTDOWN" Value="1" />
+    <Property Id="REBOOT" Value="ReallySuppress" />
 
     <Media Id="1" Cabinet="$(var.AppShortName).cab" EmbedCab="yes" />
 
@@ -88,9 +90,6 @@
 
         <!-- Uninstall: Cleanup the Registry -->
         <Custom Action="RegistryCleanupCustomAction" After="RemoveFiles">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
-
-        <!-- Schedule Reboot for the Shell Extensions (in silent installation mode only, or if SCHEDULE_REBOOT argument is set-->
-        <ScheduleReboot After="InstallFinalize">(SCHEDULE_REBOOT=1) OR NOT (UILevel=2)</ScheduleReboot>
     </InstallExecuteSequence>
 
     <!-- "Add or Remove" Programs Entries -->
diff -Nru nextcloud-desktop-3.16.4/debian/changelog nextcloud-desktop-3.16.7/debian/changelog
--- nextcloud-desktop-3.16.4/debian/changelog	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/changelog	2025-08-16 01:15:24.000000000 +0200
@@ -1,3 +1,30 @@
+nextcloud-desktop (3.16.7-1~deb13u1) XXXX; urgency=medium
+
+  * Rebuild for trixie.
+
+ -- Sandro Knauß <hefee@debian.org>  DATE
+
+nextcloud-desktop (3.16.7-1) unstable; urgency=medium
+
+  * New upstream release.
+
+ -- Sandro Knauß <hefee@debian.org>  Sat, 16 Aug 2025 01:15:24 +0200
+
+nextcloud-desktop (3.16.6-3) unstable; urgency=medium
+
+  * Release to unstable (#1091614 is fixed).
+
+ -- Sandro Knauß <hefee@debian.org>  Sun, 27 Jul 2025 14:55:49 +0200
+
+nextcloud-desktop (3.16.6-2) experimental; urgency=medium
+
+  * Fix again "nextcloud enters busy loop when using a share on NTFS."
+    (Closes: 1091614)
+
+ -- Sandro Knauß <hefee@debian.org>  Sun, 27 Jul 2025 12:54:15 +0200
+
+nextcloud-desktop (3.16.6-1) experimental; urgency=medium
+
+  * New upstream release.
+  * Update patch hunks.
+  * Remove patch for #1091614, it seems fixed on upstream.
+
+ -- Sandro Knauß <hefee@debian.org>  Mon, 21 Jul 2025 20:21:07 +0200
+
 nextcloud-desktop (3.16.4-1) unstable; urgency=medium
 
   * New upstream release.
diff -Nru nextcloud-desktop-3.16.4/debian/patches/0003-Use-release-version-for-Debian.patch nextcloud-desktop-3.16.7/debian/patches/0003-Use-release-version-for-Debian.patch
--- nextcloud-desktop-3.16.4/debian/patches/0003-Use-release-version-for-Debian.patch	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/patches/0003-Use-release-version-for-Debian.patch	2025-08-16 01:15:24.000000000 +0200
@@ -8,10 +8,10 @@
  1 file changed, 5 insertions(+)
 
 diff --git a/VERSION.cmake b/VERSION.cmake
-index 3fe5f77..1600bcd 100644
+index dd96a86..b41828b 100644
 --- a/VERSION.cmake
 +++ b/VERSION.cmake
-@@ -64,3 +64,8 @@ endif()
+@@ -66,3 +66,8 @@ endif()
  # ------------------------------------
  # Not used anymore. For brander, please maintain craftmaster.ini
  set(QT_MAJOR_VERSION 6)
diff -Nru nextcloud-desktop-3.16.4/debian/patches/0004-Don-t-use-GuiPrivate.patch nextcloud-desktop-3.16.7/debian/patches/0004-Don-t-use-GuiPrivate.patch
--- nextcloud-desktop-3.16.4/debian/patches/0004-Don-t-use-GuiPrivate.patch	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/patches/0004-Don-t-use-GuiPrivate.patch	2025-08-16 01:15:24.000000000 +0200
@@ -10,7 +10,7 @@
  1 file changed, 5 deletions(-)
 
 diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
-index 2bfa004..d3fb5aa 100644
+index a58117f..21be04c 100644
 --- a/src/gui/CMakeLists.txt
 +++ b/src/gui/CMakeLists.txt
 @@ -3,10 +3,6 @@ find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS Widgets Svg Qml Quick Qui
@@ -24,7 +24,7 @@
  if(CMAKE_BUILD_TYPE MATCHES Debug)
      add_definitions(-DQT_QML_DEBUG)
  endif()
-@@ -556,7 +552,6 @@ target_link_libraries(nextcloudCore
+@@ -560,7 +556,6 @@ target_link_libraries(nextcloudCore
    PUBLIC
    Nextcloud::sync
    Qt::Widgets
diff -Nru nextcloud-desktop-3.16.4/debian/patches/0004-GIT_SHA1-points-to-the-sha1-of-upstream.patch nextcloud-desktop-3.16.7/debian/patches/0004-GIT_SHA1-points-to-the-sha1-of-upstream.patch
--- nextcloud-desktop-3.16.4/debian/patches/0004-GIT_SHA1-points-to-the-sha1-of-upstream.patch	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/patches/0004-GIT_SHA1-points-to-the-sha1-of-upstream.patch	2025-08-16 01:15:24.000000000 +0200
@@ -10,7 +10,7 @@
  1 file changed, 2 insertions(+), 3 deletions(-)
 
 diff --git a/CMakeLists.txt b/CMakeLists.txt
-index 5b7448b..ee4a394 100644
+index 140ffa8..8cf4499 100644
 --- a/CMakeLists.txt
 +++ b/CMakeLists.txt
 @@ -112,9 +112,8 @@ include(GNUInstallDirs)
diff -Nru nextcloud-desktop-3.16.4/debian/patches/0006-Revert-better-logs-and-factor-common-code-in-folder-.patch nextcloud-desktop-3.16.7/debian/patches/0006-Revert-better-logs-and-factor-common-code-in-folder-.patch
--- nextcloud-desktop-3.16.4/debian/patches/0006-Revert-better-logs-and-factor-common-code-in-folder-.patch	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/patches/0006-Revert-better-logs-and-factor-common-code-in-folder-.patch	1970-01-01 01:00:00.000000000 +0100
@@ -1,41 +0,0 @@
-From: =?utf-8?q?Sandro_Knau=C3=9F?= <hefee@debian.org>
-Date: Sun, 5 Jan 2025 20:33:16 +0100
-Subject: Revert "better logs and factor common code in folder permissions
- handling"
-
-This reverts commit 1417e8cb60e84762f94345b21d587fb54bc90b51.
----
- src/libsync/owncloudpropagator.cpp | 19 ++++++++-----------
- 1 file changed, 8 insertions(+), 11 deletions(-)
-
-diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp
-index e239f05..04e556f 100644
---- a/src/libsync/owncloudpropagator.cpp
-+++ b/src/libsync/owncloudpropagator.cpp
-@@ -1497,18 +1497,15 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
-                 }
-             } else {
-                 try {
--                    const auto permissionsChangeHelper = [] (const auto fileName)
--                    {
--                        qCDebug(lcDirectory) << fileName << "permissions changed: old permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
--                        FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadWrite);
--                        qCDebug(lcDirectory) << fileName << "applied new permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
--                    };
--
--                    if (const auto fileName = propagator()->fullLocalPath(_item->_file); FileSystem::fileExists(fileName)) {
--                        permissionsChangeHelper(fileName);
-+                    if (FileSystem::fileExists(propagator()->fullLocalPath(_item->_file))) {
-+                        qCDebug(lcDirectory) << propagator()->fullLocalPath(_item->_file) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-+                        FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_file), FileSystem::FolderPermissions::ReadWrite);
-+                        qCDebug(lcDirectory) << propagator()->fullLocalPath(_item->_file) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-                     }
--                    if (const auto fileName = propagator()->fullLocalPath(_item->_renameTarget); !_item->_renameTarget.isEmpty() && FileSystem::fileExists(fileName)) {
--                        permissionsChangeHelper(fileName);
-+                    if (!_item->_renameTarget.isEmpty() && FileSystem::fileExists(propagator()->fullLocalPath(_item->_renameTarget))) {
-+                        qCDebug(lcDirectory) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-+                        FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_renameTarget), FileSystem::FolderPermissions::ReadWrite);
-+                        qCDebug(lcDirectory) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-                     }
-                 }
-                 catch (const std::filesystem::filesystem_error &e)
diff -Nru nextcloud-desktop-3.16.4/debian/patches/0006-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch nextcloud-desktop-3.16.7/debian/patches/0006-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch
--- nextcloud-desktop-3.16.4/debian/patches/0006-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch	1970-01-01 01:00:00.000000000 +0100
+++ nextcloud-desktop-3.16.7/debian/patches/0006-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch	2025-08-16 01:15:24.000000000 +0200
@@ -0,0 +1,169 @@
+From: =?utf-8?q?Sandro_Knau=C3=9F?= <hefee@debian.org>
+Date: Sun, 27 Jul 2025 12:24:51 +0200
+Subject: Revert "ensure no any user writable permissions in Nextcloud sync
+ folder"
+
+This reverts commit 5b2af166d3d9c8537c565922750392d4a3f6610e.
+
+Updated to apply and match for 3.16.6.
+
+Origin: backport
+Bug-Debian: https://bugs.debian.org/1091614
+Bug: https://github.com/nextcloud/desktop/issues/7613
+Forwarded: not-needed
+Last-Update: 2025-07-25
+---
+ src/csync/csync.h                      |  2 --
+ src/csync/vio/csync_vio_local_unix.cpp |  1 -
+ src/libsync/discovery.cpp              | 11 -----------
+ src/libsync/discoveryphase.cpp         |  1 -
+ src/libsync/discoveryphase.h           |  1 -
+ src/libsync/owncloudpropagator.cpp     |  7 +++++++
+ src/libsync/syncengine.cpp             |  4 ----
+ src/libsync/syncfileitem.h             |  2 --
+ 8 files changed, 7 insertions(+), 22 deletions(-)
+
+diff --git a/src/csync/csync.h b/src/csync/csync.h
+index 8329020..ff1ec56 100644
+--- a/src/csync/csync.h
++++ b/src/csync/csync.h
+@@ -218,7 +218,6 @@ struct OCSYNC_EXPORT csync_file_stat_s {
+   bool is_hidden BITFIELD(1); // Not saved in the DB, only used during discovery for local files.
+   bool isE2eEncrypted BITFIELD(1);
+   bool is_metadata_missing BITFIELD(1); // Indicates the file has missing metadata, f.ex. the file is not a placeholder in case of vfs.
+-  bool isPermissionsInvalid BITFIELD(1);
+ 
+   QByteArray path;
+   QByteArray rename_path;
+@@ -246,7 +245,6 @@ struct OCSYNC_EXPORT csync_file_stat_s {
+     , is_hidden(false)
+     , isE2eEncrypted(false)
+     , is_metadata_missing(false)
+-    , isPermissionsInvalid(false)
+   { }
+ };
+ 
+diff --git a/src/csync/vio/csync_vio_local_unix.cpp b/src/csync/vio/csync_vio_local_unix.cpp
+index 8f319a3..55cd0f0 100644
+--- a/src/csync/vio/csync_vio_local_unix.cpp
++++ b/src/csync/vio/csync_vio_local_unix.cpp
+@@ -169,7 +169,6 @@ static int _csync_vio_local_stat_mb(const mbchar_t *wuri, csync_file_stat_t *buf
+   buf->inode = sb.st_ino;
+   buf->modtime = sb.st_mtime;
+   buf->size = sb.st_size;
+-  buf->isPermissionsInvalid = (sb.st_mode & S_IWOTH) == S_IWOTH;
+ 
+   return 0;
+ }
+diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp
+index c95af63..39c65a2 100644
+--- a/src/libsync/discovery.cpp
++++ b/src/libsync/discovery.cpp
+@@ -1130,10 +1130,6 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
+         if (_queryLocal != NormalQuery && _queryServer != NormalQuery)
+             recurse = false;
+ 
+-        if (localEntry.isPermissionsInvalid) {
+-            recurse = true;
+-        }
+-
+         if ((item->_direction == SyncFileItem::Down || item->_instruction == CSYNC_INSTRUCTION_CONFLICT || item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_SYNC) &&
+                 item->_direction != SyncFileItem::Up &&
+                 (item->_modtime <= 0 || item->_modtime >= 0xFFFFFFFF)) {
+@@ -1162,13 +1158,6 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
+             }
+         }
+ 
+-        if (localEntry.isPermissionsInvalid && item->_instruction == CSyncEnums::CSYNC_INSTRUCTION_NONE) {
+-            item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
+-            item->_direction = SyncFileItem::Down;
+-        }
+-
+-        item->isPermissionsInvalid = localEntry.isPermissionsInvalid;
+-
+         auto recurseQueryLocal = _queryLocal == ParentNotChanged ? ParentNotChanged : localEntry.isDirectory || item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist;
+         if (item->isDirectory() && serverEntry.isValid() && dbEntry.isValid() && serverEntry.etag == dbEntry._etag && serverEntry.remotePerm != dbEntry._remotePerm) {
+             recurseQueryServer = ParentNotChanged;
+diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp
+index 7edd684..f881a21 100644
+--- a/src/libsync/discoveryphase.cpp
++++ b/src/libsync/discoveryphase.cpp
+@@ -370,7 +370,6 @@ void DiscoverySingleLocalDirectoryJob::run() {
+         i.isSymLink = dirent->type == ItemTypeSoftLink;
+         i.isVirtualFile = dirent->type == ItemTypeVirtualFile || dirent->type == ItemTypeVirtualFileDownload;
+         i.isMetadataMissing = dirent->is_metadata_missing;
+-        i.isPermissionsInvalid = dirent->isPermissionsInvalid;
+         i.type = dirent->type;
+         results.push_back(i);
+     }
+diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h
+index 0c9edce..913f37f 100644
+--- a/src/libsync/discoveryphase.h
++++ b/src/libsync/discoveryphase.h
+@@ -107,7 +107,6 @@ struct LocalInfo
+     bool isVirtualFile = false;
+     bool isSymLink = false;
+     bool isMetadataMissing = false;
+-    bool isPermissionsInvalid = false;
+     [[nodiscard]] bool isValid() const { return !name.isNull(); }
+ };
+ 
+diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp
+index fba85c5..10eedaf 100644
+--- a/src/libsync/owncloudpropagator.cpp
++++ b/src/libsync/owncloudpropagator.cpp
+@@ -1472,12 +1472,18 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
+                 try {
+                     if (const auto fileName = propagator()->fullLocalPath(_item->_file); FileSystem::fileExists(fileName)) {
+                         FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadOnly);
++                        qCDebug(lcDirectory) << fileName << "permissions changed: old permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
++                        std::filesystem::permissions(fileName.toStdWString(), std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
+                         Q_EMIT propagator()->touchedFile(fileName);
++                        qCDebug(lcDirectory) << fileName << "applied new permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
+                     }
+                     if (!_item->_renameTarget.isEmpty() && FileSystem::fileExists(propagator()->fullLocalPath(_item->_renameTarget))) {
+                         const auto fileName = propagator()->fullLocalPath(_item->_renameTarget);
+                         FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadOnly);
++                        qCDebug(lcDirectory) << fileName << "permissions changed: old permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
++                        std::filesystem::permissions(fileName.toStdWString(), std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
+                         Q_EMIT propagator()->touchedFile(fileName);
++                        qCDebug(lcDirectory) << fileName << "applied new permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
+                     }
+                 }
+                 catch (const std::filesystem::filesystem_error &e)
+@@ -1504,6 +1510,7 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
+                     {
+                         qCDebug(lcDirectory) << fileName << "permissions changed: old permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
+                         FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadWrite);
++                        std::filesystem::permissions(fileName.toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
+                         Q_EMIT propagator()->touchedFile(fileName);
+                         qCDebug(lcDirectory) << fileName << "applied new permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
+                     };
+diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp
+index e24ce8a..8237905 100644
+--- a/src/libsync/syncengine.cpp
++++ b/src/libsync/syncengine.cpp
+@@ -362,10 +362,6 @@ void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item)
+                 const bool isReadOnly = !item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite);
+                 modificationHappened = FileSystem::setFileReadOnlyWeak(filePath, isReadOnly);
+             }
+-            if (item->isPermissionsInvalid) {
+-                const auto isReadOnly = !item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite);
+-                FileSystem::setFileReadOnly(filePath, isReadOnly);
+-            }
+ 
+             modificationHappened |= item->_size != prev._fileSize;
+ 
+diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h
+index 154d13a..041e48d 100644
+--- a/src/libsync/syncfileitem.h
++++ b/src/libsync/syncfileitem.h
+@@ -344,8 +344,6 @@ public:
+     bool _isLivePhoto = false;
+     QString _livePhotoFile;
+ 
+-    bool isPermissionsInvalid = false;
+-
+     QString _discoveryResult;
+ 
+     /// if true, requests the file to be permanently deleted instead of moved to the trashbin
diff -Nru nextcloud-desktop-3.16.4/debian/patches/0007-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch nextcloud-desktop-3.16.7/debian/patches/0007-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch
--- nextcloud-desktop-3.16.4/debian/patches/0007-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/patches/0007-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch	1970-01-01 01:00:00.000000000 +0100
@@ -1,182 +0,0 @@
-From: =?utf-8?q?Sandro_Knau=C3=9F?= <hefee@debian.org>
-Date: Sun, 5 Jan 2025 23:17:28 +0100
-Subject: Revert "ensure no any user writable permissions in Nextcloud sync
- folder"
-
-This reverts commit 5b2af166d3d9c8537c565922750392d4a3f6610e.
----
- src/csync/csync.h                      |  2 --
- src/csync/vio/csync_vio_local_unix.cpp |  2 --
- src/libsync/discovery.cpp              | 11 -----------
- src/libsync/discoveryphase.cpp         |  1 -
- src/libsync/discoveryphase.h           |  1 -
- src/libsync/filesystem.cpp             |  1 -
- src/libsync/owncloudpropagator.cpp     | 14 +++++++++++---
- src/libsync/syncengine.cpp             |  4 ----
- src/libsync/syncfileitem.h             |  2 --
- 9 files changed, 11 insertions(+), 27 deletions(-)
-
-diff --git a/src/csync/csync.h b/src/csync/csync.h
-index 8329020..ff1ec56 100644
---- a/src/csync/csync.h
-+++ b/src/csync/csync.h
-@@ -218,7 +218,6 @@ struct OCSYNC_EXPORT csync_file_stat_s {
-   bool is_hidden BITFIELD(1); // Not saved in the DB, only used during discovery for local files.
-   bool isE2eEncrypted BITFIELD(1);
-   bool is_metadata_missing BITFIELD(1); // Indicates the file has missing metadata, f.ex. the file is not a placeholder in case of vfs.
--  bool isPermissionsInvalid BITFIELD(1);
- 
-   QByteArray path;
-   QByteArray rename_path;
-@@ -246,7 +245,6 @@ struct OCSYNC_EXPORT csync_file_stat_s {
-     , is_hidden(false)
-     , isE2eEncrypted(false)
-     , is_metadata_missing(false)
--    , isPermissionsInvalid(false)
-   { }
- };
- 
-diff --git a/src/csync/vio/csync_vio_local_unix.cpp b/src/csync/vio/csync_vio_local_unix.cpp
-index 8f319a3..b68eb31 100644
---- a/src/csync/vio/csync_vio_local_unix.cpp
-+++ b/src/csync/vio/csync_vio_local_unix.cpp
-@@ -169,7 +169,5 @@ static int _csync_vio_local_stat_mb(const mbchar_t *wuri, csync_file_stat_t *buf
-   buf->inode = sb.st_ino;
-   buf->modtime = sb.st_mtime;
-   buf->size = sb.st_size;
--  buf->isPermissionsInvalid = (sb.st_mode & S_IWOTH) == S_IWOTH;
--
-   return 0;
- }
-diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp
-index a97585a..769a559 100644
---- a/src/libsync/discovery.cpp
-+++ b/src/libsync/discovery.cpp
-@@ -1117,10 +1117,6 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
-         if (_queryLocal != NormalQuery && _queryServer != NormalQuery)
-             recurse = false;
- 
--        if (localEntry.isPermissionsInvalid) {
--            recurse = true;
--        }
--
-         if ((item->_direction == SyncFileItem::Down || item->_instruction == CSYNC_INSTRUCTION_CONFLICT || item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_SYNC) &&
-                 item->_direction != SyncFileItem::Up &&
-                 (item->_modtime <= 0 || item->_modtime >= 0xFFFFFFFF)) {
-@@ -1149,13 +1145,6 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
-             }
-         }
- 
--        if (localEntry.isPermissionsInvalid && item->_instruction == CSyncEnums::CSYNC_INSTRUCTION_NONE) {
--            item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
--            item->_direction = SyncFileItem::Down;
--        }
--
--        item->isPermissionsInvalid = localEntry.isPermissionsInvalid;
--
-         auto recurseQueryLocal = _queryLocal == ParentNotChanged ? ParentNotChanged : localEntry.isDirectory || item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist;
-         processFileFinalize(item, path, recurse, recurseQueryLocal, recurseQueryServer);
-     };
-diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp
-index 9b72732..51813ae 100644
---- a/src/libsync/discoveryphase.cpp
-+++ b/src/libsync/discoveryphase.cpp
-@@ -371,7 +371,6 @@ void DiscoverySingleLocalDirectoryJob::run() {
-         i.isSymLink = dirent->type == ItemTypeSoftLink;
-         i.isVirtualFile = dirent->type == ItemTypeVirtualFile || dirent->type == ItemTypeVirtualFileDownload;
-         i.isMetadataMissing = dirent->is_metadata_missing;
--        i.isPermissionsInvalid = dirent->isPermissionsInvalid;
-         i.type = dirent->type;
-         results.push_back(i);
-     }
-diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h
-index 0c9edce..913f37f 100644
---- a/src/libsync/discoveryphase.h
-+++ b/src/libsync/discoveryphase.h
-@@ -107,7 +107,6 @@ struct LocalInfo
-     bool isVirtualFile = false;
-     bool isSymLink = false;
-     bool isMetadataMissing = false;
--    bool isPermissionsInvalid = false;
-     [[nodiscard]] bool isValid() const { return !name.isNull(); }
- };
- 
-diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp
-index 3150819..28c3533 100644
---- a/src/libsync/filesystem.cpp
-+++ b/src/libsync/filesystem.cpp
-@@ -507,7 +507,6 @@ bool FileSystem::setFolderPermissions(const QString &path,
-         case OCC::FileSystem::FolderPermissions::ReadOnly:
-             break;
-         case OCC::FileSystem::FolderPermissions::ReadWrite:
--            std::filesystem::permissions(stdStrPath, std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
-             std::filesystem::permissions(stdStrPath, std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
-             break;
-         }
-diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp
-index 480baa8..220d7f7 100644
---- a/src/libsync/owncloudpropagator.cpp
-+++ b/src/libsync/owncloudpropagator.cpp
-@@ -1470,9 +1470,15 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
-                 try {
-                     if (FileSystem::fileExists(propagator()->fullLocalPath(_item->_file))) {
-                         FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_file), FileSystem::FolderPermissions::ReadOnly);
-+                        qCDebug(lcDirectory) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-+                        std::filesystem::permissions(propagator()->fullLocalPath(_item->_file).toStdWString(), std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
-+                        qCDebug(lcDirectory) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-                     }
-                     if (!_item->_renameTarget.isEmpty() && FileSystem::fileExists(propagator()->fullLocalPath(_item->_renameTarget))) {
-                         FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_renameTarget), FileSystem::FolderPermissions::ReadOnly);
-+                        qCDebug(lcDirectory) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-+                        std::filesystem::permissions(propagator()->fullLocalPath(_item->_renameTarget).toStdWString(), std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
-+                        qCDebug(lcDirectory) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-                     }
-                 }
-                 catch (const std::filesystem::filesystem_error &e)
-@@ -1496,13 +1502,15 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
-             } else {
-                 try {
-                     if (FileSystem::fileExists(propagator()->fullLocalPath(_item->_file))) {
--                        qCDebug(lcDirectory) << propagator()->fullLocalPath(_item->_file) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-                         FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_file), FileSystem::FolderPermissions::ReadWrite);
--                        qCDebug(lcDirectory) << propagator()->fullLocalPath(_item->_file) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-+                        qCDebug(lcDirectory) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-+                        std::filesystem::permissions(propagator()->fullLocalPath(_item->_file).toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
-+                        qCDebug(lcDirectory) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_file).toStdWString()).permissions());
-                     }
-                     if (!_item->_renameTarget.isEmpty() && FileSystem::fileExists(propagator()->fullLocalPath(_item->_renameTarget))) {
--                        qCDebug(lcDirectory) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-                         FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_renameTarget), FileSystem::FolderPermissions::ReadWrite);
-+                        qCDebug(lcDirectory) << "old permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-+                        std::filesystem::permissions(propagator()->fullLocalPath(_item->_renameTarget).toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
-                         qCDebug(lcDirectory) << "new permissions" << static_cast<int>(std::filesystem::status(propagator()->fullLocalPath(_item->_renameTarget).toStdWString()).permissions());
-                     }
-                 }
-diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp
-index e24ce8a..8237905 100644
---- a/src/libsync/syncengine.cpp
-+++ b/src/libsync/syncengine.cpp
-@@ -362,10 +362,6 @@ void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item)
-                 const bool isReadOnly = !item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite);
-                 modificationHappened = FileSystem::setFileReadOnlyWeak(filePath, isReadOnly);
-             }
--            if (item->isPermissionsInvalid) {
--                const auto isReadOnly = !item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite);
--                FileSystem::setFileReadOnly(filePath, isReadOnly);
--            }
- 
-             modificationHappened |= item->_size != prev._fileSize;
- 
-diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h
-index 154d13a..041e48d 100644
---- a/src/libsync/syncfileitem.h
-+++ b/src/libsync/syncfileitem.h
-@@ -344,8 +344,6 @@ public:
-     bool _isLivePhoto = false;
-     QString _livePhotoFile;
- 
--    bool isPermissionsInvalid = false;
--
-     QString _discoveryResult;
- 
-     /// if true, requests the file to be permanently deleted instead of moved to the trashbin
diff -Nru nextcloud-desktop-3.16.4/debian/patches/series nextcloud-desktop-3.16.7/debian/patches/series
--- nextcloud-desktop-3.16.4/debian/patches/series	2025-05-03 23:57:43.000000000 +0200
+++ nextcloud-desktop-3.16.7/debian/patches/series	2025-08-16 01:15:24.000000000 +0200
@@ -3,5 +3,4 @@
 0003-Use-release-version-for-Debian.patch
 0004-GIT_SHA1-points-to-the-sha1-of-upstream.patch
 0004-Don-t-use-GuiPrivate.patch
-0006-Revert-better-logs-and-factor-common-code-in-folder-.patch
-0007-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch
+0006-Revert-ensure-no-any-user-writable-permissions-in-Ne.patch
diff -Nru nextcloud-desktop-3.16.4/.github/workflows/macos-build-and-test.yml nextcloud-desktop-3.16.7/.github/workflows/macos-build-and-test.yml
--- nextcloud-desktop-3.16.4/.github/workflows/macos-build-and-test.yml	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/.github/workflows/macos-build-and-test.yml	2025-07-28 10:08:26.000000000 +0200
@@ -46,7 +46,7 @@
 
       - name: Download Craft
         run: |
-          git clone -q --depth=1 https://invent.kde.org/ggadinger/craftmaster.git ${{ env.CRAFT_MASTER_LOCATION }}
+          git clone -q --depth=1 https://invent.kde.org/packaging/craftmaster.git ${{ env.CRAFT_MASTER_LOCATION }}
 
       - name: Add Nextcloud client blueprints
         run: |
diff -Nru nextcloud-desktop-3.16.4/.github/workflows/windows-build-and-test.yml nextcloud-desktop-3.16.7/.github/workflows/windows-build-and-test.yml
--- nextcloud-desktop-3.16.4/.github/workflows/windows-build-and-test.yml	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/.github/workflows/windows-build-and-test.yml	2025-07-28 10:08:26.000000000 +0200
@@ -22,7 +22,7 @@
       - name: Install Craft Master with Nextcloud Client Deps
         shell: pwsh
         run: |
-          & cmd /C "git clone -q --depth=1 https://invent.kde.org/ggadinger/craftmaster.git ${{ env.CRAFT_MASTER_LOCATION }} 2>&1"
+          & cmd /C "git clone -q --depth=1 https://invent.kde.org/packaging/craftmaster.git ${{ env.CRAFT_MASTER_LOCATION }} 2>&1"
           
           function craft() {
               python "${{ env.CRAFT_MASTER_LOCATION }}\CraftMaster.py" --config "${{ env.CRAFT_MASTER_CONFIG }}" --target ${{ env.CRAFT_TARGET }} -c $args
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift	2025-07-28 10:08:26.000000000 +0200
@@ -105,7 +105,11 @@
     }
 
     @objc func setupDomainAccount(
-        user: String, userId: String, serverUrl: String, password: String
+        user: String,
+        userId: String,
+        serverUrl: String,
+        password: String,
+        userAgent: String = "Nextcloud-macOS/FileProviderExt"
     ) {
         let account = Account(user: user, id: userId, serverUrl: serverUrl, password: password)
         guard account != ncAccount else { return }
@@ -117,7 +121,7 @@
                 user: user,
                 userId: userId,
                 password: password,
-                userAgent: "Nextcloud-macOS/FileProviderExt",
+                userAgent: userAgent,
                 nextcloudVersion: 25,
                 groupIdentifier: ""
             )
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderSocketLineProcessor.swift nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderSocketLineProcessor.swift
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderSocketLineProcessor.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderSocketLineProcessor.swift	2025-07-28 10:08:26.000000000 +0200
@@ -46,14 +46,21 @@
             delegate.removeAccountConfig()
         } else if command == "ACCOUNT_DETAILS" {
             guard let accountDetailsSubsequence = splitLine.last else { return }
-            let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 3)
+            let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 4)
 
-            let user = String(splitAccountDetails[0])
-            let userId = String(splitAccountDetails[1])
-            let serverUrl = String(splitAccountDetails[2])
-            let password = String(splitAccountDetails[3])
+            let userAgent = String(splitAccountDetails[0])
+            let user = String(splitAccountDetails[1])
+            let userId = String(splitAccountDetails[2])
+            let serverUrl = String(splitAccountDetails[3])
+            let password = String(splitAccountDetails[4])
 
-            delegate.setupDomainAccount(user: user, userId: userId, serverUrl: serverUrl, password: password)
+            delegate.setupDomainAccount(
+                user: user,
+                userId: userId,
+                serverUrl: serverUrl,
+                password: password,
+                userAgent: userAgent
+            )
         }
     }
 }
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h	2025-07-28 10:08:26.000000000 +0200
@@ -23,7 +23,8 @@
 - (void)configureAccountWithUser:(NSString *)user
                           userId:(NSString *)userId
                        serverUrl:(NSString *)serverUrl
-                        password:(NSString *)password;
+                        password:(NSString *)password
+                       userAgent:(NSString *)userAgent;
 - (void)removeAccountConfig;
 - (void)createDebugLogStringWithCompletionHandler:(void(^)(NSString *debugLogString, NSError *error))completionHandler;
 - (void)getFastEnumerationStateWithCompletionHandler:(void(^)(BOOL enabled, BOOL set))completionHandler;
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift	2025-07-28 10:08:26.000000000 +0200
@@ -48,15 +48,21 @@
         completionHandler(accountUserId, nil)
     }
 
-    func configureAccount(withUser user: String,
-                          userId: String,
-                          serverUrl: String,
-                          password: String) {
+    func configureAccount(
+        withUser user: String,
+        userId: String,
+        serverUrl: String,
+        password: String,
+        userAgent: String
+    ) {
         Logger.desktopClientConnection.info("Received configure account information over client communication service")
-        self.fpExtension.setupDomainAccount(user: user,
-                                            userId: userId,
-                                            serverUrl: serverUrl,
-                                            password: password)
+        self.fpExtension.setupDomainAccount(
+            user: user,
+            userId: userId,
+            serverUrl: serverUrl,
+            password: password,
+            userAgent: userAgent
+        )
     }
 
     func removeAccountConfig() {
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift	2025-07-28 10:08:26.000000000 +0200
@@ -40,6 +40,14 @@
 
     //MARK: - FPUIExtensionService protocol methods
 
+    func userAgent() async -> NSString? {
+        guard let account = fpExtension.ncAccount?.ncKitAccount else {
+            return nil
+        }
+        let nkSession = fpExtension.ncKit.getSession(account: account)
+        return nkSession?.userAgent as NSString?
+    }
+
     func credentials() async -> NSDictionary {
         return (fpExtension.ncAccount?.dictionary() ?? [:]) as NSDictionary
     }
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift	2025-07-28 10:08:26.000000000 +0200
@@ -13,6 +13,7 @@
 )
 
 @objc protocol FPUIExtensionService {
+    func userAgent() async -> NSString?
     func credentials() async -> NSDictionary
     func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString?
 }
diff -Nru nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift
--- nextcloud-desktop-3.16.4/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift	2025-07-28 10:08:26.000000000 +0200
@@ -36,6 +36,7 @@
     private(set) var shares: [NKShare] = [] {
         didSet { Task { @MainActor in sharesTableView?.reloadData() } }
     }
+    private(set) var userAgent: String = "Nextcloud-macOS/FileProviderUIExt"
     private(set) var account: Account? {
         didSet {
             guard let account = account else { return }
@@ -45,7 +46,7 @@
                 user: account.username,
                 userId: account.username,
                 password: account.password,
-                userAgent: "Nextcloud-macOS/FileProviderUIExt",
+                userAgent: userAgent,
                 nextcloudVersion: 25,
                 groupIdentifier: ""
             )
@@ -93,6 +94,9 @@
             let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
                 Logger.sharesDataSource.error("Service connection interrupted")
             })
+            if let acquiredUserAgent = await connection.userAgent() {
+                userAgent = acquiredUserAgent as String
+            }
             guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
                   let credentials = await connection.credentials() as? Dictionary<String, String>,
                   let convertedAccount = Account(dictionary: credentials),
diff -Nru nextcloud-desktop-3.16.4/shell_integration/windows/NCUtil/CMakeLists.txt nextcloud-desktop-3.16.7/shell_integration/windows/NCUtil/CMakeLists.txt
--- nextcloud-desktop-3.16.4/shell_integration/windows/NCUtil/CMakeLists.txt	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/windows/NCUtil/CMakeLists.txt	2025-07-28 10:08:26.000000000 +0200
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2018 ownCloud GmbH
+# SPDX-License-Identifier: LGPL-2.1-or-later
+configure_file(Version.h.in ${CMAKE_CURRENT_BINARY_DIR}/Version.h)
+
 add_library(NCUtil STATIC
     CommunicationSocket.cpp
     RemotePathChecker.cpp
@@ -7,5 +11,6 @@
 
 target_include_directories(NCUtil
     PUBLIC
-        "${CMAKE_CURRENT_SOURCE_DIR}"
+        ${CMAKE_CURRENT_SOURCE_DIR}
+        ${CMAKE_CURRENT_BINARY_DIR}
 )
diff -Nru nextcloud-desktop-3.16.4/shell_integration/windows/NCUtil/Version.h nextcloud-desktop-3.16.7/shell_integration/windows/NCUtil/Version.h
--- nextcloud-desktop-3.16.4/shell_integration/windows/NCUtil/Version.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/shell_integration/windows/NCUtil/Version.h	1970-01-01 01:00:00.000000000 +0100
@@ -1,11 +0,0 @@
-#pragma once
-
-// This is the number that will end up in the version window of the DLLs.
-// Increment this version before committing a new build if you are today's shell_integration build master.
-#define NCEXT_BUILD_NUM 47
-
-#define STRINGIZE2(s) #s
-#define STRINGIZE(s) STRINGIZE2(s)
-
-#define NCEXT_VERSION 3,0,0,NCEXT_BUILD_NUM
-#define NCEXT_VERSION_STRING STRINGIZE(NCEXT_VERSION)
diff -Nru nextcloud-desktop-3.16.4/shell_integration/windows/NCUtil/Version.h.in nextcloud-desktop-3.16.7/shell_integration/windows/NCUtil/Version.h.in
--- nextcloud-desktop-3.16.4/shell_integration/windows/NCUtil/Version.h.in	1970-01-01 01:00:00.000000000 +0100
+++ nextcloud-desktop-3.16.7/shell_integration/windows/NCUtil/Version.h.in	2025-07-28 10:08:26.000000000 +0200
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2016 ownCloud GmbH
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+// This is the number that will end up in the version window of the DLLs.
+// Increment this version before committing a new build if you are today's shell_integration build master.
+#cmakedefine NCEXT_BUILD_NUM @NCEXT_BUILD_NUM@
+
+#define STRINGIZE2(s) #s
+#define STRINGIZE(s) STRINGIZE2(s)
+
+#cmakedefine NCEXT_VERSION @NCEXT_VERSION@
+#define NCEXT_VERSION_STRING STRINGIZE(NCEXT_VERSION)
diff -Nru nextcloud-desktop-3.16.4/src/common/filesystembase.cpp nextcloud-desktop-3.16.7/src/common/filesystembase.cpp
--- nextcloud-desktop-3.16.4/src/common/filesystembase.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/common/filesystembase.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -35,6 +35,8 @@
 #include <winbase.h>
 #include <fcntl.h>
 #include <io.h>
+#include <securitybaseapi.h>
+#include <sddl.h>
 #endif
 
 namespace OCC {
@@ -116,15 +118,16 @@
         return;
     }
 
-    const auto fileAttributes = GetFileAttributesW(filename.toStdWString().c_str());
+    const auto windowsFilename = QDir::toNativeSeparators(filename);
+    const auto fileAttributes = GetFileAttributesW(windowsFilename.toStdWString().c_str());
     if (fileAttributes == INVALID_FILE_ATTRIBUTES) {
         const auto lastError = GetLastError();
         auto errorMessage = static_cast<char*>(nullptr);
         if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                           nullptr, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), errorMessage, 0, nullptr) == 0) {
-            qCWarning(lcFileSystem()) << "GetFileAttributesW" << filename << (readonly ? "readonly" : "read write") << errorMessage;
+            qCWarning(lcFileSystem()) << "GetFileAttributesW" << windowsFilename << (readonly ? "readonly" : "read write") << errorMessage;
         } else {
-            qCWarning(lcFileSystem()) << "GetFileAttributesW" << filename << (readonly ? "readonly" : "read write") << "unknown error" << lastError;
+            qCWarning(lcFileSystem()) << "GetFileAttributesW" << windowsFilename << (readonly ? "readonly" : "read write") << "unknown error" << lastError;
         }
         return;
     }
@@ -136,17 +139,23 @@
         newFileAttributes = newFileAttributes & (~FILE_ATTRIBUTE_READONLY);
     }
 
-    if (SetFileAttributesW(filename.toStdWString().c_str(), newFileAttributes) == 0) {
+    if (SetFileAttributesW(windowsFilename.toStdWString().c_str(), newFileAttributes) == 0) {
         const auto lastError = GetLastError();
         auto errorMessage = static_cast<char*>(nullptr);
         if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                            nullptr, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), errorMessage, 0, nullptr) == 0) {
-            qCWarning(lcFileSystem()) << "SetFileAttributesW" << filename << (readonly ? "readonly" : "read write") << errorMessage;
+            qCWarning(lcFileSystem()) << "SetFileAttributesW" << windowsFilename << (readonly ? "readonly" : "read write") << errorMessage;
         } else {
-            qCWarning(lcFileSystem()) << "SetFileAttributesW" << filename << (readonly ? "readonly" : "read write") << "unknown error" << lastError;
+            qCWarning(lcFileSystem()) << "SetFileAttributesW" << windowsFilename << (readonly ? "readonly" : "read write") << "unknown error" << lastError;
         }
     }
 
+    if (!readonly) {
+        // current read-only folder ACL needs to be removed from files also when making a folder read-write
+        // we currently have a too limited set of authorization for files when applying the restrictive ACL for folders on the child files
+        setAclPermission(filename, FileSystem::FolderPermissions::ReadWrite, false);
+    }
+
     return;
 #endif
     QFile file(filename);
@@ -392,8 +401,7 @@
     // not valid. There needs to be one initialised here. Otherwise the incoming
     // fileInfo is re-used.
     if (fileInfo.filePath() != filename) {
-        QFileInfo myFI(filename);
-        re = myFI.exists();
+        re = QFileInfo::exists(filename);
     }
     return re;
 }
@@ -547,22 +555,23 @@
 
 bool FileSystem::remove(const QString &fileName, QString *errorString)
 {
+    const auto &windowsSafeFileName = FileSystem::longWinPath(fileName);
 #ifdef Q_OS_WIN
     // You cannot delete a read-only file on windows, but we want to
     // allow that.
-    setFileReadOnly(fileName, false);
+    setFileReadOnly(windowsSafeFileName, false);
 #endif
-    const auto deletedFileInfo = QFileInfo{fileName};
+    const auto deletedFileInfo = QFileInfo{windowsSafeFileName};
     if (!deletedFileInfo.exists()) {
-        qCWarning(lcFileSystem()) << fileName << "has been already deleted";
+        qCWarning(lcFileSystem()) << windowsSafeFileName << "has been already deleted";
     }
 
-    QFile f(fileName);
+    QFile f(windowsSafeFileName);
     if (!f.remove()) {
         if (errorString) {
             *errorString = f.errorString();
         }
-        qCWarning(lcFileSystem()) << f.errorString() << fileName;
+        qCWarning(lcFileSystem()) << f.errorString() << windowsSafeFileName;
 
 #if defined Q_OS_WIN
         const auto permissionsDisplayHelper = [] (std::filesystem::perms currentPermissions) {
@@ -581,10 +590,10 @@
                                    << unitaryHelper(std::filesystem::perms::others_exec, 'x');
         };
 
-        const auto unsafeFilePermissions = filePermissionsWin(fileName);
+        const auto unsafeFilePermissions = filePermissionsWin(windowsSafeFileName);
         permissionsDisplayHelper(unsafeFilePermissions);
 
-        const auto safeFilePermissions = filePermissionsWinSymlinkSafe(fileName);
+        const auto safeFilePermissions = filePermissionsWinSymlinkSafe(windowsSafeFileName);
         permissionsDisplayHelper(safeFilePermissions);
 #endif
 
@@ -688,6 +697,151 @@
     }
     return QStringLiteral(R"(\\?\)") + str;
 }
+
+bool FileSystem::setAclPermission(const QString &unsafePath, FolderPermissions permissions, bool applyAlsoToFiles)
+{
+    SECURITY_INFORMATION info = DACL_SECURITY_INFORMATION;
+    std::unique_ptr<char[]> securityDescriptor;
+    auto neededLength = 0ul;
+
+    const auto path = longWinPath(unsafePath);
+
+    const auto safePathFileInfo = QFileInfo{path};
+
+    if (!GetFileSecurityW(path.toStdWString().c_str(), info, nullptr, 0, &neededLength)) {
+        const auto lastError = GetLastError();
+        if (lastError != ERROR_INSUFFICIENT_BUFFER) {
+            qCWarning(lcFileSystem) << "error when calling GetFileSecurityW" << path << lastError;
+            return false;
+        }
+
+        securityDescriptor.reset(new char[neededLength]);
+
+        if (!GetFileSecurityW(path.toStdWString().c_str(), info, securityDescriptor.get(), neededLength, &neededLength)) {
+            qCWarning(lcFileSystem) << "error when calling GetFileSecurityW" << path << GetLastError();
+            return false;
+        }
+    }
+
+    int daclPresent = false, daclDefault = false;
+    PACL resultDacl = nullptr;
+    if (!GetSecurityDescriptorDacl(securityDescriptor.get(), &daclPresent, &resultDacl, &daclDefault)) {
+        qCWarning(lcFileSystem) << "error when calling GetSecurityDescriptorDacl" << path << GetLastError();
+        return false;
+    }
+    if (!daclPresent || !resultDacl) {
+        qCWarning(lcFileSystem) << "error when calling DACL needed to set a folder read-only or read-write is missing" << path;
+        return false;
+    }
+
+    PSID sid = nullptr;
+    if (!ConvertStringSidToSidW(L"S-1-5-32-545", &sid))
+    {
+        qCWarning(lcFileSystem) << "error when calling ConvertStringSidToSidA" << path << GetLastError();
+        return false;
+    }
+
+    ACL_SIZE_INFORMATION aclSize;
+    if (!GetAclInformation(resultDacl, &aclSize, sizeof(aclSize), AclSizeInformation)) {
+        qCWarning(lcFileSystem) << "error when calling GetAclInformation" << path << GetLastError();
+        return false;
+    }
+
+    const auto newAclSize = aclSize.AclBytesInUse + sizeof(ACCESS_DENIED_ACE) + GetLengthSid(sid);
+    qCDebug(lcFileSystem) << "allocated a new DACL object of size" << newAclSize;
+
+    std::unique_ptr<ACL> newDacl{reinterpret_cast<PACL>(new char[newAclSize])};
+    if (!InitializeAcl(newDacl.get(), newAclSize, ACL_REVISION)) {
+        const auto lastError = GetLastError();
+        if (lastError != ERROR_INSUFFICIENT_BUFFER) {
+            qCWarning(lcFileSystem) << "insufficient memory error when calling InitializeAcl" << path;
+            return false;
+        }
+
+        qCWarning(lcFileSystem) << "error when calling InitializeAcl" << path << lastError;
+        return false;
+    }
+
+    if (permissions == FileSystem::FolderPermissions::ReadOnly) {
+        if (!AddAccessDeniedAceEx(newDacl.get(), ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
+                                  FILE_DELETE_CHILD | DELETE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA, sid)) {
+            qCWarning(lcFileSystem) << "error when calling AddAccessDeniedAce << path" << GetLastError();
+            return false;
+        }
+    }
+
+    for (int i = 0; i < aclSize.AceCount; ++i) {
+        void *currentAce = nullptr;
+        if (!GetAce(resultDacl, i, &currentAce)) {
+            qCWarning(lcFileSystem) << "error when calling GetAce" << path << GetLastError();
+            return false;
+        }
+
+        const auto currentAceHeader = reinterpret_cast<PACE_HEADER>(currentAce);
+
+        if (permissions == FileSystem::FolderPermissions::ReadWrite && (ACCESS_DENIED_ACE_TYPE == (currentAceHeader->AceType & ACCESS_DENIED_ACE_TYPE))) {
+            qCWarning(lcFileSystem) << "AceHeader" << path << currentAceHeader->AceFlags << currentAceHeader->AceSize << currentAceHeader->AceType;
+            continue;
+        }
+
+        if (!AddAce(newDacl.get(), ACL_REVISION, i + 1, currentAce, currentAceHeader->AceSize)) {
+            const auto lastError = GetLastError();
+            if (lastError != ERROR_INSUFFICIENT_BUFFER) {
+                qCWarning(lcFileSystem) << "insufficient memory error when calling AddAce" << path;
+                return false;
+            }
+
+            if (lastError != ERROR_INVALID_PARAMETER) {
+                qCWarning(lcFileSystem) << "invalid parameter error when calling AddAce" << path << "ACL size" << newAclSize;
+                return false;
+            }
+
+            qCWarning(lcFileSystem) << "error when calling AddAce" << path << lastError << "acl index" << (i + 1);
+            return false;
+        }
+    }
+
+    SECURITY_DESCRIPTOR newSecurityDescriptor;
+    if (!InitializeSecurityDescriptor(&newSecurityDescriptor, SECURITY_DESCRIPTOR_REVISION)) {
+        qCWarning(lcFileSystem) << "error when calling InitializeSecurityDescriptor" << path << GetLastError();
+        return false;
+    }
+
+    if (!SetSecurityDescriptorDacl(&newSecurityDescriptor, true, newDacl.get(), false)) {
+        qCWarning(lcFileSystem) << "error when calling SetSecurityDescriptorDacl" << path << GetLastError();
+        return false;
+    }
+
+    if (safePathFileInfo.isDir() && applyAlsoToFiles) {
+        const auto currentFolder = safePathFileInfo.dir();
+        const auto childFiles = currentFolder.entryList(QDir::Filter::Files);
+        for (const auto &oneEntry : childFiles) {
+            const auto childFile = QDir::toNativeSeparators(path + QDir::separator() + oneEntry);
+
+            const auto &childFileStdWString = childFile.toStdWString();
+            const auto attributes = GetFileAttributes(childFileStdWString.c_str());
+
+                   // testing if that could be a pure virtual placeholder file (i.e. CfApi file without data)
+                   // we do not want to trigger implicit hydration ourself
+            if ((attributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0) {
+                continue;
+            }
+
+            if (!SetFileSecurityW(childFileStdWString.c_str(), info, &newSecurityDescriptor)) {
+                qCWarning(lcFileSystem) << "error when calling SetFileSecurityW" << childFile << GetLastError();
+                return false;
+            }
+        }
+    }
+
+    if (!SetFileSecurityW(QDir::toNativeSeparators(path).toStdWString().c_str(), info, &newSecurityDescriptor)) {
+        qCWarning(lcFileSystem) << "error when calling SetFileSecurityW" << QDir::toNativeSeparators(path) << GetLastError();
+        return false;
+    }
+
+    return true;
+}
+
 #endif
 
 } // namespace OCC
diff -Nru nextcloud-desktop-3.16.4/src/common/filesystembase.h nextcloud-desktop-3.16.7/src/common/filesystembase.h
--- nextcloud-desktop-3.16.4/src/common/filesystembase.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/common/filesystembase.h	2025-07-28 10:08:26.000000000 +0200
@@ -176,6 +176,8 @@
     std::filesystem::perms OCSYNC_EXPORT filePermissionsWinSymlinkSafe(const QString &filename);
     std::filesystem::perms OCSYNC_EXPORT filePermissionsWin(const QString &filename);
     void OCSYNC_EXPORT setFilePermissionsWin(const QString &filename, const std::filesystem::perms &perms);
+
+    bool OCSYNC_EXPORT setAclPermission(const QString &path, FileSystem::FolderPermissions permissions, bool applyAlsoToFiles);
 #endif
 
     /**
diff -Nru nextcloud-desktop-3.16.4/src/common/remotepermissions.cpp nextcloud-desktop-3.16.7/src/common/remotepermissions.cpp
--- nextcloud-desktop-3.16.4/src/common/remotepermissions.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/common/remotepermissions.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -27,7 +27,7 @@
 
 Q_LOGGING_CATEGORY(lcRemotePermissions, "nextcloud.sync.remotepermissions", QtInfoMsg)
 
-static const char letters[] = " WDNVCKRSMm";
+static const char letters[] = " GWDNVCKRSMm";
 
 
 template <typename Char>
diff -Nru nextcloud-desktop-3.16.4/src/common/remotepermissions.h nextcloud-desktop-3.16.7/src/common/remotepermissions.h
--- nextcloud-desktop-3.16.4/src/common/remotepermissions.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/common/remotepermissions.h	2025-07-28 10:08:26.000000000 +0200
@@ -41,18 +41,19 @@
 
 public:
     enum Permissions {
-        CanWrite = 1,             // W
-        CanDelete = 2,            // D
-        CanRename = 3,            // N
-        CanMove = 4,              // V
-        CanAddFile = 5,           // C
-        CanAddSubDirectories = 6, // K
-        CanReshare = 7,           // R
+        CanRead = 1,              // G
+        CanWrite,                 // W
+        CanDelete,                // D
+        CanRename,                // N
+        CanMove,                  // V
+        CanAddFile,               // C
+        CanAddSubDirectories,     // K
+        CanReshare,               // R
         // Note: on the server, this means SharedWithMe, but in discoveryphase.cpp we also set
         // this permission when the server reports the any "share-types"
-        IsShared = 8,             // S
-        IsMounted = 9,            // M
-        IsMountedSub = 10,        // m (internal: set if the parent dir has IsMounted)
+        IsShared,                 // S
+        IsMounted,                // M
+        IsMountedSub,             // m (internal: set if the parent dir has IsMounted)
 
         // Note: when adding support for more permissions, we need to invalid the cache in the database.
         // (by setting forceRemoteDiscovery in SyncJournalDb::checkConnect)
diff -Nru nextcloud-desktop-3.16.4/src/gui/accountmanager.cpp nextcloud-desktop-3.16.7/src/gui/accountmanager.cpp
--- nextcloud-desktop-3.16.4/src/gui/accountmanager.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/accountmanager.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -254,14 +254,18 @@
     }
 
     ConfigFile configFile;
-    configFile.setVfsEnabled(settings->value(configFile.isVfsEnabledC).toBool());
-    configFile.setLaunchOnSystemStartup(settings->value(configFile.launchOnSystemStartupC).toBool());
-    configFile.setOptionalServerNotifications(settings->value(configFile.optionalServerNotificationsC).toBool());
-    configFile.setPromptDeleteFiles(settings->value(configFile.promptDeleteC).toBool());
-    configFile.setShowCallNotifications(settings->value(configFile.showCallNotificationsC).toBool());
-    configFile.setShowChatNotifications(settings->value(configFile.showChatNotificationsC).toBool());
-    configFile.setShowInExplorerNavigationPane(settings->value(configFile.showInExplorerNavigationPaneC).toBool());
+    configFile.setVfsEnabled(settings->value(ConfigFile::isVfsEnabledC, configFile.isVfsEnabled()).toBool());
+    configFile.setLaunchOnSystemStartup(settings->value(ConfigFile::launchOnSystemStartupC, configFile.launchOnSystemStartup()).toBool());
+    configFile.setOptionalServerNotifications(settings->value(ConfigFile::optionalServerNotificationsC, configFile.optionalServerNotifications()).toBool());
+    configFile.setPromptDeleteFiles(settings->value(ConfigFile::promptDeleteC, configFile.promptDeleteFiles()).toBool());
+    configFile.setShowCallNotifications(settings->value(ConfigFile::showCallNotificationsC, configFile.showCallNotifications()).toBool());
+    configFile.setShowChatNotifications(settings->value(ConfigFile::showChatNotificationsC, configFile.showChatNotifications()).toBool());
+    configFile.setShowInExplorerNavigationPane(settings->value(ConfigFile::showInExplorerNavigationPaneC, configFile.showInExplorerNavigationPane()).toBool());
     ClientProxy().saveProxyConfigurationFromSettings(*settings);
+    configFile.setUseUploadLimit(settings->value(ConfigFile::useUploadLimitC, configFile.useUploadLimit()).toInt());
+    configFile.setUploadLimit(settings->value(ConfigFile::uploadLimitC, configFile.uploadLimit()).toInt());
+    configFile.setUseDownloadLimit(settings->value(ConfigFile::useDownloadLimitC, configFile.useDownloadLimit()).toInt());
+    configFile.setDownloadLimit(settings->value(ConfigFile::downloadLimitC, configFile.downloadLimit()).toInt());
 
     // Try to load the single account.
     if (!settings->childKeys().isEmpty()) {
diff -Nru nextcloud-desktop-3.16.4/src/gui/application.cpp nextcloud-desktop-3.16.7/src/gui/application.cpp
--- nextcloud-desktop-3.16.4/src/gui/application.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/application.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -261,21 +261,54 @@
     setWindowIcon(_theme->applicationIcon());
 
     if (!ConfigFile().exists()) {
-        if (const auto genericConfigLocation = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + APPLICATION_CONFIG_NAME;
-            setupConfigFolderFromLegacyLocation(genericConfigLocation)) {
-            qCWarning(lcApplication) << "Setup of config folder and files from legacy location" << genericConfigLocation << "failed.";
+        setApplicationName(_theme->appNameGUI());
+        QString legacyDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + APPLICATION_CONFIG_NAME;
+
+        if (legacyDir.endsWith('/')) {
+            legacyDir.chop(1); // macOS 10.11.x does not like trailing slash for rename/move.
         }
-    } else {
-        if (const auto appDataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
-            setupConfigFolderFromLegacyLocation(appDataLocation)) {
-            qCWarning(lcApplication) << "Setup of config folder and files from legacy location" << appDataLocation << "failed.";
+        setApplicationName(_theme->appName());
+        if (QFileInfo(legacyDir).isDir()) {
+            auto confDir = ConfigFile().configPath();
+
+            // macOS 10.11.x does not like trailing slash for rename/move.
+            if (confDir.endsWith('/')) {
+                confDir.chop(1);
+            }
+
+            qCInfo(lcApplication) << "Migrating old config from" << legacyDir << "to" << confDir;
+
+            if (!QFile::rename(legacyDir, confDir)) {
+                qCWarning(lcApplication) << "Failed to move the old config directory to its new location (" << legacyDir << "to" << confDir << ")";
+
+                // Try to move the files one by one
+                if (QFileInfo(confDir).isDir() || QDir().mkdir(confDir)) {
+                    const QStringList filesList = QDir(legacyDir).entryList(QDir::Files);
+                    qCInfo(lcApplication) << "Will move the individual files" << filesList;
+                    for (const auto &name : filesList) {
+                        if (!QFile::rename(legacyDir + "/" + name,  confDir + "/" + name)) {
+                            qCWarning(lcApplication) << "Fallback move of " << name << "also failed";
+                        }
+                    }
+                }
+            } else {
+#ifndef Q_OS_WIN
+                // Create a symbolic link so a downgrade of the client would still find the config.
+                QFile::link(confDir, legacyDir);
+#endif
+            }
         }
+    } else {
+        setupConfigFile();
     }
 
-    // try to migrate legacy accounts and folders from a previous client version
-    // only copy the settings and check what should be skipped
-    if (ConfigFile().exists() && !configVersionMigration()) {
-        qCWarning(lcApplication) << "Config version migration was not possible.";
+    if (_theme->doNotUseProxy()) {
+        ConfigFile().setProxyType(QNetworkProxy::NoProxy);
+        for (const auto &accountState : AccountManager::instance()->accounts()) {
+            if (accountState && accountState->account()) {
+                accountState->account()->setNetworkProxySetting(Account::AccountNetworkProxySetting::GlobalProxy);
+            }
+        }
     }
 
     parseOptions(arguments());
@@ -308,6 +341,12 @@
     setupLogging();
     setupTranslations();
 
+    // try to migrate legacy accounts and folders from a previous client version
+    // only copy the settings and check what should be skipped
+    if (!configVersionMigration()) {
+        qCWarning(lcApplication) << "Config version migration was not possible.";
+    }
+
     ConfigFile cfg;
     {
         auto shouldExit = false;
@@ -377,14 +416,6 @@
     _gui->setupCloudProviders();
 #endif
 
-    if (_theme->doNotUseProxy()) {
-        ConfigFile().setProxyType(QNetworkProxy::NoProxy);
-        for (const auto &accountState : AccountManager::instance()->accounts()) {
-            if (accountState && accountState->account()) {
-                accountState->account()->setNetworkProxySetting(Account::AccountNetworkProxySetting::GlobalProxy);
-            }
-        }
-    }
     _proxy.setupQtProxyFromConfig(); // folders have to be defined first, than we set up the Qt proxy.
 
     connect(AccountManager::instance(), &AccountManager::accountAdded,
@@ -508,7 +539,7 @@
     }
 }
 
-bool Application::setupConfigFolderFromLegacyLocation(const QString &legacyLocation) const
+void Application::setupConfigFile()
 {
     // Migrate from version <= 2.4
     setApplicationName(_theme->appNameGUI());
@@ -520,45 +551,44 @@
     QT_WARNING_POP
     setApplicationName(_theme->appName());
 
-    auto legacyDir = legacyLocation;
-    if (legacyDir.endsWith('/')) {
-        legacyDir.chop(1); // macOS 10.11.x does not like trailing slash for rename/move.
+    auto oldDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
+
+    // macOS 10.11.x does not like trailing slash for rename/move.
+    if (oldDir.endsWith('/')) {
+        oldDir.chop(1);
     }
 
-    if (!QFileInfo(legacyDir).isDir()) {
-        return false;
+    if (!QFileInfo(oldDir).isDir()) {
+        return;
     }
 
     auto confDir = ConfigFile().configPath();
+
+    // macOS 10.11.x does not like trailing slash for rename/move.
     if (confDir.endsWith('/')) {
         confDir.chop(1);
     }
 
-    qCInfo(lcApplication) << "Migrating old config from" << legacyDir << "to" << confDir;
-    if (!QFile::rename(legacyDir, confDir)) {
-        qCWarning(lcApplication) << "Failed to move the old config directory" << legacyDir << "to new location" << confDir;
+    qCInfo(lcApplication) << "Migrating old config from" << oldDir << "to" << confDir;
+    if (!QFile::rename(oldDir, confDir)) {
+        qCWarning(lcApplication) << "Failed to move the old config directory to its new location (" << oldDir << "to" << confDir << ")";
+
+        // Try to move the files one by one
         if (QFileInfo(confDir).isDir() || QDir().mkdir(confDir)) {
-            const QStringList filesList = QDir(legacyDir).entryList(QDir::Files);
-            qCInfo(lcApplication) << "Will move the individual files:" << filesList;
-            auto setupCompleted = false;
+            const QStringList filesList = QDir(oldDir).entryList(QDir::Files);
+            qCInfo(lcApplication) << "Will move the individual files" << filesList;
             for (const auto &name : filesList) {
-                if (!QFile::rename(legacyDir + "/" + name,  confDir + "/" + name)) {
-                    qCDebug(lcApplication) << "Fallback move of " << name << "also failed";
-                    continue;
+                if (!QFile::rename(oldDir + "/" + name,  confDir + "/" + name)) {
+                    qCWarning(lcApplication) << "Fallback move of " << name << "also failed";
                 }
-                setupCompleted = true;
-                qCInfo(lcApplication)  << "Move of " << name << "succeeded.";
             }
-            return setupCompleted;
         }
     } else {
 #ifndef Q_OS_WIN
         // Create a symbolic link so a downgrade of the client would still find the config.
-        return QFile::link(confDir, legacyDir);
+        QFile::link(confDir, oldDir);
 #endif
     }
-
-    return false;
 }
 
 AccountManager::AccountsRestoreResult Application::restoreLegacyAccount()
diff -Nru nextcloud-desktop-3.16.4/src/gui/application.h nextcloud-desktop-3.16.7/src/gui/application.h
--- nextcloud-desktop-3.16.4/src/gui/application.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/application.h	2025-07-28 10:08:26.000000000 +0200
@@ -113,7 +113,7 @@
     void handleEditLocallyFromOptions();
 
     AccountManager::AccountsRestoreResult restoreLegacyAccount();
-    bool setupConfigFolderFromLegacyLocation(const QString &legacyLocation) const;
+    void setupConfigFile();
     void setupAccountsAndFolders();
 
     /**
diff -Nru nextcloud-desktop-3.16.4/src/gui/folder.cpp nextcloud-desktop-3.16.7/src/gui/folder.cpp
--- nextcloud-desktop-3.16.4/src/gui/folder.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/folder.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -998,6 +998,26 @@
     }
 }
 
+QString Folder::filePath(const QString& fileName)
+{
+    const auto folderDir = QDir(_canonicalLocalPath);
+
+#ifdef Q_OS_WIN
+    // Edge case time!
+    // QDir::filePath checks whether the passed `fileName` is absolute (essentialy by using `!QFileInfo::isRelative()`).
+    // In the case it's absolute, the `fileName` will be returned instead of the complete file path.
+    //
+    // On Windows, if `fileName` starts with a letter followed by a colon (e.g. "A:BCDEF"), it is considered to be an
+    // absolute path.
+    // Since this method should return the file name file path starting with the canonicalLocalPath, catch that special case here and prefix it ourselves...
+    return fileName.length() >= 2 && fileName[1] == ':'
+           ? _canonicalLocalPath + fileName
+           : folderDir.filePath(fileName);
+#else
+    return folderDir.filePath(fileName);
+#endif
+}
+
 bool Folder::isFileExcludedAbsolute(const QString &fullPath) const
 {
     return _engine->excludedFiles().isExcluded(fullPath, path(), _definition.ignoreHiddenFiles);
@@ -1578,19 +1598,19 @@
 
 void Folder::slotHydrationStarts()
 {
-    // Abort any running full sync run and reschedule
-    if (_engine->isSyncRunning()) {
-        setSilenceErrorsUntilNextSync(true);
-        slotTerminateSync();
-        scheduleThisFolderSoon();
-        // TODO: This sets the sync state to AbortRequested on done, we don't want that
-    }
-
-    // Let everyone know we're syncing
-    _syncResult.reset();
-    _syncResult.setStatus(SyncResult::SyncRunning);
-    emit syncStarted();
-    emit syncStateChange();
+    // // Abort any running full sync run and reschedule
+    // if (_engine->isSyncRunning()) {
+    //     setSilenceErrorsUntilNextSync(true);
+    //     slotTerminateSync();
+    //     scheduleThisFolderSoon();
+    //     // TODO: This sets the sync state to AbortRequested on done, we don't want that
+    // }
+
+    // // Let everyone know we're syncing
+    // _syncResult.reset();
+    // _syncResult.setStatus(SyncResult::SyncRunning);
+    // emit syncStarted();
+    // emit syncStateChange();
 }
 
 void Folder::slotHydrationDone()
diff -Nru nextcloud-desktop-3.16.4/src/gui/folder.h nextcloud-desktop-3.16.7/src/gui/folder.h
--- nextcloud-desktop-3.16.4/src/gui/folder.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/folder.h	2025-07-28 10:08:26.000000000 +0200
@@ -165,6 +165,14 @@
      */
     [[nodiscard]] QString remotePathTrailingSlash() const;
 
+    /**
+     * Returns the path name of a file in the local canonical path.
+     *
+     * Similar to `QDir(path()).filePath(...)`, except file names like "Z:test"
+     * are treated as relative paths on Windows.
+     */
+    [[nodiscard]] QString filePath(const QString& fileName);
+
     [[nodiscard]] QString fulllRemotePathToPathInSyncJournalDb(const QString &fullRemotePath) const;
 
     void setNavigationPaneClsid(const QUuid &clsid) { _definition.navigationPaneClsid = clsid; }
diff -Nru nextcloud-desktop-3.16.4/src/gui/invalidfilenamedialog.cpp nextcloud-desktop-3.16.7/src/gui/invalidfilenamedialog.cpp
--- nextcloud-desktop-3.16.4/src/gui/invalidfilenamedialog.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/invalidfilenamedialog.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -83,7 +83,9 @@
     const auto filePathFileInfo = QFileInfo(_filePath);
     _relativeFilePath = filePathFileInfo.path() + QStringLiteral("/");
     _relativeFilePath = _relativeFilePath.replace(folder->path(), QStringLiteral(""));
-    _relativeFilePath = _relativeFilePath.isEmpty() ? QStringLiteral("") : _relativeFilePath + QStringLiteral("/");
+    if (!(_relativeFilePath.isEmpty() || _relativeFilePath.endsWith(QStringLiteral("/")))) {
+        _relativeFilePath += QStringLiteral("/");
+    }
 
     _originalFileName = _relativeFilePath + filePathFileInfo.fileName();
 
@@ -97,7 +99,7 @@
     switch (invalidMode) {
     case InvalidMode::SystemInvalid:
         _ui->descriptionLabel->setText(tr("The file \"%1\" could not be synced because the name contains characters which are not allowed on this system.").arg(_originalFileName));
-        _ui->explanationLabel->setText(tr("The following characters are not allowed on the system: * \" | & ? , ; : \\ / ~ < > leading/trailing spaces"));
+        _ui->explanationLabel->setText(tr("The following characters are not allowed on the system: \\ / : ? * \"  < > | leading/trailing spaces"));
         break;
     case InvalidMode::ServerInvalid:
         _ui->descriptionLabel->setText(tr("The file \"%1\" could not be synced because the name contains characters which are not allowed on the server.").arg(_originalFileName));
@@ -140,7 +142,6 @@
 
     if (_fileLocation == FileLocation::NewLocalFile) {
         allowRenaming();
-        _ui->errorLabel->setText({});
     } else {
         checkIfAllowedToRename();
     }
@@ -223,6 +224,7 @@
     _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
     _ui->filenameLineEdit->setEnabled(true);
     _ui->filenameLineEdit->selectAll();
+    _ui->errorLabel->setText({});
 
     const auto filePathFileInfo = QFileInfo(_filePath);
     const auto fileName = filePathFileInfo.fileName();
diff -Nru nextcloud-desktop-3.16.4/src/gui/macOS/ClientCommunicationProtocol.h nextcloud-desktop-3.16.7/src/gui/macOS/ClientCommunicationProtocol.h
--- nextcloud-desktop-3.16.4/src/gui/macOS/ClientCommunicationProtocol.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/macOS/ClientCommunicationProtocol.h	2025-07-28 10:08:26.000000000 +0200
@@ -23,7 +23,8 @@
 - (void)configureAccountWithUser:(NSString *)user
                           userId:(NSString *)userId
                        serverUrl:(NSString *)serverUrl
-                        password:(NSString *)password;
+                        password:(NSString *)password
+                       userAgent:(NSString *)userAgent;
 - (void)removeAccountConfig;
 - (void)createDebugLogStringWithCompletionHandler:(void(^)(NSString *debugLogString, NSError *error))completionHandler;
 - (void)getFastEnumerationStateWithCompletionHandler:(void(^)(BOOL enabled, BOOL set))completionHandler;
diff -Nru nextcloud-desktop-3.16.4/src/gui/macOS/fileprovidersocketcontroller.cpp nextcloud-desktop-3.16.7/src/gui/macOS/fileprovidersocketcontroller.cpp
--- nextcloud-desktop-3.16.4/src/gui/macOS/fileprovidersocketcontroller.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/macOS/fileprovidersocketcontroller.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -18,6 +18,7 @@
 #include <QLoggingCategory>
 
 #include "accountmanager.h"
+#include "common/utility.h"
 #include "fileproviderdomainmanager.h"
 
 namespace OCC {
@@ -237,6 +238,7 @@
 
     // We cannot use colons as separators here due to "https://"; in the url
     const auto message = QString(QStringLiteral("ACCOUNT_DETAILS:") +
+                                 Utility::userAgentString() + "~" +
                                  accountUser + "~" +
                                  accountUserId + "~" +
                                  accountUrl + "~" +
diff -Nru nextcloud-desktop-3.16.4/src/gui/macOS/fileproviderxpc_mac.mm nextcloud-desktop-3.16.7/src/gui/macOS/fileproviderxpc_mac.mm
--- nextcloud-desktop-3.16.4/src/gui/macOS/fileproviderxpc_mac.mm	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/macOS/fileproviderxpc_mac.mm	2025-07-28 10:08:26.000000000 +0200
@@ -12,6 +12,7 @@
  * for more details.
  */
 
+#include "common/utility.h"
 #include "fileproviderxpc.h"
 
 #include <QLoggingCategory>
@@ -69,12 +70,14 @@
     NSString *const userId = account->davUser().toNSString();
     NSString *const serverUrl = account->url().toString().toNSString();
     NSString *const password = credentials->password().toNSString();
+    NSString *const userAgent = QString::fromUtf8(Utility::userAgentString()).toNSString();
 
     const auto clientCommService = (NSObject<ClientCommunicationProtocol> *)_clientCommServices.value(extensionAccountId);
     [clientCommService configureAccountWithUser:user
                                          userId:userId
                                       serverUrl:serverUrl
-                                       password:password];
+                                       password:password
+                                      userAgent:userAgent];
 }
 
 void FileProviderXPC::unauthenticateExtension(const QString &extensionAccountId) const
diff -Nru nextcloud-desktop-3.16.4/src/gui/tray/activitydata.cpp nextcloud-desktop-3.16.7/src/gui/tray/activitydata.cpp
--- nextcloud-desktop-3.16.4/src/gui/tray/activitydata.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/tray/activitydata.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -130,7 +130,6 @@
             auto word = match.captured(1);
             word.remove(subjectRichParameterBracesRe);
 
-            Q_ASSERT(activity._subjectRichParameters.contains(word));
             displayString = displayString.replace(match.captured(1), activity._subjectRichParameters[word].value<Activity::RichSubjectParameter>().name);
         }
 
diff -Nru nextcloud-desktop-3.16.4/src/gui/tray/activitylistmodel.cpp nextcloud-desktop-3.16.7/src/gui/tray/activitylistmodel.cpp
--- nextcloud-desktop-3.16.4/src/gui/tray/activitylistmodel.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/tray/activitylistmodel.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -166,7 +166,7 @@
             // If this is an E2EE file or folder, pretend we got no path, hiding the share button which is what we want
             if (folder) {
                 SyncJournalFileRecord rec;
-                if (!folder->journalDb()->getFileRecord(fileName.mid(1), &rec)) {
+                if (!folder->journalDb()->getFileRecord(relPath.mid(1), &rec)) {
                     qCWarning(lcActivity) << "could not get file from local DB" << fileName.mid(1);
                 }
                 if (rec.isValid() && (rec.isE2eEncrypted() || !rec._e2eMangledName.isEmpty())) {
@@ -540,15 +540,13 @@
     beginInsertRows({}, startRow, startRow + activityList.count() - 1);
     for(const auto &activity : activityList) {
         _finalList.append(activity);
+        if (activity._syncFileItemStatus == SyncFileItem::Conflict) {
+            _conflictsList.push_back(activity);
+        }
     }
     endInsertRows();
 
-    const auto deselectedConflictIt = std::find_if(_finalList.constBegin(), _finalList.constEnd(), [] (const auto activity) {
-        return activity._syncFileItemStatus == SyncFileItem::Conflict;
-    });
-    const auto conflictsFound = (deselectedConflictIt != _finalList.constEnd());
-
-    setHasSyncConflicts(conflictsFound);
+    setHasSyncConflicts(!_conflictsList.isEmpty());
 }
 
 void ActivityListModel::accountStateHasChanged()
@@ -633,6 +631,10 @@
         endRemoveRows();
     }
 
+    if (activity._syncFileItemStatus == SyncFileItem::Conflict) {
+        _conflictsList.removeOne(activity);
+    }
+
     if (activity._type != Activity::ActivityType &&
         activity._type != Activity::DummyFetchingActivityType &&
         activity._type != Activity::DummyMoreActivitiesAvailableType &&
@@ -686,7 +688,6 @@
         }
 
         auto folder = FolderMan::instance()->folder(activity._folder);
-        const auto folderDir = QDir(folder->path());
         const auto fileLocation = activity._syncFileItemStatus == SyncFileItem::FileNameInvalidOnServer
             ? InvalidFilenameDialog::FileLocation::NewLocalFile
             : InvalidFilenameDialog::FileLocation::Default;
@@ -695,7 +696,7 @@
             : InvalidFilenameDialog::InvalidMode::SystemInvalid;
 
         _currentInvalidFilenameDialog = new InvalidFilenameDialog(_accountState->account(), folder,
-            folderDir.filePath(activity._file), fileLocation, invalidMode);
+            folder->filePath(activity._file), fileLocation, invalidMode);
         connect(_currentInvalidFilenameDialog, &InvalidFilenameDialog::accepted, folder, [folder]() {
             folder->scheduleThisFolderSoon();
         });
@@ -934,6 +935,7 @@
 void ActivityListModel::slotRemoveAccount()
 {
     _finalList.clear();
+    _conflictsList.clear();
     _activityLists.clear();
     _presentedActivities.clear();
     setAndRefreshCurrentlyFetching(false);
@@ -966,15 +968,7 @@
 
 ActivityList ActivityListModel::allConflicts() const
 {
-    auto result = ActivityList{};
-
-    for(const auto &activity : _finalList) {
-        if (activity._syncFileItemStatus == SyncFileItem::Conflict) {
-            result.push_back(activity);
-        }
-    }
-
-    return result;
+    return _conflictsList;
 }
 
 }
diff -Nru nextcloud-desktop-3.16.4/src/gui/tray/activitylistmodel.h nextcloud-desktop-3.16.7/src/gui/tray/activitylistmodel.h
--- nextcloud-desktop-3.16.4/src/gui/tray/activitylistmodel.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/gui/tray/activitylistmodel.h	2025-07-28 10:08:26.000000000 +0200
@@ -191,6 +191,7 @@
     ActivityList _notificationLists;
     ActivityList _listOfIgnoredFiles;
     ActivityList _notificationErrorsLists;
+    ActivityList _conflictsList;
     ActivityList _finalList;
 
     QSet<qint64> _presentedActivities;
diff -Nru nextcloud-desktop-3.16.4/src/libsync/configfile.cpp nextcloud-desktop-3.16.7/src/libsync/configfile.cpp
--- nextcloud-desktop-3.16.4/src/libsync/configfile.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/configfile.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -83,11 +83,6 @@
 static constexpr char proxyPassC[] = "Proxy/pass";
 static constexpr char proxyNeedsAuthC[] = "Proxy/needsAuth";
 
-static constexpr char useUploadLimitC[] = "BWLimit/useUploadLimit";
-static constexpr char useDownloadLimitC[] = "BWLimit/useDownloadLimit";
-static constexpr char uploadLimitC[] = "BWLimit/uploadLimit";
-static constexpr char downloadLimitC[] = "BWLimit/downloadLimit";
-
 static constexpr char newBigFolderSizeLimitC[] = "newBigFolderSizeLimit";
 static constexpr char useNewBigFolderSizeLimitC[] = "useNewBigFolderSizeLimit";
 static constexpr char notifyExistingFoldersOverLimitC[] = "notifyExistingFoldersOverLimit";
diff -Nru nextcloud-desktop-3.16.4/src/libsync/configfile.h nextcloud-desktop-3.16.7/src/libsync/configfile.h
--- nextcloud-desktop-3.16.4/src/libsync/configfile.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/configfile.h	2025-07-28 10:08:26.000000000 +0200
@@ -260,6 +260,11 @@
     static constexpr char showChatNotificationsC[] = "showChatNotifications";
     static constexpr char showInExplorerNavigationPaneC[] = "showInExplorerNavigationPane";
 
+    static constexpr char useUploadLimitC[] = "BWLimit/useUploadLimit";
+    static constexpr char useDownloadLimitC[] = "BWLimit/useDownloadLimit";
+    static constexpr char uploadLimitC[] = "BWLimit/uploadLimit";
+    static constexpr char downloadLimitC[] = "BWLimit/downloadLimit";
+
 protected:
     [[nodiscard]] QVariant getPolicySetting(const QString &policy, const QVariant &defaultValue = QVariant()) const;
     void storeData(const QString &group, const QString &key, const QVariant &value);
diff -Nru nextcloud-desktop-3.16.4/src/libsync/discovery.cpp nextcloud-desktop-3.16.7/src/libsync/discovery.cpp
--- nextcloud-desktop-3.16.4/src/libsync/discovery.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/discovery.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -858,6 +858,9 @@
             item->_modtime = serverEntry.modtime;
             item->_size = sizeOnServer;
             item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
+        } else if (serverEntry.isValid() && !serverEntry.isDirectory && !serverEntry.remotePerm.isNull() && !serverEntry.remotePerm.hasPermission(RemotePermissions::CanRead)) {
+            item->_instruction = CSYNC_INSTRUCTION_REMOVE;
+            item->_direction = SyncFileItem::Down;
         } else if (dbEntry._remotePerm != serverEntry.remotePerm || dbEntry._fileId != serverEntry.fileId || metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
             if (metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
                 // we are updating placeholder sizes after migrating from older versions with VFS + E2EE implicit hydration not supported
@@ -921,6 +924,13 @@
         return;
     }
 
+    if (serverEntry.isValid() && !serverEntry.isDirectory && !serverEntry.remotePerm.isNull() && !serverEntry.remotePerm.hasPermission(RemotePermissions::CanRead)) {
+        item->_instruction = CSYNC_INSTRUCTION_IGNORE;
+        emit _discoveryData->itemDiscovered(item);
+
+        return;
+    }
+
     // Potential NEW/NEW conflict is handled in AnalyzeLocal
     if (localEntry.isValid()) {
         postProcessServerNew(item, path, localEntry, serverEntry, dbEntry);
@@ -1053,6 +1063,9 @@
                 _discoveryData->findAndCancelDeletedJob(originalPath);
 
                 postProcessRename(path);
+                if (item->isDirectory() && serverEntry.isValid() && dbEntry.isValid() && serverEntry.etag == dbEntry._etag && serverEntry.remotePerm != dbEntry._remotePerm) {
+                    _queryServer = ParentNotChanged;
+                }
                 processFileFinalize(item, path, item->isDirectory(), item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist, _queryServer);
             });
             job->start();
@@ -1157,6 +1170,9 @@
         item->isPermissionsInvalid = localEntry.isPermissionsInvalid;
 
         auto recurseQueryLocal = _queryLocal == ParentNotChanged ? ParentNotChanged : localEntry.isDirectory || item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist;
+        if (item->isDirectory() && serverEntry.isValid() && dbEntry.isValid() && serverEntry.etag == dbEntry._etag && serverEntry.remotePerm != dbEntry._remotePerm) {
+            recurseQueryServer = ParentNotChanged;
+        }
         processFileFinalize(item, path, recurse, recurseQueryLocal, recurseQueryServer);
     };
 
@@ -1577,6 +1593,9 @@
                 processRename(path);
                 recurseQueryServer = etag.get() == base._etag ? ParentNotChanged : NormalQuery;
             }
+            if (item->isDirectory() && serverEntry.isValid() && dbEntry.isValid() && serverEntry.etag == dbEntry._etag && serverEntry.remotePerm != dbEntry._remotePerm) {
+                recurseQueryServer = ParentNotChanged;
+            }
             processFileFinalize(item, path, item->isDirectory(), NormalQuery, recurseQueryServer);
             _pendingAsyncJobs--;
             QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
diff -Nru nextcloud-desktop-3.16.4/src/libsync/discoveryphase.cpp nextcloud-desktop-3.16.7/src/libsync/discoveryphase.cpp
--- nextcloud-desktop-3.16.4/src/libsync/discoveryphase.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/discoveryphase.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -180,11 +180,10 @@
             result = true;
             oldEtag = (*it)->_etag;
         } else {
-            if (!(instruction == CSYNC_INSTRUCTION_REMOVE
-                    // re-creation of virtual files count as a delete
-                    || ((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)
-                    || ((*it)->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)))
-            {
+            if (!(instruction == CSYNC_INSTRUCTION_REMOVE ||
+                  instruction == CSYNC_INSTRUCTION_IGNORE ||
+                  ((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW) ||// re-creation of virtual files count as a delete
+                  ((*it)->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW))) {
                 qCWarning(lcDiscovery) << "ENFORCE(FAILING)" << originalPath;
                 qCWarning(lcDiscovery) << "instruction == CSYNC_INSTRUCTION_REMOVE" << (instruction == CSYNC_INSTRUCTION_REMOVE);
                 qCWarning(lcDiscovery) << "((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)"
diff -Nru nextcloud-desktop-3.16.4/src/libsync/filesystem.cpp nextcloud-desktop-3.16.7/src/libsync/filesystem.cpp
--- nextcloud-desktop-3.16.4/src/libsync/filesystem.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/filesystem.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -353,137 +353,61 @@
 }
 
 bool FileSystem::setFolderPermissions(const QString &path,
-                                      FileSystem::FolderPermissions permissions) noexcept
+                                      FileSystem::FolderPermissions permissions,
+                                      bool * const permissionsChanged) noexcept
 {
-#ifdef Q_OS_WIN
-    SECURITY_INFORMATION info = DACL_SECURITY_INFORMATION;
-    std::unique_ptr<char[]> securityDescriptor;
-    auto neededLength = 0ul;
-
-    if (!GetFileSecurityW(path.toStdWString().c_str(), info, nullptr, 0, &neededLength)) {
-        const auto lastError = GetLastError();
-        if (lastError != ERROR_INSUFFICIENT_BUFFER) {
-            qCWarning(lcFileSystem) << "error when calling GetFileSecurityW" << path << lastError;
-            return false;
-        }
-
-        securityDescriptor.reset(new char[neededLength]);
-
-        if (!GetFileSecurityW(path.toStdWString().c_str(), info, securityDescriptor.get(), neededLength, &neededLength)) {
-            qCWarning(lcFileSystem) << "error when calling GetFileSecurityW" << path << GetLastError();
-            return false;
-        }
-    }
+    bool permissionsDidChange = false;
 
-    int daclPresent = false, daclDefault = false;
-    PACL resultDacl = nullptr;
-    if (!GetSecurityDescriptorDacl(securityDescriptor.get(), &daclPresent, &resultDacl, &daclDefault)) {
-        qCWarning(lcFileSystem) << "error when calling GetSecurityDescriptorDacl" << path << GetLastError();
-        return false;
-    }
-    if (!daclPresent || !resultDacl) {
-        qCWarning(lcFileSystem) << "error when calling DACL needed to set a folder read-only or read-write is missing" << path;
-        return false;
+    if (permissionsChanged) {
+        *permissionsChanged = false;
     }
 
-    PSID sid = nullptr;
-    if (!ConvertStringSidToSidW(L"S-1-5-32-545", &sid))
-    {
-        qCWarning(lcFileSystem) << "error when calling ConvertStringSidToSidA" << path << GetLastError();
-        return false;
-    }
-
-    ACL_SIZE_INFORMATION aclSize;
-    if (!GetAclInformation(resultDacl, &aclSize, sizeof(aclSize), AclSizeInformation)) {
-        qCWarning(lcFileSystem) << "error when calling GetAclInformation" << path << GetLastError();
-        return false;
-    }
-
-    const auto newAclSize = aclSize.AclBytesInUse + sizeof(ACCESS_DENIED_ACE) + GetLengthSid(sid);
-    qCDebug(lcFileSystem) << "allocated a new DACL object of size" << newAclSize;
-
-    std::unique_ptr<ACL> newDacl{reinterpret_cast<PACL>(new char[newAclSize])};
-    if (!InitializeAcl(newDacl.get(), newAclSize, ACL_REVISION)) {
-        const auto lastError = GetLastError();
-        if (lastError != ERROR_INSUFFICIENT_BUFFER) {
-            qCWarning(lcFileSystem) << "insufficient memory error when calling InitializeAcl" << path;
-            return false;
-        }
-
-        qCWarning(lcFileSystem) << "error when calling InitializeAcl" << path << lastError;
-        return false;
-    }
+#ifdef Q_OS_WIN
+    // current read-only folder ACL needs to be removed from files also when making a folder read-write
+    // we currently have a too limited set of authorization for files when applying the restrictive ACL for folders on the child files
+    setFileReadOnly(path, permissions == FileSystem::FolderPermissions::ReadOnly);
+    setAclPermission(path, permissions, permissions == FileSystem::FolderPermissions::ReadWrite ? true : false);
 
-    if (permissions == FileSystem::FolderPermissions::ReadOnly) {
-        qCInfo(lcFileSystem) << path << "will be read only";
-        if (!AddAccessDeniedAce(newDacl.get(), ACL_REVISION, FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | FILE_DELETE_CHILD, sid)) {
-            qCWarning(lcFileSystem) << "error when calling AddAccessDeniedAce << path" << GetLastError();
-            return false;
-        }
-    }
+    permissionsDidChange = true;
+#else
+    static constexpr auto writePerms = std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write;
+    const auto stdStrPath = path.toStdWString();
 
-    if (permissions == FileSystem::FolderPermissions::ReadWrite) {
-        qCInfo(lcFileSystem) << path << "will be read write";
-    }
+    const auto currentPermissions = std::filesystem::status(stdStrPath).permissions();
+    qCDebug(lcFileSystem()).nospace() << "current permissions path=" << path << " perms=" << Qt::showbase << Qt::oct << static_cast<int>(currentPermissions);
 
-    for (int i = 0; i < aclSize.AceCount; ++i) {
-        void *currentAce = nullptr;
-        if (!GetAce(resultDacl, i, &currentAce)) {
-            qCWarning(lcFileSystem) << "error when calling GetAce" << path << GetLastError();
-            return false;
-        }
+    try
+    {
+        switch (permissions) {
+        case OCC::FileSystem::FolderPermissions::ReadOnly: {
+            qCDebug(lcFileSystem()).nospace() << "ensuring folder is read only path=" << path;
 
-        const auto currentAceHeader = reinterpret_cast<PACE_HEADER>(currentAce);
+            if ((currentPermissions & writePerms) != std::filesystem::perms::none) {
+                qCDebug(lcFileSystem()).nospace() << "removing owner/group/others write permissions path=" << path;
+                std::filesystem::permissions(stdStrPath, writePerms, std::filesystem::perm_options::remove);
+                permissionsDidChange = true;
+            }
 
-        if (permissions == FileSystem::FolderPermissions::ReadWrite && (ACCESS_DENIED_ACE_TYPE == (currentAceHeader->AceType & ACCESS_DENIED_ACE_TYPE))) {
-            qCWarning(lcFileSystem) << "AceHeader" << path << currentAceHeader->AceFlags << currentAceHeader->AceSize << currentAceHeader->AceType;
-            continue;
+            break;
         }
+        case OCC::FileSystem::FolderPermissions::ReadWrite: {
+            qCDebug(lcFileSystem()).nospace() << "ensuring folder is read/writable path=" << path;
 
-        if (!AddAce(newDacl.get(), ACL_REVISION, i + 1, currentAce, currentAceHeader->AceSize)) {
-            const auto lastError = GetLastError();
-            if (lastError != ERROR_INSUFFICIENT_BUFFER) {
-                qCWarning(lcFileSystem) << "insufficient memory error when calling AddAce" << path;
-                return false;
+            if ((currentPermissions & std::filesystem::perms::others_write) != std::filesystem::perms::none) {
+                qCDebug(lcFileSystem()).nospace() << "removing others write permissions path=" << path;
+                std::filesystem::permissions(stdStrPath, std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
+                permissionsDidChange = true;
             }
 
-            if (lastError != ERROR_INVALID_PARAMETER) {
-                qCWarning(lcFileSystem) << "invalid parameter error when calling AddAce" << path << "ACL size" << newAclSize;
-                return false;
+            if ((currentPermissions & std::filesystem::perms::owner_write) == std::filesystem::perms::none) {
+                qCDebug(lcFileSystem()).nospace() << "adding owner write permissions path=" << path;
+                std::filesystem::permissions(stdStrPath, std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
+                permissionsDidChange = true;
             }
 
-            qCWarning(lcFileSystem) << "error when calling AddAce" << path << lastError << "acl index" << (i + 1);
-            return false;
-        }
-    }
-
-    SECURITY_DESCRIPTOR newSecurityDescriptor;
-    if (!InitializeSecurityDescriptor(&newSecurityDescriptor, SECURITY_DESCRIPTOR_REVISION)) {
-        qCWarning(lcFileSystem) << "error when calling InitializeSecurityDescriptor" << path << GetLastError();
-        return false;
-    }
-
-    if (!SetSecurityDescriptorDacl(&newSecurityDescriptor, true, newDacl.get(), false)) {
-        qCWarning(lcFileSystem) << "error when calling SetSecurityDescriptorDacl" << path << GetLastError();
-        return false;
-    }
-
-    if (!SetFileSecurityW(path.toStdWString().c_str(), info, &newSecurityDescriptor)) {
-        qCWarning(lcFileSystem) << "error when calling SetFileSecurityW" << path << GetLastError();
-        return false;
-    }
-#else
-    static constexpr auto writePerms = std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write;
-    const auto stdStrPath = path.toStdWString();
-    try
-    {
-        switch (permissions) {
-        case OCC::FileSystem::FolderPermissions::ReadOnly:
-            std::filesystem::permissions(stdStrPath, writePerms, std::filesystem::perm_options::remove);
-            break;
-        case OCC::FileSystem::FolderPermissions::ReadWrite:
             break;
         }
+        }
     }
     catch (const std::filesystem::filesystem_error &e)
     {
@@ -501,34 +425,16 @@
         return false;
     }
 
-    try
-    {
-        switch (permissions) {
-        case OCC::FileSystem::FolderPermissions::ReadOnly:
-            break;
-        case OCC::FileSystem::FolderPermissions::ReadWrite:
-            std::filesystem::permissions(stdStrPath, std::filesystem::perms::others_write, std::filesystem::perm_options::remove);
-            std::filesystem::permissions(stdStrPath, std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
-            break;
-        }
-    }
-    catch (const std::filesystem::filesystem_error &e)
-    {
-        qCWarning(lcFileSystem()) << "exception when modifying folder permissions" << e.what() << e.path1().c_str() << e.path2().c_str();
-        return false;
-    }
-    catch (const std::system_error &e)
-    {
-        qCWarning(lcFileSystem()) << "exception when modifying folder permissions" << e.what() << "- path:" << stdStrPath;
-        return false;
-    }
-    catch (...)
-    {
-        qCWarning(lcFileSystem()) << "exception when modifying folder permissions -  path:" << stdStrPath;
-        return false;
+    if (permissionsDidChange) {
+        const auto newPermissions = std::filesystem::status(stdStrPath).permissions();
+        qCDebug(lcFileSystem()).nospace() << "updated permissions path=" << path << " perms=" << Qt::showbase << Qt::oct << static_cast<int>(newPermissions);
     }
 #endif
 
+    if (permissionsChanged) {
+        *permissionsChanged = permissionsDidChange;
+    }
+
     return true;
 }
 
@@ -626,12 +532,13 @@
 {
     try
     {
-        const auto stdStrPath = _path.toStdWString();
-        _initialPermissions = FileSystem::isFolderReadOnly(stdStrPath) ? OCC::FileSystem::FolderPermissions::ReadOnly : OCC::FileSystem::FolderPermissions::ReadWrite;
-        if (_initialPermissions != temporaryPermissions) {
+        const auto &stdStrPath = _path.toStdWString();
+        const auto fsPath = std::filesystem::path{stdStrPath};
+        if ((temporaryPermissions == OCC::FileSystem::FolderPermissions::ReadOnly && !FileSystem::isFolderReadOnly(fsPath)) ||
+            (temporaryPermissions == OCC::FileSystem::FolderPermissions::ReadWrite && FileSystem::isFolderReadOnly(fsPath))) {
+            FileSystem::setFolderPermissions(_path, temporaryPermissions);
             _rollbackNeeded = true;
         }
-        FileSystem::setFolderPermissions(_path, temporaryPermissions);
     }
     catch (const std::filesystem::filesystem_error &e)
     {
diff -Nru nextcloud-desktop-3.16.4/src/libsync/filesystem.h nextcloud-desktop-3.16.7/src/libsync/filesystem.h
--- nextcloud-desktop-3.16.4/src/libsync/filesystem.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/filesystem.h	2025-07-28 10:08:26.000000000 +0200
@@ -127,7 +127,8 @@
                                                const std::function<void(const QString &path, bool isDir)> &onError = nullptr);
 
     bool OWNCLOUDSYNC_EXPORT setFolderPermissions(const QString &path,
-                                                  FileSystem::FolderPermissions permissions) noexcept;
+                                                  FileSystem::FolderPermissions permissions,
+                                                  bool *permissionsChanged = nullptr) noexcept;
 
     bool OWNCLOUDSYNC_EXPORT isFolderReadOnly(const std::filesystem::path &path) noexcept;
 
diff -Nru nextcloud-desktop-3.16.4/src/libsync/lockfilejobs.cpp nextcloud-desktop-3.16.7/src/libsync/lockfilejobs.cpp
--- nextcloud-desktop-3.16.4/src/libsync/lockfilejobs.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/lockfilejobs.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -16,6 +16,7 @@
 
 #include "account.h"
 #include "common/syncjournaldb.h"
+#include "common/syncjournalfilerecord.h"
 #include "filesystem.h"
 
 #include <QLoggingCategory>
@@ -49,7 +50,16 @@
 
 void LockFileJob::start()
 {
-    qCInfo(lcLockFileJob()) << "start with path:" << path()
+    auto remotePath = path();
+
+    SyncJournalFileRecord record;
+    const auto relativePathInDb = path().mid(_remoteSyncPathWithTrailingSlash.size());
+    if (_journal->getFileRecord(relativePathInDb, &record) && record.isValid() && record.isE2eEncrypted()) {
+        remotePath = _remoteSyncPathWithTrailingSlash + record.e2eMangledName();
+        qCDebug(lcLockFileJob).nospace() << "will (un)lock e2ee file path=" << path() << " remotePath=" << remotePath;
+    }
+
+    qCInfo(lcLockFileJob()) << "start with path:" << remotePath
                             << "lock state:" <<  _requestedLockState
                             << "lock owner type:" << _requestedLockOwnerType;
 
@@ -77,7 +87,7 @@
         verb = "UNLOCK";
         break;
     }
-    sendRequest(verb, makeDavUrl(path()), request);
+    sendRequest(verb, makeDavUrl(remotePath), request);
 
     AbstractNetworkJob::start();
 }
diff -Nru nextcloud-desktop-3.16.4/src/libsync/owncloudpropagator.cpp nextcloud-desktop-3.16.7/src/libsync/owncloudpropagator.cpp
--- nextcloud-desktop-3.16.4/src/libsync/owncloudpropagator.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/owncloudpropagator.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -1293,9 +1293,11 @@
 
     // Delete the job and remove it from our list of jobs.
     subJob->deleteLater();
-    int i = _runningJobs.indexOf(subJob);
-    Q_ASSERT(i >= 0); // should only happen if this function is called more than once
-    _runningJobs.remove(i);
+    const auto i = _runningJobs.indexOf(subJob);
+    Q_ASSERT(i >= 0 && i < _runningJobs.size()); // should only happen if this function is called more than once
+    if (i >= 0 && i < _runningJobs.size()) {
+        _runningJobs.remove(i);
+    }
 
     // Any sub job error will cause the whole composite to fail. This is important
     // for knowing whether to update the etag in PropagateDirectory, for example.
@@ -1468,11 +1470,14 @@
                 !_item->_remotePerm.hasPermission(RemotePermissions::CanAddFile) &&
                 !_item->_remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories)) {
                 try {
-                    if (FileSystem::fileExists(propagator()->fullLocalPath(_item->_file))) {
-                        FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_file), FileSystem::FolderPermissions::ReadOnly);
+                    if (const auto fileName = propagator()->fullLocalPath(_item->_file); FileSystem::fileExists(fileName)) {
+                        FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadOnly);
+                        Q_EMIT propagator()->touchedFile(fileName);
                     }
                     if (!_item->_renameTarget.isEmpty() && FileSystem::fileExists(propagator()->fullLocalPath(_item->_renameTarget))) {
-                        FileSystem::setFolderPermissions(propagator()->fullLocalPath(_item->_renameTarget), FileSystem::FolderPermissions::ReadOnly);
+                        const auto fileName = propagator()->fullLocalPath(_item->_renameTarget);
+                        FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadOnly);
+                        Q_EMIT propagator()->touchedFile(fileName);
                     }
                 }
                 catch (const std::filesystem::filesystem_error &e)
@@ -1495,10 +1500,11 @@
                 }
             } else {
                 try {
-                    const auto permissionsChangeHelper = [] (const auto fileName)
+                    const auto permissionsChangeHelper = [this] (const auto fileName)
                     {
                         qCDebug(lcDirectory) << fileName << "permissions changed: old permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
                         FileSystem::setFolderPermissions(fileName, FileSystem::FolderPermissions::ReadWrite);
+                        Q_EMIT propagator()->touchedFile(fileName);
                         qCDebug(lcDirectory) << fileName << "applied new permissions" << static_cast<int>(std::filesystem::status(fileName.toStdWString()).permissions());
                     };
 
diff -Nru nextcloud-desktop-3.16.4/src/libsync/propagateremotemove.cpp nextcloud-desktop-3.16.7/src/libsync/propagateremotemove.cpp
--- nextcloud-desktop-3.16.4/src/libsync/propagateremotemove.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/propagateremotemove.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -209,7 +209,9 @@
             &propagator()->_anotherSyncNeeded);
         const auto filePath = propagator()->fullLocalPath(_item->_renameTarget);
         const auto filePathOriginal = propagator()->fullLocalPath(_item->_originalFile);
+        const auto oldFile = QFileInfo{filePathOriginal};
         QFile file(filePath);
+        auto permissionsHandler = FileSystem::FilePermissionsRestore{oldFile.absolutePath(), FileSystem::FolderPermissions::ReadWrite};
         if (!file.rename(filePathOriginal)) {
             qCWarning(lcPropagateRemoteMove) << "Could not MOVE file" << filePathOriginal << " to" << filePath
                                              << " with error:" << _job->errorString() << " and failed to restore it !";
diff -Nru nextcloud-desktop-3.16.4/src/libsync/propagatorjobs.cpp nextcloud-desktop-3.16.7/src/libsync/propagatorjobs.cpp
--- nextcloud-desktop-3.16.4/src/libsync/propagatorjobs.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/propagatorjobs.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -107,26 +107,60 @@
     }
 
     QString removeError;
-    if (_moveToTrash && propagator()->syncOptions()._vfs->mode() != OCC::Vfs::WindowsCfApi) {
-        if ((QDir(filename).exists() || FileSystem::fileExists(filename))
-            && !FileSystem::moveToTrash(filename, &removeError)) {
-            done(SyncFileItem::NormalError, tr("Temporary error when removing local item removed from server."), ErrorCategory::GenericError);
-            return;
+    auto moveToTrashIsFeasible = true;
+    if (propagator()->syncOptions()._vfs->mode() != OCC::Vfs::WindowsCfApi) {
+        moveToTrashIsFeasible = false;
+    }
+    const auto fileInfo = QFileInfo{filename};
+    if (fileInfo.isDir()) {
+        try {
+            if (FileSystem::isFolderReadOnly(fileInfo.filesystemAbsolutePath())) {
+                moveToTrashIsFeasible = false;
+            }
+        }
+        catch (const std::filesystem::filesystem_error &e)
+        {
+            qCWarning(lcPropagateLocalRemove) << "exception when checking parent folder read only status" << e.what() << e.path1().c_str() << e.path2().c_str();
+        }
+        catch (const std::system_error &e)
+        {
+            qCWarning(lcPropagateLocalRemove) << "exception when checking parent folder read only status" << e.what();
+        }
+        catch (...)
+        {
+            qCWarning(lcPropagateLocalRemove) << "exception when checking parent folder read only status";
+        }
+    } else {
+        if (!FileSystem::isWritable(filename, fileInfo)) {
+            moveToTrashIsFeasible = false;
+        }
+    }
+    if (_moveToTrash && moveToTrashIsFeasible) {
+        if (FileSystem::fileExists(filename, fileInfo)) {
+            const auto parentFolderPath = fileInfo.dir().absolutePath();
+            const auto parentPermissionsHandler = FileSystem::FilePermissionsRestore{parentFolderPath, FileSystem::FolderPermissions::ReadWrite};
+
+            if (!FileSystem::moveToTrash(filename, &removeError)) {
+                qCWarning(lcPropagateLocalRemove()) << "move to trash failed" << filename << removeError;
+                done(SyncFileItem::NormalError, tr("Temporary error when removing local item removed from server."), ErrorCategory::GenericError);
+                return;
+            }
+        } else {
+            qCWarning(lcPropagateLocalRemove()) << "move to trash failed" << filename << "was already deleted";
         }
     } else {
         if (_item->isDirectory()) {
-            if (QDir(filename).exists() && !removeRecursively(QString())) {
+            if (FileSystem::fileExists(filename, fileInfo) && !removeRecursively(QString())) {
                 done(SyncFileItem::NormalError, tr("Temporary error when removing local item removed from server."), ErrorCategory::GenericError);
                 return;
             }
         } else {
-            if (FileSystem::fileExists(filename)) {
-                const auto fileInfo = QFileInfo{filename};
+            if (FileSystem::fileExists(filename, fileInfo)) {
                 const auto parentFolderPath = fileInfo.dir().absolutePath();
-
                 const auto parentPermissionsHandler = FileSystem::FilePermissionsRestore{parentFolderPath, FileSystem::FolderPermissions::ReadWrite};
 
                 if (!FileSystem::remove(filename, &removeError)) {
+                    qCWarning(lcPropagateLocalRemove()) << "remove failed" << filename << removeError;
                     done(SyncFileItem::NormalError, tr("Temporary error when removing local item removed from server."), ErrorCategory::GenericError);
                     return;
                 }
diff -Nru nextcloud-desktop-3.16.4/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.rc nextcloud-desktop-3.16.7/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.rc
--- nextcloud-desktop-3.16.4/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.rc	1970-01-01 01:00:00.000000000 +0100
+++ nextcloud-desktop-3.16.7/src/libsync/vfs/cfapi/shellext/CfApiShellIntegration.rc	2025-07-28 10:08:26.000000000 +0200
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+// Microsoft Visual C++ generated resource script.
+//
+#include "CfApiShellIntegrationVersion.h"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "windows.h"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION NCEXT_VERSION
+ PRODUCTVERSION NCEXT_VERSION
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x2L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+            VALUE "CompanyName", "Nextcloud GmbH"
+            VALUE "FileDescription", "Nextcloud CfApi shell extension"
+            VALUE "FileVersion", NCEXT_VERSION_STRING
+            VALUE "InternalName", "NCOverlays"
+            VALUE "LegalCopyright", "Copyright (C) 2023 Nextcloud GmbH"
+            VALUE "ProductName", "Nextcloud shell extension"
+            VALUE "ProductVersion", NCEXT_VERSION_STRING
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif    // not APSTUDIO_INVOKED
+
diff -Nru nextcloud-desktop-3.16.4/src/libsync/vfs/cfapi/shellext/CfApiShellIntegrationVersion.h.in nextcloud-desktop-3.16.7/src/libsync/vfs/cfapi/shellext/CfApiShellIntegrationVersion.h.in
--- nextcloud-desktop-3.16.4/src/libsync/vfs/cfapi/shellext/CfApiShellIntegrationVersion.h.in	1970-01-01 01:00:00.000000000 +0100
+++ nextcloud-desktop-3.16.7/src/libsync/vfs/cfapi/shellext/CfApiShellIntegrationVersion.h.in	2025-07-28 10:08:26.000000000 +0200
@@ -0,0 +1,14 @@
+#pragma once
+
+// SPDX-FileCopyrightText: 2016 ownCloud GmbH
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+// This is the number that will end up in the version window of the DLLs.
+// Increment this version before committing a new build if you are today's shell_integration build master.
+#cmakedefine NCEXT_BUILD_NUM @NCEXT_BUILD_NUM@
+
+#define STRINGIZE2(s) #s
+#define STRINGIZE(s) STRINGIZE2(s)
+
+#cmakedefine NCEXT_VERSION @NCEXT_VERSION@
+#define NCEXT_VERSION_STRING STRINGIZE(NCEXT_VERSION)
diff -Nru nextcloud-desktop-3.16.4/src/libsync/vfs/cfapi/shellext/CMakeLists.txt nextcloud-desktop-3.16.7/src/libsync/vfs/cfapi/shellext/CMakeLists.txt
--- nextcloud-desktop-3.16.4/src/libsync/vfs/cfapi/shellext/CMakeLists.txt	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/src/libsync/vfs/cfapi/shellext/CMakeLists.txt	2025-07-28 10:08:26.000000000 +0200
@@ -153,6 +153,8 @@
 message("cppWinRtExe: ${cppWinRtExe}")
 message("midlExe: ${midlExe}")
 
+configure_file(CfApiShellIntegrationVersion.h.in ${CMAKE_CURRENT_BINARY_DIR}/CfApiShellIntegrationVersion.h)
+
 # use midl.exe and cppwinrt.exe to generate files for CustomStateProvider (WinRT class)
 add_custom_command(OUTPUT ${MidlOutputPathHeader}
    COMMAND ${midlExe} /winrt /h nul /tlb ${MidlOutputPathTlb} /winmd ${MidlOutputPathWinmd} /metadata_dir "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}" /nomidl /reference "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}\\Windows.Foundation.FoundationContract.winmd" /reference "${WindowsSDKReferencesPath}\\Windows.Storage.Provider.CloudFilesContract\\${WindowsStorageProviderCloudFilesContractVersion}\\Windows.Storage.Provider.CloudFilesContract.winmd" /I ${MidleFileFolder} customstateprovider.idl
@@ -170,6 +172,7 @@
     ${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp
     customstateprovider.cpp
     CfApiShellIntegration.def
+    CfApiShellIntegration.rc
 )
 
 message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
diff -Nru nextcloud-desktop-3.16.4/.tag nextcloud-desktop-3.16.7/.tag
--- nextcloud-desktop-3.16.4/.tag	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/.tag	2025-07-28 10:08:26.000000000 +0200
@@ -1 +1 @@
-0febbee77be35f9a17f591b22767e1500192d14f
+5584b240f9299c236ba99206b53577460fc12516
diff -Nru nextcloud-desktop-3.16.4/test/CMakeLists.txt nextcloud-desktop-3.16.7/test/CMakeLists.txt
--- nextcloud-desktop-3.16.4/test/CMakeLists.txt	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/CMakeLists.txt	2025-07-28 10:08:26.000000000 +0200
@@ -95,6 +95,8 @@
 nextcloud_add_test(DateFieldBackend)
 nextcloud_add_test(ClientStatusReporting)
 
+nextcloud_add_test(FileSystem)
+
 nextcloud_add_test(FolderStatusModel)
 
 target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync)
diff -Nru nextcloud-desktop-3.16.4/test/sharetestutils.cpp nextcloud-desktop-3.16.7/test/sharetestutils.cpp
--- nextcloud-desktop-3.16.4/test/sharetestutils.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/sharetestutils.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -153,6 +153,7 @@
 
     const auto fakeFileInfo = fakeFolder.remoteModifier().find(testFileName);
     QVERIFY(fakeFileInfo);
+    fakeFileInfo->permissions.setPermission(RemotePermissions::CanRead);
     fakeFileInfo->permissions.setPermission(RemotePermissions::CanReshare);
     QVERIFY(fakeFolder.syncOnce());
     QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
diff -Nru nextcloud-desktop-3.16.4/test/syncenginetestutils.cpp nextcloud-desktop-3.16.7/test/syncenginetestutils.cpp
--- nextcloud-desktop-3.16.4/test/syncenginetestutils.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/syncenginetestutils.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -406,7 +406,7 @@
         xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
         xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
         xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag)));
-        xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
+        xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("GSRDNVCKW") : QStringLiteral("GRDNVCKW"));
         xml.writeTextElement(ocUri, QStringLiteral("share-permissions"), QString::number(static_cast<int>(OCC::SharePermissions(OCC::SharePermissionRead |
                                                                                                                                 OCC::SharePermissionUpdate |
                                                                                                                                 OCC::SharePermissionCreate |
diff -Nru nextcloud-desktop-3.16.4/test/syncenginetestutils.h nextcloud-desktop-3.16.7/test/syncenginetestutils.h
--- nextcloud-desktop-3.16.4/test/syncenginetestutils.h	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/syncenginetestutils.h	2025-07-28 10:08:26.000000000 +0200
@@ -575,7 +575,7 @@
     [[nodiscard]] OCC::SyncJournalDb &syncJournal() const { return *_journalDb; }
     [[nodiscard]] FakeQNAM* networkAccessManager() const { return _fakeQnam; }
 
-    FileModifier &localModifier() { return _localModifier; }
+    DiskFileModifier &localModifier() { return _localModifier; }
     FileInfo &remoteModifier() { return _fakeQnam->currentRemoteState(); }
     FileInfo currentLocalState();
 
diff -Nru nextcloud-desktop-3.16.4/test/testallfilesdeleted.cpp nextcloud-desktop-3.16.7/test/testallfilesdeleted.cpp
--- nextcloud-desktop-3.16.4/test/testallfilesdeleted.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testallfilesdeleted.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -76,7 +76,7 @@
                 fakeFolder.syncEngine().journal()->clearFileTable(); // That's what Folder is doing
             });
 
-        auto &modifier = deleteOnRemote ? fakeFolder.remoteModifier() : fakeFolder.localModifier();
+        auto &modifier = deleteOnRemote ? fakeFolder.remoteModifier() : static_cast<FileModifier&>(fakeFolder.localModifier());
         const auto childrenKeys = fakeFolder.currentRemoteState().children.keys();
         for (const auto &key : childrenKeys) {
             modifier.remove(key);
@@ -118,7 +118,7 @@
                 callback(false);
             });
 
-        auto &modifier = deleteOnRemote ? fakeFolder.remoteModifier() : fakeFolder.localModifier();
+        auto &modifier = deleteOnRemote ? fakeFolder.remoteModifier() : static_cast<FileModifier&>(fakeFolder.localModifier());
         const auto childrenKeys = fakeFolder.currentRemoteState().children.keys();
         for (const auto &key : childrenKeys) {
             modifier.remove(key);
diff -Nru nextcloud-desktop-3.16.4/test/testblacklist.cpp nextcloud-desktop-3.16.7/test/testblacklist.cpp
--- nextcloud-desktop-3.16.4/test/testblacklist.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testblacklist.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -46,7 +46,7 @@
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         ItemCompletedSpy completeSpy(fakeFolder);
 
-        auto &modifier = remote ? fakeFolder.remoteModifier() : fakeFolder.localModifier();
+        auto &modifier = remote ? fakeFolder.remoteModifier() : static_cast<FileModifier&>(fakeFolder.localModifier());
 
         int counter = 0;
         const QByteArray testFileName = QByteArrayLiteral("A/new");
diff -Nru nextcloud-desktop-3.16.4/test/testfilesystem.cpp nextcloud-desktop-3.16.7/test/testfilesystem.cpp
--- nextcloud-desktop-3.16.4/test/testfilesystem.cpp	1970-01-01 01:00:00.000000000 +0100
+++ nextcloud-desktop-3.16.7/test/testfilesystem.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -0,0 +1,133 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * This software is in the public domain, furnished "as is", without technical
+ * support, and with no warranty, express or implied, as to its usefulness for
+ * any purpose.
+ */
+
+#include <QtTest>
+#include <QTemporaryDir>
+
+#include "common/filesystembase.h"
+#include "logger.h"
+
+#include "libsync/filesystem.h"
+
+using namespace OCC;
+namespace std_fs = std::filesystem;
+
+class TestFileSystem : public QObject
+{
+    Q_OBJECT
+
+private:
+    QTemporaryDir testDir;
+
+private Q_SLOTS:
+    void initTestCase()
+    {
+        OCC::Logger::instance()->setLogFlush(true);
+        OCC::Logger::instance()->setLogDebug(true);
+
+        QStandardPaths::setTestModeEnabled(true);
+
+        QDir dir(testDir.path());
+        dir.mkdir("existingDirectory");
+    }
+
+#ifndef Q_OS_WIN
+    void testSetFolderPermissionsExistingDirectory_data()
+    {
+        constexpr auto perms_0555 =
+            std_fs::perms::owner_read | std_fs::perms::owner_exec
+            | std_fs::perms::group_read | std_fs::perms::group_exec
+            | std_fs::perms::others_read | std_fs::perms::others_exec;
+        constexpr auto perms_0755 = perms_0555 | std_fs::perms::owner_write;
+        constexpr auto perms_0775 = perms_0755 | std_fs::perms::group_write;
+
+        QTest::addColumn<std_fs::perms>("originalPermissions");
+        QTest::addColumn<FileSystem::FolderPermissions>("folderPermissions");
+        QTest::addColumn<bool>("expectedResult");
+        QTest::addColumn<bool>("expectedPermissionsChanged");
+        QTest::addColumn<std_fs::perms>("expectedPermissions");
+
+        QTest::newRow("0777, readonly -> 0555, changed")
+            << std_fs::perms::all
+            << FileSystem::FolderPermissions::ReadOnly
+            << true
+            << true
+            << perms_0555;
+
+        QTest::newRow("0555, readonly -> 0555, not changed")
+            << perms_0555
+            << FileSystem::FolderPermissions::ReadOnly
+            << true
+            << false
+            << perms_0555;
+
+        QTest::newRow("0777, readwrite -> 0775, changed")
+            << std_fs::perms::all
+            << FileSystem::FolderPermissions::ReadWrite
+            << true
+            << true
+            << perms_0775;
+
+        QTest::newRow("0775, readwrite -> 0775, not changed")
+            << perms_0775
+            << FileSystem::FolderPermissions::ReadWrite
+            << true
+            << false
+            << perms_0775;
+
+        QTest::newRow("0755, readwrite -> 0755, not changed")
+            << perms_0755
+            << FileSystem::FolderPermissions::ReadWrite
+            << true
+            << false
+            << perms_0755;
+
+        QTest::newRow("0555, readwrite -> 0755, changed")
+            << perms_0555
+            << FileSystem::FolderPermissions::ReadWrite
+            << true
+            << true
+            << perms_0755;
+    }
+
+    void testSetFolderPermissionsExistingDirectory()
+    {
+        QFETCH(std_fs::perms, originalPermissions);
+        QFETCH(FileSystem::FolderPermissions, folderPermissions);
+        QFETCH(bool, expectedResult);
+        QFETCH(bool, expectedPermissionsChanged);
+        QFETCH(std_fs::perms, expectedPermissions);
+
+        bool permissionsDidChange = false;
+        QString fullPath = testDir.filePath("existingDirectory");
+        const auto stdStrPath = fullPath.toStdWString();
+
+        std_fs::permissions(stdStrPath, originalPermissions);
+
+        QCOMPARE(FileSystem::setFolderPermissions(fullPath, folderPermissions, &permissionsDidChange), expectedResult);
+
+        const auto newPermissions = std_fs::status(stdStrPath).permissions();
+        QCOMPARE(newPermissions, expectedPermissions);
+        QCOMPARE(permissionsDidChange, expectedPermissionsChanged);
+    }
+
+    void testSetFolderPermissionsNonexistentDirectory()
+    {
+        bool permissionsDidChange = false;
+
+        QString fullPath = testDir.filePath("nonexistentDirectory");
+
+        QCOMPARE(FileSystem::setFolderPermissions("nonexistentDirectory", FileSystem::FolderPermissions::ReadOnly, &permissionsDidChange), false);
+        QCOMPARE(permissionsDidChange, false);
+    }
+#endif
+};
+
+QTEST_GUILESS_MAIN(TestFileSystem)
+#include "testfilesystem.moc"
diff -Nru nextcloud-desktop-3.16.4/test/testfolderman.cpp nextcloud-desktop-3.16.7/test/testfolderman.cpp
--- nextcloud-desktop-3.16.4/test/testfolderman.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testfolderman.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -178,6 +178,7 @@
         fakeFolder.remoteModifier().insert(firstSharePath, 100);
         const auto firstShare = fakeFolder.remoteModifier().find(firstSharePath);
         QVERIFY(firstShare);
+        firstShare->permissions.setPermission(OCC::RemotePermissions::CanRead);
         firstShare->permissions.setPermission(OCC::RemotePermissions::IsShared);
 
         fakeFolder.remoteModifier().mkdir("A/B");
@@ -185,6 +186,7 @@
         fakeFolder.remoteModifier().insert(secondSharePath, 100);
         const auto secondShare = fakeFolder.remoteModifier().find(secondSharePath);
         QVERIFY(secondShare);
+        secondShare->permissions.setPermission(OCC::RemotePermissions::CanRead);
         secondShare->permissions.setPermission(OCC::RemotePermissions::IsShared);
 
         FolderMan *folderman = FolderMan::instance();
diff -Nru nextcloud-desktop-3.16.4/test/testlockfile.cpp nextcloud-desktop-3.16.7/test/testlockfile.cpp
--- nextcloud-desktop-3.16.4/test/testlockfile.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testlockfile.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -789,6 +789,83 @@
 
         QCOMPARE(lockFileDetectedNewlyUploadedSpy.count(), 1);
     }
+
+    void testLockFile_verifyE2eeFilesUseCorrectPath()
+    {
+        const auto e2eeRoot = QStringLiteral("encrypted");
+        const auto cleartextFilePath = QStringLiteral("encrypted/document.odt");
+        const auto encryptedFilePath = QStringLiteral("encrypted/1e4c70c057994f9daf7bbab71b046d5b");
+
+        FakeFolder fakeFolder{FileInfo{}};
+
+        fakeFolder.localModifier().mkdir(e2eeRoot);
+        fakeFolder.remoteModifier().mkdir(e2eeRoot);
+        fakeFolder.localModifier().insert(cleartextFilePath);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        // modify local entry for the file to be locked to pretend it's E2E encrypted
+        OCC::SyncJournalFileRecord record;
+        QVERIFY(fakeFolder.syncJournal().getFileRecord(cleartextFilePath, &record));
+        record._e2eEncryptionStatus = OCC::SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV2_0;
+        record._e2eMangledName = encryptedFilePath.toUtf8();
+        record._path = cleartextFilePath.toUtf8();
+        QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
+
+        // do something similar on the remote -- the encrypted file has a different name
+        fakeFolder.remoteModifier().rename(cleartextFilePath, encryptedFilePath);
+        fakeFolder.remoteModifier().setE2EE(encryptedFilePath, true);
+
+        // another sync run should not fail now, even with our pretended E2Ee setup :-)
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(),
+                                        &fakeFolder.syncJournal(),
+                                        QStringLiteral("/") + cleartextFilePath,
+                                        QStringLiteral("/"),
+                                        fakeFolder.localPath(),
+                                        {},
+                                        OCC::SyncFileItem::LockStatus::LockedItem,
+                                        OCC::SyncFileItem::LockOwnerType::UserLock);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        QString lockRequestPath;
+        connect(fakeFolder.networkAccessManager(), &QNetworkAccessManager::finished, [&lockRequestPath](QNetworkReply *reply) {
+            const auto request = reply->request();
+            if (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() != QStringLiteral("LOCK")) {
+                return;
+            }
+
+            QVERIFY(lockRequestPath.isEmpty());
+            lockRequestPath = request.url().path();
+        });
+
+        job->start();
+
+        QVERIFY(jobSuccess.wait());
+        QCOMPARE(jobFailure.count(), 0);
+
+        // expect the path of the LOCK request to have used the mangled name
+        QVERIFY(!lockRequestPath.isEmpty());
+        QVERIFY(lockRequestPath.contains(encryptedFilePath));
+        QVERIFY(!lockRequestPath.contains(cleartextFilePath));
+
+        auto fileRecord = OCC::SyncJournalFileRecord{};
+        QVERIFY(fakeFolder.syncJournal().getFileRecord(cleartextFilePath, &fileRecord));
+        QVERIFY(fileRecord.isE2eEncrypted());
+        QCOMPARE(fileRecord.e2eMangledName(), encryptedFilePath);
+        QCOMPARE(fileRecord._lockstate._locked, true);
+        QCOMPARE(fileRecord._lockstate._lockEditorApp, QString{});
+        QCOMPARE(fileRecord._lockstate._lockOwnerDisplayName, QStringLiteral("John Doe"));
+        QCOMPARE(fileRecord._lockstate._lockOwnerId, QStringLiteral("admin"));
+        QCOMPARE(fileRecord._lockstate._lockOwnerType, static_cast<qint64>(OCC::SyncFileItem::LockOwnerType::UserLock));
+        QCOMPARE(fileRecord._lockstate._lockTime, 1234560);
+        QCOMPARE(fileRecord._lockstate._lockTimeout, 1800);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
 };
 
 QTEST_GUILESS_MAIN(TestLockFile)
diff -Nru nextcloud-desktop-3.16.4/test/testpermissions.cpp nextcloud-desktop-3.16.7/test/testpermissions.cpp
--- nextcloud-desktop-3.16.4/test/testpermissions.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testpermissions.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -94,9 +94,23 @@
         QStandardPaths::setTestModeEnabled(true);
     }
 
+    void t7pl_data()
+    {
+        QTest::addColumn<bool>("moveToTrashEnabled");
+        QTest::newRow("move to trash") << true;
+        QTest::newRow("delete") << false;
+    }
+
     void t7pl()
     {
+        QFETCH(bool, moveToTrashEnabled);
+
         FakeFolder fakeFolder{ FileInfo() };
+
+        auto syncOptions = fakeFolder.syncEngine().syncOptions();
+        syncOptions._moveFilesToTrash = moveToTrashEnabled;
+        fakeFolder.syncEngine().setSyncOptions(syncOptions);
+
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
         // Some of this test depends on the order of discovery. With threading
@@ -111,11 +125,11 @@
 
         //create some files
         auto insertIn = [&](const QString &dir) {
-            fakeFolder.remoteModifier().insert(dir + "normalFile_PERM_WVND_.data", 100 );
-            fakeFolder.remoteModifier().insert(dir + "cannotBeRemoved_PERM_WVN_.data", 101 );
-            fakeFolder.remoteModifier().insert(dir + "canBeRemoved_PERM_D_.data", 102 );
-            fakeFolder.remoteModifier().insert(dir + "cannotBeModified_PERM_DVN_.data", cannotBeModifiedSize , 'A');
-            fakeFolder.remoteModifier().insert(dir + "canBeModified_PERM_W_.data", canBeModifiedSize );
+            fakeFolder.remoteModifier().insert(dir + "normalFile_PERM_GWVND_.data", 100 );
+            fakeFolder.remoteModifier().insert(dir + "cannotBeRemoved_PERM_GWVN_.data", 101 );
+            fakeFolder.remoteModifier().insert(dir + "canBeRemoved_PERM_GD_.data", 102 );
+            fakeFolder.remoteModifier().insert(dir + "cannotBeModified_PERM_GDVN_.data", cannotBeModifiedSize , 'A');
+            fakeFolder.remoteModifier().insert(dir + "canBeModified_PERM_GW_.data", canBeModifiedSize );
         };
 
         //put them in some directories
@@ -125,7 +139,7 @@
         insertIn("readonlyDirectory_PERM_M_/" );
         fakeFolder.remoteModifier().mkdir("readonlyDirectory_PERM_M_/subdir_PERM_CK_");
         fakeFolder.remoteModifier().mkdir("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_");
-        fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data", 100);
+        fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data", 100);
         applyPermissionsFromName(fakeFolder.remoteModifier());
 
         QVERIFY(fakeFolder.syncOnce());
@@ -196,13 +210,13 @@
 
         //1. remove the file than cannot be removed
         //  (they should be recovered)
-        fakeFolder.localModifier().remove("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data");
-        removeReadOnly("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data");
+        fakeFolder.localModifier().remove("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_GWVN_.data");
+        removeReadOnly("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_GWVN_.data");
 
         //2. remove the file that can be removed
         //  (they should properly be gone)
-        removeReadOnly("normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_D_.data");
-        removeReadOnly("readonlyDirectory_PERM_M_/canBeRemoved_PERM_D_.data");
+        removeReadOnly("normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_GD_.data");
+        removeReadOnly("readonlyDirectory_PERM_M_/canBeRemoved_PERM_GD_.data");
 
         //3. Edit the files that cannot be modified
         //  (they should be recovered, and a conflict shall be created)
@@ -211,17 +225,19 @@
             QFile(fakeFolder.localPath() + file).setPermissions(QFile::WriteOwner | QFile::ReadOwner);
             fakeFolder.localModifier().appendByte(file);
         };
-        editReadOnly("normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_DVN_.data");
-        editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data");
+        editReadOnly("normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_GDVN_.data");
+#if !defined Q_OS_WINDOWS
+        editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data");
+#endif
 
         //4. Edit other files
         //  (they should be uploaded)
-        fakeFolder.localModifier().appendByte("normalDirectory_PERM_CKDNV_/canBeModified_PERM_W_.data");
-        fakeFolder.localModifier().appendByte("readonlyDirectory_PERM_M_/canBeModified_PERM_W_.data");
+        fakeFolder.localModifier().appendByte("normalDirectory_PERM_CKDNV_/canBeModified_PERM_GW_.data");
+        fakeFolder.localModifier().appendByte("readonlyDirectory_PERM_M_/canBeModified_PERM_GW_.data");
 
         //5. Create a new file in a read write folder
         // (should be uploaded)
-        fakeFolder.localModifier().insert("normalDirectory_PERM_CKDNV_/newFile_PERM_WDNV_.data", 106 );
+        fakeFolder.localModifier().insert("normalDirectory_PERM_CKDNV_/newFile_PERM_GWDNV_.data", 106 );
         applyPermissionsFromName(fakeFolder.remoteModifier());
 
         //do the sync
@@ -231,36 +247,42 @@
 
         //1.
         // File should be recovered
-        QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data"));
-        QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data"));
+        QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_GWVN_.data"));
+#if !defined Q_OS_WINDOWS
+        QCOMPARE(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data")->size, cannotBeModifiedSize);
+#endif
+        QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_GWVN_.data"));
 
         //2.
         // File should be deleted
-        QVERIFY(!currentLocalState.find("normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_D_.data"));
-        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/canBeRemoved_PERM_D_.data"));
+        QVERIFY(!currentLocalState.find("normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_GD_.data"));
+        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/canBeRemoved_PERM_GD_.data"));
 
         //3.
         // File should be recovered
-        QCOMPARE(currentLocalState.find("normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_DVN_.data")->size, cannotBeModifiedSize);
-        QCOMPARE(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data")->size, cannotBeModifiedSize);
+        QCOMPARE(currentLocalState.find("normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_GDVN_.data")->size, cannotBeModifiedSize);
         // and conflict created
-        auto c1 = findConflict(currentLocalState, "normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_DVN_.data");
+        auto c1 = findConflict(currentLocalState, "normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_GDVN_.data");
         QVERIFY(c1);
         QCOMPARE(c1->size, cannotBeModifiedSize + 1);
-        auto c2 = findConflict(currentLocalState, "readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data");
+#if !defined Q_OS_WINDOWS
+        auto c2 = findConflict(currentLocalState, "readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data");
         QVERIFY(c2);
         QCOMPARE(c2->size, cannotBeModifiedSize + 1);
+#endif
         // remove the conflicts for the next state comparison
         fakeFolder.localModifier().remove(c1->path());
+#if !defined Q_OS_WINDOWS
         removeReadOnly(c2->path());
+#endif
 
         //4. File should be updated, that's tested by assertLocalAndRemoteDir
-        QCOMPARE(currentLocalState.find("normalDirectory_PERM_CKDNV_/canBeModified_PERM_W_.data")->size, canBeModifiedSize + 1);
-        QCOMPARE(currentLocalState.find("readonlyDirectory_PERM_M_/canBeModified_PERM_W_.data")->size, canBeModifiedSize + 1);
+        QCOMPARE(currentLocalState.find("normalDirectory_PERM_CKDNV_/canBeModified_PERM_GW_.data")->size, canBeModifiedSize + 1);
+        QCOMPARE(currentLocalState.find("readonlyDirectory_PERM_M_/canBeModified_PERM_GW_.data")->size, canBeModifiedSize + 1);
 
         //5.
         // the file should be in the server and local
-        QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/newFile_PERM_WDNV_.data"));
+        QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/newFile_PERM_GWDNV_.data"));
 
         // Both side should still be the same
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -269,7 +291,7 @@
 
         //6. Create a new file in a read only folder
         // (they should not be uploaded)
-        insertReadOnly("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data", 105 );
+        insertReadOnly("readonlyDirectory_PERM_M_/newFile_PERM_GWDNV_.data", 105 );
 
         applyPermissionsFromName(fakeFolder.remoteModifier());
         // error: can't upload to readonly
@@ -280,8 +302,8 @@
 
         //6.
         // The file should not exist on the remote, and not be there
-        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data"));
-        QVERIFY(!fakeFolder.currentRemoteState().find("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data"));
+        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/newFile_PERM_GWDNV_.data"));
+        QVERIFY(!fakeFolder.currentRemoteState().find("readonlyDirectory_PERM_M_/newFile_PERM_GWDNV_.data"));
         // Both side should still be the same
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
@@ -294,7 +316,7 @@
         QVERIFY(fakeFolder.syncOnce());
         assertCsyncJournalOk(fakeFolder.syncJournal());
         currentLocalState = fakeFolder.currentLocalState();
-        QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data"));
+        QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_GWVN_.data"));
         QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_"));
         // the subdirectory had delete permissions, but, it was within the recovered directory, so must also get recovered
         QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_"));
@@ -319,16 +341,16 @@
         QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_"));
         // contents moved (had move permissions)
         QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_"));
-        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data"));
+        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data"));
 
         // new still exist  (and is uploaded)
-        QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data"));
+        QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data"));
 
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
         // restore for further tests
         fakeFolder.remoteModifier().mkdir("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_");
-        fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data");
+        fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data");
         applyPermissionsFromName(fakeFolder.remoteModifier());
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -342,7 +364,7 @@
         QVERIFY(fakeFolder.syncOnce());
         assertCsyncJournalOk(fakeFolder.syncJournal());
 
-        QVERIFY(fakeFolder.currentLocalState().find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" ));
+        QVERIFY(fakeFolder.currentLocalState().find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data" ));
 
         //1. rename a directory in a read only folder
         //Missing directory should be restored
@@ -360,9 +382,9 @@
         // old name restored
         QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_" ));
         // including contents
-        QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" ));
+        QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data" ));
         // new no longer exists
-        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/newname_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" ));
+        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/newname_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data" ));
         // but is not on server: should have been locally removed
         QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/newname_PERM_CK_"));
 
@@ -372,7 +394,7 @@
         // but still on the server: the rename causing an error meant the deletes didn't execute
         QVERIFY(fakeFolder.currentRemoteState().find("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_"));
         // new no longer exists
-        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/moved_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" ));
+        QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/moved_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_GWVND_.data" ));
         // should have been cleaned up as invalid item inside read-only folder
         QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/moved_PERM_CK_"));
         fakeFolder.remoteModifier().remove("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_");
@@ -383,20 +405,20 @@
         //######################################################################
         qInfo( "multiple restores of a file create different conflict files" );
 
-        fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data");
+        fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data");
         applyPermissionsFromName(fakeFolder.remoteModifier());
         QVERIFY(fakeFolder.syncOnce());
 
-        editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data");
-        fakeFolder.localModifier().setContents("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data", 's');
+        editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data");
+        fakeFolder.localModifier().setContents("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data", 's');
         //do the sync
         applyPermissionsFromName(fakeFolder.remoteModifier());
         QVERIFY(fakeFolder.syncOnce());
         assertCsyncJournalOk(fakeFolder.syncJournal());
 
         QThread::sleep(1); // make sure changes have different mtime
-        editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data");
-        fakeFolder.localModifier().setContents("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data", 'd');
+        editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data");
+        fakeFolder.localModifier().setContents("readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data", 'd');
 
         //do the sync
         applyPermissionsFromName(fakeFolder.remoteModifier());
@@ -406,7 +428,7 @@
         // there should be two conflict files
         currentLocalState = fakeFolder.currentLocalState();
         int count = 0;
-        while (auto i = findConflict(currentLocalState, "readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data")) {
+        while (auto i = findConflict(currentLocalState, "readonlyDirectory_PERM_M_/cannotBeModified_PERM_GDVN_.data")) {
             QVERIFY((i->contentChar == 's') || (i->contentChar == 'd'));
             removeReadOnly(i->path());
             currentLocalState = fakeFolder.currentLocalState();
@@ -461,10 +483,10 @@
         rm.insert("zallowed/sub/file");
         rm.insert("zallowed/sub2/file");
 
-        setAllPerm(rm.find("norename"), RemotePermissions::fromServerString("WDVCK"));
-        setAllPerm(rm.find("nomove"), RemotePermissions::fromServerString("WDNCK"));
-        setAllPerm(rm.find("nocreatefile"), RemotePermissions::fromServerString("WDNVK"));
-        setAllPerm(rm.find("nocreatedir"), RemotePermissions::fromServerString("WDNVC"));
+        setAllPerm(rm.find("norename"), RemotePermissions::fromServerString("GWDVCK"));
+        setAllPerm(rm.find("nomove"), RemotePermissions::fromServerString("GWDNCK"));
+        setAllPerm(rm.find("nocreatefile"), RemotePermissions::fromServerString("GWDNVK"));
+        setAllPerm(rm.find("nocreatedir"), RemotePermissions::fromServerString("GWDNVC"));
 
         QVERIFY(fakeFolder.syncOnce());
 
@@ -689,7 +711,7 @@
         remote.mkdir("testFolder/newSubFolder");
         remote.create("testFolder/testFile", 12, '9');
         remote.create("testFolder/testReadOnlyFile", 13, '8');
-        remote.find("testFolder/testReadOnlyFile")->permissions = RemotePermissions::fromServerString("m");
+        remote.find("testFolder/testReadOnlyFile")->permissions = RemotePermissions::fromServerString("mG");
 
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -720,7 +742,7 @@
 
         remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
         remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
-        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
+        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("mG");
 
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -750,7 +772,7 @@
 
         remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
         remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
-        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
+        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("mG");
 
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -784,7 +806,7 @@
         remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
         remote.find("readOnlyFolder/child")->permissions = RemotePermissions::fromServerString("m");
         remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
-        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
+        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("mG");
 
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -817,13 +839,13 @@
 
         remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
         remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
-        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
+        remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("mG");
 
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
         remote.insert("readOnlyFolder/test/newFile.txt");
-        remote.find("readOnlyFolder/test/newFile.txt")->permissions = RemotePermissions::fromServerString("m");
+        remote.find("readOnlyFolder/test/newFile.txt")->permissions = RemotePermissions::fromServerString("mG");
         remote.mkdir("readOnlyFolder/test/newFolder");
         remote.find("readOnlyFolder/test/newFolder")->permissions = RemotePermissions::fromServerString("m");
         remote.appendByte("readOnlyFolder/readOnlyFile.txt");
@@ -841,6 +863,113 @@
         QVERIFY(ensureReadOnlyItem("/readOnlyFolder/test/newFile.txt"));
         QVERIFY(ensureReadOnlyItem("/readOnlyFolder/newFolder"));
     }
+
+    void testForbiddenDownload()
+    {
+        FakeFolder fakeFolder{FileInfo{}};
+        QObject parent;
+
+        fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
+            Q_UNUSED(outgoingData)
+
+            if (op == QNetworkAccessManager::GetOperation) {
+                return new FakeErrorReply(op, request, &parent, 403, "Access to this shared resource has been denied because its download permission is disabled.");
+            }
+
+            return nullptr;
+        });
+
+        fakeFolder.remoteModifier().insert("file");
+
+        setAllPerm(fakeFolder.remoteModifier().find("file"), RemotePermissions::fromServerString("DNVRS"));
+
+        // also hook into discovery!!
+        SyncFileItemVector discovery;
+        connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, this, [&discovery](auto v) { discovery = v; });
+        ItemCompletedSpy completeSpy(fakeFolder);
+        QVERIFY(fakeFolder.syncOnce());
+
+        QVERIFY(itemInstruction(completeSpy, "file", CSYNC_INSTRUCTION_IGNORE));
+        QVERIFY(discoveryInstruction(discovery, "file", CSYNC_INSTRUCTION_IGNORE));
+    }
+
+    void testExistingFileBecomeForbiddenDownload()
+    {
+        FakeFolder fakeFolder{FileInfo{}};
+        QObject parent;
+
+        fakeFolder.remoteModifier().insert("file");
+        auto fileInfo = fakeFolder.remoteModifier().find("file");
+        Q_ASSERT(fileInfo);
+        fileInfo->isShared = true;
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
+            Q_UNUSED(outgoingData)
+
+            if (op == QNetworkAccessManager::GetOperation) {
+                return new FakeErrorReply(op, request, &parent, 403, "Access to this shared resource has been denied because its download permission is disabled.");
+            }
+
+            return nullptr;
+        });
+
+        setAllPerm(fileInfo, RemotePermissions::fromServerString("DNVRS"));
+
+        // also hook into discovery!!
+        SyncFileItemVector discovery;
+        connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, this, [&discovery](auto v) { discovery = v; });
+        ItemCompletedSpy completeSpy(fakeFolder);
+        QVERIFY(fakeFolder.syncOnce());
+
+        QVERIFY(itemInstruction(completeSpy, "file", CSYNC_INSTRUCTION_REMOVE));
+        QVERIFY(discoveryInstruction(discovery, "file", CSYNC_INSTRUCTION_REMOVE));
+    }
+
+    void testChangingPermissionsWithoutEtagChange()
+    {
+        FakeFolder fakeFolder{FileInfo{}};
+        QObject parent;
+
+        fakeFolder.setServerVersion(QStringLiteral("27.0.0"));
+
+        fakeFolder.remoteModifier().mkdir("groupFolder");
+        fakeFolder.remoteModifier().mkdir("groupFolder/simpleChildFolder");
+        fakeFolder.remoteModifier().insert("groupFolder/simpleChildFolder/otherFile");
+        fakeFolder.remoteModifier().mkdir("groupFolder/folderParent");
+        fakeFolder.remoteModifier().mkdir("groupFolder/folderParent/childFolder");
+        fakeFolder.remoteModifier().insert("groupFolder/folderParent/childFolder/file");
+
+        auto groupFolderRoot = fakeFolder.remoteModifier().find("groupFolder");
+        setAllPerm(groupFolderRoot, RemotePermissions::fromServerString("WDNVCKRMG"));
+
+        auto propfindCounter = 0;
+
+        fakeFolder.setServerOverride([&propfindCounter](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
+            Q_UNUSED(outgoingData)
+
+            if (op == QNetworkAccessManager::CustomOperation &&
+                request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("PROPFIND")) {
+                ++propfindCounter;
+            }
+
+            return nullptr;
+        });
+
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(propfindCounter, 5);
+
+        fakeFolder.setServerVersion(QStringLiteral("31.0.0"));
+
+        auto groupFolderRoot2 = fakeFolder.remoteModifier().find("groupFolder");
+        groupFolderRoot2->extraDavProperties = "<nc:is-mount-root>true</nc:is-mount-root>";
+
+        fakeFolder.remoteModifier().insert("groupFolder/simpleChildFolder/otherFile", 12);
+
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(propfindCounter, 8);
+    }
 };
 
 QTEST_GUILESS_MAIN(TestPermissions)
diff -Nru nextcloud-desktop-3.16.4/test/testremotediscovery.cpp nextcloud-desktop-3.16.7/test/testremotediscovery.cpp
--- nextcloud-desktop-3.16.4/test/testremotediscovery.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testremotediscovery.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -27,7 +27,7 @@
                                const QNetworkRequest &request, QObject *parent)
         : FakePropfindReply(remoteRootFileInfo, op, request, parent) {
         // If the propfind contains a single file without permissions, this is a server error
-        const char toRemove[] = "<oc:permissions>RDNVCKW</oc:permissions>";
+        const char toRemove[] = "<oc:permissions>GRDNVCKW</oc:permissions>";
         auto pos = payload.indexOf(toRemove, payload.size()/2);
         QVERIFY(pos > 0);
         payload.remove(pos, sizeof(toRemove) - 1);
diff -Nru nextcloud-desktop-3.16.4/test/testsyncengine.cpp nextcloud-desktop-3.16.7/test/testsyncengine.cpp
--- nextcloud-desktop-3.16.4/test/testsyncengine.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testsyncengine.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -216,8 +216,23 @@
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
     }
 
+
+    void testLocalDelete_data()
+    {
+        QTest::addColumn<bool>("moveToTrashEnabled");
+        QTest::newRow("move to trash") << true;
+        QTest::newRow("delete") << false;
+    }
+
     void testLocalDelete() {
+        QFETCH(bool, moveToTrashEnabled);
+
         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
+
+        auto syncOptions = fakeFolder.syncEngine().syncOptions();
+        syncOptions._moveFilesToTrash = moveToTrashEnabled;
+        fakeFolder.syncEngine().setSyncOptions(syncOptions);
+
         ItemCompletedSpy completeSpy(fakeFolder);
         fakeFolder.remoteModifier().remove("A/a1");
         fakeFolder.syncOnce();
@@ -234,10 +249,23 @@
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
     }
 
+    void testLocalDeleteWithReuploadForNewLocalFiles_data()
+    {
+        QTest::addColumn<bool>("moveToTrashEnabled");
+        QTest::newRow("move to trash") << true;
+        QTest::newRow("delete") << false;
+    }
+
     void testLocalDeleteWithReuploadForNewLocalFiles()
     {
+        QFETCH(bool, moveToTrashEnabled);
+
         FakeFolder fakeFolder{FileInfo{}};
 
+        auto syncOptions = fakeFolder.syncEngine().syncOptions();
+        syncOptions._moveFilesToTrash = moveToTrashEnabled;
+        fakeFolder.syncEngine().setSyncOptions(syncOptions);
+
         // create folders hierarchy with some nested dirs and files
         fakeFolder.localModifier().mkdir("A");
         fakeFolder.localModifier().insert("A/existingfile_A.txt", 100);
@@ -1563,9 +1591,23 @@
         QCOMPARE(fileThirdSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME);
     }
 
+    void testFolderRemovalWithCaseClash_data()
+    {
+        QTest::addColumn<bool>("moveToTrashEnabled");
+        QTest::newRow("move to trash") << true;
+        QTest::newRow("delete") << false;
+    }
+
     void testFolderRemovalWithCaseClash()
     {
-        FakeFolder fakeFolder{ FileInfo{} };
+        QFETCH(bool, moveToTrashEnabled);
+
+        FakeFolder fakeFolder{FileInfo{}};
+
+        auto syncOptions = fakeFolder.syncEngine().syncOptions();
+        syncOptions._moveFilesToTrash = moveToTrashEnabled;
+        fakeFolder.syncEngine().setSyncOptions(syncOptions);
+
         fakeFolder.remoteModifier().mkdir("A");
         fakeFolder.remoteModifier().mkdir("toDelete");
         fakeFolder.remoteModifier().insert("A/file");
@@ -1848,8 +1890,17 @@
         }
     }
 
+    void testServer_caseClash_createConflict_thenRemoveOneRemoteFile_data()
+    {
+        QTest::addColumn<bool>("moveToTrashEnabled");
+        QTest::newRow("move to trash") << true;
+        QTest::newRow("delete") << false;
+    }
+
     void testServer_caseClash_createConflict_thenRemoveOneRemoteFile()
     {
+        QFETCH(bool, moveToTrashEnabled);
+
         constexpr auto testLowerCaseFile = "test";
         constexpr auto testUpperCaseFile = "TEST";
 
@@ -1861,6 +1912,10 @@
 
         FakeFolder fakeFolder{FileInfo{}};
 
+        auto syncOptions = fakeFolder.syncEngine().syncOptions();
+        syncOptions._moveFilesToTrash = moveToTrashEnabled;
+        fakeFolder.syncEngine().setSyncOptions(syncOptions);
+
         fakeFolder.remoteModifier().insert("otherFile.txt");
         fakeFolder.remoteModifier().insert(testLowerCaseFile);
         fakeFolder.remoteModifier().insert(testUpperCaseFile);
@@ -2200,9 +2255,9 @@
         fakeFolder.remoteModifier().insert("file3");
 
         fakeFolder.remoteModifier().find("folder")->permissions = RemotePermissions::fromServerString("DNVS");
-        fakeFolder.remoteModifier().find("folder/file1.lnk")->permissions = RemotePermissions::fromServerString("S");
-        fakeFolder.remoteModifier().find("folder/file2.lnk")->permissions = RemotePermissions::fromServerString("S");
-        fakeFolder.remoteModifier().find("folder/file3.lnk")->permissions = RemotePermissions::fromServerString("S");
+        fakeFolder.remoteModifier().find("folder/file1.lnk")->permissions = RemotePermissions::fromServerString("SG");
+        fakeFolder.remoteModifier().find("folder/file2.lnk")->permissions = RemotePermissions::fromServerString("SG");
+        fakeFolder.remoteModifier().find("folder/file3.lnk")->permissions = RemotePermissions::fromServerString("SG");
 
         fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd")->permissions = RemotePermissions::fromServerString("DNVS");
         fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd/abcdef abcdef abcdef a")->permissions = RemotePermissions::fromServerString("DNVS");
@@ -2212,7 +2267,7 @@
         fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd/abcdef abcdef abcdef a/abcdef abcdef/abcdef acbdef abcd/123abcdefabcdef1/123123abcdef123 abcdef1")->permissions = RemotePermissions::fromServerString("DNVS");
         fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd/abcdef abcdef abcdef a/abcdef abcdef/abcdef acbdef abcd/123abcdefabcdef1/123123abcdef123 abcdef1/12abcabc")->permissions = RemotePermissions::fromServerString("DNVS");
         fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd/abcdef abcdef abcdef a/abcdef abcdef/abcdef acbdef abcd/123abcdefabcdef1/123123abcdef123 abcdef1/12abcabc/12abcabd")->permissions = RemotePermissions::fromServerString("DNVS");
-        fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd/abcdef abcdef abcdef a/abcdef abcdef/abcdef acbdef abcd/123abcdefabcdef1/123123abcdef123 abcdef1/12abcabc/12abcabd/this is a long long long long long long long long long long long long long long long long l.docx - Sh.lnk")->permissions = RemotePermissions::fromServerString("S");
+        fakeFolder.remoteModifier().find("abcdefabcdefabcdefabcdefabcdefabcd/abcdef abcdef abcdef a/abcdef abcdef/abcdef acbdef abcd/123abcdefabcdef1/123123abcdef123 abcdef1/12abcabc/12abcabd/this is a long long long long long long long long long long long long long long long long l.docx - Sh.lnk")->permissions = RemotePermissions::fromServerString("SG");
 
         QVERIFY(fakeFolder.syncOnce());
     }
@@ -2290,6 +2345,78 @@
         QCOMPARE(completeSpy.findItem(fileWithoutSpaces6)->_status, SyncFileItem::Status::Success);
         QCOMPARE(completeSpy.findItem(extraFileNameWithoutSpaces)->_status, SyncFileItem::Status::Success);
     }
+
+    void testTouchedFilesWhenChangingFolderPermissionsDuringSync()
+    {
+        FakeFolder fakeFolder{FileInfo{}};
+        fakeFolder.localModifier().mkdir("directory");
+        fakeFolder.localModifier().mkdir("directory/subdir");
+        fakeFolder.remoteModifier().mkdir("directory");
+        fakeFolder.remoteModifier().mkdir("directory/subdir");
+
+        // perform an initial sync to ensure local and remote have the same state
+        QVERIFY(fakeFolder.syncOnce());
+
+        QStringList touchedFiles;
+
+        // syncEngine->_propagator is only set during a sync, which doesn't work with QSignalSpy :(
+        connect(&fakeFolder.syncEngine(), &SyncEngine::started, this, [&]() {
+            // at this point we have a propagator to connect signals to
+            connect(fakeFolder.syncEngine().getPropagator().get(), &OwncloudPropagator::touchedFile, this, [&touchedFiles](const QString& fileName) {
+                touchedFiles.append(fileName);
+            });
+        });
+
+        const auto syncAndExpectNoTouchedFiles = [&]() {
+            touchedFiles.clear();
+            QVERIFY(fakeFolder.syncOnce());
+            QCOMPARE(touchedFiles.size(), 0);
+        };
+
+        // when nothing changed expect no files to be touched
+        syncAndExpectNoTouchedFiles();
+
+        // when the remote etag of a subsubdir changes expect the parent+subdirs to be touched
+        fakeFolder.remoteModifier().findInvalidatingEtags("directory/subdir");
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(touchedFiles.size(), 2);
+        QVERIFY(touchedFiles.contains(fakeFolder.localModifier().find("directory/subdir").fileName()));
+        QVERIFY(touchedFiles.contains(fakeFolder.localModifier().find("directory").fileName()));
+
+        // nothing changed again, expect no files to be touched
+        syncAndExpectNoTouchedFiles();
+
+        // when subdir folder permissions change, expect the parent to be touched
+        touchedFiles.clear();
+        fakeFolder.remoteModifier().find("directory")->permissions = RemotePermissions::fromServerString("S");
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(touchedFiles.size(), 1);
+        QVERIFY(touchedFiles.contains(fakeFolder.localModifier().find("directory").fileName()));
+
+        // another sync without changes, expect no files to be touched
+        syncAndExpectNoTouchedFiles();
+
+        // remote etag of the subdir changed, expect the parent to be touched
+        touchedFiles.clear();
+        fakeFolder.remoteModifier().findInvalidatingEtags("directory");
+        QVERIFY(fakeFolder.syncOnce());
+        QCOMPARE(touchedFiles.size(), 1);
+        QVERIFY(touchedFiles.contains(fakeFolder.localModifier().find("directory").fileName()));
+
+        // same as usual, expect no files to be touched
+        syncAndExpectNoTouchedFiles();
+
+        // remote rename of the subdir folder, expect the new name to be touched
+        touchedFiles.clear();
+        fakeFolder.remoteModifier().rename("directory", "renamedDirectory");
+        QVERIFY(fakeFolder.syncOnce());
+        qDebug() << touchedFiles;
+        QCOMPARE_GT(touchedFiles.size(), 1);
+        QVERIFY(touchedFiles.contains(fakeFolder.localModifier().find("renamedDirectory").fileName()));
+
+        // last sync without changes, expect no files to be touched
+        syncAndExpectNoTouchedFiles();
+    }
 };
 
 QTEST_GUILESS_MAIN(TestSyncEngine)
diff -Nru nextcloud-desktop-3.16.4/test/testsyncmove.cpp nextcloud-desktop-3.16.7/test/testsyncmove.cpp
--- nextcloud-desktop-3.16.4/test/testsyncmove.cpp	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/test/testsyncmove.cpp	2025-07-28 10:08:26.000000000 +0200
@@ -326,7 +326,7 @@
         fakeFolder.remoteModifier().mkdir("external-storage");
         auto externalStorage = fakeFolder.remoteModifier().find("external-storage");
         externalStorage->extraDavProperties = "<nc:is-mount-root>true</nc:is-mount-root>";
-        setAllPerm(externalStorage, RemotePermissions::fromServerString("WDNVCKRM"));
+        setAllPerm(externalStorage, RemotePermissions::fromServerString("WDNVCKRMG"));
         QVERIFY(fakeFolder.syncOnce());
 
         OperationCounter operationCounter;
@@ -812,7 +812,7 @@
     {
         QFETCH(bool, local);
         FakeFolder fakeFolder { FileInfo::A12_B12_C12_S12() };
-        auto &modifier = local ? fakeFolder.localModifier() : fakeFolder.remoteModifier();
+        auto &modifier = local ? static_cast<FileModifier&>(fakeFolder.localModifier()) : fakeFolder.remoteModifier();
 
         modifier.mkdir("FolA");
         modifier.mkdir("FolA/FolB");
@@ -1159,7 +1159,7 @@
         fakeFolder.remoteModifier().mkdir("FolA");
         auto groupFolderRoot = fakeFolder.remoteModifier().find("FolA");
         groupFolderRoot->extraDavProperties = "<nc:is-mount-root>true</nc:is-mount-root>";
-        setAllPerm(groupFolderRoot, RemotePermissions::fromServerString("WDNVCKRM"));
+        setAllPerm(groupFolderRoot, RemotePermissions::fromServerString("WDNVCKRMG"));
         fakeFolder.remoteModifier().mkdir("FolA/FolB");
         fakeFolder.remoteModifier().mkdir("FolA/FolB/FolC");
         fakeFolder.remoteModifier().mkdir("FolA/FolB/FolC/FolD");
@@ -1196,7 +1196,7 @@
         fakeFolder.remoteModifier().mkdir("FolA");
         auto groupFolderRoot = fakeFolder.remoteModifier().find("FolA");
         groupFolderRoot->extraDavProperties = "<nc:is-mount-root>true</nc:is-mount-root>";
-        setAllPerm(groupFolderRoot, RemotePermissions::fromServerString("WDNVCKRM"));
+        setAllPerm(groupFolderRoot, RemotePermissions::fromServerString("WDNVCKRMG"));
         fakeFolder.remoteModifier().mkdir("FolA/FolB");
         fakeFolder.remoteModifier().mkdir("FolA/FolB/FolC");
         fakeFolder.remoteModifier().mkdir("FolA/FolB/FolC/FolD");
@@ -1372,6 +1372,54 @@
         QVERIFY(dbResult);
         QCOMPARE(itemsCounter, 12);
     }
+
+    void testRenameFileThatExistsInMultiplePaths()
+    {
+        FakeFolder fakeFolder{FileInfo{}};
+        QObject parent;
+
+        fakeFolder.setServerOverride([&parent](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
+            if (op == QNetworkAccessManager::CustomOperation
+                && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) {
+                return new FakeErrorReply(op, request, &parent, 507);
+            }
+            return nullptr;
+        });
+
+        fakeFolder.remoteModifier().mkdir("FolderA");
+        fakeFolder.remoteModifier().mkdir("FolderA/folderParent");
+        fakeFolder.remoteModifier().insert("FolderA/folderParent/FileA.txt");
+        fakeFolder.remoteModifier().mkdir("FolderB");
+        fakeFolder.remoteModifier().mkdir("FolderB/folderChild");
+        fakeFolder.remoteModifier().insert("FolderB/folderChild/FileA.txt");
+        fakeFolder.remoteModifier().mkdir("FolderC");
+
+        const auto fileAFileInfo = fakeFolder.remoteModifier().find("FolderB/folderChild/FileA.txt");
+        const auto fileAInFolderAFolderFileId = fileAFileInfo->fileId;
+        const auto fileAInFolderAEtag = fileAFileInfo->etag;
+        const auto duplicatedFileAFileInfo = fakeFolder.remoteModifier().find("FolderB/folderChild/FileA.txt");
+
+        duplicatedFileAFileInfo->fileId = fileAInFolderAFolderFileId;
+        duplicatedFileAFileInfo->etag = fileAInFolderAEtag;
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.localModifier().rename("FolderA/folderParent/FileA.txt", "FolderC/FileA.txt");
+
+        qDebug() << fakeFolder.currentLocalState();
+
+        fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly);
+        QVERIFY(!fakeFolder.syncOnce());
+
+        qDebug() << fakeFolder.currentLocalState();
+
+        fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly);
+        QVERIFY(fakeFolder.syncOnce());
+
+        qDebug() << fakeFolder.currentLocalState();
+    }
 };
 
 QTEST_GUILESS_MAIN(TestSyncMove)
diff -Nru nextcloud-desktop-3.16.4/VERSION.cmake nextcloud-desktop-3.16.7/VERSION.cmake
--- nextcloud-desktop-3.16.4/VERSION.cmake	2025-04-28 12:10:35.000000000 +0200
+++ nextcloud-desktop-3.16.7/VERSION.cmake	2025-07-28 10:08:26.000000000 +0200
@@ -3,10 +3,12 @@
 # ------------------------------------
 set(MIRALL_VERSION_MAJOR 3)
 set(MIRALL_VERSION_MINOR 16)
-set(MIRALL_VERSION_PATCH 4)
+set(MIRALL_VERSION_PATCH 7)
 set(MIRALL_VERSION_YEAR  2025)
 set(MIRALL_SOVERSION     0)
-set(MIRALL_PREVERSION_HUMAN "3.16.4")  # For preversions where PATCH>=50. Use version + alpha, rc1, rc2, etc.
+set(MIRALL_PREVERSION_HUMAN "3.16.7")  # For preversions where PATCH>=50. Use version + alpha, rc1, rc2, etc.
+set(NCEXT_BUILD_NUM 47)
+set(NCEXT_VERSION 3,0,0,${NCEXT_BUILD_NUM})
 
 # ------------------------------------
 # Minimum supported server versions

Attachment: signature.asc
Description: This is a digitally signed message part.


Reply to: