From 71d52bf2a521bfd8c44faa5397ba6d552972d2b5 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 13:50:39 +0200 Subject: [PATCH 01/15] fix: Log details. - Removed FileProviderLogDetailKey.metadata because it was intended for Realm data models which actually are unsafe to pass across concurrency domains and caused a crash while doing so. - Removed FileProviderLogDetailKey.ocId because it is the same as "item" which is intended for the primary key of file provider items. Signed-off-by: Iva Horn --- .../FilesDatabaseManager+Directories.swift | 16 ++++++------ .../FilesDatabaseManager+KeepDownloaded.swift | 6 ++--- .../Database/FilesDatabaseManager.swift | 20 +++++++-------- .../Enumeration/Enumerator+Trash.swift | 2 +- .../MaterialisedEnumerationObserver.swift | 6 ++--- .../Item/Item+Fetch.swift | 2 +- .../Item/Item+Modify.swift | 4 +-- .../Log/FileProviderLog.swift | 25 +++++++++++++------ .../Log/FileProviderLogDetailKey.swift | 20 ++++++--------- .../Metadata/SendableItemMetadata+Array.swift | 8 +++--- 10 files changed, 56 insertions(+), 53 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+Directories.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+Directories.swift index a2d336a9..77668b91 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+Directories.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+Directories.swift @@ -52,7 +52,7 @@ extension FilesDatabaseManager { .where({ $0.ocId == ocId && $0.directory }) .first else { - logger.error("Could not find directory metadata for ocId. Not proceeding with deletion.", [.ocId: ocId]) + logger.error("Could not find directory metadata for ocId. Not proceeding with deletion.", [.item: ocId]) return nil } @@ -62,13 +62,13 @@ extension FilesDatabaseManager { let directoryAccount = directoryMetadata.account let directoryEtag = directoryMetadata.etag - logger.debug("Deleting root directory metadata in recursive delete.", [.eTag: directoryEtag, .ocId: directoryMetadata.ocId, .url: directoryUrlPath]) + logger.debug("Deleting root directory metadata in recursive delete.", [.eTag: directoryEtag, .item: directoryMetadata.ocId, .url: directoryUrlPath]) let database = ncDatabase() do { try database.write { directoryMetadata.deleted = true } } catch let error { - logger.error("Failure to delete root directory metadata in recursive delete.", [.error: error, .eTag: directoryEtag, .ocId: directoryOcId, .url: directoryUrlPath]) + logger.error("Failure to delete root directory metadata in recursive delete.", [.error: error, .eTag: directoryEtag, .item: directoryOcId, .url: directoryUrlPath]) return nil } @@ -84,11 +84,11 @@ extension FilesDatabaseManager { try database.write { result.deleted = true } deletedMetadatas.append(inactiveItemMetadata) } catch let error { - logger.error("Failure to delete directory metadata child in recursive delete", [.error: error, .eTag: directoryEtag, .ocId: directoryOcId, .url: directoryUrlPath]) + logger.error("Failure to delete directory metadata child in recursive delete", [.error: error, .eTag: directoryEtag, .item: directoryOcId, .url: directoryUrlPath]) } } - logger.debug("Completed deletions in directory recursive delete.", [.eTag: directoryEtag, .ocId: directoryOcId, .url: directoryUrlPath]) + logger.debug("Completed deletions in directory recursive delete.", [.eTag: directoryEtag, .item: directoryOcId, .url: directoryUrlPath]) return deletedMetadatas } @@ -100,7 +100,7 @@ extension FilesDatabaseManager { .where({ $0.ocId == ocId && $0.directory }) .first else { - logger.error("Could not find a directory with ocID \(ocId), cannot proceed with recursive renaming.", [.ocId: ocId]) + logger.error("Could not find a directory with ocID \(ocId), cannot proceed with recursive renaming.", [.item: ocId]) return nil } @@ -114,7 +114,7 @@ extension FilesDatabaseManager { } renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName) - logger.debug("Renamed root renaming directory from \"\(oldDirectoryServerUrl)\" to \"\(newDirectoryServerUrl)\".", [.ocId: ocId]) + logger.debug("Renamed root renaming directory from \"\(oldDirectoryServerUrl)\" to \"\(newDirectoryServerUrl)\".", [.item: ocId]) do { let database = ncDatabase() @@ -133,7 +133,7 @@ extension FilesDatabaseManager { } } } catch { - logger.error("Could not rename directory metadata.", [.error: error, .ocId: ocId, .url: newServerUrl]) + logger.error("Could not rename directory metadata.", [.error: error, .item: ocId, .url: newServerUrl]) return nil } diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+KeepDownloaded.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+KeepDownloaded.swift index d738673b..73e6aba4 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+KeepDownloaded.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager+KeepDownloaded.swift @@ -8,7 +8,7 @@ public extension FilesDatabaseManager { func set(keepDownloaded: Bool, for metadata: SendableItemMetadata) throws -> SendableItemMetadata? { guard #available(macOS 13.0, iOS 16.0, visionOS 1.0, *) else { let error = "Could not update keepDownloaded status for item because the system does not support this state." - logger.error(error, [.ocId: metadata.ocId, .name: metadata.fileName]) + logger.error(error, [.item: metadata.ocId, .name: metadata.fileName]) throw NSError( domain: Self.errorDomain, @@ -19,7 +19,7 @@ public extension FilesDatabaseManager { guard let result = itemMetadatas.where({ $0.ocId == metadata.ocId }).first else { let error = "Did not update keepDownloaded for item metadata as it was not found." - logger.error(error, [.ocId: metadata.ocId, .name: metadata.fileName]) + logger.error(error, [.item: metadata.ocId, .name: metadata.fileName]) throw NSError( domain: Self.errorDomain, @@ -31,7 +31,7 @@ public extension FilesDatabaseManager { try ncDatabase().write { result.keepDownloaded = keepDownloaded - logger.debug("Updated keepDownloaded status for item metadata.", [.ocId: metadata.ocId, .name: metadata.fileName]) + logger.debug("Updated keepDownloaded status for item metadata.", [.item: metadata.ocId, .name: metadata.fileName]) } return SendableItemMetadata(value: result) } diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift index b05af64f..b1a69b21 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift @@ -267,7 +267,7 @@ public final class FilesDatabaseManager: Sendable { deletedMetadatas.append(metadataToDelete) - logger.debug("Deleting item metadata during update.", [.metadata: existingMetadata]) + logger.debug("Deleting item metadata during update.", [.item: existingMetadata.ocId]) } return deletedMetadatas @@ -309,14 +309,14 @@ public final class FilesDatabaseManager: Sendable { returningUpdatedMetadatas.append(updatedMetadata) logger.debug("Updated existing item metadata.", [ - .ocId: updatedMetadata.ocId, + .item: updatedMetadata.ocId, .eTag: updatedMetadata.etag, .name: updatedMetadata.fileName, .syncTime: updatedMetadata.syncTime.description, ]) } else { logger.debug("Skipping item metadata update; same as existing, or still in transit.", [ - .ocId: updatedMetadata.ocId, + .item: updatedMetadata.ocId, .eTag: updatedMetadata.etag, .name: updatedMetadata.fileName, .syncTime: updatedMetadata.syncTime.description, @@ -326,7 +326,7 @@ public final class FilesDatabaseManager: Sendable { } else { // This is a new metadata returningNewMetadatas.append(updatedMetadata) - logger.debug("Created new item metadata during update.", [.metadata: updatedMetadata]) + logger.debug("Created new item metadata during update.", [.item: updatedMetadata.ocId]) } } @@ -469,7 +469,7 @@ public final class FilesDatabaseManager: Sendable { } logger.debug("Updated status for item metadata.", [ - .ocId: metadata.ocId, + .item: metadata.ocId, .eTag: metadata.etag, .name: metadata.fileName, .syncTime: metadata.syncTime, @@ -478,7 +478,7 @@ public final class FilesDatabaseManager: Sendable { return SendableItemMetadata(value: result) } catch { logger.error("Could not update status for item metadata.", [ - .ocId: metadata.ocId, + .item: metadata.ocId, .eTag: metadata.etag, .error: error, .name: metadata.fileName @@ -494,10 +494,10 @@ public final class FilesDatabaseManager: Sendable { do { try database.write { database.add(RealmItemMetadata(value: metadata), update: .all) - logger.debug("Added item metadata.", [.metadata: metadata]) + logger.debug("Added item metadata.", [.item: metadata.ocId, .name: metadata.name, .url: metadata.serverUrl]) } } catch { - logger.error("Failed to add item metadata.", [.metadata: metadata, .error: error]) + logger.error("Failed to add item metadata.", [.item: metadata.ocId, .name: metadata.name, .url: metadata.serverUrl, .error: error]) } } @@ -554,7 +554,7 @@ public final class FilesDatabaseManager: Sendable { } guard let parentDirectoryMetadata = parentDirectoryMetadataForItem(metadata) else { - logger.error("Could not get item parent directory item metadata for metadata.", [.metadata: metadata]) + logger.error("Could not get item parent directory item metadata for metadata.", [.item: metadata.ocId]) return nil } @@ -583,7 +583,7 @@ public final class FilesDatabaseManager: Sendable { guard error == nil, let parentMetadata = metadatas?.first else { logger.error("Could not retrieve parent item identifier remotely.", [ .error: error, - .ocId: metadata.ocId, + .item: metadata.ocId, .name: metadata.fileName ]) diff --git a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift index eee92fa3..01dd4e5b 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift @@ -77,7 +77,7 @@ extension Enumerator { ) newTrashedItems.append(item) - logger.debug("Will enumerate changed trash item.", [.ocId: metadata.ocId, .name: metadata.fileName]) + logger.debug("Will enumerate changed trash item.", [.item: metadata.ocId, .name: metadata.fileName]) } let deletedTrashedItemsIdentifiers = existingTrashedItems.map { diff --git a/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift b/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift index 2ee3d0b4..05b75635 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift @@ -90,18 +90,18 @@ public class MaterialisedEnumerationObserver: NSObject, NSFileProviderEnumeratio metadata.downloaded = true } - logger.info("Updating materialisation state for item to MATERIALISED with id \(enumeratedId) with filename \(metadata.fileName)", [.ocId: enumeratedId, .name: metadata.fileName]) + logger.info("Updating materialisation state for item to MATERIALISED with id \(enumeratedId) with filename \(metadata.fileName)", [.item: enumeratedId, .name: metadata.fileName]) dbManager.addItemMetadata(metadata) } } for unmaterialisedId in unmaterialisedIds { guard var metadata = materialisedMetadatasMap[unmaterialisedId] else { - logger.error("No materialised for \(unmaterialisedId) found.", [.ocId: unmaterialisedId]) + logger.error("No materialised for \(unmaterialisedId) found.", [.item: unmaterialisedId]) continue } - logger.info("Updating materialisation state for item to DATALESS with id \(unmaterialisedId) with filename \(metadata.fileName).", [.name: metadata.fileName, .ocId: unmaterialisedId]) + logger.info("Updating materialisation state for item to DATALESS with id \(unmaterialisedId) with filename \(metadata.fileName).", [.name: metadata.fileName, .item: unmaterialisedId]) metadata.downloaded = false metadata.visitedDirectory = false diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift b/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift index f7cdb3bf..89771baf 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift @@ -211,7 +211,7 @@ public extension Item { progress: progress ) } catch { - logger.error("Could not fetch directory contents.", [.ocId: ocId, .error: error]) + logger.error("Could not fetch directory contents.", [.item: ocId, .error: error]) updatedMetadata.status = Status.downloadError.rawValue updatedMetadata.sessionError = error.localizedDescription diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift index 14d7c0db..1585a7d8 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift @@ -757,7 +757,7 @@ public extension Item { ) guard renameError == nil, let renameModifiedItem else { - logger.error("Could not rename pre-trash item.", [.ocId: ocId, .name: modifiedItem.filename, .error: error]) + logger.error("Could not rename pre-trash item.", [.item: modifiedItem.filename, .error: error]) return (nil, renameError) } @@ -875,7 +875,7 @@ public extension Item { } guard contentError == nil, let contentModifiedItem else { - logger.error("Could not modify contents.", [.ocId: ocId, .name: modifiedItem.filename, .error: contentError]) + logger.error("Could not modify contents.", [.item: modifiedItem, .error: contentError]) return (nil, contentError) } diff --git a/Sources/NextcloudFileProviderKit/Log/FileProviderLog.swift b/Sources/NextcloudFileProviderKit/Log/FileProviderLog.swift index ffe115a9..80cfc17d 100644 --- a/Sources/NextcloudFileProviderKit/Log/FileProviderLog.swift +++ b/Sources/NextcloudFileProviderKit/Log/FileProviderLog.swift @@ -1,5 +1,6 @@ import FileProvider import Foundation +import NextcloudKit import os /// @@ -212,29 +213,37 @@ public actor FileProviderLog: FileProviderLogging { logger.log(level: level, "\(message, privacy: .public)") return } - - let detailsDescription = details.map { key, value in + + let sortedKeys = details.keys.sorted() + + let detailDescriptions: [String] = sortedKeys.compactMap { key in + guard let value = details[key] else { + return nil + } + let valueDescription: String? - + switch value { case let account as Account: valueDescription = account.ncKitAccount + case let lock as NKLock: + valueDescription = lock.token case let item as NSFileProviderItem: valueDescription = item.itemIdentifier.rawValue case let identifier as NSFileProviderItemIdentifier: valueDescription = identifier.rawValue - case let item as any ItemMetadata: - valueDescription = item.ocId + case let url as URL: + valueDescription = url.absoluteString case let text as String: valueDescription = text default: valueDescription = String(describing: value) } - + return "- \(key.rawValue): \(valueDescription ?? "nil")" - }.joined(separator: "\n") + } - logger.log(level: level, "\(message, privacy: .public)\n\n\(detailsDescription, privacy: .public)") + logger.log(level: level, "\(message, privacy: .public)\n\n\(detailDescriptions.joined(separator: "\n"), privacy: .public)") } public func write(category: String, level: OSLogType, message: String, details: [FileProviderLogDetailKey: Any?]) { diff --git a/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift b/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift index ef3e8b1c..2a974f0d 100644 --- a/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift +++ b/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift @@ -28,6 +28,7 @@ public enum FileProviderLogDetailKey: String { /// /// The raw value of an `NSFileProviderItemIdentifier`. + /// Also known and used as `ocId`. /// case item @@ -36,24 +37,11 @@ public enum FileProviderLogDetailKey: String { /// case lock - /// - /// A ``SendableItemMetadata`` object. - /// - /// This will automatically encode all important properties as a dictionary in the log. - /// Always prefer this over individual log detail arguments to keep the call points concise. - /// - case metadata - /// /// The name of a file or directory in the file system. /// case name - /// - /// The server-side item identifier. - /// - case ocId - /// /// The last time item metadata was synchronized with the server. /// @@ -64,3 +52,9 @@ public enum FileProviderLogDetailKey: String { /// case url } + +extension FileProviderLogDetailKey: Comparable { + public static func < (lhs: FileProviderLogDetailKey, rhs: FileProviderLogDetailKey) -> Bool { + lhs.rawValue < rhs.rawValue + } +} diff --git a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift index 691e4357..f57b5723 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift @@ -13,19 +13,19 @@ extension Array { let result: [Item] = try await self.concurrentChunkedCompactMap { (itemMetadata: SendableItemMetadata) -> Item? in guard !itemMetadata.e2eEncrypted else { - logger.info("Skipping encrypted metadata in enumeration.", [.ocId: itemMetadata.ocId, .name: itemMetadata.fileName]) + logger.info("Skipping encrypted metadata in enumeration.", [.item: itemMetadata.ocId, .name: itemMetadata.fileName]) return nil } guard !isLockFileName(itemMetadata.fileName) else { - logger.info("Skipping remote lock file item metadata in enumeration.", [.ocId: itemMetadata.ocId, .name: itemMetadata.fileName]) + logger.info("Skipping remote lock file item metadata in enumeration.", [.item: itemMetadata.ocId, .name: itemMetadata.fileName]) return nil } guard let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata( itemMetadata ) else { - logger.error("Could not get valid parentItemIdentifier for item by ocId.", [.ocId: itemMetadata.ocId, .name: itemMetadata.fileName]) + logger.error("Could not get valid parentItemIdentifier for item by ocId.", [.item: itemMetadata.ocId, .name: itemMetadata.fileName]) let targetUrl = itemMetadata.serverUrl throw FilesDatabaseManager.parentMetadataNotFoundError(itemUrl: targetUrl) } @@ -40,7 +40,7 @@ extension Array { log: log ) - logger.debug("Will enumerate item.", [.ocId: itemMetadata.ocId, .name: itemMetadata.fileName]) + logger.debug("Will enumerate item.", [.item: itemMetadata.ocId, .name: itemMetadata.fileName]) return item } From 0b10d9d5469d86dd7133f9d059438c1b8bdf66ef Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 13:51:48 +0200 Subject: [PATCH 02/15] fix: Moved isLockFileOfLocalOrigin. - It was specific to the SendableItemMetadata implementation before. - It actually belongs into the common requirements definition of ItemMetadata. Signed-off-by: Iva Horn --- .../NextcloudFileProviderKit/Metadata/ItemMetadata.swift | 5 +++++ .../Metadata/SendableItemMetadata.swift | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift index 4726a5aa..d9566cad 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift @@ -51,7 +51,12 @@ public protocol ItemMetadata: Equatable { var hidden: Bool { get set } var iconName: String { get set } var iconUrl: String { get set } + + /// + /// This is a lock file which was created on the local device and not introduced through synchronization with the server. + /// var isLockFileOfLocalOrigin: Bool { get set } + var mountType: String { get set } var name: String { get set } // for unifiedSearch is the provider.id var note: String { get set } diff --git a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift index 2fa69d0c..1a895160 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift @@ -35,12 +35,7 @@ public struct SendableItemMetadata: ItemMetadata, Sendable { public var hidden: Bool public var iconName: String public var iconUrl: String - - /// - /// This is a lock file which was created on the local device and not introduced through synchronization with the server. - /// public var isLockFileOfLocalOrigin: Bool - public var mountType: String public var name: String public var note: String @@ -189,7 +184,6 @@ public struct SendableItemMetadata: ItemMetadata, Sendable { self.lockOwnerDisplayName = lockOwnerDisplayName self.lockTime = lockTime self.lockTimeOut = lockTimeOut - self.lockToken = lockToken self.path = path self.permissions = permissions self.quotaUsedBytes = quotaUsedBytes From b7540928bf27aa24a5f7bf9faf42038ac3a3afef Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 13:54:57 +0200 Subject: [PATCH 03/15] fix: Retaining the lock token during synchronization. - Lock tokens are available only once during the LOCK request. - During every follow up request like PROPFIND, they are not returned for any item. - The items in the client database are updated atomically with the remote state. - The remote state does not provide the lock token. - So, during any metadata update after locking an item, the locally stored lock token is lost because it is overwritten with the new empty value retrieved from the server. Signed-off-by: Iva Horn --- .../Enumeration/Enumerator+SyncEngine.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift index c16bcc3a..08cab138 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift @@ -192,6 +192,7 @@ extension Enumerator { let updatedItems: [SendableItemMetadata] = isNew ? [] : [metadata] metadata.downloaded = existing?.downloaded == true metadata.keepDownloaded = existing?.keepDownloaded == true + metadata.lockToken = existing?.lockToken dbManager.addItemMetadata(metadata) return ([metadata], newItems, updatedItems, nil, nextPage, nil) } From ea4329ab6755e7d9e3fefa902e7848167976bed6 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 13:55:44 +0200 Subject: [PATCH 04/15] fix: Adapting to the changes of c0ee0c3 in NextcloudKit. Signed-off-by: Iva Horn --- .../Extensions/NKFile+Extensions.swift | 15 +++++++-------- .../Log/FileProviderLogDetail.swift | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift b/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift index 569120df..4002e147 100644 --- a/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift +++ b/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift @@ -62,14 +62,13 @@ extension NKFile { note: note, ownerId: ownerId, ownerDisplayName: ownerDisplayName, - lock: lock != nil ? true : false, - lockOwner: lock?.owner, - lockOwnerEditor: lock?.ownerEditor, - lockOwnerType: lock?.ownerType.rawValue, - lockOwnerDisplayName: lock?.ownerDisplayName, - lockTime: lock?.time, - lockTimeOut: lock?.timeOut, - lockToken: lock?.token, + lock: lock, + lockOwner: lockOwner, + lockOwnerEditor: lockOwnerEditor, + lockOwnerType: lockOwnerType, + lockOwnerDisplayName: lockOwnerDisplayName, + lockTime: lockTime, + lockTimeOut: lockTimeOut, path: path, permissions: permissions, quotaUsedBytes: quotaUsedBytes, diff --git a/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetail.swift b/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetail.swift index f8876670..eed6930b 100644 --- a/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetail.swift +++ b/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetail.swift @@ -135,7 +135,7 @@ public enum FileProviderLogDetail: Encodable { self = .dictionary([ "owner": .string(lock.owner), "ownerDisplayName": .string(lock.ownerDisplayName), - "ownerEditor": .string(lock.ownerEditor), + "ownerEditor": lock.ownerEditor == nil ? .null : .string(lock.ownerEditor!), "ownerType": .int(lock.ownerType.rawValue), "time": lock.time == nil ? .null : .date(lock.time!), "timeOut": lock.timeOut == nil ? .null : .date(lock.timeOut!), From 078595b6cba44a468e8945fb401a90c9024dec6f Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 13:57:00 +0200 Subject: [PATCH 05/15] fix: Maintaining associated lock data. - When a file is either locked or unlocked because of a lock file, its metadata must be updated accordingly. Signed-off-by: Iva Horn --- .../Item/Item+LockFile.swift | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift b/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift index 39f839bb..50703c0d 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift @@ -87,10 +87,10 @@ extension Item { return (nil, NSFileProviderError(.cannotSynchronize)) } } - + logger.debug("Derived target file name for lock file.", [.name: targetFileName]) let targetFileRemotePath = parentItemRemotePath + "/" + targetFileName - + let metadata = SendableItemMetadata( ocId: itemTemplate.itemIdentifier.rawValue, account: account.ncKitAccount, @@ -136,7 +136,22 @@ extension Item { }) if let lock { - logger.info("Locked file and received lock.", [.name: targetFileName, .lock: lock]) + logger.info("Locked file and received lock, will update target item.", [.name: targetFileName, .lock: lock]) + + if let targetMetadata = dbManager.itemMetadatas.where({ $0.fileName.equals(targetFileName) }).where({ $0.serverUrl.equals(parentItemRemotePath) }).first { + try dbManager.ncDatabase().write { + targetMetadata.lock = true + targetMetadata.lockOwner = lock.owner + targetMetadata.lockOwnerDisplayName = lock.ownerDisplayName + targetMetadata.lockOwnerEditor = lock.ownerEditor + targetMetadata.lockOwnerType = lock.ownerType.rawValue + targetMetadata.lockTime = lock.time + targetMetadata.lockTimeOut = lock.timeOut + targetMetadata.lockToken = lock.token + } + } else { + logger.error("Failed to find target item for acquired lock.", [.lock: lock]) + } } else { logger.info("Locked file but did not receive lock information.", [.name: targetFileName]) } @@ -269,6 +284,22 @@ extension Item { logger.info("Unlocked file but did not receive lock information.", [.name: originalFileName]) } + logger.info("Removing lock from locally stored target item.", [.name: originalFileName]) + + if let targetMetadata = dbManager.itemMetadatas.where({ $0.fileName.equals(originalFileName) }).where({ $0.serverUrl.equals(metadata.serverUrl) }).first { + try dbManager.ncDatabase().write { + targetMetadata.lock = false + targetMetadata.lockOwner = nil + targetMetadata.lockOwnerDisplayName = nil + targetMetadata.lockOwnerEditor = nil + targetMetadata.lockOwnerType = nil + targetMetadata.lockTime = nil + targetMetadata.lockTimeOut = nil + targetMetadata.lockToken = nil + } + } else { + logger.error("Failed to find target item for released lock.", [.lock: lock]) + } } catch { logger.error("Could not unlock item.", [.name: self.filename, .error: error]) From 86e838a4f7c6e86c6396da7a5cef5ab5494c1feb Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 13:58:22 +0200 Subject: [PATCH 06/15] fix: Refactored logging calls in Item+Modify.swift Signed-off-by: Iva Horn --- .../Item/Item+Modify.swift | 161 +++--------------- 1 file changed, 28 insertions(+), 133 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift index 1585a7d8..40859109 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift @@ -104,22 +104,12 @@ public extension Item { let ocId = itemIdentifier.rawValue guard let newContents else { - logger.error( - """ - ERROR. Could not upload modified contents as was provided nil contents url. - ocId: \(ocId) filename: \(self.filename) - """ - ) - return ( - nil, - NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier) - ) + logger.error("Cannot upload modified content because a nil URL was provided.", [.item: itemIdentifier]) + return (nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier)) } guard var metadata = dbManager.itemMetadata(ocId: ocId) else { - logger.error( - "Could not acquire metadata of item with identifier: \(ocId)" - ) + logger.error("Could not acquire metadata of item.", [.item: itemIdentifier]) return ( nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier) @@ -127,16 +117,8 @@ public extension Item { } guard let updatedMetadata = dbManager.setStatusForItemMetadata(metadata, status: .uploading) else { - logger.info( - """ - Could not acquire updated metadata of item: \(ocId), - unable to update item status to uploading - """ - ) - return ( - nil, - NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier) - ) + logger.info("Could not acquire updated metadata of item. Unable to update item status to uploading.", [.item: itemIdentifier]) + return (nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier)) } let (_, _, etag, date, size, error) = await upload( @@ -565,17 +547,11 @@ public extension Item { ) } - let relativePath = ( - metadata.serverUrl + "/" + metadata.fileName - ).replacingOccurrences(of: account.davFilesUrl, with: "") + let relativePath = (metadata.serverUrl + "/" + metadata.fileName).replacingOccurrences(of: account.davFilesUrl, with: "") guard ignoredFiles == nil || ignoredFiles?.isExcluded(relativePath) == false else { - logger.info( - """ - File \(modifiedItem.filename) is in the ignore list. - Will delete locally with no remote effect. - """ - ) + logger.info("File is in the ignore list. Will delete locally with no remote effect.", [.item: modifiedItem.itemIdentifier, .name: modifiedItem.filename]) + guard let modifiedIgnored = await modifyUnuploaded( itemTarget: itemTarget, baseVersion: baseVersion, @@ -589,12 +565,12 @@ public extension Item { progress: progress, dbManager: dbManager ) else { - logger.error( - "Unable to modify ignored file, got nil item: \(relativePath)" - ) + logger.error("Unable to modify ignored file, got nil item: \(relativePath)") return (nil, NSFileProviderError(.cannotSynchronize)) } + modifiedItem = modifiedIgnored + if #available(macOS 13.0, *) { return (modifiedItem, NSFileProviderError(.excludedFromSync)) } else { @@ -620,32 +596,19 @@ public extension Item { ) } - let ocId = modifiedItem.itemIdentifier.rawValue guard itemTarget.itemIdentifier == modifiedItem.itemIdentifier else { - logger.error( - """ - Could not modify item: \(ocId), different identifier to the - item the modification was targeting - (\(itemTarget.itemIdentifier.rawValue)) - """ - ) - return ( - nil, - NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier) - ) + logger.error("Could not modify item, different identifier to the item the modification was targeting (\(itemTarget.itemIdentifier.rawValue)).", [.item: modifiedItem]) + + return (nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier)) } let newParentItemIdentifier = itemTarget.parentItemIdentifier let isFolder = modifiedItem.contentType.conforms(to: .directory) - let bundleOrPackage = - modifiedItem.contentType.conforms(to: .bundle) || - modifiedItem.contentType.conforms(to: .package) + let bundleOrPackage = modifiedItem.contentType.conforms(to: .bundle) || modifiedItem.contentType.conforms(to: .package) if options.contains(.mayAlreadyExist) { // TODO: This needs to be properly handled with a check in the db - logger.info( - "Modification for item: \(ocId) may already exist" - ) + logger.info("Modification for item may already exist.", [.item: modifiedItem]) } var newParentItemRemoteUrl: String @@ -658,16 +621,8 @@ public extension Item { } else if newParentItemIdentifier == .trashContainer { newParentItemRemoteUrl = account.trashUrl } else { - guard let parentItemMetadata = dbManager.directoryMetadata( - ocId: newParentItemIdentifier.rawValue - ) else { - logger.error( - """ - Not modifying item: \(ocId), - could not find metadata for target parentItemIdentifier - \(newParentItemIdentifier.rawValue) - """ - ) + guard let parentItemMetadata = dbManager.directoryMetadata(ocId: newParentItemIdentifier.rawValue) else { + logger.error("Not modifying item, could not find metadata for target parentItemIdentifier \"\(newParentItemIdentifier.rawValue)\"!", [.item: modifiedItem]) return ( nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier) @@ -679,66 +634,29 @@ public extension Item { let newServerUrlFileName = newParentItemRemoteUrl + "/" + itemTarget.filename - logger.debug("About to modify item.", [.ocId: ocId]) - - logger.debug( - """ - About to modify item with identifier: \(ocId) - of type: \(modifiedItem.contentType.identifier) - (is folder: \(isFolder ? "yes" : "no") - with filename: \(modifiedItem.filename) - to filename: \(itemTarget.filename) - from old server url: - \(modifiedItem.metadata.serverUrl + "/" + modifiedItem.metadata.fileName) - to server url: \(newServerUrlFileName) - old parent identifier: \(modifiedItem.parentItemIdentifier.rawValue) - new parent identifier: \(itemTarget.parentItemIdentifier.rawValue) - with contents located at: \(newContents?.path ?? "") - """ - ) + logger.debug("About to modify item.", [.item: modifiedItem]) if changedFields.contains(.parentItemIdentifier) && newParentItemIdentifier == .trashContainer && modifiedItem.metadata.isTrashed { if (changedFields.contains(.filename)) { - logger.info( - """ - Tried to modify filename of already trashed item. This is not supported. - ocId: \(modifiedItem.itemIdentifier.rawValue) - filename: \(modifiedItem.metadata.fileName) - new filename: \(itemTarget.filename) - """ - ) + logger.error("Tried to modify filename of already trashed item. This is not supported.", [.item: modifiedItem]) } - logger.info( - """ - Tried to trash item that is in fact already trashed. - ocId: \(modifiedItem.itemIdentifier.rawValue) - filename: \(modifiedItem.metadata.fileName) - serverUrl: \(modifiedItem.metadata.serverUrl) - """ - ) + logger.info("Tried to trash item that is in fact already trashed.", [.item: modifiedItem]) + return (modifiedItem, nil) } else if changedFields.contains(.parentItemIdentifier) && newParentItemIdentifier == .trashContainer { let (_, capabilities, _, error) = await remoteInterface.currentCapabilities( account: account, options: .init(), taskHandler: { _ in } ) guard let capabilities, error == .success else { - logger.error( - """ - Could not acquire capabilities during item move to trash, won't proceed. - Error: \(error.errorDescription) - Item: \(modifiedItem.filename) - """ - ) + logger.error("Could not acquire capabilities during item move to trash, won't proceed.", [.item: modifiedItem, .error: error]) return (nil, error.fileProviderError) } guard capabilities.files?.undelete == true else { - logger.error( - "Cannot delete \(modifiedItem.filename) as server does not support trashing." - ) + logger.error("Cannot delete item as server does not support trashing.", [.item: modifiedItem]) return (nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)) } @@ -812,35 +730,17 @@ public extension Item { } guard !isFolder || bundleOrPackage else { - logger.debug( - """ - System requested modification for folder with ocID \(ocId) - (\(newServerUrlFileName)) of something other than folder name. - This is not supported. - """ - ) + logger.debug("System requested modification for folder of something other than folder name. This is not supported.", [.item: modifiedItem]) return (modifiedItem, nil) } guard newParentItemIdentifier != .trashContainer else { - logger.debug( - """ - System requested modification in trash for item with ocID \(ocId) - (\(newServerUrlFileName)) - This is not supported. - """ - ) + logger.debug("System requested modification of item in trash. This is not supported.", [.item: modifiedItem]) return (modifiedItem, nil) } if changedFields.contains(.contents) { - logger.debug( - """ - Item modification for \(ocId) - \(modifiedItem.filename) - includes contents. Will begin upload. - """ - ) + logger.debug("Item content modified.", [.item: modifiedItem]) let newCreationDate = itemTarget.creationDate ?? creationDate let newContentModificationDate = @@ -882,12 +782,7 @@ public extension Item { modifiedItem = contentModifiedItem } - logger.debug( - """ - Nothing more to do with \(ocId) - \(modifiedItem.filename), modifications complete - """ - ) + logger.debug("All modifications processed.", [.item: modifiedItem]) return (modifiedItem, nil) } } From a80723ba17317cf91bbc5bcff48366bbf7b1cb7b Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 16:10:40 +0200 Subject: [PATCH 07/15] fix: Conditionally adding HTTP header for token lock. Signed-off-by: Iva Horn --- .../NextcloudFileProviderKit/Item/Item+Modify.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift index 40859109..20d1582a 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift @@ -120,7 +120,15 @@ public extension Item { logger.info("Could not acquire updated metadata of item. Unable to update item status to uploading.", [.item: itemIdentifier]) return (nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier)) } - + + var headers = [String: String]() + + if let token = metadata.lockToken { + headers["If"] = "<\(remotePath)> ()" + } + + let options = NKRequestOptions(customHeader: headers, queue: .global(qos: .utility)) + let (_, _, etag, date, size, error) = await upload( fileLocatedAt: newContents.path, toRemotePath: remotePath, @@ -131,6 +139,7 @@ public extension Item { dbManager: dbManager, creationDate: newCreationDate, modificationDate: newContentModificationDate, + options: options, log: logger.log, requestHandler: { progress.setHandlersFromAfRequest($0) }, taskHandler: { task in From dcfe0a5e186982305ec1376155cf42cdf21c1eda Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 9 Oct 2025 16:11:09 +0200 Subject: [PATCH 08/15] fix: Capabilities and file system flags taking token lock into account. Signed-off-by: Iva Horn --- .../NextcloudFileProviderKit/Item/Item.swift | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Item/Item.swift b/Sources/NextcloudFileProviderKit/Item/Item.swift index 7cdc81b6..456a03ea 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item.swift @@ -36,7 +36,7 @@ public class Item: NSObject, NSFileProviderItem { capabilities.insert(.allowsReading) } - if !metadata.lock { + if metadata.lock == false || (metadata.lock == true && metadata.lockOwnerType == NKLockType.token.rawValue && metadata.ownerId == metadata.lockOwner && metadata.lockToken != nil) { if permissions.contains("D") { // Deletable capabilities.insert(.allowsDeleting) } @@ -57,6 +57,7 @@ public class Item: NSObject, NSFileProviderItem { capabilities.insert(.allowsWriting) } } + // .allowsEvicting deprecated on macOS 13.0+, use contentPolicy instead if #unavailable(macOS 13.0), !metadata.keepDownloaded { capabilities.insert(.allowsEvicting) @@ -171,13 +172,24 @@ public class Item: NSObject, NSFileProviderItem { } public var fileSystemFlags: NSFileProviderFileSystemFlags { - if metadata.lock, - (metadata.lockOwnerType != 0 || metadata.lockOwner != account.username), - metadata.lockTimeOut ?? Date() > Date() - { - return [.userReadable] + if metadata.isLockFileOfLocalOrigin { + return [ + .hidden, + .userReadable, + .userWritable + ] } - return [.userReadable, .userWritable] + + if metadata.lock, (metadata.lockOwnerType != NKLockType.user.rawValue || metadata.lockOwner != account.username), metadata.lockTimeOut ?? Date() > Date() { + return [ + .userReadable + ] + } + + return [ + .userReadable, + .userWritable + ] } public var userInfo: [AnyHashable : Any]? { From 849f1108bce2ca21ec34aa800308be7ada832180 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Fri, 10 Oct 2025 14:26:48 +0200 Subject: [PATCH 09/15] fix: Refactored logging calls. Signed-off-by: Iva Horn --- .../Enumeration/Enumerator.swift | 99 +++++-------------- .../MaterialisedEnumerationObserver.swift | 6 +- .../Enumeration/RemoteChangeObserver.swift | 4 +- .../Item/Item+Fetch.swift | 93 ++++------------- 4 files changed, 43 insertions(+), 159 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift index f29b2f8e..2135e823 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift @@ -13,8 +13,7 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { let domain: NSFileProviderDomain? let dbManager: FilesDatabaseManager - private let currentAnchor = - NSFileProviderSyncAnchor(ISO8601DateFormatter().string(from: Date()).data(using: .utf8)!) + private let currentAnchor = NSFileProviderSyncAnchor(ISO8601DateFormatter().string(from: Date()).data(using: .utf8)!) private let pageItemCount: Int let logger: FileProviderLogger let account: Account @@ -235,17 +234,9 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { } } - public func enumerateChanges( - for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor - ) { - logger.debug( - """ - Received enumerate changes request for enumerator - for user: \(self.account.ncKitAccount) - with serverUrl: \(self.serverUrl) - with sync anchor: \(String(data: anchor.rawValue, encoding: .utf8) ?? "") - """ - ) + public func enumerateChanges(for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor) { + logger.debug("Enumerating changes (anchor: \(String(data: anchor.rawValue, encoding: .utf8) ?? "")).", [.url: self.serverUrl]) + /* - query the server for updates since the passed-in sync anchor (TODO) @@ -257,7 +248,7 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { */ if enumeratedItemIdentifier == .workingSet { - logger.debug("Enumerating working set changes.", [.account: self.account]) + logger.debug("Enumerating changes in working set.", [.account: self.account]) let formatter = ISO8601DateFormatter() @@ -292,11 +283,7 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { account: account, options: .init(), taskHandler: { _ in } ) guard let capabilities, error == .success else { - logger.error( - """ - Could not acquire capabilities, cannot check trash. - Error: \(error.errorDescription) - """) + logger.error("Could not acquire capabilities, cannot check trash.", [.error: error]) observer.finishEnumeratingWithError(NSFileProviderError(.serverUnreachable)) return } @@ -341,15 +328,11 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { log: logger.log ) } + return } - logger.info( - """ - Enumerating changes for user: \(self.account.ncKitAccount) - with serverUrl: \(self.serverUrl) - """ - ) + logger.info("Enumerating changes in item.", [.url: self.serverUrl]) // No matter what happens here we finish enumeration in some way, either from the error // handling below or from the completeChangesObserver @@ -372,51 +355,24 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { } guard readError == nil else { - logger.error( - """ - Finishing enumeration of changes for: \(self.account.ncKitAccount) - with serverUrl: \(self.serverUrl) - with error: \(readError!.errorDescription) - """ - ) + logger.error("Finished enumerating changes.", [.url: self.serverUrl, .error: readError]) - let error = readError?.fileProviderError( - handlingNoSuchItemErrorUsingItemIdentifier: self.enumeratedItemIdentifier - ) ?? NSFileProviderError(.cannotSynchronize) + let error = readError?.fileProviderError(handlingNoSuchItemErrorUsingItemIdentifier: self.enumeratedItemIdentifier) ?? NSFileProviderError(.cannotSynchronize) if readError!.isNotFoundError { - logger.info( - """ - 404 error means item no longer exists. - Deleting metadata and reporting \(self.serverUrl) - as deletion without error - """ - ) + logger.info("404 error means item no longer exists. Deleting metadata and reporting deletion without error.", [.url: self.serverUrl]) guard let itemMetadata = self.enumeratedItemMetadata else { - logger.error( - """ - Invalid enumeratedItemMetadata. - Could not delete metadata nor report deletion. - """ - ) + logger.error("Invalid enumeratedItemMetadata. Could not delete metadata nor report deletion.") observer.finishEnumeratingWithError(error) return } if itemMetadata.directory { - if let deletedDirectoryMetadatas = - dbManager.deleteDirectoryAndSubdirectoriesMetadata( - ocId: itemMetadata.ocId) - { + if let deletedDirectoryMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: itemMetadata.ocId) { currentDeletedMetadatas += deletedDirectoryMetadatas } else { - logger.error( - """ - Something went wrong when recursively deleting directory. - It's metadata was not found. Cannot report it as deleted. - """ - ) + logger.error("Something went wrong when recursively deleting directory. It's metadata was not found. Cannot report it as deleted.") } } else { dbManager.deleteItemMetadata(ocId: itemMetadata.ocId) @@ -485,8 +441,7 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { Task { @MainActor in observer.didEnumerate(items) - logger.info("Did enumerate \(items.count) items.") - logger.info("Next page is nil: \(nextPage == nil)") + logger.info("Did enumerate \(items.count) items. Next page is nil: \(nextPage == nil)") if let nextPage, let nextPageData = try? JSONEncoder().encode(nextPage) { logger.info( @@ -538,13 +493,8 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { handleInvalidParent: Bool = true ) { guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else { - logger.error( - """ - Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. - Finished enumeration of changes with error. - """ - ) - + logger.error("Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. Finished enumeration of changes with error.") + observer.finishEnumeratingWithError( NSError.fileProviderErrorForNonExistentItem( withIdentifier: enumeratedItemIdentifier @@ -587,13 +537,8 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { observer.didUpdate(updatedItems) } - logger.info( - """ - Processed \(updatedItems.count) new or updated metadatas. - \(allDeletedMetadatas.count) deleted metadatas. - """ - ) - + logger.info("Processed \(updatedItems.count) new or updated metadatas. \(allDeletedMetadatas.count) deleted metadatas.") + observer.finishEnumeratingChanges(upTo: anchor, moreComing: false) } } catch let error as NSError { // This error can only mean a missing parent item identifier @@ -649,9 +594,8 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { throw NSError() } - logger.info( - "Recovering from invalid parent identifier at \(urlToEnumerate)" - ) + logger.info("Recovering from invalid parent identifier at \(urlToEnumerate)") + let (metadatas, _, _, _, _, error) = await Enumerator.readServerUrl( urlToEnumerate, account: account, @@ -668,6 +612,7 @@ public class Enumerator: NSObject, NSFileProviderEnumerator { Metadatas: \(metadatas?.count ?? -1) """ ) + throw error?.fileProviderError ?? NSFileProviderError(.cannotSynchronize) } // Provide it to the caller method so it can ingest it into the database and fix future errs diff --git a/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift b/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift index 05b75635..51af7a36 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/MaterialisedEnumerationObserver.swift @@ -90,18 +90,18 @@ public class MaterialisedEnumerationObserver: NSObject, NSFileProviderEnumeratio metadata.downloaded = true } - logger.info("Updating materialisation state for item to MATERIALISED with id \(enumeratedId) with filename \(metadata.fileName)", [.item: enumeratedId, .name: metadata.fileName]) + logger.info("Updating state for item to materialized.", [.item: enumeratedId, .name: metadata.fileName]) dbManager.addItemMetadata(metadata) } } for unmaterialisedId in unmaterialisedIds { guard var metadata = materialisedMetadatasMap[unmaterialisedId] else { - logger.error("No materialised for \(unmaterialisedId) found.", [.item: unmaterialisedId]) + logger.error("No materialized found.", [.item: unmaterialisedId]) continue } - logger.info("Updating materialisation state for item to DATALESS with id \(unmaterialisedId) with filename \(metadata.fileName).", [.name: metadata.fileName, .item: unmaterialisedId]) + logger.info("Updating state for item to dataless.", [.name: metadata.fileName, .item: unmaterialisedId]) metadata.downloaded = false metadata.visitedDirectory = false diff --git a/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift b/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift index 0f5e59a1..e02b249e 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift @@ -591,9 +591,7 @@ final public class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess } _ = await task.result - logger.info("Finished change checking of working set for user: \(self.accountId)") - logger.debug("Examined item ids: \(examinedItemIds)") - logger.debug("Materialised item ids: \(materialisedItems.map(\.ocId))") + logger.info("Finished change enumeration of working set. Examined item IDs: \(examinedItemIds), materialized item IDs: \(materialisedItems.map(\.ocId))") if allUpdatedMetadatas.isEmpty, allDeletedMetadatas.isEmpty { logger.info("No changes found.") diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift b/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift index 89771baf..4fd1fe67 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift @@ -32,28 +32,15 @@ public extension Item { ) if let readError, readError != .success { - logger.error( - """ - Could not enumerate directory contents for - \(self.metadata.fileName) - at \(remoteDirectoryPath) - error: \(readError.errorCode) - \(readError.errorDescription) - """ - ) + logger.error("Could not enumerate directory contents.", [.name: self.metadata.fileName, .url: remoteDirectoryPath, .error: readError]) + throw readError.fileProviderError( handlingNoSuchItemErrorUsingItemIdentifier: itemIdentifier ) ?? NSFileProviderError(.cannotSynchronize) } guard var metadatas else { - logger.error( - """ - Could not fetch directory contents for - \(self.metadata.fileName) - at \(remoteDirectoryPath), received nil metadatas - """ - ) + logger.error("Could not fetch directory contents.", [.name: self.metadata.fileName, .url: remoteDirectoryPath]) throw NSFileProviderError(.cannotSynchronize) } @@ -96,14 +83,7 @@ public extension Item { ) guard error == .success else { - logger.error( - """ - Could not acquire contents of item: \(metadata.fileName) - at \(remotePath) - error: \(error.errorCode) - \(error.errorDescription) - """ - ) + logger.error("Could not acquire contents of item.", [.name: metadata.fileName, .url: remotePath, .error: error]) metadata.status = Status.downloadError.rawValue metadata.sessionError = error.errorDescription dbManager.addItemMetadata(metadata) @@ -136,40 +116,27 @@ public extension Item { ) async -> (URL?, Item?, Error?) { let ocId = itemIdentifier.rawValue guard metadata.classFile != "lock", !isLockFileName(filename) else { - logger.info( - """ - System requested fetch of lock file \(self.filename) - will just provide local contents URL if possible. - """ - ) + logger.info("System requested fetch of lock file, will just provide local contents URL if possible.", [.name: self.filename]) + if let domain, let localUrl = await localUrlForContents(domain: domain) { return (localUrl, self, nil) } else if #available(macOS 13.0, *) { - logger.error("Could not get local contents URL for lock file, erroring") + logger.error("Could not get local contents URL for lock file, erroring.") return (nil, self, NSFileProviderError(.excludedFromSync)) } else { - logger.error("Could not get local contents URL for lock file, nilling") + logger.error("Could not get local contents URL for lock file, nilling.") return (nil, self, nil) } } let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName - logger.debug( - """ - Fetching item with name \(self.metadata.fileName) - at URL: \(serverUrlFileName) - """ - ) + logger.debug("Fetching item.", [.name: self.metadata.fileName, .url: serverUrlFileName]) let localPath = FileManager.default.temporaryDirectory.appendingPathComponent(metadata.ocId) guard var updatedMetadata = dbManager.setStatusForItemMetadata(metadata, status: .downloading) else { - logger.error( - """ - Could not acquire updated metadata of item \(ocId), - unable to update item status to downloading - """ - ) + logger.error("Could not acquire updated metadata, unable to update item status to downloading.", [.item: itemIdentifier]) + return ( nil, nil, @@ -179,13 +146,7 @@ public extension Item { let isDirectory = contentType.conforms(to: .directory) if isDirectory { - logger.debug( - """ - Item with identifier: \(ocId) - and filename: \(updatedMetadata.fileName) - is a directory, creating dir locally and fetching its contents - """ - ) + logger.debug("is a directory, creating directory locally and fetching its contents.", [.item: ocId, .name: updatedMetadata.fileName]) do { try FileManager.default.createDirectory( @@ -231,15 +192,7 @@ public extension Item { ) if error != .success { - logger.error( - """ - Could not acquire contents of item with identifier: \(ocId) - and fileName: \(updatedMetadata.fileName) - at \(serverUrlFileName) - error: \(error.errorCode) - \(error.errorDescription) - """ - ) + logger.error("Could not acquire contents of item.", [.item: ocId, .name: updatedMetadata.fileName, .error: error]) updatedMetadata.status = Status.downloadError.rawValue updatedMetadata.sessionError = error.errorDescription @@ -250,12 +203,7 @@ public extension Item { } } - logger.debug( - """ - Acquired contents of item with identifier: \(ocId) - and filename: \(updatedMetadata.fileName) - """ - ) + logger.debug("Acquired contents of item.", [.item: ocId, .name: updatedMetadata.fileName]) updatedMetadata.status = Status.normal.rawValue updatedMetadata.downloaded = true @@ -272,16 +220,9 @@ public extension Item { remoteInterface: remoteInterface, account: account ) else { - logger.error( - """ - Could not find parent item id for file \(self.metadata.fileName) - """ - ) - return ( - nil, - nil, - NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier) - ) + logger.error("Could not find parent item id for file.", [.name: self.metadata.fileName]) + + return (nil, nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: self.itemIdentifier)) } let fpItem = Item( From afc0bd758d27d8e06b8e5af6708f18a7d5bdcc71 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Fri, 10 Oct 2025 15:35:03 +0200 Subject: [PATCH 10/15] fix: Code readability improvements. Signed-off-by: Iva Horn --- .../Database/FilesDatabaseManager.swift | 19 ++++--------------- .../Enumeration/Enumerator+SyncEngine.swift | 6 +++--- .../Extensions/NKFile+Extensions.swift | 1 + 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift index b1a69b21..a759798f 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift @@ -273,26 +273,14 @@ public final class FilesDatabaseManager: Sendable { return deletedMetadatas } - private func processItemMetadatasToUpdate( - existingMetadatas: Results, - updatedMetadatas: [SendableItemMetadata], - keepExistingDownloadState: Bool - ) -> ( - newMetadatas: [SendableItemMetadata], - updatedMetadatas: [SendableItemMetadata], - directoriesNeedingRename: [SendableItemMetadata] - ) { + private func processItemMetadatasToUpdate(existingMetadatas: Results, updatedMetadatas: [SendableItemMetadata], keepExistingDownloadState: Bool) -> (newMetadatas: [SendableItemMetadata], updatedMetadatas: [SendableItemMetadata], directoriesNeedingRename: [SendableItemMetadata]) { var returningNewMetadatas: [SendableItemMetadata] = [] var returningUpdatedMetadatas: [SendableItemMetadata] = [] var directoriesNeedingRename: [SendableItemMetadata] = [] for var updatedMetadata in updatedMetadatas { - if let existingMetadata = existingMetadatas.first(where: { - $0.ocId == updatedMetadata.ocId - }) { - if existingMetadata.status == Status.normal.rawValue, - !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) - { + if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) { + if existingMetadata.status == Status.normal.rawValue, !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) { if updatedMetadata.directory, updatedMetadata.serverUrl != existingMetadata.serverUrl || updatedMetadata.fileName != existingMetadata.fileName @@ -303,6 +291,7 @@ public final class FilesDatabaseManager: Sendable { if keepExistingDownloadState { updatedMetadata.downloaded = existingMetadata.downloaded } + updatedMetadata.visitedDirectory = existingMetadata.visitedDirectory updatedMetadata.keepDownloaded = existingMetadata.keepDownloaded diff --git a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift index 08cab138..25658611 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift @@ -56,19 +56,19 @@ extension Enumerator { return (metadatas, nil, nil, nil, error) } - guard var (directoryMetadata, _, metadatas) = - await files.toDirectoryReadMetadatas(account: account) - else { + guard var (directoryMetadata, _, metadatas) = await files.toDirectoryReadMetadatas(account: account) else { logger.error("Could not convert NKFiles to DirectoryReadMetadatas!") return (nil, nil, nil, nil, .invalidData) } // STORE DATA FOR CURRENTLY SCANNED DIRECTORY assert(directoryMetadata.directory) + if let existingMetadata = dbManager.itemMetadata(ocId: directoryMetadata.ocId) { directoryMetadata.downloaded = existingMetadata.downloaded directoryMetadata.keepDownloaded = existingMetadata.keepDownloaded } + directoryMetadata.visitedDirectory = true metadatas.insert(directoryMetadata, at: 0) diff --git a/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift b/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift index 4002e147..5588fa68 100644 --- a/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift +++ b/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift @@ -69,6 +69,7 @@ extension NKFile { lockOwnerDisplayName: lockOwnerDisplayName, lockTime: lockTime, lockTimeOut: lockTimeOut, + lockToken: nil, // This is not available at this point and must be fetched from the local persistence later. path: path, permissions: permissions, quotaUsedBytes: quotaUsedBytes, From 3defc44f18a90d062a671154a35796b9bf9027d3 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Fri, 10 Oct 2025 15:35:25 +0200 Subject: [PATCH 11/15] fix: Retain lock token during metadata updates from remote. Signed-off-by: Iva Horn --- .../Database/FilesDatabaseManager.swift | 1 + .../Enumeration/Enumerator+SyncEngine.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift index a759798f..1f3259ed 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift @@ -294,6 +294,7 @@ public final class FilesDatabaseManager: Sendable { updatedMetadata.visitedDirectory = existingMetadata.visitedDirectory updatedMetadata.keepDownloaded = existingMetadata.keepDownloaded + updatedMetadata.lockToken = existingMetadata.lockToken returningUpdatedMetadatas.append(updatedMetadata) diff --git a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift index 25658611..81104751 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift @@ -189,10 +189,10 @@ extension Enumerator { let existing = dbManager.itemMetadata(ocId: metadata.ocId) let isNew = existing == nil let newItems: [SendableItemMetadata] = isNew ? [metadata] : [] + metadata.lockToken = existing?.lockToken let updatedItems: [SendableItemMetadata] = isNew ? [] : [metadata] metadata.downloaded = existing?.downloaded == true metadata.keepDownloaded = existing?.keepDownloaded == true - metadata.lockToken = existing?.lockToken dbManager.addItemMetadata(metadata) return ([metadata], newItems, updatedItems, nil, nextPage, nil) } From 98d6a065fb86bc9a29c30b1c06e8916ea9af913a Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Fri, 10 Oct 2025 15:35:46 +0200 Subject: [PATCH 12/15] fix: Accept lock file modifications without returning a semantically incorrect error. Signed-off-by: Iva Horn --- Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift b/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift index 50703c0d..733eb442 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift @@ -237,13 +237,7 @@ extension Item { ) } - var returnError: Error? = nil - - if #available(macOS 13.0, *) { - returnError = NSFileProviderError(.excludedFromSync) - } - - return (modifiedItem, returnError) + return (modifiedItem, nil) } func deleteLockFile(domain: NSFileProviderDomain? = nil, dbManager: FilesDatabaseManager) async -> Error? { From 7b26ae40d1398d3c449acbc08ff4503e154cb717 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Mon, 13 Oct 2025 09:11:01 +0200 Subject: [PATCH 13/15] fix: Unit tests. Signed-off-by: Iva Horn --- .../Enumeration/Enumerator+SyncEngine.swift | 9 ++++--- Tests/Interface/MockRemoteItem.swift | 26 +++---------------- .../ItemModifyTests.swift | 6 +---- ...eChangeObserverEtagOptimizationTests.swift | 3 +-- 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift index 81104751..8a9f6c88 100644 --- a/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift +++ b/Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift @@ -62,13 +62,16 @@ extension Enumerator { } // STORE DATA FOR CURRENTLY SCANNED DIRECTORY - assert(directoryMetadata.directory) + guard directoryMetadata.directory else { + logger.error("Expected directory metadata but received file metadata for serverUrl: \(serverUrl)") + return (nil, nil, nil, nil, .invalidData) + } if let existingMetadata = dbManager.itemMetadata(ocId: directoryMetadata.ocId) { directoryMetadata.downloaded = existingMetadata.downloaded directoryMetadata.keepDownloaded = existingMetadata.keepDownloaded } - + directoryMetadata.visitedDirectory = true metadatas.insert(directoryMetadata, at: 0) @@ -214,7 +217,7 @@ extension Enumerator { let ( allMetadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError ) = await handleDepth1ReadFileOrFolder( - serverUrl: serverUrl, + serverUrl: serverUrl, account: account, dbManager: dbManager, files: files, diff --git a/Tests/Interface/MockRemoteItem.swift b/Tests/Interface/MockRemoteItem.swift index 70f68d55..9f3ba7d1 100644 --- a/Tests/Interface/MockRemoteItem.swift +++ b/Tests/Interface/MockRemoteItem.swift @@ -23,12 +23,7 @@ public class MockRemoteItem: Equatable { public var data: Data? public var locked: Bool public var lockOwner: String - public var lockOwnerEditor: String - public var lockOwnerType: Int - public var lockOwnerDisplayName: String - public var lockTime: Date? public var lockTimeOut: Date? - public var lockToken: String? public var size: Int64 { Int64(data?.count ?? 0) } public var account: String public var username: String @@ -45,12 +40,7 @@ public class MockRemoteItem: Equatable { lhs.directory == rhs.directory && lhs.locked == rhs.locked && lhs.lockOwner == rhs.lockOwner && - lhs.lockOwnerEditor == rhs.lockOwnerEditor && - lhs.lockOwnerType == rhs.lockOwnerType && - lhs.lockOwnerDisplayName == rhs.lockOwnerDisplayName && - lhs.lockTime == rhs.lockTime && lhs.lockTimeOut == rhs.lockTimeOut && - lhs.lockToken == rhs.lockToken && lhs.data == rhs.data && lhs.size == rhs.size && lhs.creationDate == rhs.creationDate && @@ -101,12 +91,7 @@ public class MockRemoteItem: Equatable { data: Data? = nil, locked: Bool = false, lockOwner: String = "", - lockOwnerEditor: String = "", - lockOwnerDisplayName: String = "", - lockOwnerType: Int = 0, - lockTime: Date? = nil, lockTimeOut: Date? = nil, - lockToken: String? = nil, account: String, username: String, userId: String, @@ -123,12 +108,7 @@ public class MockRemoteItem: Equatable { self.data = data self.locked = locked self.lockOwner = lockOwner - self.lockOwnerEditor = lockOwnerEditor - self.lockOwnerDisplayName = lockOwnerDisplayName - self.lockOwnerType = lockOwnerType - self.lockTime = lockTime self.lockTimeOut = lockTimeOut - self.lockToken = lockToken self.account = account self.username = username self.userId = userId @@ -155,7 +135,9 @@ public class MockRemoteItem: Equatable { file.user = username file.userId = userId file.urlBase = serverUrl - file.lock = NKLock(owner: lockOwner, ownerEditor: lockOwnerEditor, ownerType: NKLockType(rawValue: lockOwnerType)!, ownerDisplayName: lockOwnerDisplayName, time: lockTime, timeOut: lockTimeOut, token: lockToken) + file.lock = locked + file.lockOwner = lockOwner + file.lockTimeOut = lockTimeOut file.trashbinFileName = name file.trashbinOriginalLocation = trashbinOriginalLocation ?? "" return file @@ -211,7 +193,7 @@ public class MockRemoteItem: Equatable { lockOwner: lockOwner, lockOwnerType: lockOwner.isEmpty ? 0 : 1, lockOwnerDisplayName: lockOwner == account.username ? account.username : "other user", - lockTime: lockTime, // Default as not set in original code + lockTime: nil, // Default as not set in original code lockTimeOut: lockTimeOut, path: "", // Placeholder as not set in original code serverUrl: trimmedServerUrl, diff --git a/Tests/NextcloudFileProviderKitTests/ItemModifyTests.swift b/Tests/NextcloudFileProviderKitTests/ItemModifyTests.swift index a52bc1c9..f20c9379 100644 --- a/Tests/NextcloudFileProviderKitTests/ItemModifyTests.swift +++ b/Tests/NextcloudFileProviderKitTests/ItemModifyTests.swift @@ -1453,11 +1453,7 @@ final class ItemModifyTests: NextcloudFileProviderKitTestCase { dbManager: Self.dbManager ) - if #available(macOS 13.0, *) { - XCTAssertEqual(error as? NSFileProviderError, NSFileProviderError(.excludedFromSync)) - } else { - XCTAssertNil(error) - } + XCTAssertNil(error) XCTAssertEqual(modifiedItem?.itemIdentifier, lockItem.itemIdentifier) XCTAssertEqual(modifiedItem?.filename, modifiedMetadata.fileName) XCTAssertEqual(modifiedItem?.documentSize?.intValue, tempData.count) diff --git a/Tests/NextcloudFileProviderKitTests/RemoteChangeObserverEtagOptimizationTests.swift b/Tests/NextcloudFileProviderKitTests/RemoteChangeObserverEtagOptimizationTests.swift index da7dd397..fccbe512 100644 --- a/Tests/NextcloudFileProviderKitTests/RemoteChangeObserverEtagOptimizationTests.swift +++ b/Tests/NextcloudFileProviderKitTests/RemoteChangeObserverEtagOptimizationTests.swift @@ -171,7 +171,6 @@ final class RemoteChangeObserverEtagOptimizationTests: NextcloudFileProviderKitT // The key optimization we want: the same folder with unchanged etag shouldn't be // enumerated repeatedly. Ideally, it should be enumerated only once. // However, without optimization, it might be enumerated 3 times (once per working set check) - XCTAssertLessThanOrEqual(customersEnumerateCount, 1, - "Customers folder with unchanged etag should not be enumerated repeatedly") + XCTAssertLessThanOrEqual(customersEnumerateCount, 1, "Customers folder with unchanged etag should not be enumerated repeatedly") } } From f6bd26cea9e0abd62e4c327104ab1b4d19f73c0a Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Mon, 13 Oct 2025 09:31:08 +0200 Subject: [PATCH 14/15] fix: Correct value for logging details. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Iva Horn --- Sources/NextcloudFileProviderKit/Item/Item+Modify.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift index 20d1582a..eb8c1fa0 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift @@ -684,7 +684,7 @@ public extension Item { ) guard renameError == nil, let renameModifiedItem else { - logger.error("Could not rename pre-trash item.", [.item: modifiedItem.filename, .error: error]) + logger.error("Could not rename pre-trash item.", [.item: modifiedItem.itemIdentifier, .error: error]) return (nil, renameError) } From a2b60399c13ff9a095f992e62788859d210570fe Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Mon, 13 Oct 2025 10:05:41 +0200 Subject: [PATCH 15/15] fix: Restored removed property initializer for lock token. Signed-off-by: Iva Horn --- .../NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift index 1a895160..b1d649ab 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift @@ -184,6 +184,7 @@ public struct SendableItemMetadata: ItemMetadata, Sendable { self.lockOwnerDisplayName = lockOwnerDisplayName self.lockTime = lockTime self.lockTimeOut = lockTimeOut + self.lockToken = lockToken self.path = path self.permissions = permissions self.quotaUsedBytes = quotaUsedBytes