diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift index 1f24d58a..24999b44 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift @@ -5,9 +5,6 @@ import FileProvider import Foundation import RealmSwift -internal let stable1_0SchemaVersion: UInt64 = 100 -internal let stable2_0SchemaVersion: UInt64 = 200 // Major change: deleted LocalFileMetadata type - /// /// Realm database abstraction and management. /// @@ -43,7 +40,7 @@ public final class FilesDatabaseManager: Sendable { ) } - private static let schemaVersion = stable2_0SchemaVersion + private static let schemaVersion = SchemaVersion.addedLockTokenPropertyToRealmItemMetadata let logger: FileProviderLogger let account: Account @@ -115,9 +112,9 @@ public final class FilesDatabaseManager: Sendable { let configuration = customConfiguration ?? Realm.Configuration( fileURL: databaseLocation, - schemaVersion: Self.schemaVersion, + schemaVersion: Self.schemaVersion.rawValue, migrationBlock: { migration, oldSchemaVersion in - if oldSchemaVersion == stable1_0SchemaVersion { + if oldSchemaVersion == SchemaVersion.initial.rawValue { var localFileMetadataOcIds = Set() migration.enumerateObjects(ofType: "LocalFileMetadata") { oldObject, _ in @@ -171,7 +168,7 @@ public final class FilesDatabaseManager: Sendable { logger.info("Migrating shared legacy database to new database for \(account.ncKitAccount)") - let legacyConfiguration = Realm.Configuration(fileURL: sharedDatabaseURL, schemaVersion: stable2_0SchemaVersion, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) + let legacyConfiguration = Realm.Configuration(fileURL: sharedDatabaseURL, schemaVersion: SchemaVersion.deletedLocalFileMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) do { let legacyRealm = try Realm(configuration: legacyConfiguration) diff --git a/Sources/NextcloudFileProviderKit/Database/SchemaVersion.swift b/Sources/NextcloudFileProviderKit/Database/SchemaVersion.swift new file mode 100644 index 00000000..9d879c31 --- /dev/null +++ b/Sources/NextcloudFileProviderKit/Database/SchemaVersion.swift @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-2.0-or-later + +/// +/// Different schema versions shipped with this project. +/// +enum SchemaVersion: UInt64 { + case initial = 100 + case deletedLocalFileMetadata = 200 + case addedLockTokenPropertyToRealmItemMetadata = 201 +} diff --git a/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift b/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift index a44e4f91..9ab45821 100644 --- a/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift +++ b/Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift @@ -63,13 +63,14 @@ extension NKFile { note: note, ownerId: ownerId, ownerDisplayName: ownerDisplayName, - lock: lock, - lockOwner: lockOwner, - lockOwnerEditor: lockOwnerEditor, - lockOwnerType: lockOwnerType, - lockOwnerDisplayName: lockOwnerDisplayName, - lockTime: lockTime, - lockTimeOut: lockTimeOut, + 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, path: path, permissions: permissions, quotaUsedBytes: quotaUsedBytes, diff --git a/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift b/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift index f8054a1f..aa02dc67 100644 --- a/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift +++ b/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift @@ -304,20 +304,8 @@ extension NextcloudKit: RemoteInterface { } } - public func setLockStateForFile( - remotePath: String, - lock: Bool, - account: Account, - options: NKRequestOptions, - taskHandler: @escaping (_ task: URLSessionTask) -> Void - ) async -> (account: String, response: HTTPURLResponse?, error: NKError) { - return await withCheckedContinuation { continuation in - lockUnlockFile( - serverUrlFileName: remotePath, shouldLock: lock, account: account.ncKitAccount - ) { account, response, error in - continuation.resume(returning: (account, response?.response, error)) - } - } + public func lockUnlockFile(serverUrlFileName: String, type: NKLockType?, shouldLock: Bool, account: Account, options: NKRequestOptions, taskHandler: @escaping (URLSessionTask) -> Void) async throws -> NKLock? { + return try await lockUnlockFile(serverUrlFileName: serverUrlFileName, type: type, shouldLock: shouldLock, account: account.ncKitAccount, options: options, taskHandler: taskHandler) } public func trashedItems( diff --git a/Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift b/Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift index b9e76f54..de213e9a 100644 --- a/Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift +++ b/Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift @@ -125,13 +125,7 @@ public protocol RemoteInterface { taskHandler: @escaping (_ task: URLSessionTask) -> Void ) async -> (account: String, response: HTTPURLResponse?, error: NKError) - func setLockStateForFile( - remotePath: String, - lock: Bool, - account: Account, - options: NKRequestOptions, - taskHandler: @escaping (_ task: URLSessionTask) -> Void - ) async -> (account: String, response: HTTPURLResponse?, error: NKError) + func lockUnlockFile(serverUrlFileName: String, type: NKLockType?, shouldLock: Bool, account: Account, options: NKRequestOptions, taskHandler: @escaping (_ task: URLSessionTask) -> Void) async throws -> NKLock? func trashedItems( account: Account, diff --git a/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift b/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift index 8570f669..56f39e40 100644 --- a/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift +++ b/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift @@ -2,11 +2,49 @@ // SPDX-License-Identifier: GPL-2.0-or-later import FileProvider +import NextcloudKit import NextcloudCapabilitiesKit extension Item { + /// + /// Shared capability assertion before dispatching (un)lock requests to the server. + /// + private static func assertRequiredCapabilities(domain: NSFileProviderDomain?, itemIdentifier: NSFileProviderItemIdentifier, account: Account, remoteInterface: RemoteInterface, logger: FileProviderLogger) async -> Bool { + let (_, capabilities, _, capabilitiesError) = await remoteInterface.currentCapabilities( + account: account, + options: .init(), + taskHandler: { task in + if let domain { + NSFileProviderManager(for: domain)?.register( + task, + forItemWithIdentifier: itemIdentifier, + completionHandler: { _ in } + ) + } + } + ) + + guard capabilitiesError == .success else { + logger.error("Request for capability assertion failed!") + return false + } + + guard let capabilities else { + logger.error("Capabilities to assert are nil!") + return false + } + + guard capabilities.files?.locking != nil else { + logger.error("Capability assertion failed because file locks are not supported!") + return false + } + + return true + } + /// /// Create a lock file in the local file provider extension database which is not synchronized to the server, if the server supports file locking. + /// The lock file itself is not uploaded and no error is reported intentionally. /// /// - Parameters: /// - basedOn: Passed through as received from the file provider framework. @@ -34,82 +72,18 @@ extension Item { let logger = FileProviderLogger(category: "Item", log: log) progress.totalUnitCount = 1 - // Lock but don't upload, do not error - let (_, capabilities, _, capabilitiesError) = await remoteInterface.currentCapabilities( - account: account, - options: .init(), - taskHandler: { task in - if let domain { - NSFileProviderManager(for: domain)?.register( - task, - forItemWithIdentifier: itemTemplate.itemIdentifier, - completionHandler: { _ in } - ) - } - } - ) - guard capabilitiesError == .success, - let capabilities, - capabilities.files?.locking != nil - else { - logger.info( - """ - Received nil capabilities data. - Received error: \(capabilitiesError.errorDescription) - Capabilities nil: \(capabilities == nil ? "YES" : "NO") - (if capabilities are not nil the server may just not have files_lock enabled). - Will not proceed with locking for \(itemTemplate.filename) - """ - ) + guard await assertRequiredCapabilities(domain: domain, itemIdentifier: itemTemplate.itemIdentifier, account: account, remoteInterface: remoteInterface, logger: logger) else { return (nil, nil) } - logger.info( - """ - Item to create: - \(itemTemplate.filename) - is a lock file. Will handle by remotely locking the target file. - """ - ) - guard let targetFileName = originalFileName( - fromLockFileName: itemTemplate.filename - ) else { - logger.error( - """ - Could not get original filename from lock file filename - \(itemTemplate.filename) - so will not lock target file. - """ - ) + logger.info("Item to create is a lock file. Will attempt to lock the associated file on the server.", [.name: itemTemplate.filename]) + + guard let targetFileName = originalFileName(fromLockFileName: itemTemplate.filename) else { + logger.error("Will not lock the target file because it could not be determined based on the lock file name.", [.name: itemTemplate.filename]) return (nil, nil) } + let targetFileRemotePath = parentItemRemotePath + "/" + targetFileName - let (_, _, error) = await remoteInterface.setLockStateForFile( // TODO: NOT WORKING - remotePath: targetFileRemotePath, - lock: true, - account: account, - options: .init(), - taskHandler: { task in - if let domain { - NSFileProviderManager(for: domain)?.register( - task, - forItemWithIdentifier: itemTemplate.itemIdentifier, - completionHandler: { _ in } - ) - } - } - ) - if error != .success { - logger.error( - """ - Failed to lock target file \(targetFileName) - for lock file: \(itemTemplate.filename) - received error: \(error.errorDescription) - """ - ) - } else { - logger.info("Locked file at: \(targetFileRemotePath)") - } let metadata = SendableItemMetadata( ocId: itemTemplate.itemIdentifier.rawValue, @@ -139,14 +113,44 @@ extension Item { user: account.username, userId: account.id ) + dbManager.addItemMetadata(metadata) + var errorToReturn: Error? - progress.completedUnitCount = 1 - var returnError = error.fileProviderError // No need to handle problem cases, no upload here - if #available(macOS 13.0, *), error == .success { - returnError = NSFileProviderError(.excludedFromSync) + do { + let lock = try await remoteInterface.lockUnlockFile(serverUrlFileName: targetFileRemotePath, type: .token, shouldLock: true, account: account, options: .init(), taskHandler: { task in + if let domain { + NSFileProviderManager(for: domain)?.register( + task, + forItemWithIdentifier: itemTemplate.itemIdentifier, + completionHandler: { _ in } + ) + } + }) + + if let lock { + logger.info("Locked file and received lock.", [.name: targetFileName, .lock: lock]) + } else { + logger.info("Locked file but did not receive lock information.", [.name: targetFileName]) + } + + if #available(macOS 13.0, *) { + errorToReturn = NSFileProviderError(.excludedFromSync) + } + } catch { + logger.error("Failed to lock file \"\(targetFileName)\" which has lock file \"\(itemTemplate.filename)\".", [.error: error]) + + if let nkError = error as? NKError { + // Attempt to map a possible NKError to an NSFileProviderError. + errorToReturn = nkError.fileProviderError + } else { + // Return the error as it is. + errorToReturn = error + } } + progress.completedUnitCount = 1 + return ( Item( metadata: metadata, @@ -157,7 +161,7 @@ extension Item { remoteSupportsTrash: await remoteInterface.supportsTrash(account: account), log: log ), - returnError + errorToReturn ) } @@ -174,13 +178,11 @@ extension Item { progress: Progress = .init(), dbManager: FilesDatabaseManager ) async -> (Item?, Error?) { - logger.info( - """ - System requested modification of lockfile \(self.filename) - Marking as complete without syncing to server. - """ - ) - assert(isLockFileName(filename), "Should not handle non-lock files here.") + logger.info("System requested modification of lock file. Marking as complete without syncing to server.", [.name: self.filename]) + + if isLockFileName(filename) == false { + logger.fault("Should not handle non-lock files here.", [.name: filename]) + } guard let modifiedItem = await modifyUnuploaded( itemTarget: itemTarget, @@ -195,20 +197,13 @@ extension Item { progress: progress, dbManager: dbManager ) else { - logger.info( - "Cannot modify lock file: \(self.filename) as received a nil modified item" - ) + logger.info("Cannot modify lock file because received a nil modified item.", [.name: self.filename]) return (nil, NSFileProviderError(.cannotSynchronize)) } if !isLockFileName(modifiedItem.filename) { - logger.info( - """ - After modification, lock file: \(self.filename) is no longer a lock file - (it is now named: \(modifiedItem.filename)) - Will proceed with creating item on server (if possible). - """ - ) + logger.info("After modification, lock file: \(self.filename) is no longer a lock file (it is now named: \(modifiedItem.filename)) Will proceed with creating item on server (if possible).") + return await modifiedItem.createUnuploaded( itemTarget: itemTarget, baseVersion: baseVersion, @@ -225,93 +220,60 @@ extension Item { } var returnError: Error? = nil + if #available(macOS 13.0, *) { returnError = NSFileProviderError(.excludedFromSync) } + return (modifiedItem, returnError) } - func deleteLockFile( - domain: NSFileProviderDomain? = nil, dbManager: FilesDatabaseManager - ) async -> Error? { - let (_, capabilities, _, capabilitiesError) = await remoteInterface.currentCapabilities( - account: account, - options: .init(), - taskHandler: { task in - if let domain { - NSFileProviderManager(for: domain)?.register( - task, - forItemWithIdentifier: self.itemIdentifier, - completionHandler: { _ in } - ) - } - } - ) - guard capabilitiesError == .success, - let capabilities, - capabilities.files?.locking != nil - else { - logger.info( - """ - Received nil capabilities data. - Received error: \(capabilitiesError.errorDescription) - Capabilities nil: \(capabilities == nil ? "YES" : "NO") - (if capabilities are not nil the server may just not have files_lock enabled). - Will not proceed with unlocking for \(self.filename) - """ - ) + func deleteLockFile(domain: NSFileProviderDomain? = nil, dbManager: FilesDatabaseManager) async -> Error? { + guard await Self.assertRequiredCapabilities(domain: domain, itemIdentifier: self.itemIdentifier, account: account, remoteInterface: remoteInterface, logger: logger) else { return nil } dbManager.deleteItemMetadata(ocId: metadata.ocId) - guard let originalFileName = originalFileName( - fromLockFileName: metadata.fileName - ) else { - logger.error( - """ - Could not get original filename from lock file filename - \(self.metadata.fileName) - so will not unlock target file. - """ - ) + guard let originalFileName = originalFileName(fromLockFileName: metadata.fileName) else { + logger.error("Could not get original filename from lock file filename so will not unlock target file.", [.name: self.metadata.fileName]) return nil } + let originalFileServerFileNameUrl = metadata.serverUrl + "/" + originalFileName - let (_, _, error) = await remoteInterface.setLockStateForFile( // TODO: NOT WORKING - remotePath: originalFileServerFileNameUrl, - lock: false, - account: account, - options: .init(), - taskHandler: { task in - if let domain { - NSFileProviderManager(for: domain)?.register( - task, - forItemWithIdentifier: self.itemIdentifier, - completionHandler: { _ in } - ) + + do { + let lock = try await remoteInterface.lockUnlockFile( + serverUrlFileName: originalFileServerFileNameUrl, + type: .token, + shouldLock: false, + account: account, + options: .init(), + taskHandler: { task in + if let domain { + NSFileProviderManager(for: domain)?.register( + task, + forItemWithIdentifier: self.itemIdentifier, + completionHandler: { _ in } + ) + } } - } - ) - guard error == .success else { - logger.error( - """ - Could not unlock item for \(self.filename)... - at \(originalFileServerFileNameUrl)... - received error: \(error.errorCode) - \(error.errorDescription) - """ - ) - return error.fileProviderError( - handlingNoSuchItemErrorUsingItemIdentifier: itemIdentifier ) + + if let lock { + logger.info("Unlocked file and received lock.", [.name: originalFileName, .lock: lock]) + } else { + logger.info("Unlocked file but did not receive lock information.", [.name: originalFileName]) + } + + } catch { + logger.error("Could not unlock item.", [.name: self.filename, .error: error]) + + if let error = error as? NKError { + return error.fileProviderError(handlingNoSuchItemErrorUsingItemIdentifier: itemIdentifier) + } } - logger.info( - """ - Successfully unlocked item for: \(self.filename)... - at: \(originalFileServerFileNameUrl) - """ - ) + return nil } } diff --git a/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift b/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift index dd182290..ef3e8b1c 100644 --- a/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift +++ b/Sources/NextcloudFileProviderKit/Log/FileProviderLogDetailKey.swift @@ -31,6 +31,11 @@ public enum FileProviderLogDetailKey: String { /// case item + /// + /// An `NKLock` as provided by NextcloudKit when a file system item is locked on the server. + /// + case lock + /// /// A ``SendableItemMetadata`` object. /// diff --git a/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift index afd3d1c3..06fc0a15 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/ItemMetadata.swift @@ -64,6 +64,7 @@ public protocol ItemMetadata: Equatable { var lockOwnerDisplayName: String? { get set } var lockTime: Date? { get set } // Time the file was locked var lockTimeOut: Date? { get set } // Time the file's lock will expire + var lockToken: String? { get set } var path: String { get set } var permissions: String { get set } var shareType: [Int] { get set } diff --git a/Sources/NextcloudFileProviderKit/Metadata/RealmItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/RealmItemMetadata.swift index 9e2b5aa0..604de960 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/RealmItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/RealmItemMetadata.swift @@ -48,6 +48,7 @@ internal class RealmItemMetadata: Object, ItemMetadata { @Persisted public var lockOwnerDisplayName: String? @Persisted public var lockTime: Date? // Time the file was locked @Persisted public var lockTimeOut: Date? // Time the file's lock will expire + @Persisted public var lockToken: String? // Token identifier for token-based locks @Persisted public var path = "" @Persisted public var permissions = "" @Persisted public var quotaUsedBytes: Int64 = 0 @@ -147,6 +148,7 @@ internal class RealmItemMetadata: Object, ItemMetadata { self.lockOwnerDisplayName = value.lockOwnerDisplayName self.lockTime = value.lockTime self.lockTimeOut = value.lockTimeOut + self.lockToken = value.lockToken self.path = value.path self.permissions = value.permissions self.quotaUsedBytes = value.quotaUsedBytes diff --git a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift index 3e225778..88ca88d1 100644 --- a/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift +++ b/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata.swift @@ -48,6 +48,7 @@ public struct SendableItemMetadata: ItemMetadata, Sendable { public var lockOwnerDisplayName: String? public var lockTime: Date? public var lockTimeOut: Date? + public var lockToken: String? public var path: String public var permissions: String public var quotaUsedBytes: Int64 @@ -114,6 +115,7 @@ public struct SendableItemMetadata: ItemMetadata, Sendable { lockOwnerDisplayName: String? = nil, lockTime: Date? = nil, lockTimeOut: Date? = nil, + lockToken: String? = nil, path: String, permissions: String = "RGDNVW", quotaUsedBytes: Int64 = 0, @@ -179,6 +181,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 @@ -246,6 +249,7 @@ public struct SendableItemMetadata: ItemMetadata, Sendable { self.lockOwnerDisplayName = value.lockOwnerDisplayName self.lockTime = value.lockTime self.lockTimeOut = value.lockTimeOut + self.lockToken = value.lockToken self.path = value.path self.permissions = value.permissions self.quotaUsedBytes = value.quotaUsedBytes diff --git a/Sources/NextcloudFileProviderKitMocks/TestableRemoteInterface.swift b/Sources/NextcloudFileProviderKitMocks/TestableRemoteInterface.swift index 19c98c3a..b99457e9 100644 --- a/Sources/NextcloudFileProviderKitMocks/TestableRemoteInterface.swift +++ b/Sources/NextcloudFileProviderKitMocks/TestableRemoteInterface.swift @@ -119,14 +119,8 @@ public struct TestableRemoteInterface: RemoteInterface { ("", nil, .invalidResponseError) } - public func setLockStateForFile( - remotePath: String, - lock: Bool, - account: Account, - options: NKRequestOptions, - taskHandler: @escaping (URLSessionTask) -> Void - ) async -> (account: String, response: HTTPURLResponse?, error: NKError) { - ("", nil, .invalidResponseError) + public func lockUnlockFile(serverUrlFileName: String, type: NKLockType?, shouldLock: Bool, account: Account, options: NKRequestOptions, taskHandler: @escaping (URLSessionTask) -> Void) async throws -> NKLock? { + throw NKError.invalidResponseError } public func trashedItems( diff --git a/Tests/Interface/MockRemoteInterface.swift b/Tests/Interface/MockRemoteInterface.swift index 806fbbd3..1a81a6ec 100644 --- a/Tests/Interface/MockRemoteInterface.swift +++ b/Tests/Interface/MockRemoteInterface.swift @@ -1153,18 +1153,14 @@ public class MockRemoteInterface: RemoteInterface { return (account.ncKitAccount, nil, .success) } - public func setLockStateForFile( - remotePath: String, - lock: Bool, - account: Account, - options: NKRequestOptions, - taskHandler: @escaping (_ task: URLSessionTask) -> Void - ) async -> (account: String, response: HTTPURLResponse?, error: NKError) { - guard let item = item(remotePath: remotePath, account: account) else { - return (account.ncKitAccount, nil, .urlError) + public func lockUnlockFile(serverUrlFileName: String, type: NKLockType?, shouldLock: Bool, account: Account, options: NKRequestOptions, taskHandler: @escaping (URLSessionTask) -> Void) async throws -> NKLock? { + guard let item = item(remotePath: serverUrlFileName, account: account) else { + throw NKError.urlError } - item.locked = lock - return (account.ncKitAccount, nil, .success) + + item.locked = shouldLock + + return nil } public func trashedItems( diff --git a/Tests/Interface/MockRemoteItem.swift b/Tests/Interface/MockRemoteItem.swift index 0a0bd27b..3b9cb0f8 100644 --- a/Tests/Interface/MockRemoteItem.swift +++ b/Tests/Interface/MockRemoteItem.swift @@ -23,7 +23,12 @@ 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 @@ -40,7 +45,12 @@ 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 && @@ -91,7 +101,12 @@ 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, @@ -108,7 +123,12 @@ 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 @@ -135,9 +155,7 @@ public class MockRemoteItem: Equatable { file.user = username file.userId = userId file.urlBase = serverUrl - file.lock = locked - file.lockOwner = lockOwner - file.lockTimeOut = lockTimeOut + file.lock = NKLock(owner: lockOwner, ownerEditor: lockOwnerEditor, ownerType: NKLockType(rawValue: lockOwnerType)!, ownerDisplayName: lockOwnerDisplayName, time: lockTime, timeOut: lockTimeOut, token: lockToken) file.trashbinFileName = name file.trashbinOriginalLocation = trashbinOriginalLocation ?? "" return file @@ -193,7 +211,7 @@ public class MockRemoteItem: Equatable { lockOwner: lockOwner, lockOwnerType: lockOwner.isEmpty ? 0 : 1, lockOwnerDisplayName: lockOwner == account.username ? account.username : "other user", - lockTime: nil, // Default as not set in original code + lockTime: lockTime, // Default as not set in original code lockTimeOut: lockTimeOut, path: "", // Placeholder as not set in original code serverUrl: trimmedServerUrl, diff --git a/Tests/NextcloudFileProviderKitTests/FilesDatabaseManagerTests.swift b/Tests/NextcloudFileProviderKitTests/FilesDatabaseManagerTests.swift index 41aefe72..9c0c437f 100644 --- a/Tests/NextcloudFileProviderKitTests/FilesDatabaseManagerTests.swift +++ b/Tests/NextcloudFileProviderKitTests/FilesDatabaseManagerTests.swift @@ -853,8 +853,8 @@ final class FilesDatabaseManagerTests: NextcloudFileProviderKitTestCase { let oldRealmURL = temporaryDirectory.appendingPathComponent(FilesDatabaseManager.databaseFilename) // Create the old Realm configuration and insert test objects. - // Use stable2_0SchemaVersion and the appropriate object types. - let oldConfig = Realm.Configuration(fileURL: oldRealmURL, schemaVersion: stable2_0SchemaVersion, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) + // Use most recent schema and the appropriate object types. + let oldConfig = Realm.Configuration(fileURL: oldRealmURL, schemaVersion: SchemaVersion.addedLockTokenPropertyToRealmItemMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) let oldRealm = try Realm(configuration: oldConfig) // Create test objects @@ -880,7 +880,7 @@ final class FilesDatabaseManagerTests: NextcloudFileProviderKitTestCase { // Prepare a new Realm configuration for the target per‑account database. let newRealmURL = temporaryDirectory.appendingPathComponent("new.realm") - let newConfig = Realm.Configuration(fileURL: newRealmURL, schemaVersion: stable2_0SchemaVersion, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) + let newConfig = Realm.Configuration(fileURL: newRealmURL, schemaVersion: SchemaVersion.addedLockTokenPropertyToRealmItemMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) // Call the initializer that performs the migration. // It will search for the old database at: @@ -932,7 +932,7 @@ final class FilesDatabaseManagerTests: NextcloudFileProviderKitTestCase { // Insert initial objects into the old realm let oldConfig = Realm.Configuration( fileURL: oldRealmURL, - schemaVersion: stable2_0SchemaVersion, + schemaVersion: SchemaVersion.addedLockTokenPropertyToRealmItemMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self] ) let oldRealm = try Realm(configuration: oldConfig) @@ -958,7 +958,7 @@ final class FilesDatabaseManagerTests: NextcloudFileProviderKitTestCase { let newRealmURL = tempDir.appendingPathComponent("new.realm") let newConfig = Realm.Configuration( fileURL: newRealmURL, - schemaVersion: stable2_0SchemaVersion, + schemaVersion: SchemaVersion.addedLockTokenPropertyToRealmItemMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self] )