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, ¤tAce)) {
+ 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, ¤tAce)) {
- 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.