From bc502025b7f1d11de233c6c5d7d1947c47027f34 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Fri, 17 Oct 2025 12:59:49 +0200 Subject: [PATCH 1/2] fix(tests): expect a different NSFileProviderError depending on availability Signed-off-by: Jyrki Gadinger --- Tests/NextcloudFileProviderKitTests/ItemCreateTests.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/NextcloudFileProviderKitTests/ItemCreateTests.swift b/Tests/NextcloudFileProviderKitTests/ItemCreateTests.swift index 1b960bd8..e7327713 100644 --- a/Tests/NextcloudFileProviderKitTests/ItemCreateTests.swift +++ b/Tests/NextcloudFileProviderKitTests/ItemCreateTests.swift @@ -715,7 +715,12 @@ final class ItemCreateTests: NextcloudFileProviderKitTestCase { XCTAssertNil(createdItem) let unwrappedError = try XCTUnwrap(error) as? NSFileProviderError - XCTAssertEqual(unwrappedError, NSFileProviderError(.cannotSynchronize)) + let expectedError = if #available(macOS 13.0, *) { + NSFileProviderError(.excludedFromSync) + } else { + NSFileProviderError(.cannotSynchronize) + } + XCTAssertEqual(unwrappedError, expectedError) XCTAssertNil(Self.dbManager.itemMetadata(ocId: lockFileMetadata.ocId)) XCTAssertFalse(targetRemote.locked) } From 58eecddc4ee591272ae60229d6485dd4bc80ae6a Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Fri, 17 Oct 2025 13:04:55 +0200 Subject: [PATCH 2/2] refactor(metadata): provide a single method for assembling the full remote path There are many places that assemble the full remote path manually, but it might not always be correct. For instance in the case of the root directory, an item's `fileName` is set to `"__NC_ROOT__"` by NextcloudKit. Signed-off-by: Jyrki Gadinger --- .../Database/FilesDatabaseManager.swift | 16 ++-------------- .../Enumeration/RemoteChangeObserver.swift | 9 +++------ .../Item/Item+Create.swift | 2 +- .../Item/Item+Delete.swift | 4 ++-- .../Item/Item+Fetch.swift | 4 ++-- .../Metadata/ItemMetadata.swift | 13 ++++++++++++- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift index 1f529605..3f79688e 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift @@ -599,11 +599,7 @@ public final class FilesDatabaseManager: Sendable { var handledUpdateOcIds = Set(updated.map(\.ocId)) updated - .map { - var serverUrl = $0.serverUrl + "/" + $0.fileName - if serverUrl.last == "/" { serverUrl.removeLast() } - return serverUrl - } + .map { $0.remotePath() } .forEach { serverUrl in logger.debug("Checking updated item...", [.url: serverUrl]) @@ -630,15 +626,7 @@ public final class FilesDatabaseManager: Sendable { let handledDeleteOcIds = Set(deleted.map(\.ocId)) deleted - .map { // assemble remote location - var serverUrl = $0.serverUrl + "/" + $0.fileName - - if serverUrl.last == "/" { - serverUrl.removeLast() - } - - return serverUrl - } + .map { $0.remotePath() } .forEach { serverUrl in logger.debug("Verifying deleted item...", [.url: serverUrl]) diff --git a/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift b/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift index 05e47a81..45790036 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift @@ -456,10 +456,7 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess // This way we ensure we visit parent folders before their children. let materialisedItems = dbManager .materialisedItemMetadatas(account: accountId) - .sorted { - ($0.serverUrl + "/" + $0.fileName).count < - ($1.serverUrl + "/" + $1.fileName).count - } + .sorted { $0.remotePath().count < $1.remotePath().count } var allNewMetadatas = [SendableItemMetadata]() var allUpdatedMetadatas = [SendableItemMetadata]() @@ -480,7 +477,7 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess continue } - let itemRemoteUrl = item.serverUrl + "/" + item.fileName + let itemRemoteUrl = item.remotePath() let (metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, _, readError) = await Enumerator.readServerUrl( itemRemoteUrl, @@ -550,7 +547,7 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess // Also mark any materialized children of this directory as examined let grandChildren = materialisedItems.filter { - $0.serverUrl.hasPrefix(localItem.serverUrl + "/" + localItem.fileName) + $0.serverUrl.hasPrefix(localItem.remotePath()) } examinedChildFilesAndDeletedItems.formUnion(grandChildren.map(\.ocId)) } diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Create.swift b/Sources/NextcloudFileProviderKit/Item/Item+Create.swift index 856ee4e5..72beccc9 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Create.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Create.swift @@ -490,7 +490,7 @@ public extension Item { ) return (nil, NSFileProviderError(.cannotSynchronize)) } - parentItemRemotePath = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName + parentItemRemotePath = parentItemMetadata.remotePath() parentItemRelativePath = parentItemRemotePath.replacingOccurrences( of: account.davFilesUrl, with: "" ) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Delete.swift b/Sources/NextcloudFileProviderKit/Item/Item+Delete.swift index deea53f3..88e1426d 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Delete.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Delete.swift @@ -24,7 +24,7 @@ public extension Item { } let ocId = itemIdentifier.rawValue - let relativePath = (metadata.serverUrl + "/" + metadata.fileName).replacingOccurrences(of: metadata.urlBase, with: "") + let relativePath = (metadata.remotePath()).replacingOccurrences(of: metadata.urlBase, with: "") guard metadata.isLockFileOfLocalOrigin == false else { return await deleteLockFile(domain: domain, dbManager: dbManager) @@ -36,7 +36,7 @@ public extension Item { return nil } - let serverFileNameUrl = metadata.serverUrl + "/" + metadata.fileName + let serverFileNameUrl = metadata.remotePath() guard serverFileNameUrl != "" else { return NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift b/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift index 5cfd5a31..7d9ff4da 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift @@ -50,7 +50,7 @@ public extension Item { progress.totalUnitCount += Int64(metadatas.count) for var metadata in metadatas { - let remotePath = metadata.serverUrl + "/" + metadata.fileName + let remotePath = metadata.remotePath() let relativePath = remotePath.replacingOccurrences(of: directoryRemotePath, with: "") let childLocalPath = directoryLocalPath + relativePath @@ -129,7 +129,7 @@ public extension Item { } } - let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName + let serverUrlFileName = metadata.remotePath() logger.debug("Fetching item.", [.name: metadata.fileName, .url: serverUrlFileName]) diff --git a/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift index 62bcca51..8aa35afc 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors // SPDX-License-Identifier: GPL-2.0-or-later +import FileProvider import Foundation /// @@ -73,7 +74,7 @@ public protocol ItemMetadata: Equatable { var quotaAvailableBytes: Int64 { get set } var resourceType: String { get set } var richWorkspace: String? { get set } - var serverUrl: String { get set } // For parent folder! Build remote url by adding fileName + var serverUrl: String { get set } // For parent folder! Retrieve the full remote url via .remotePath() var session: String? { get set } var sessionError: String? { get set } var sessionTaskIdentifier: Int? { get set } @@ -170,4 +171,14 @@ public extension ItemMetadata { .init(name: "a", value: "true"), ]) } + + func remotePath() -> String { + if ocId == NSFileProviderItemIdentifier.rootContainer.rawValue { + // For the root container the fileName is "__NC_ROOT__" (aka NextcloudKit.shared.nkCommonInstance.rootFileName) + // --> appending the fileName to that is not correct, as it most likely won't ecist + return serverUrl + } + + return "\(serverUrl)/\(fileName)" + } }