From 2660fb9a5f4a1fcf1084c9c942247419edc41ddc Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Sun, 17 May 2026 14:58:16 +0200 Subject: [PATCH 1/6] Derive cloudPath from parentID chain instead of denormalized column --- .../CloudTask/CloudTask.swift | 8 +- .../CloudTask/DeletionTask.swift | 10 +- .../CloudTask/DownloadTask.swift | 8 +- .../CloudTask/FolderCreationTask.swift | 7 +- .../CloudTask/ItemEnumerationTask.swift | 9 +- .../CloudTask/ReparentTask.swift | 10 +- .../CloudTask/UploadTask.swift | 7 +- .../DB/DatabaseHelper.swift | 26 +++ .../DB/DeletionTaskDBManager.swift | 14 +- .../DB/DownloadTaskDBManager.swift | 17 +- .../DB/ItemEnumerationTaskDBManager.swift | 15 +- CryptomatorFileProvider/DB/ItemMetadata.swift | 12 +- .../DB/ItemMetadataDBManager.swift | 101 ++++++++++-- .../DB/ReparentTaskDBManager.swift | 8 +- .../DB/UploadTaskDBManager.swift | 15 +- .../FileProviderAdapter.swift | 36 ++-- .../FileProviderAdapterError.swift | 2 + .../FileProviderAdapterManager.swift | 6 +- .../FileProviderItem.swift | 2 +- .../OnlineItemNameCollisionHandler.swift | 29 +++- .../TaskExecutor/DeletionTaskExecutor.swift | 10 +- .../TaskExecutor/DownloadTaskExecutor.swift | 5 +- .../FolderCreationTaskExecutor.swift | 2 +- .../ItemEnumerationTaskExecutor.swift | 25 +-- .../TaskExecutor/UploadTaskExecutor.swift | 2 +- .../DB/CachedFileManagerTests.swift | 22 +-- .../DB/DeletionTaskManagerTests.swift | 28 ++-- .../DB/DownloadTaskManagerTests.swift | 27 ++- .../DB/ItemEnumerationTaskManagerTests.swift | 4 +- .../DB/MaintenanceManagerTests.swift | 36 ++-- .../DB/MetadataManagerTests.swift | 68 ++++---- .../DB/ReparentTaskManagerTests.swift | 26 +-- .../DB/UploadTaskManagerTests.swift | 16 +- .../FileImportingServiceSourceTests.swift | 2 +- ...eProviderAdapterCreateDirectoryTests.swift | 8 +- .../FileProviderAdapterDeleteItemTests.swift | 8 +- ...ileProviderAdapterEnumerateItemTests.swift | 12 +- ...roviderAdapterGetItemIdentifierTests.swift | 48 ++---- .../FileProviderAdapterGetItemTests.swift | 4 +- ...leProviderAdapterImportDocumentTests.swift | 19 +-- .../FileProviderAdapterMoveItemTests.swift | 46 +++--- ...leProviderAdapterRecoverUploadsTests.swift | 8 +- ...eProviderAdapterSetFavoriteRankTests.swift | 2 +- .../FileProviderAdapterSetTagDataTests.swift | 4 +- ...oviderAdapterStartProvidingItemTests.swift | 4 +- .../FileProviderAdapterTestCase.swift | 4 +- .../FileProviderEnumeratorTests.swift | 4 +- .../FileProviderItemTests.swift | 28 ++-- .../FileProviderNotificatorTests.swift | 2 +- .../Middleware/ErrorMapperTests.swift | 4 + .../OnlineItemNameCollisionHandlerTests.swift | 35 ++-- .../CloudTaskExecutorTestCase.swift | 97 +++++++++-- .../DeletionTaskExecutorTests.swift | 16 +- .../DownloadTaskExecutorTests.swift | 16 +- .../FolderCreationTaskExecutorTests.swift | 10 +- .../ItemEnumerationTaskTests.swift | 154 +++++++++--------- .../ReparentTaskExecutorTests.swift | 16 +- .../UploadTaskExecutorTests.swift | 24 +-- .../PermissionProviderImplTests.swift | 22 +-- .../CacheManagingServiceSourceTests.swift | 2 +- .../WorkingSetObserverTests.swift | 2 +- 61 files changed, 713 insertions(+), 501 deletions(-) diff --git a/CryptomatorFileProvider/CloudTask/CloudTask.swift b/CryptomatorFileProvider/CloudTask/CloudTask.swift index 4ed46f1ff..4f6b13c69 100644 --- a/CryptomatorFileProvider/CloudTask/CloudTask.swift +++ b/CryptomatorFileProvider/CloudTask/CloudTask.swift @@ -11,10 +11,6 @@ import Foundation protocol CloudTask { var itemMetadata: ItemMetadata { get } -} - -extension CloudTask { - var cloudPath: CloudPath { - return itemMetadata.cloudPath - } + /// Snapshot captured at task construction; survives concurrent local renames of the same row. + var cloudPath: CloudPath { get } } diff --git a/CryptomatorFileProvider/CloudTask/DeletionTask.swift b/CryptomatorFileProvider/CloudTask/DeletionTask.swift index e612216cb..36634d893 100644 --- a/CryptomatorFileProvider/CloudTask/DeletionTask.swift +++ b/CryptomatorFileProvider/CloudTask/DeletionTask.swift @@ -6,14 +6,14 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // -import GRDB +import CryptomatorCloudAccessCore -struct DeletionTask: CloudTask, FetchableRecord, Decodable { +struct DeletionTask: CloudTask { let taskRecord: DeletionTaskRecord let itemMetadata: ItemMetadata + let cloudPath: CloudPath - enum CodingKeys: String, CodingKey { - case taskRecord = "deletionTask" - case itemMetadata + func with(cloudPath: CloudPath) -> DeletionTask { + return DeletionTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath) } } diff --git a/CryptomatorFileProvider/CloudTask/DownloadTask.swift b/CryptomatorFileProvider/CloudTask/DownloadTask.swift index e125a7e11..37bc6a1cf 100644 --- a/CryptomatorFileProvider/CloudTask/DownloadTask.swift +++ b/CryptomatorFileProvider/CloudTask/DownloadTask.swift @@ -6,17 +6,17 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Foundation -import GRDB struct DownloadTask: CloudTask { let taskRecord: DownloadTaskRecord let itemMetadata: ItemMetadata + let cloudPath: CloudPath let onURLSessionTaskCreation: URLSessionTaskCreationClosure? - enum CodingKeys: String, CodingKey { - case taskRecord = "downloadTask" - case itemMetadata + func with(cloudPath: CloudPath) -> DownloadTask { + return DownloadTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: onURLSessionTaskCreation) } } diff --git a/CryptomatorFileProvider/CloudTask/FolderCreationTask.swift b/CryptomatorFileProvider/CloudTask/FolderCreationTask.swift index c4b402d81..f95d757b9 100644 --- a/CryptomatorFileProvider/CloudTask/FolderCreationTask.swift +++ b/CryptomatorFileProvider/CloudTask/FolderCreationTask.swift @@ -6,8 +6,13 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // -import Foundation +import CryptomatorCloudAccessCore struct FolderCreationTask: CloudTask { let itemMetadata: ItemMetadata + let cloudPath: CloudPath + + func with(cloudPath: CloudPath) -> FolderCreationTask { + return FolderCreationTask(itemMetadata: itemMetadata, cloudPath: cloudPath) + } } diff --git a/CryptomatorFileProvider/CloudTask/ItemEnumerationTask.swift b/CryptomatorFileProvider/CloudTask/ItemEnumerationTask.swift index a01000d00..ad47e4dd5 100644 --- a/CryptomatorFileProvider/CloudTask/ItemEnumerationTask.swift +++ b/CryptomatorFileProvider/CloudTask/ItemEnumerationTask.swift @@ -6,15 +6,14 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // -import Foundation -import GRDB +import CryptomatorCloudAccessCore struct ItemEnumerationTask: CloudTask { let taskRecord: ItemEnumerationTaskRecord let itemMetadata: ItemMetadata + let cloudPath: CloudPath - enum CodingKeys: String, CodingKey { - case taskRecord = "itemEnumerationTask" - case itemMetadata + func with(cloudPath: CloudPath) -> ItemEnumerationTask { + return ItemEnumerationTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath) } } diff --git a/CryptomatorFileProvider/CloudTask/ReparentTask.swift b/CryptomatorFileProvider/CloudTask/ReparentTask.swift index d671bad13..56d027d12 100644 --- a/CryptomatorFileProvider/CloudTask/ReparentTask.swift +++ b/CryptomatorFileProvider/CloudTask/ReparentTask.swift @@ -6,14 +6,14 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // -import GRDB +import CryptomatorCloudAccessCore -struct ReparentTask: CloudTask, FetchableRecord, Decodable { +struct ReparentTask: CloudTask { let taskRecord: ReparentTaskRecord let itemMetadata: ItemMetadata + let cloudPath: CloudPath - enum CodingKeys: String, CodingKey { - case taskRecord = "reparentTask" - case itemMetadata + func with(cloudPath: CloudPath, taskRecord: ReparentTaskRecord) -> ReparentTask { + return ReparentTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath) } } diff --git a/CryptomatorFileProvider/CloudTask/UploadTask.swift b/CryptomatorFileProvider/CloudTask/UploadTask.swift index ac604cc9a..729c0003d 100644 --- a/CryptomatorFileProvider/CloudTask/UploadTask.swift +++ b/CryptomatorFileProvider/CloudTask/UploadTask.swift @@ -6,11 +6,16 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Foundation -import GRDB struct UploadTask: CloudTask { let taskRecord: UploadTaskRecord let itemMetadata: ItemMetadata + let cloudPath: CloudPath let onURLSessionTaskCreation: URLSessionTaskCreationClosure? + + func with(cloudPath: CloudPath) -> UploadTask { + return UploadTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: onURLSessionTaskCreation) + } } diff --git a/CryptomatorFileProvider/DB/DatabaseHelper.swift b/CryptomatorFileProvider/DB/DatabaseHelper.swift index 0a182e01f..98ef24364 100644 --- a/CryptomatorFileProvider/DB/DatabaseHelper.swift +++ b/CryptomatorFileProvider/DB/DatabaseHelper.swift @@ -209,6 +209,32 @@ public struct DatabaseHelper: DatabaseHelping { ) """) } + migrator.registerMigration("v5", foreignKeyChecks: .deferred) { db in + // SQLite refuses DROP COLUMN on UNIQUE-constrained columns, so we must rebuild to drop `cloudPath`. + try db.execute(sql: """ + CREATE TABLE itemMetadata_tmp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + size INTEGER, + parentID INTEGER REFERENCES itemMetadata(id) ON DELETE CASCADE, + lastModifiedDate DATE, + statusCode TEXT NOT NULL, + isPlaceholderItem BOOLEAN NOT NULL DEFAULT 0, + isMaybeOutdated BOOLEAN NOT NULL DEFAULT 0, + favoriteRank INTEGER, + tagData BLOB, + lastEnumeratedAt DATE + ) + """) + try db.execute(sql: """ + INSERT INTO itemMetadata_tmp (id, name, type, size, parentID, lastModifiedDate, statusCode, isPlaceholderItem, isMaybeOutdated, favoriteRank, tagData, lastEnumeratedAt) + SELECT id, name, type, size, parentID, lastModifiedDate, statusCode, isPlaceholderItem, isMaybeOutdated, favoriteRank, tagData, lastEnumeratedAt FROM itemMetadata + """) + try db.execute(sql: "DROP TABLE itemMetadata") + try db.execute(sql: "ALTER TABLE itemMetadata_tmp RENAME TO itemMetadata") + try db.execute(sql: "CREATE INDEX itemMetadata_parentID_idx ON itemMetadata(parentID)") + } try migrator.migrate(dbWriter) } diff --git a/CryptomatorFileProvider/DB/DeletionTaskDBManager.swift b/CryptomatorFileProvider/DB/DeletionTaskDBManager.swift index 0c786ba0c..6bca6caf5 100644 --- a/CryptomatorFileProvider/DB/DeletionTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/DeletionTaskDBManager.swift @@ -6,11 +6,12 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Foundation import GRDB protocol DeletionTaskManager { - func createTaskRecord(for item: ItemMetadata) throws -> DeletionTaskRecord + func createTaskRecord(for item: ItemMetadata, cloudPath: CloudPath) throws -> DeletionTaskRecord func getTaskRecord(for id: Int64) throws -> DeletionTaskRecord func removeTaskRecord(_ task: DeletionTaskRecord) throws func getTaskRecordsForItemsWhichWere(in parentID: Int64) throws -> [DeletionTaskRecord] @@ -27,9 +28,12 @@ class DeletionTaskDBManager: DeletionTaskManager { } } - func createTaskRecord(for item: ItemMetadata) throws -> DeletionTaskRecord { - try database.write { db in - let task = DeletionTaskRecord(correspondingItem: item.id!, cloudPath: item.cloudPath, parentID: item.parentID, itemType: item.type) + func createTaskRecord(for item: ItemMetadata, cloudPath: CloudPath) throws -> DeletionTaskRecord { + guard let id = item.id else { + throw DBManagerError.nonSavedItemMetadata + } + return try database.write { db in + let task = DeletionTaskRecord(correspondingItem: id, cloudPath: cloudPath, parentID: item.parentID, itemType: item.type) try task.save(db) return task } @@ -63,7 +67,7 @@ class DeletionTaskDBManager: DeletionTaskManager { guard let itemMetadata = try taskRecord.itemMetadata.fetchOne(db) else { throw DBManagerError.missingItemMetadata } - return DeletionTask(taskRecord: taskRecord, itemMetadata: itemMetadata) + return DeletionTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: taskRecord.cloudPath) } } } diff --git a/CryptomatorFileProvider/DB/DownloadTaskDBManager.swift b/CryptomatorFileProvider/DB/DownloadTaskDBManager.swift index ee94e818d..18bcb79e8 100644 --- a/CryptomatorFileProvider/DB/DownloadTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/DownloadTaskDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Foundation import GRDB @@ -16,19 +17,25 @@ protocol DownloadTaskManager { class DownloadTaskDBManager: DownloadTaskManager { private let database: DatabaseWriter + private let itemMetadataManager: ItemMetadataManager - init(database: DatabaseWriter) throws { + init(database: DatabaseWriter, itemMetadataManager: ItemMetadataManager) throws { self.database = database + self.itemMetadataManager = itemMetadataManager _ = try database.write { db in - try ItemEnumerationTaskRecord.deleteAll(db) + try DownloadTaskRecord.deleteAll(db) } } func createTask(for item: ItemMetadata, replaceExisting: Bool, localURL: URL, onURLSessionTaskCreation: URLSessionTaskCreationClosure?) throws -> DownloadTask { - try database.write { db in - let taskRecord = DownloadTaskRecord(correspondingItem: item.id!, replaceExisting: replaceExisting, localURL: localURL) + guard let id = item.id else { + throw DBManagerError.nonSavedItemMetadata + } + let cloudPath = try itemMetadataManager.getCloudPath(for: id) + return try database.write { db in + let taskRecord = DownloadTaskRecord(correspondingItem: id, replaceExisting: replaceExisting, localURL: localURL) try taskRecord.save(db) - return DownloadTask(taskRecord: taskRecord, itemMetadata: item, onURLSessionTaskCreation: onURLSessionTaskCreation) + return DownloadTask(taskRecord: taskRecord, itemMetadata: item, cloudPath: cloudPath, onURLSessionTaskCreation: onURLSessionTaskCreation) } } diff --git a/CryptomatorFileProvider/DB/ItemEnumerationTaskDBManager.swift b/CryptomatorFileProvider/DB/ItemEnumerationTaskDBManager.swift index aaab7c6e5..aafa33d91 100644 --- a/CryptomatorFileProvider/DB/ItemEnumerationTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/ItemEnumerationTaskDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Foundation import GRDB @@ -16,19 +17,25 @@ protocol ItemEnumerationTaskManager { class ItemEnumerationTaskDBManager: ItemEnumerationTaskManager { private let database: DatabaseWriter + private let itemMetadataManager: ItemMetadataManager - init(database: DatabaseWriter) throws { + init(database: DatabaseWriter, itemMetadataManager: ItemMetadataManager) throws { self.database = database + self.itemMetadataManager = itemMetadataManager _ = try database.write { db in try ItemEnumerationTaskRecord.deleteAll(db) } } func createTask(for item: ItemMetadata, pageToken: String?) throws -> ItemEnumerationTask { - try database.write { db in - let taskRecord = ItemEnumerationTaskRecord(correspondingItem: item.id!, pageToken: pageToken) + guard let id = item.id else { + throw DBManagerError.nonSavedItemMetadata + } + let cloudPath = try itemMetadataManager.getCloudPath(for: id) + return try database.write { db in + let taskRecord = ItemEnumerationTaskRecord(correspondingItem: id, pageToken: pageToken) try taskRecord.save(db) - return ItemEnumerationTask(taskRecord: taskRecord, itemMetadata: item) + return ItemEnumerationTask(taskRecord: taskRecord, itemMetadata: item, cloudPath: cloudPath) } } diff --git a/CryptomatorFileProvider/DB/ItemMetadata.swift b/CryptomatorFileProvider/DB/ItemMetadata.swift index 2625dff18..c21c1a02e 100644 --- a/CryptomatorFileProvider/DB/ItemMetadata.swift +++ b/CryptomatorFileProvider/DB/ItemMetadata.swift @@ -26,7 +26,6 @@ public class ItemMetadata: Record, Codable { var parentID: Int64 var lastModifiedDate: Date? var statusCode: ItemStatus - var cloudPath: CloudPath var isPlaceholderItem: Bool var isMaybeOutdated: Bool var favoriteRank: Int64? @@ -41,7 +40,6 @@ public class ItemMetadata: Record, Codable { self.parentID = row[Columns.parentID] self.lastModifiedDate = row[Columns.lastModifiedDate] self.statusCode = row[Columns.statusCode] - self.cloudPath = row[Columns.cloudPath] self.isPlaceholderItem = row[Columns.isPlaceholderItem] self.isMaybeOutdated = row[Columns.isMaybeOutdated] self.favoriteRank = row[Columns.favoriteRank] @@ -51,10 +49,10 @@ public class ItemMetadata: Record, Codable { } convenience init(item: CloudItemMetadata, withParentID parentID: Int64, isPlaceholderItem: Bool = false) { - self.init(name: item.name, type: item.itemType, size: item.size, parentID: parentID, lastModifiedDate: item.lastModifiedDate, statusCode: .isUploaded, cloudPath: item.cloudPath, isPlaceholderItem: isPlaceholderItem) + self.init(name: item.name, type: item.itemType, size: item.size, parentID: parentID, lastModifiedDate: item.lastModifiedDate, statusCode: .isUploaded, isPlaceholderItem: isPlaceholderItem) } - init(id: Int64? = nil, name: String, type: CloudItemType, size: Int?, parentID: Int64, lastModifiedDate: Date?, statusCode: ItemStatus, cloudPath: CloudPath, isPlaceholderItem: Bool, isCandidateForCacheCleanup: Bool = false, favoriteRank: Int64? = nil, tagData: Data? = nil, lastEnumeratedAt: Date? = nil) { + init(id: Int64? = nil, name: String, type: CloudItemType, size: Int?, parentID: Int64, lastModifiedDate: Date?, statusCode: ItemStatus, isPlaceholderItem: Bool, isCandidateForCacheCleanup: Bool = false, favoriteRank: Int64? = nil, tagData: Data? = nil, lastEnumeratedAt: Date? = nil) { self.id = id self.name = name self.type = type @@ -62,7 +60,6 @@ public class ItemMetadata: Record, Codable { self.parentID = parentID self.lastModifiedDate = lastModifiedDate self.statusCode = statusCode - self.cloudPath = cloudPath self.isPlaceholderItem = isPlaceholderItem self.isMaybeOutdated = isCandidateForCacheCleanup self.favoriteRank = favoriteRank @@ -83,7 +80,6 @@ public class ItemMetadata: Record, Codable { container[Columns.parentID] = parentID container[Columns.lastModifiedDate] = lastModifiedDate container[Columns.statusCode] = statusCode - container[Columns.cloudPath] = cloudPath container[Columns.isPlaceholderItem] = isPlaceholderItem container[Columns.isMaybeOutdated] = isMaybeOutdated container[Columns.favoriteRank] = favoriteRank @@ -92,7 +88,7 @@ public class ItemMetadata: Record, Codable { } enum Columns: String, ColumnExpression { - case id, name, type, size, parentID, lastModifiedDate, statusCode, cloudPath, isPlaceholderItem, isMaybeOutdated, favoriteRank, tagData, lastEnumeratedAt + case id, name, type, size, parentID, lastModifiedDate, statusCode, isPlaceholderItem, isMaybeOutdated, favoriteRank, tagData, lastEnumeratedAt } static func filterWorkingSet() -> QueryInterfaceRequest { @@ -103,6 +99,6 @@ public class ItemMetadata: Record, Codable { extension ItemStatus: DatabaseValueConvertible {} extension ItemMetadata: Equatable { public static func == (lhs: ItemMetadata, rhs: ItemMetadata) -> Bool { - lhs.id == rhs.id && lhs.name == rhs.name && lhs.type == rhs.type && lhs.size == rhs.size && lhs.parentID == rhs.parentID && lhs.lastModifiedDate == rhs.lastModifiedDate && lhs.statusCode == rhs.statusCode && lhs.cloudPath == rhs.cloudPath && lhs.isPlaceholderItem == rhs.isPlaceholderItem && lhs.isMaybeOutdated == rhs.isMaybeOutdated && lhs.favoriteRank == rhs.favoriteRank && lhs.tagData == rhs.tagData && lhs.lastEnumeratedAt == rhs.lastEnumeratedAt + lhs.id == rhs.id && lhs.name == rhs.name && lhs.type == rhs.type && lhs.size == rhs.size && lhs.parentID == rhs.parentID && lhs.lastModifiedDate == rhs.lastModifiedDate && lhs.statusCode == rhs.statusCode && lhs.isPlaceholderItem == rhs.isPlaceholderItem && lhs.isMaybeOutdated == rhs.isMaybeOutdated && lhs.favoriteRank == rhs.favoriteRank && lhs.tagData == rhs.tagData && lhs.lastEnumeratedAt == rhs.lastEnumeratedAt } } diff --git a/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift b/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift index 0f7ddc189..14e6e7ea2 100644 --- a/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift +++ b/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift @@ -15,6 +15,8 @@ protocol ItemMetadataManager { func cacheMetadata(_ metadata: ItemMetadata) throws func updateMetadata(_ metadata: ItemMetadata) throws func cacheMetadata(_ metadataList: [ItemMetadata]) throws + /// Resolves the cloud path by walking the `parentID` chain to root. + func getCloudPath(for id: Int64) throws -> CloudPath /** Returns the item metadata that has the same path. @@ -32,7 +34,7 @@ protocol ItemMetadataManager { func removeItemMetadata(_ ids: [Int64]) throws func getCachedMetadata(forIDs ids: [Int64]) throws -> [ItemMetadata] /** - Returns the items that have the item as parent because of its cloud path. This also includes all subfolders including their items. + Returns every descendant of the given folder, walking the `parentID` chain downward (deep, not just direct children). */ func getAllCachedMetadata(inside parent: ItemMetadata) throws -> [ItemMetadata] // Returns all items that have a `favoriteRank` or `tagData`. @@ -69,9 +71,15 @@ class ItemMetadataDBManager: ItemMetadataManager { } } + func getCloudPath(for id: Int64) throws -> CloudPath { + return try database.read { db in + try resolveCloudPath(for: id, database: db) + } + } + func getCachedMetadata(for cloudPath: CloudPath) throws -> ItemMetadata? { return try database.read { db in - return try ItemMetadata.filter(ItemMetadata.Columns.cloudPath.lowercased == cloudPath.path.lowercased()).fetchOne(db) + try resolveMetadata(for: cloudPath, database: db) } } @@ -135,13 +143,21 @@ class ItemMetadataDBManager: ItemMetadataManager { func getAllCachedMetadata(inside parent: ItemMetadata) throws -> [ItemMetadata] { precondition(parent.type == .folder) return try database.read { db in - let request: QueryInterfaceRequest - if parent.id == NSFileProviderItemIdentifier.rootContainerDatabaseValue { - request = ItemMetadata.filter(ItemMetadata.Columns.id != NSFileProviderItemIdentifier.rootContainerDatabaseValue) - } else { - request = ItemMetadata.filter(ItemMetadata.Columns.cloudPath.like("\(parent.cloudPath.path + "/")_%")) + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + if parent.id == rootID { + return try ItemMetadata.filter(ItemMetadata.Columns.id != rootID).fetchAll(db) } - return try request.fetchAll(db) + return try ItemMetadata.fetchAll(db, sql: """ + WITH RECURSIVE descendants(id, depth) AS ( + SELECT id, 0 FROM itemMetadata WHERE parentID = ? AND id != parentID + UNION ALL + SELECT m.id, d.depth + 1 + FROM itemMetadata m + JOIN descendants d ON m.parentID = d.id + WHERE d.depth < 1024 + ) + SELECT * FROM itemMetadata WHERE id IN (SELECT id FROM descendants) + """, arguments: [parent.id!]) } } @@ -179,13 +195,70 @@ class ItemMetadataDBManager: ItemMetadataManager { return try ItemMetadata.fetchOne(database, key: id) } + private func childOfFolder(parentID: Int64, name: String, database: Database) throws -> ItemMetadata? { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let lowercasedName = name.lowercased() + let siblings = try ItemMetadata + .filter(ItemMetadata.Columns.parentID == parentID && ItemMetadata.Columns.id != rootID) + .fetchAll(database) + return siblings.first { $0.name.lowercased() == lowercasedName } + } + + private func resolveMetadata(for cloudPath: CloudPath, database db: Database) throws -> ItemMetadata? { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + if cloudPath == CloudPath("/") { + return try ItemMetadata.fetchOne(db, key: rootID) + } + let components = cloudPath.pathComponents.dropFirst() + var currentID = rootID + var current: ItemMetadata? + for component in components { + guard let child = try childOfFolder(parentID: currentID, name: String(component), database: db) else { + return nil + } + currentID = child.id! + current = child + } + return current + } + + private func resolveCloudPath(for id: Int64, database db: Database) throws -> CloudPath { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + if id == rootID { + return CloudPath("/") + } + let rows = try Row.fetchAll(db, sql: """ + WITH RECURSIVE ancestors(id, parentID, name, depth) AS ( + SELECT id, parentID, name, 0 FROM itemMetadata WHERE id = ? + UNION ALL + SELECT m.id, m.parentID, m.name, a.depth + 1 + FROM itemMetadata m + JOIN ancestors a ON m.id = a.parentID + WHERE a.id != 1 AND a.depth < 1024 + ) + SELECT id, name, depth FROM ancestors ORDER BY depth DESC + """, arguments: [id]) + guard !rows.isEmpty else { + throw FileProviderAdapterError.itemNotFound + } + // Chain must terminate at root; otherwise it's a cycle or orphan. + let topRow = rows.first! + let topID: Int64 = topRow["id"] + if topID != rootID { + throw FileProviderAdapterError.unresolvableParentChain + } + let names: [String] = rows.dropFirst().map { $0["name"] } + return names.reduce(CloudPath("/")) { $0.appendingPathComponent($1) } + } + private func cacheMetadata(_ metadata: ItemMetadata, database: Database) throws { - if let cachedMetadata = try ItemMetadata.fetchOne(database, key: ["cloudPath": metadata.cloudPath]) { - metadata.id = cachedMetadata.id - metadata.statusCode = cachedMetadata.statusCode - metadata.tagData = cachedMetadata.tagData - metadata.favoriteRank = cachedMetadata.favoriteRank - metadata.lastEnumeratedAt = cachedMetadata.lastEnumeratedAt + let cached = try childOfFolder(parentID: metadata.parentID, name: metadata.name, database: database) + if let cached = cached { + metadata.id = cached.id + metadata.statusCode = cached.statusCode + metadata.tagData = cached.tagData + metadata.favoriteRank = cached.favoriteRank + metadata.lastEnumeratedAt = cached.lastEnumeratedAt try metadata.update(database) } else { try metadata.insert(database) diff --git a/CryptomatorFileProvider/DB/ReparentTaskDBManager.swift b/CryptomatorFileProvider/DB/ReparentTaskDBManager.swift index b71058cf4..ab656c2e0 100644 --- a/CryptomatorFileProvider/DB/ReparentTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/ReparentTaskDBManager.swift @@ -11,7 +11,7 @@ import Foundation import GRDB protocol ReparentTaskManager { - func createTaskRecord(for itemMetadata: ItemMetadata, targetCloudPath: CloudPath, newParentID: Int64) throws -> ReparentTaskRecord + func createTaskRecord(for itemMetadata: ItemMetadata, sourceCloudPath: CloudPath, targetCloudPath: CloudPath, newParentID: Int64) throws -> ReparentTaskRecord func removeTaskRecord(_ task: ReparentTaskRecord) throws func getTaskRecordsForItemsWhichWere(in parentID: Int64) throws -> [ReparentTaskRecord] func getTaskRecordsForItemsWhichAreSoon(in parentID: Int64) throws -> [ReparentTaskRecord] @@ -28,12 +28,12 @@ class ReparentTaskDBManager: ReparentTaskManager { } } - func createTaskRecord(for itemMetadata: ItemMetadata, targetCloudPath: CloudPath, newParentID: Int64) throws -> ReparentTaskRecord { + func createTaskRecord(for itemMetadata: ItemMetadata, sourceCloudPath: CloudPath, targetCloudPath: CloudPath, newParentID: Int64) throws -> ReparentTaskRecord { guard let id = itemMetadata.id else { throw DBManagerError.nonSavedItemMetadata } return try database.write { db in - let task = ReparentTaskRecord(correspondingItem: id, sourceCloudPath: itemMetadata.cloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: newParentID) + let task = ReparentTaskRecord(correspondingItem: id, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: newParentID) try task.save(db) return task } @@ -66,7 +66,7 @@ class ReparentTaskDBManager: ReparentTaskManager { guard let itemMetadata = try taskRecord.itemMetadata.fetchOne(db) else { throw DBManagerError.missingItemMetadata } - return ReparentTask(taskRecord: taskRecord, itemMetadata: itemMetadata) + return ReparentTask(taskRecord: taskRecord, itemMetadata: itemMetadata, cloudPath: taskRecord.sourceCloudPath) } } } diff --git a/CryptomatorFileProvider/DB/UploadTaskDBManager.swift b/CryptomatorFileProvider/DB/UploadTaskDBManager.swift index ab104ca88..ad20c8783 100644 --- a/CryptomatorFileProvider/DB/UploadTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/UploadTaskDBManager.swift @@ -58,9 +58,11 @@ extension UploadTaskManager { class UploadTaskDBManager: UploadTaskManager { private let database: DatabaseWriter + private let itemMetadataManager: ItemMetadataManager - init(database: DatabaseWriter) { + init(database: DatabaseWriter, itemMetadataManager: ItemMetadataManager) { self.database = database + self.itemMetadataManager = itemMetadataManager } func createNewTaskRecord(for itemMetadata: ItemMetadata) throws -> UploadTaskRecord { @@ -114,12 +116,17 @@ class UploadTaskDBManager: UploadTaskManager { } func getTask(for uploadTask: UploadTaskRecord, onURLSessionTaskCreation: URLSessionTaskCreationClosure?) throws -> UploadTask { - return try database.read { db in - guard let itemMetadata = try uploadTask.itemMetadata.fetchOne(db) else { + let itemMetadata: ItemMetadata = try database.read { db in + guard let metadata = try uploadTask.itemMetadata.fetchOne(db) else { throw DBManagerError.missingItemMetadata } - return UploadTask(taskRecord: uploadTask, itemMetadata: itemMetadata, onURLSessionTaskCreation: onURLSessionTaskCreation) + return metadata + } + guard let id = itemMetadata.id else { + throw DBManagerError.nonSavedItemMetadata } + let cloudPath = try itemMetadataManager.getCloudPath(for: id) + return UploadTask(taskRecord: uploadTask, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: onURLSessionTaskCreation) } func getActiveUploadTaskRecords() throws -> [UploadTaskRecord] { diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index 3bf92e0b9..bce269ec5 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -478,7 +478,14 @@ public class FileProviderAdapter: FileProviderAdapterType { return completionHandler(nil, error) } - let task = FolderCreationTask(itemMetadata: placeholderItem.metadata) + let task: FolderCreationTask + do { + let cloudPath = try itemMetadataManager.getCloudPath(for: placeholderItem.metadata.id!) + task = FolderCreationTask(itemMetadata: placeholderItem.metadata, cloudPath: cloudPath) + } catch { + DDLogError("Create directory: getCloudPath failed with error: \(error)") + return completionHandler(nil, error) + } workflowFactory.createWorkflow(for: task).then { workflow -> Workflow in completionHandler(placeholderItem, nil) return workflow @@ -568,12 +575,12 @@ public class FileProviderAdapter: FileProviderAdapterType { name = itemMetadata.name } - let cloudPath = try getCloudPathForPlaceholderItem(withName: name, in: parentID, type: itemMetadata.type) - try checkLocalItemCollision(for: cloudPath) - let taskRecord = try reparentTaskManager.createTaskRecord(for: itemMetadata, targetCloudPath: cloudPath, newParentID: parentID) + let sourceCloudPath = try itemMetadataManager.getCloudPath(for: itemMetadata.id!) + let targetCloudPath = try getCloudPathForPlaceholderItem(withName: name, in: parentID, type: itemMetadata.type) + try checkLocalItemCollision(for: targetCloudPath) + let taskRecord = try reparentTaskManager.createTaskRecord(for: itemMetadata, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, newParentID: parentID) itemMetadata.name = name - itemMetadata.cloudPath = cloudPath itemMetadata.parentID = parentID itemMetadata.statusCode = .isUploading try itemMetadataManager.updateMetadata(itemMetadata) @@ -636,9 +643,11 @@ public class FileProviderAdapter: FileProviderAdapterType { */ func deleteItemLocally(withIdentifier itemIdentifier: NSFileProviderItemIdentifier) throws -> DeletionTaskRecord { let itemMetadata = try getCachedMetadata(for: itemIdentifier) + // Resolve the cloud path BEFORE removeItemFromCache deletes the row — afterwards the resolver would fail. + let cloudPath = try itemMetadataManager.getCloudPath(for: itemMetadata.id!) let deletionHelper = DeleteItemHelper(itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager) try deletionHelper.removeItemFromCache(itemMetadata) - return try deletionTaskManager.createTaskRecord(for: itemMetadata) + return try deletionTaskManager.createTaskRecord(for: itemMetadata, cloudPath: cloudPath) } // MARK: Start Providing Item @@ -849,7 +858,7 @@ public class FileProviderAdapter: FileProviderAdapterType { let name = localURL.lastPathComponent.precomposedStringWithCanonicalMapping let cloudPath = try getCloudPathForPlaceholderItem(withName: name, in: parentID, type: .file) try checkLocalItemCollision(for: cloudPath) - let placeholderMetadata = ItemMetadata(name: name, type: .file, size: size, parentID: parentID, lastModifiedDate: lastModifiedDate, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true) + let placeholderMetadata = ItemMetadata(name: name, type: .file, size: size, parentID: parentID, lastModifiedDate: lastModifiedDate, statusCode: .isUploading, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(placeholderMetadata) return placeholderMetadata } @@ -868,7 +877,7 @@ public class FileProviderAdapter: FileProviderAdapterType { let normalizedName = name.precomposedStringWithCanonicalMapping let cloudPath = try getCloudPathForPlaceholderItem(withName: normalizedName, in: parentID, type: .folder) try checkLocalItemCollision(for: cloudPath) - let placeholderMetadata = ItemMetadata(name: normalizedName, type: .folder, size: nil, parentID: parentID, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true) + let placeholderMetadata = ItemMetadata(name: normalizedName, type: .folder, size: nil, parentID: parentID, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(placeholderMetadata) return FileProviderItem(metadata: placeholderMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true) } @@ -882,7 +891,7 @@ public class FileProviderAdapter: FileProviderAdapterType { guard let parentItemMetadata = try itemMetadataManager.getCachedMetadata(for: parentID), parentItemMetadata.type == .folder else { throw FileProviderAdapterError.parentFolderNotFound } - let parentCloudPath = parentItemMetadata.cloudPath + let parentCloudPath = try itemMetadataManager.getCloudPath(for: parentID) return parentCloudPath.appendingPathComponent(name) } @@ -954,17 +963,16 @@ public class FileProviderAdapter: FileProviderAdapterType { return parentItemIdentifier.then { self.enumerateItemsExtensively(for: $0) - }.then { itemList -> NSFileProviderItemIdentifier in - let items = itemList.items - guard let item = items.first(where: { $0.metadata.cloudPath == cloudPath }) else { + }.then { _ -> NSFileProviderItemIdentifier in + guard let metadata = try self.itemMetadataManager.getCachedMetadata(for: cloudPath) else { throw NSFileProviderError(.noSuchItem) } - return item.itemIdentifier + return NSFileProviderItemIdentifier(domainIdentifier: self.domainIdentifier, itemID: metadata.id!) } } func enumerateItemsExtensively(for identifier: NSFileProviderItemIdentifier, withPageToken pageToken: String? = nil) -> Promise { - return enumerateItems(for: identifier, withPageToken: pageToken).then { itemList -> Promisein + return enumerateItems(for: identifier, withPageToken: pageToken).then { itemList -> Promise in if let pageToken = pageToken { return self.enumerateItemsExtensively(for: identifier, withPageToken: pageToken) } diff --git a/CryptomatorFileProvider/FileProviderAdapterError.swift b/CryptomatorFileProvider/FileProviderAdapterError.swift index a2e23434f..06387f2e0 100644 --- a/CryptomatorFileProvider/FileProviderAdapterError.swift +++ b/CryptomatorFileProvider/FileProviderAdapterError.swift @@ -14,4 +14,6 @@ enum FileProviderAdapterError: Error { case parentFolderNotFound case parentItemTypeMismatch case unsupportedItemType + case itemNotFound + case unresolvableParentChain } diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index 59461809d..0e970ab29 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -179,11 +179,11 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { let itemMetadataManager = ItemMetadataDBManager(database: database) let cachedFileManager = CachedFileDBManager(database: database, fileManagerHelper: .init(fileCoordinator: fileCoordinator)) - let uploadTaskManager = UploadTaskDBManager(database: database) + let uploadTaskManager = UploadTaskDBManager(database: database, itemMetadataManager: itemMetadataManager) let reparentTaskManager = try ReparentTaskDBManager(database: database) let deletionTaskManager = try DeletionTaskDBManager(database: database) - let itemEnumerationTaskManager = try ItemEnumerationTaskDBManager(database: database) - let downloadTaskManager = try DownloadTaskDBManager(database: database) + let itemEnumerationTaskManager = try ItemEnumerationTaskDBManager(database: database, itemMetadataManager: itemMetadataManager) + let downloadTaskManager = try DownloadTaskDBManager(database: database, itemMetadataManager: itemMetadataManager) let maintenanceManager = MaintenanceDBManager(database: database) let adapter = FileProviderAdapter(domainIdentifier: domainIdentifier, diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index e74124db9..b849bddcc 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -88,7 +88,7 @@ public class FileProviderItem: NSObject, NSFileProviderItem { case .folder: return kUTTypeFolder as String default: - if let typeIdentifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, metadata.cloudPath.pathExtension as CFString, nil) { + if let typeIdentifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (metadata.name as NSString).pathExtension as CFString, nil) { let uti = typeIdentifier.takeRetainedValue() as String // Reject dynamic created UTI and use generic UTI (kUTTypeData) instead. See: https://github.com/cryptomator/ios/issues/67#issuecomment-898371262 if uti.hasPrefix("dyn.") { diff --git a/CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift b/CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift index e6ab7f710..03e661a03 100644 --- a/CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift +++ b/CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift @@ -40,15 +40,30 @@ class OnlineItemNameCollisionHandler: WorkflowMiddleware { guard case CloudProviderError.itemAlreadyExists = error else { throw error } - let collisionFreeCloudPath = task.itemMetadata.cloudPath.createCollisionCloudPath() - try self.cloudPathCollisionUpdate(with: collisionFreeCloudPath, itemMetadata: task.itemMetadata) - return nextMiddleware.execute(task: task) + let refreshedTask = try self.cloudPathCollisionUpdate(for: task) + return nextMiddleware.execute(task: refreshedTask) } } - func cloudPathCollisionUpdate(with collisionFreeCloudPath: CloudPath, itemMetadata: ItemMetadata) throws { - itemMetadata.name = collisionFreeCloudPath.lastPathComponent - itemMetadata.cloudPath = collisionFreeCloudPath - try itemMetadataManager.updateMetadata(itemMetadata) + func cloudPathCollisionUpdate(for task: CloudTask) throws -> CloudTask { + let collisionFreeCloudPath = task.cloudPath.createCollisionCloudPath() + task.itemMetadata.name = collisionFreeCloudPath.lastPathComponent + try itemMetadataManager.updateMetadata(task.itemMetadata) + switch task { + case let uploadTask as UploadTask: + return uploadTask.with(cloudPath: collisionFreeCloudPath) + case let downloadTask as DownloadTask: + return downloadTask.with(cloudPath: collisionFreeCloudPath) + case let folderCreationTask as FolderCreationTask: + return folderCreationTask.with(cloudPath: collisionFreeCloudPath) + case let itemEnumerationTask as ItemEnumerationTask: + return itemEnumerationTask.with(cloudPath: collisionFreeCloudPath) + case let deletionTask as DeletionTask: + return deletionTask.with(cloudPath: collisionFreeCloudPath) + case let reparentTask as ReparentTask: + return reparentTask.with(cloudPath: collisionFreeCloudPath, taskRecord: reparentTask.taskRecord) + default: + throw FileProviderAdapterError.unsupportedItemType + } } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/DeletionTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/DeletionTaskExecutor.swift index c77770efd..573444d52 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/DeletionTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/DeletionTaskExecutor.swift @@ -22,17 +22,17 @@ class DeletionTaskExecutor: WorkflowMiddleware { func execute(task: CloudTask) -> Promise { let itemMetadata = task.itemMetadata - return deleteItemInCloud(itemMetadata).then { + return deleteItemInCloud(type: itemMetadata.type, at: task.cloudPath).then { try self.itemMetadataManager.removeItemMetadata(with: itemMetadata.id!) } } - private func deleteItemInCloud(_ itemMetadata: ItemMetadata) -> Promise { - switch itemMetadata.type { + private func deleteItemInCloud(type: CloudItemType, at cloudPath: CloudPath) -> Promise { + switch type { case .file: - return provider.deleteFile(at: itemMetadata.cloudPath) + return provider.deleteFile(at: cloudPath) case .folder: - return provider.deleteFolder(at: itemMetadata.cloudPath) + return provider.deleteFolder(at: cloudPath) default: return Promise(FileProviderAdapterError.unsupportedItemType) } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift index 6e1c23446..c75be04f1 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift @@ -53,9 +53,10 @@ class DownloadTaskExecutor: WorkflowMiddleware { let downloadDestination = taskRecord.replaceExisting ? taskRecord.localURL.createCollisionURL() : taskRecord.localURL var lastModifiedDate: Date? let itemMetadata = task.itemMetadata - return provider.fetchItemMetadata(at: itemMetadata.cloudPath).then { cloudMetadata -> Promise in + let cloudPath = task.cloudPath + return provider.fetchItemMetadata(at: cloudPath).then { cloudMetadata -> Promise in lastModifiedDate = cloudMetadata.lastModifiedDate - return self.provider.downloadFile(from: itemMetadata.cloudPath, to: downloadDestination, onTaskCreation: { task in + return self.provider.downloadFile(from: cloudPath, to: downloadDestination, onTaskCreation: { task in guard let task else { return } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift index fd2623508..17807da33 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift @@ -55,7 +55,7 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { assert(itemMetadata.id != nil) assert(itemMetadata.type == .folder) - return provider.createFolder(at: itemMetadata.cloudPath).then { [domainIdentifier, itemMetadataManager] _ -> FileProviderItem in + return provider.createFolder(at: task.cloudPath).then { [domainIdentifier, itemMetadataManager] _ -> FileProviderItem in itemMetadata.statusCode = .isUploaded itemMetadata.isPlaceholderItem = false try itemMetadataManager.updateMetadata(itemMetadata) diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift index 706903ae7..c7ebe4741 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift @@ -63,13 +63,14 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { return Promise(WorkflowMiddlewareError.incompatibleCloudTask) } let itemMetadata = enumerationTask.itemMetadata + let cloudPath = enumerationTask.cloudPath let promise: Promise switch itemMetadata.type { case .folder: let taskRecord = enumerationTask.taskRecord - promise = fetchItemList(folderMetadata: itemMetadata, pageToken: taskRecord.pageToken) + promise = fetchItemList(folderMetadata: itemMetadata, folderCloudPath: cloudPath, pageToken: taskRecord.pageToken) case .file: - promise = fetchItemMetadata(fileMetadata: itemMetadata) + promise = fetchItemMetadata(fileMetadata: itemMetadata, fileCloudPath: cloudPath) default: DDLogError("Unable to enumerate items on metadata type: \(itemMetadata.type)") promise = Promise(NSFileProviderError(.noSuchItem)) @@ -84,8 +85,8 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { return promise } - func fetchItemList(folderMetadata: ItemMetadata, pageToken: String?) -> Promise { - return provider.fetchItemList(forFolderAt: folderMetadata.cloudPath, withPageToken: pageToken).then { itemList -> FileProviderItemList in + func fetchItemList(folderMetadata: ItemMetadata, folderCloudPath: CloudPath, pageToken: String?) -> Promise { + return provider.fetchItemList(forFolderAt: folderCloudPath, withPageToken: pageToken).then { itemList -> FileProviderItemList in if pageToken == nil { try self.itemMetadataManager.flagAllItemsAsMaybeOutdated(withParentID: folderMetadata.id!) } @@ -117,21 +118,21 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { guard pageToken == nil else { return Promise(error) } - return self.buildOfflineItemList(folderMetadata: folderMetadata, originalError: error) + return self.buildOfflineItemList(folderMetadata: folderMetadata, folderCloudPath: folderCloudPath, originalError: error) } } - private func buildOfflineItemList(folderMetadata: ItemMetadata, originalError: Error) -> Promise { + private func buildOfflineItemList(folderMetadata: ItemMetadata, folderCloudPath: CloudPath, originalError: Error) -> Promise { do { guard folderMetadata.lastEnumeratedAt != nil else { return Promise(originalError) } let cachedMetadata = try itemMetadataManager.getCachedMetadata(withParentID: folderMetadata.id!) let items = try FileProviderItem.items(from: cachedMetadata, domainIdentifier: domainIdentifier, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager) - DDLogInfo("Offline fallback: serving \(items.count) cached items for folder \(folderMetadata.cloudPath.path)") + DDLogInfo("Offline fallback: serving \(items.count) cached items for folder \(folderCloudPath.path)") return Promise(FileProviderItemList(items: items, nextPageToken: nil)) } catch { - DDLogError("Offline fallback failed for folder \(folderMetadata.cloudPath.path): \(error)") + DDLogError("Offline fallback failed for folder \(folderCloudPath.path): \(error)") return Promise(error) } } @@ -144,19 +145,19 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { func filterOutWaitingReparentTasks(parentID: Int64, for itemMetadata: [ItemMetadata]) throws -> [ItemMetadata] { let runningReparentTasks = try reparentTaskManager.getTaskRecordsForItemsWhichWere(in: parentID) return itemMetadata.filter { element in - !runningReparentTasks.contains { $0.sourceCloudPath == element.cloudPath } + !runningReparentTasks.contains { $0.oldParentID == element.parentID && $0.sourceCloudPath.lastPathComponent == element.name } } } func filterOutWaitingDeletionTasks(parentID: Int64, for itemMetadata: [ItemMetadata]) throws -> [ItemMetadata] { let runningDeletionTasks = try deletionTaskManager.getTaskRecordsForItemsWhichWere(in: parentID) return itemMetadata.filter { element in - !runningDeletionTasks.contains { $0.cloudPath == element.cloudPath } + !runningDeletionTasks.contains { $0.parentID == element.parentID && $0.cloudPath.lastPathComponent == element.name } } } - func fetchItemMetadata(fileMetadata: ItemMetadata) -> Promise { - return provider.fetchItemMetadata(at: fileMetadata.cloudPath).then { cloudItem -> FileProviderItemList in + func fetchItemMetadata(fileMetadata: ItemMetadata, fileCloudPath: CloudPath) -> Promise { + return provider.fetchItemMetadata(at: fileCloudPath).then { cloudItem -> FileProviderItemList in let fileProviderItemMetadata = ItemMetadata(item: cloudItem, withParentID: fileMetadata.parentID) try self.itemMetadataManager.cacheMetadata(fileProviderItemMetadata) assert(fileProviderItemMetadata.id == fileMetadata.id) diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift index 9963721fc..296a5d166 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift @@ -79,7 +79,7 @@ class UploadTaskExecutor: WorkflowMiddleware { } progress.becomeCurrent(withPendingUnitCount: 1) let uploadPromise = provider.uploadFile(from: localURL, - to: itemMetadata.cloudPath, + to: task.cloudPath, replaceExisting: !itemMetadata.isPlaceholderItem, onTaskCreation: { task in guard let task else { diff --git a/CryptomatorFileProviderTests/DB/CachedFileManagerTests.swift b/CryptomatorFileProviderTests/DB/CachedFileManagerTests.swift index cf305a7cd..21cb416ca 100644 --- a/CryptomatorFileProviderTests/DB/CachedFileManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/CachedFileManagerTests.swift @@ -61,7 +61,7 @@ class CachedFileManagerTests: CacheTestCase { func testRemoveCachedFile() throws { let url = tmpDirURL.appendingPathComponent("foo") - let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath(url.path), isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let data = getRandomData(sizeInBytes: 256) try createTestData(localURL: url, data: data, metadata: metadata) @@ -74,14 +74,14 @@ class CachedFileManagerTests: CacheTestCase { func testRemoveCachedFileForPendingUpload() throws { let url = tmpDirURL.appendingPathComponent("foo") - let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath(url.path), isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let data = getRandomData(sizeInBytes: 256) try createTestData(localURL: url, data: data, metadata: metadata) XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) // Simulate Pending Upload of file foo - let uploadManager = UploadTaskDBManager(database: inMemoryDB) + let uploadManager = UploadTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) _ = try uploadManager.createNewTaskRecord(for: metadata) XCTAssertThrowsError(try manager.removeCachedFile(for: XCTUnwrap(metadata.id))) { error in @@ -97,7 +97,7 @@ class CachedFileManagerTests: CacheTestCase { func testRemoveCachedFileForMissingFile() throws { let url = tmpDirURL.appendingPathComponent("foo") - let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath(url.path), isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let data = getRandomData(sizeInBytes: 256) try createTestData(localURL: url, data: data, metadata: metadata) try FileManager.default.removeItem(at: url) @@ -108,7 +108,7 @@ class CachedFileManagerTests: CacheTestCase { func testRemoveCachedFileForFailingFileRemoval() throws { let url = tmpDirURL.appendingPathComponent("foo") - let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath(url.path), isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let data = getRandomData(sizeInBytes: 256) let cachedFileManagerHelperMock = CachedFileManagerHelperMock() @@ -136,7 +136,7 @@ class CachedFileManagerTests: CacheTestCase { func testGetLocalCacheSizeInBytes() throws { let urls = [tmpDirURL.appendingPathComponent("foo"), tmpDirURL.appendingPathComponent("bar")] - let metadata = urls.enumerated().map { ItemMetadata(id: Int64($0 + 2), name: "Item", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath($1.path), isPlaceholderItem: false) } + let metadata = urls.enumerated().map { idx, url in ItemMetadata(id: Int64(idx + 2), name: url.lastPathComponent, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) } let data = getRandomData(sizeInBytes: 256) for (index, url) in urls.enumerated() { @@ -149,7 +149,7 @@ class CachedFileManagerTests: CacheTestCase { func testGetLocalCacheSizeInBytesFiltersOutPendingUploads() throws { let urls = [tmpDirURL.appendingPathComponent("foo"), tmpDirURL.appendingPathComponent("bar")] - let metadata = urls.enumerated().map { ItemMetadata(id: Int64($0 + 2), name: "Item", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath($1.path), isPlaceholderItem: false) } + let metadata = urls.enumerated().map { idx, url in ItemMetadata(id: Int64(idx + 2), name: url.lastPathComponent, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) } let data = getRandomData(sizeInBytes: 256) for (index, url) in urls.enumerated() { @@ -157,7 +157,7 @@ class CachedFileManagerTests: CacheTestCase { } // Simulate Pending Upload of file foo - let uploadManager = UploadTaskDBManager(database: inMemoryDB) + let uploadManager = UploadTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) _ = try uploadManager.createNewTaskRecord(for: metadata[0]) XCTAssertEqual(256, try manager.getLocalCacheSizeInBytes()) @@ -166,7 +166,7 @@ class CachedFileManagerTests: CacheTestCase { func testClearCache() throws { let urls = [tmpDirURL.appendingPathComponent("foo"), tmpDirURL.appendingPathComponent("bar")] - let metadata = urls.enumerated().map { ItemMetadata(id: Int64($0 + 2), name: "Item", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath($1.path), isPlaceholderItem: false) } + let metadata = urls.enumerated().map { idx, url in ItemMetadata(id: Int64(idx + 2), name: url.lastPathComponent, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) } let data = getRandomData(sizeInBytes: 256) for (index, url) in urls.enumerated() { @@ -185,7 +185,7 @@ class CachedFileManagerTests: CacheTestCase { let urls = [tmpDirURL.appendingPathComponent("foo"), tmpDirURL.appendingPathComponent("bar"), tmpDirURL.appendingPathComponent("baz")] - let metadata = urls.enumerated().map { ItemMetadata(id: Int64($0 + 2), name: "Item", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath($1.path), isPlaceholderItem: false) } + let metadata = urls.enumerated().map { idx, url in ItemMetadata(id: Int64(idx + 2), name: url.lastPathComponent, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) } let data = getRandomData(sizeInBytes: 256) let cachedFileManagerHelperMock = CachedFileManagerHelperMock() @@ -218,7 +218,7 @@ class CachedFileManagerTests: CacheTestCase { func testGetLocalCachedFileInfoBatch() throws { let urls = [tmpDirURL.appendingPathComponent("foo"), tmpDirURL.appendingPathComponent("bar")] - let metadata = urls.enumerated().map { ItemMetadata(id: Int64($0 + 2), name: "Item", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath($1.path), isPlaceholderItem: false) } + let metadata = urls.enumerated().map { idx, url in ItemMetadata(id: Int64(idx + 2), name: url.lastPathComponent, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) } let data = getRandomData(sizeInBytes: 256) for (index, url) in urls.enumerated() { diff --git a/CryptomatorFileProviderTests/DB/DeletionTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/DeletionTaskManagerTests.swift index c9dfdef85..c55ce90ef 100644 --- a/CryptomatorFileProviderTests/DB/DeletionTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/DeletionTaskManagerTests.swift @@ -24,9 +24,9 @@ class DeletionTaskManagerTests: XCTestCase { func testCreateAndGetTaskRecord() throws { let cloudPath = CloudPath("/Test") - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) - let createdTask = try manager.createTaskRecord(for: itemMetadata) + let createdTask = try manager.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let fetchedTask = try manager.getTaskRecord(for: XCTUnwrap(itemMetadata.id)) XCTAssertEqual(fetchedTask, createdTask) XCTAssertEqual(itemMetadata.id, fetchedTask.correspondingItem) @@ -36,9 +36,9 @@ class DeletionTaskManagerTests: XCTestCase { func testRemoveTaskRecord() throws { let cloudPath = CloudPath("/Test") - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) - let createdTask = try manager.createTaskRecord(for: itemMetadata) + let createdTask = try manager.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) try manager.removeTaskRecord(createdTask) XCTAssertThrowsError(try manager.getTaskRecord(for: XCTUnwrap(itemMetadata.id))) { error in guard case DBManagerError.taskNotFound = error else { @@ -50,21 +50,21 @@ class DeletionTaskManagerTests: XCTestCase { func testGetTaskRecordsWhichWereIn() throws { let cloudPath = CloudPath("/Test") - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try manager.createTaskRecord(for: itemMetadata) + _ = try manager.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let folderCloudPath = CloudPath("/Folder") - let folderMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: false) + let folderMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(folderMetadata) - _ = try manager.createTaskRecord(for: folderMetadata) + _ = try manager.createTaskRecord(for: folderMetadata, cloudPath: CloudPath("/").appendingPathComponent(folderMetadata.name)) let subfolderCloudPath = CloudPath("/Folder/SubFolder/") - let subfolderMetadata = try ItemMetadata(name: "SubFolder", type: .folder, size: nil, parentID: XCTUnwrap(folderMetadata.id), lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: subfolderCloudPath, isPlaceholderItem: false) + let subfolderMetadata = try ItemMetadata(name: "SubFolder", type: .folder, size: nil, parentID: XCTUnwrap(folderMetadata.id), lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(subfolderMetadata) - _ = try manager.createTaskRecord(for: subfolderMetadata) + _ = try manager.createTaskRecord(for: subfolderMetadata, cloudPath: subfolderCloudPath) let fileInsideFolderCloudPath = CloudPath("/Folder/FileInsideFolder") - let fileInsideFolderMetadata = try ItemMetadata(name: "FileInsideFolder", type: .file, size: nil, parentID: XCTUnwrap(folderMetadata.id), lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileInsideFolderCloudPath, isPlaceholderItem: false) + let fileInsideFolderMetadata = try ItemMetadata(name: "FileInsideFolder", type: .file, size: nil, parentID: XCTUnwrap(folderMetadata.id), lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(fileInsideFolderMetadata) - _ = try manager.createTaskRecord(for: fileInsideFolderMetadata) + _ = try manager.createTaskRecord(for: fileInsideFolderMetadata, cloudPath: fileInsideFolderCloudPath) let fetchedTasks = try manager.getTaskRecordsForItemsWhichWere(in: XCTUnwrap(folderMetadata.id)) XCTAssertEqual(2, fetchedTasks.count) XCTAssert(fetchedTasks.contains(where: { $0.correspondingItem == fileInsideFolderMetadata.id && $0.cloudPath == fileInsideFolderCloudPath })) @@ -73,9 +73,9 @@ class DeletionTaskManagerTests: XCTestCase { func testGetTask() throws { let cloudPath = CloudPath("/Test") - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) - let taskRecord = try manager.createTaskRecord(for: itemMetadata) + let taskRecord = try manager.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let fetchedTask = try manager.getTask(for: taskRecord) XCTAssertEqual(itemMetadata, fetchedTask.itemMetadata) XCTAssertEqual(itemMetadata.id, fetchedTask.taskRecord.correspondingItem) diff --git a/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift index e1fd5935a..7d8db1564 100644 --- a/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift @@ -19,7 +19,7 @@ class DownloadTaskManagerTests: XCTestCase { override func setUpWithError() throws { inMemoryDB = try DatabaseQueue() try DatabaseHelper.migrate(inMemoryDB) - manager = try DownloadTaskDBManager(database: inMemoryDB) + manager = try DownloadTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) itemMetadataManager = ItemMetadataDBManager(database: inMemoryDB) } @@ -32,6 +32,29 @@ class DownloadTaskManagerTests: XCTestCase { XCTAssert(fetchedTask.replaceExisting) } + func testInitWipesPendingDownloadTasksOnly() throws { + // Init must wipe only download tasks, not the enumeration table. + let freshDB = try DatabaseQueue() + try DatabaseHelper.migrate(freshDB) + let metadataManager = ItemMetadataDBManager(database: freshDB) + + let downloadItem = ItemMetadata(name: "DL.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let enumItem = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try metadataManager.cacheMetadata(downloadItem) + try metadataManager.cacheMetadata(enumItem) + try freshDB.write { db in + try DownloadTaskRecord(correspondingItem: downloadItem.id!, replaceExisting: false, localURL: URL(string: "/tmp/dl")!).save(db) + try ItemEnumerationTaskRecord(correspondingItem: enumItem.id!, pageToken: nil).save(db) + } + + _ = try DownloadTaskDBManager(database: freshDB, itemMetadataManager: metadataManager) + + try freshDB.read { db in + XCTAssertEqual(0, try DownloadTaskRecord.fetchCount(db), "init should wipe pending download tasks") + XCTAssertEqual(1, try ItemEnumerationTaskRecord.fetchCount(db), "init must not touch the enumeration table") + } + } + func testRemoveTaskRecord() throws { let createdTask = try createTestTask() try manager.removeTaskRecord(createdTask.taskRecord) @@ -45,7 +68,7 @@ class DownloadTaskManagerTests: XCTestCase { private func createTestTask() throws -> DownloadTask { let cloudPath = CloudPath("/Test") - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) let localURL = URL(string: "/Test")! return try manager.createTask(for: itemMetadata, replaceExisting: true, localURL: localURL, onURLSessionTaskCreation: nil) diff --git a/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift index 2a73e1cb0..0c378c158 100644 --- a/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift @@ -19,13 +19,13 @@ class ItemEnumerationTaskManagerTests: XCTestCase { override func setUpWithError() throws { inMemoryDB = try DatabaseQueue() try DatabaseHelper.migrate(inMemoryDB) - manager = try ItemEnumerationTaskDBManager(database: inMemoryDB) + manager = try ItemEnumerationTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) itemMetadataManager = ItemMetadataDBManager(database: inMemoryDB) } func testCreateAndGetTaskRecord() throws { let cloudPath = CloudPath("/Test") - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) let pageToken: String? = nil let createdTask = try manager.createTask(for: itemMetadata, pageToken: pageToken) diff --git a/CryptomatorFileProviderTests/DB/MaintenanceManagerTests.swift b/CryptomatorFileProviderTests/DB/MaintenanceManagerTests.swift index fdcc38c32..3a14202d8 100644 --- a/CryptomatorFileProviderTests/DB/MaintenanceManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/MaintenanceManagerTests.swift @@ -27,11 +27,11 @@ class MaintenanceManagerTests: XCTestCase { try DatabaseHelper.migrate(inMemoryDB) manager = MaintenanceDBManager(database: inMemoryDB) itemMetadataManager = ItemMetadataDBManager(database: inMemoryDB) - uploadTaskManager = UploadTaskDBManager(database: inMemoryDB) + uploadTaskManager = UploadTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) reparentTaskManager = try ReparentTaskDBManager(database: inMemoryDB) deletionTaskManager = try DeletionTaskDBManager(database: inMemoryDB) - itemEnumerationTaskManager = try ItemEnumerationTaskDBManager(database: inMemoryDB) - downloadTaskManager = try DownloadTaskDBManager(database: inMemoryDB) + itemEnumerationTaskManager = try ItemEnumerationTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) + downloadTaskManager = try DownloadTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) } func testPreventEnablingMaintenanceModeTwice() throws { @@ -45,7 +45,7 @@ class MaintenanceManagerTests: XCTestCase { // MARK: - Prevent the creation of New tasks when in maintenance mode func testPreventCreatingUploadTasks() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) // Prevent INSERT @@ -54,25 +54,25 @@ class MaintenanceManagerTests: XCTestCase { } func testPreventCreatingReparentTasks() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) // Prevent INSERT try manager.enableMaintenanceMode() - try checkThrowsMaintenanceError(reparentTaskManager.createTaskRecord(for: itemMetadata, targetCloudPath: CloudPath("Foo"), newParentID: 1)) + try checkThrowsMaintenanceError(reparentTaskManager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: CloudPath("Foo"), newParentID: 1)) } func testPreventCreatingDeletionTasks() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) // Prevent INSERT try manager.enableMaintenanceMode() - try checkThrowsMaintenanceError(deletionTaskManager.createTaskRecord(for: itemMetadata)) + try checkThrowsMaintenanceError(deletionTaskManager.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name))) } func testPreventCreatingItemEnumerationTasks() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) // Prevent INSERT @@ -81,7 +81,7 @@ class MaintenanceManagerTests: XCTestCase { } func testPreventCreatingDownloadTasks() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) // Prevent INSERT @@ -92,14 +92,14 @@ class MaintenanceManagerTests: XCTestCase { // MARK: - Prevent enabling maintenance mode for running tasks func testPreventEnablingMaintenanceModeForRunningUploadTask() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try uploadTaskManager.createNewTaskRecord(for: itemMetadata) try assertOnlyFalseAllowedForInsertOrUpdate() } func testAllowMaintenanceModeForFailedUploadTask() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) var task = try uploadTaskManager.createNewTaskRecord(for: itemMetadata) @@ -109,28 +109,28 @@ class MaintenanceManagerTests: XCTestCase { } func testPreventEnablingMaintenanceModeForRunningReparentTask() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try reparentTaskManager.createTaskRecord(for: itemMetadata, targetCloudPath: CloudPath("Foo"), newParentID: 1) + _ = try reparentTaskManager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: CloudPath("Foo"), newParentID: 1) try assertOnlyFalseAllowedForInsertOrUpdate() } func testPreventEnablingMaintenanceModeForRunningDeletionTask() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try deletionTaskManager.createTaskRecord(for: itemMetadata) + _ = try deletionTaskManager.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) try assertOnlyFalseAllowedForInsertOrUpdate() } func testPreventEnablingMaintenanceModeForRunningItemEnumerationTask() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try itemEnumerationTaskManager.createTask(for: itemMetadata, pageToken: nil) try assertOnlyFalseAllowedForInsertOrUpdate() } func testPreventEnablingMaintenanceModeForRunningDownloadTask() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try downloadTaskManager.createTask(for: itemMetadata, replaceExisting: false, localURL: XCTUnwrap(URL(string: "/Test")), onURLSessionTaskCreation: nil) try assertOnlyFalseAllowedForInsertOrUpdate() diff --git a/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift b/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift index e4be8e517..e81a5006e 100644 --- a/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift @@ -23,7 +23,7 @@ class MetadataManagerTests: XCTestCase { func testCacheMetadataForFile() throws { let cloudPath = CloudPath("/TestFile") - let itemMetadata = ItemMetadata(name: "TestFile", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "TestFile", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) XCTAssertNil(itemMetadata.id) try manager.cacheMetadata(itemMetadata) XCTAssertNotNil(itemMetadata.id) @@ -38,7 +38,7 @@ class MetadataManagerTests: XCTestCase { func testCacheMetadataForFolder() throws { let cloudPath = CloudPath("/Test Folder/") - let itemMetadata = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) XCTAssertNil(itemMetadata.id) try manager.cacheMetadata(itemMetadata) guard let fetchedMetadata = try manager.getCachedMetadata(for: cloudPath) else { @@ -52,8 +52,8 @@ class MetadataManagerTests: XCTestCase { func testCacheMultipleEntries() throws { let fileCloudPath = CloudPath("/TestFile") let folderCloudPath = CloudPath("/TestFolder/") - let itemMetadataForFile = ItemMetadata(name: "TestFile", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileCloudPath, isPlaceholderItem: false) - let itemMetadataForFolder = ItemMetadata(name: "TestFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: true) + let itemMetadataForFile = ItemMetadata(name: "TestFile", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let itemMetadataForFolder = ItemMetadata(name: "TestFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) XCTAssertNil(itemMetadataForFile.id) XCTAssertNil(itemMetadataForFolder.id) let itemMetadataList = [itemMetadataForFile, itemMetadataForFolder] @@ -76,9 +76,9 @@ class MetadataManagerTests: XCTestCase { let fileCloudPath = CloudPath("/Test File.txt") let folderCloudPath = CloudPath("/Test Folder/") let secondFolderCloudPath = CloudPath("/SecondFolder/") - let placeholderItemMetadataForFile = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileCloudPath, isPlaceholderItem: true) - let placeholderItemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: true) - let itemMetadataForFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: secondFolderCloudPath, isPlaceholderItem: false) + let placeholderItemMetadataForFile = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) + let placeholderItemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) + let itemMetadataForFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) XCTAssertNil(placeholderItemMetadataForFile.id) XCTAssertNil(placeholderItemMetadataForFolder.id) XCTAssertNil(itemMetadataForFolder.id) @@ -95,8 +95,8 @@ class MetadataManagerTests: XCTestCase { let fileCloudPath = CloudPath("/Test File.txt") let folderCloudPath = CloudPath("/Test Folder/") let secondFolderCloudPath = CloudPath("/Test Folder/SecondFolder/") - let placeholderItemMetadataForFile = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileCloudPath, isPlaceholderItem: false) - let placeholderItemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: false) + let placeholderItemMetadataForFile = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let placeholderItemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) XCTAssertNil(placeholderItemMetadataForFile.id) XCTAssertNil(placeholderItemMetadataForFolder.id) try manager.cacheMetadata([placeholderItemMetadataForFile, placeholderItemMetadataForFolder]) @@ -105,7 +105,7 @@ class MetadataManagerTests: XCTestCase { XCTFail("Test Folder ID is nil") return } - let itemMetadataForFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: testFolderId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: secondFolderCloudPath, isPlaceholderItem: true) + let itemMetadataForFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: testFolderId, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) XCTAssertNil(itemMetadataForFolder.id) try manager.cacheMetadata(itemMetadataForFolder) XCTAssertNotNil(itemMetadataForFolder.id) @@ -116,7 +116,7 @@ class MetadataManagerTests: XCTestCase { func testOverwriteMetadata() throws { let cloudPath = CloudPath("/TestFolder/") - let itemMetadata = ItemMetadata(name: "TestFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "TestFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(itemMetadata) let id = try XCTUnwrap(itemMetadata.id) XCTAssertEqual(2, id) @@ -127,12 +127,12 @@ class MetadataManagerTests: XCTestCase { XCTAssertEqual(itemMetadata, fetchedItemMetadata) let fileCloudPath = CloudPath("/Existing File.txt") - let itemMetadataForFile = ItemMetadata(name: "Existing File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileCloudPath, isPlaceholderItem: false) + let itemMetadataForFile = ItemMetadata(name: "Existing File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(itemMetadataForFile) let secondItemID = try XCTUnwrap(itemMetadataForFile.id) XCTAssertEqual(3, secondItemID) - let changedItemMetadataAtSameRemoteURL = ItemMetadata(name: "TestFolder", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let changedItemMetadataAtSameRemoteURL = ItemMetadata(name: "TestFolder", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(changedItemMetadataAtSameRemoteURL) let changedItemID = try XCTUnwrap(changedItemMetadataAtSameRemoteURL.id) XCTAssertEqual(2, changedItemID) @@ -148,8 +148,8 @@ class MetadataManagerTests: XCTestCase { func testGetCachedMetadataInsideParentId() throws { let fileCloudPath = CloudPath("/Existing File.txt") let folderCloudPath = CloudPath("/Existing Folder/") - let itemMetadataForFile = ItemMetadata(name: "Existing File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileCloudPath, isPlaceholderItem: false) - let itemMetadataForFolder = ItemMetadata(name: "Existing Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: false) + let itemMetadataForFile = ItemMetadata(name: "Existing File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let itemMetadataForFolder = ItemMetadata(name: "Existing Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) XCTAssertNil(itemMetadataForFile.id) XCTAssertNil(itemMetadataForFolder.id) try manager.cacheMetadata([itemMetadataForFile, itemMetadataForFolder]) @@ -165,12 +165,12 @@ class MetadataManagerTests: XCTestCase { func testFlagAllNonPlaceholderItemsAsCacheCleanupCandidates() throws { let placeholderFileCloudPath = CloudPath("/Placeholder File.txt") let placeholderFolderCloudPath = CloudPath("/Placeholder Folder/") - let placeholderItemMetadataForFile = ItemMetadata(name: "Placeholder File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: placeholderFileCloudPath, isPlaceholderItem: true) - let placeholderItemMetadataForFolder = ItemMetadata(name: "Placeholder Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: placeholderFolderCloudPath, isPlaceholderItem: true) + let placeholderItemMetadataForFile = ItemMetadata(name: "Placeholder File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) + let placeholderItemMetadataForFolder = ItemMetadata(name: "Placeholder Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) let fileCloudPath = CloudPath("/Existing File.txt") let folderCloudPath = CloudPath("/Existing Folder/") - let itemMetadataForFile = ItemMetadata(name: "Existing File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileCloudPath, isPlaceholderItem: false) - let itemMetadataForFolder = ItemMetadata(name: "Existing Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: false) + let itemMetadataForFile = ItemMetadata(name: "Existing File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let itemMetadataForFolder = ItemMetadata(name: "Existing Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata([placeholderItemMetadataForFile, placeholderItemMetadataForFolder, itemMetadataForFile, itemMetadataForFolder]) try manager.flagAllItemsAsMaybeOutdated(withParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) let cachedMetadata = try manager.getCachedMetadata(withParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) @@ -188,21 +188,21 @@ class MetadataManagerTests: XCTestCase { let secondFolderCloudPath = CloudPath("/Test Folder 1/") let subFolderCloudPath = CloudPath("/Test Folder/SecondFolder/") - let itemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: false) - let secondItemMetadataForFolder = ItemMetadata(name: "Test Folder 1", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: secondFolderCloudPath, isPlaceholderItem: false) + let itemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let secondItemMetadataForFolder = ItemMetadata(name: "Test Folder 1", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata([itemMetadataForFolder, secondItemMetadataForFolder]) guard let folderId = itemMetadataForFolder.id else { XCTFail("Folder has no ID") return } - let itemMetadataForFileInFolder = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: folderId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileInFolderCloudPath, isPlaceholderItem: false) - let itemMetadataForSubFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: folderId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: subFolderCloudPath, isPlaceholderItem: false) + let itemMetadataForFileInFolder = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: folderId, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let itemMetadataForSubFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: folderId, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(itemMetadataForSubFolder) guard let subFolderId = itemMetadataForSubFolder.id else { XCTFail("Folder has no ID") return } - let itemMetadataForFileInSubFolder = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: subFolderId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: fileInSubFolderCloudPath, isPlaceholderItem: false) + let itemMetadataForFileInSubFolder = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: subFolderId, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata([itemMetadataForFileInFolder, itemMetadataForFileInSubFolder]) let cachedMetadata = try manager.getAllCachedMetadata(inside: itemMetadataForFolder) @@ -214,7 +214,7 @@ class MetadataManagerTests: XCTestCase { func testGetMetadataWithCaseMismatchPath() throws { let cloudPath = CloudPath("/File.txt") - let itemMetadataForFile = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadataForFile = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(itemMetadataForFile) guard let fetchedMetadataForSensitivePath = try manager.getCachedMetadata(for: cloudPath) else { @@ -227,14 +227,14 @@ class MetadataManagerTests: XCTestCase { return } XCTAssertEqual(fetchedMetadataForSensitivePath, fetchedMetadataForInSensitivePath) - XCTAssertEqual(cloudPath, fetchedMetadataForInSensitivePath.cloudPath) + XCTAssertEqual("File.txt", fetchedMetadataForInSensitivePath.name) } // MARK: Set Tag Data func testSetTagData() throws { let cloudPath = CloudPath("/File.txt") - let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(itemMetadata) let tagData = Data("Foo".utf8) let id = try XCTUnwrap(itemMetadata.id) @@ -247,7 +247,7 @@ class MetadataManagerTests: XCTestCase { func testSetTagDataToNil() throws { let cloudPath = CloudPath("/File.txt") let tagData = Data("Foo".utf8) - let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false, tagData: tagData) + let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, tagData: tagData) try manager.cacheMetadata(itemMetadata) let id = try XCTUnwrap(itemMetadata.id) @@ -260,7 +260,7 @@ class MetadataManagerTests: XCTestCase { func testCacheMetadataDoesNotOverwriteExistingTagData() throws { let cloudPath = CloudPath("/File.txt") let tagData = Data("Foo".utf8) - let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false, tagData: tagData) + let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, tagData: tagData) try manager.cacheMetadata(itemMetadata) itemMetadata.tagData = nil @@ -275,7 +275,7 @@ class MetadataManagerTests: XCTestCase { func testSetFavoriteRank() throws { let cloudPath = CloudPath("/Folder") - let itemMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try manager.cacheMetadata(itemMetadata) let favoriteRank: Int64 = 100 let id = try XCTUnwrap(itemMetadata.id) @@ -287,7 +287,7 @@ class MetadataManagerTests: XCTestCase { func testSetFavoriteRankToNil() throws { let cloudPath = CloudPath("/Folder") - let itemMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false, favoriteRank: 100) + let itemMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, favoriteRank: 100) try manager.cacheMetadata(itemMetadata) let id = try XCTUnwrap(itemMetadata.id) try manager.setFavoriteRank(to: nil, forItemWithID: id) @@ -299,7 +299,7 @@ class MetadataManagerTests: XCTestCase { func testCacheMetadataDoesNotOverwriteExistingFavoriteRank() throws { let cloudPath = CloudPath("/Folder") let favoriteRank: Int64 = 100 - let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false, favoriteRank: favoriteRank) + let itemMetadata = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, favoriteRank: favoriteRank) try manager.cacheMetadata(itemMetadata) itemMetadata.favoriteRank = nil @@ -315,8 +315,4 @@ extension ItemMetadata: Comparable { public static func < (lhs: ItemMetadata, rhs: ItemMetadata) -> Bool { return lhs.name < rhs.name } - - public static func == (lhs: ItemMetadata, rhs: ItemMetadata) -> Bool { - return lhs.name == rhs.name && lhs.type == rhs.type && lhs.size == rhs.size && lhs.parentID == rhs.parentID && lhs.lastModifiedDate == rhs.lastModifiedDate && lhs.statusCode == rhs.statusCode && lhs.cloudPath == rhs.cloudPath && lhs.isPlaceholderItem == rhs.isPlaceholderItem && lhs.isMaybeOutdated == rhs.isMaybeOutdated - } } diff --git a/CryptomatorFileProviderTests/DB/ReparentTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/ReparentTaskManagerTests.swift index 78cbd2fce..7d7bda50f 100644 --- a/CryptomatorFileProviderTests/DB/ReparentTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/ReparentTaskManagerTests.swift @@ -27,10 +27,10 @@ class ReparentTaskManagerTests: XCTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Foo.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(itemMetadata) let newParentID: Int64 = 3 - let createdTask = try manager.createTaskRecord(for: itemMetadata, targetCloudPath: targetCloudPath, newParentID: newParentID) + let createdTask = try manager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: targetCloudPath, newParentID: newParentID) let fetchedTask = try getTaskRecord(for: itemID) XCTAssertEqual(createdTask, fetchedTask) XCTAssertEqual(itemID, fetchedTask.correspondingItem) @@ -44,9 +44,9 @@ class ReparentTaskManagerTests: XCTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Foo.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try manager.createTaskRecord(for: itemMetadata, targetCloudPath: targetCloudPath, newParentID: 3) + _ = try manager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: targetCloudPath, newParentID: 3) let task = try getTaskRecord(for: itemID) try manager.removeTaskRecord(task) XCTAssertThrowsError(try getTaskRecord(for: itemID)) { error in @@ -62,14 +62,14 @@ class ReparentTaskManagerTests: XCTestCase { let targetCloudPath = CloudPath("/Foo/Bar.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(itemMetadata) let folderItemID: Int64 = 3 - let folderItemMetadata = ItemMetadata(id: folderItemID, name: "Foo", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Foo"), isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let folderItemMetadata = ItemMetadata(id: folderItemID, name: "Foo", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(folderItemMetadata) - _ = try manager.createTaskRecord(for: itemMetadata, targetCloudPath: targetCloudPath, newParentID: folderItemID) + _ = try manager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: targetCloudPath, newParentID: folderItemID) let retrievedTasks = try manager.getTaskRecordsForItemsWhichWere(in: NSFileProviderItemIdentifier.rootContainerDatabaseValue) XCTAssertEqual(1, retrievedTasks.count) @@ -85,10 +85,10 @@ class ReparentTaskManagerTests: XCTestCase { let targetCloudPath = CloudPath("/Test2 - Only Renamed.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try manager.createTaskRecord(for: itemMetadata, targetCloudPath: targetCloudPath, newParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) + _ = try manager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: targetCloudPath, newParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) let retrievedTasks = try manager.getTaskRecordsForItemsWhichWere(in: NSFileProviderItemIdentifier.rootContainerDatabaseValue) XCTAssertEqual(1, retrievedTasks.count) XCTAssertEqual(itemID, retrievedTasks[0].correspondingItem) @@ -104,10 +104,10 @@ class ReparentTaskManagerTests: XCTestCase { let newParentID: Int64 = 3 let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try manager.createTaskRecord(for: itemMetadata, targetCloudPath: targetCloudPath, newParentID: newParentID) + _ = try manager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: targetCloudPath, newParentID: newParentID) let retrievedTasks = try manager.getTaskRecordsForItemsWhichAreSoon(in: newParentID) XCTAssertEqual(1, retrievedTasks.count) @@ -123,10 +123,10 @@ class ReparentTaskManagerTests: XCTestCase { let targetCloudPath = CloudPath("/Test2 - Only Renamed.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try itemMetadataManager.cacheMetadata(itemMetadata) - _ = try manager.createTaskRecord(for: itemMetadata, targetCloudPath: targetCloudPath, newParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) + _ = try manager.createTaskRecord(for: itemMetadata, sourceCloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name), targetCloudPath: targetCloudPath, newParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) let retrievedTasks = try manager.getTaskRecordsForItemsWhichAreSoon(in: NSFileProviderItemIdentifier.rootContainerDatabaseValue) XCTAssertEqual(1, retrievedTasks.count) diff --git a/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift index 2e6e20cc0..a69f4e7c9 100644 --- a/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift @@ -20,12 +20,12 @@ class UploadTaskManagerTests: XCTestCase { override func setUpWithError() throws { inMemoryDB = try DatabaseQueue() try DatabaseHelper.migrate(inMemoryDB) - manager = UploadTaskDBManager(database: inMemoryDB) + manager = UploadTaskDBManager(database: inMemoryDB, itemMetadataManager: ItemMetadataDBManager(database: inMemoryDB)) itemMetadataManager = ItemMetadataDBManager(database: inMemoryDB) } func testCachedAndFetchEntry() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try manager.createNewTaskRecord(for: itemMetadata) guard let fetchedUploadTask = try manager.getTaskRecord(for: XCTUnwrap(itemMetadata.id)) else { @@ -39,7 +39,7 @@ class UploadTaskManagerTests: XCTestCase { } func testUpdateTaskRecord() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try manager.createNewTaskRecord(for: itemMetadata) let lastFailedUploadDate = Date(timeIntervalSinceReferenceDate: 0) @@ -65,7 +65,7 @@ class UploadTaskManagerTests: XCTestCase { } func testDeleteCascadeWorks() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try manager.createNewTaskRecord(for: itemMetadata) let taskBeforeRemoval = try manager.getTaskRecord(for: XCTUnwrap(itemMetadata.id)) @@ -77,7 +77,7 @@ class UploadTaskManagerTests: XCTestCase { } func testGetCorrespondingTaskRecords() throws { - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) let savedTask = try manager.createNewTaskRecord(for: itemMetadata) let ids = try [XCTUnwrap(itemMetadata.id) + 1, XCTUnwrap(itemMetadata.id), XCTUnwrap(itemMetadata.id) - 1] @@ -91,7 +91,7 @@ class UploadTaskManagerTests: XCTestCase { func testGetTask() throws { let cloudPath = CloudPath("/Test") let itemMetadataManager = ItemMetadataDBManager(database: inMemoryDB) - let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) let taskRecord = try manager.createNewTaskRecord(for: itemMetadata) let fetchedTask = try manager.getTask(for: taskRecord, onURLSessionTaskCreation: nil) @@ -115,7 +115,7 @@ class UploadTaskManagerTests: XCTestCase { } func testGetRetryableUploadTaskRecordsExcludesNilErrorTasks() throws { - let itemMetadata = ItemMetadata(name: "ActiveUpload", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/ActiveUpload"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "ActiveUpload", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try manager.createNewTaskRecord(for: itemMetadata) let retryable = try manager.getRetryableUploadTaskRecords() @@ -127,7 +127,7 @@ class UploadTaskManagerTests: XCTestCase { @discardableResult private func createTaskRecordWithError(domain: String, code: Int, name: String? = nil) throws -> UploadTaskRecord { let fileName = name ?? "File-\(domain)-\(code)" - let itemMetadata = ItemMetadata(name: fileName, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: CloudPath("/\(fileName)"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: fileName, type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) _ = try manager.createNewTaskRecord(for: itemMetadata) let id = try XCTUnwrap(itemMetadata.id) diff --git a/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift b/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift index 63e10857b..50478c504 100644 --- a/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift @@ -22,7 +22,7 @@ class FileImportingServiceSourceTests: XCTestCase { var taskRegistratorMock: SessionTaskRegistratorMock! let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(identifier: .test, displayName: "Foo", pathRelativeToDocumentStorage: "/") - let itemStub = FileProviderItem(metadata: .init(name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo"), isPlaceholderItem: false), domainIdentifier: .test) + let itemStub = FileProviderItem(metadata: .init(name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false), domainIdentifier: .test) override func setUpWithError() throws { notificatorMock = FileProviderNotificatorTypeMock() diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift index 11f975d17..50ca1730f 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift @@ -13,7 +13,7 @@ import XCTest class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { func testCreateDirectory() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, coordinator: fileCoordinator, localURLProvider: localURLProviderMock, taskRegistrator: taskRegistratorMock) adapter.createDirectory(withName: "TestFolder", inParentItemIdentifier: .rootContainer) { item, error in @@ -27,7 +27,7 @@ class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { XCTAssertFalse(fileProviderItem.isUploaded) XCTAssertEqual("public.folder", fileProviderItem.typeIdentifier) XCTAssert(fileProviderItem.metadata.isPlaceholderItem) - XCTAssertEqual(CloudPath("/TestFolder"), fileProviderItem.metadata.cloudPath) + XCTAssertEqual("TestFolder", fileProviderItem.metadata.name) XCTAssertNotNil(fileProviderItem.metadata.id) XCTAssertEqual(2, self.metadataManagerMock.cachedMetadata.count) @@ -62,7 +62,7 @@ class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { // MARK: Create Placeholder func testCreatePlaceholderItemForFolder() throws { - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let placeholderItem = try adapter.createPlaceholderItemForFolder(withName: "TestFolder", in: .rootContainer) @@ -71,7 +71,7 @@ class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { XCTAssertFalse(placeholderItem.isUploaded) XCTAssertEqual("public.folder", placeholderItem.typeIdentifier) XCTAssert(placeholderItem.metadata.isPlaceholderItem) - XCTAssertEqual(CloudPath("/TestFolder"), placeholderItem.metadata.cloudPath) + XCTAssertEqual("TestFolder", placeholderItem.metadata.name) XCTAssertNotNil(placeholderItem.metadata.id) XCTAssertEqual(2, metadataManagerMock.cachedMetadata.count) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift index 29e3911b6..0b8f6c540 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift @@ -15,7 +15,7 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { let expectation = XCTestExpectation() let itemID: Int64 = 2 let cloudPath = CloudPath("/test.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let adapter = createFullyMockedAdapter() let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) @@ -39,9 +39,9 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { let folderItemID: Int64 = 2 let fileItemID: Int64 = 3 let folderCloudPath = CloudPath("/Folder/") - let folderItemMetadata = ItemMetadata(id: folderItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: folderCloudPath, isPlaceholderItem: false) + let folderItemMetadata = ItemMetadata(id: folderItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let cloudPath = CloudPath("Folder/test.txt") - let fileItemMetadata = ItemMetadata(id: fileItemID, name: "test.txt", type: .file, size: nil, parentID: folderItemID, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let fileItemMetadata = ItemMetadata(id: fileItemID, name: "test.txt", type: .file, size: nil, parentID: folderItemID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([folderItemMetadata, fileItemMetadata]) let folderItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: folderItemID) @@ -69,7 +69,7 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { let expectation = XCTestExpectation() let itemID: Int64 = 2 let cloudPath = CloudPath("/test.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let adapter = createFullyMockedAdapter() let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift index 30c97c11e..05bece288 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift @@ -22,18 +22,20 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { // MARK: Error Handling func testEnumerateItemsFailedWithNoInternetConnection() throws { - let metadata = ItemMetadata(id: 2, name: "noInternetConnection", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/noInternetConnection"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data()) + let metadata = ItemMetadata(id: 2, name: "noInternetConnection", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data()) try metadataManagerMock.cacheMetadata(metadata) + itemEnumerationTaskManagerMock.stubCloudPath = CloudPath("/noInternetConnection") XCTAssertRejects(adapter.enumerateItems(for: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), withPageToken: nil), with: NSFileProviderError(.serverUnreachable)) } func testEnumerateItemsOfflineFallbackToCache() throws { let expectation = XCTestExpectation() - let metadata = ItemMetadata(id: 2, name: "noInternetConnection", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/noInternetConnection"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data(), lastEnumeratedAt: Date()) + let metadata = ItemMetadata(id: 2, name: "noInternetConnection", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data(), lastEnumeratedAt: Date()) try metadataManagerMock.cacheMetadata(metadata) - let child = ItemMetadata(id: 3, name: "CachedFile", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/noInternetConnection/CachedFile"), isPlaceholderItem: false) + let child = ItemMetadata(id: 3, name: "CachedFile", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(child) + itemEnumerationTaskManagerMock.stubCloudPath = CloudPath("/noInternetConnection") let permissionProviderMock = PermissionProviderMock() withDependencies { @@ -58,8 +60,8 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { func testWorkingSet() { let mockMetadata = [ - ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data()), - ItemMetadata(id: 3, name: "TestFolder", type: .file, size: nil, parentID: 4, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Foo/TestFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: 1, tagData: nil) + ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data()), + ItemMetadata(id: 3, name: "TestFolder", type: .file, size: nil, parentID: 4, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: 1, tagData: nil) ] metadataManagerMock.workingSetMetadata = mockMetadata let permissionProviderMock = PermissionProviderMock() diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemIdentifierTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemIdentifierTests.swift index 910616b6c..d62b580a5 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemIdentifierTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemIdentifierTests.swift @@ -24,7 +24,6 @@ class FileProviderAdapterGetItemIdentifierTests: FileProviderAdapterTestCase { parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, - cloudPath: CloudPath("/"), isPlaceholderItem: false) } @@ -45,7 +44,6 @@ class FileProviderAdapterGetItemIdentifierTests: FileProviderAdapterTestCase { parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, - cloudPath: CloudPath("/Directory 1"), isPlaceholderItem: false) let folderMetadataID = try XCTUnwrap(folderMetadata.id) @@ -58,7 +56,6 @@ class FileProviderAdapterGetItemIdentifierTests: FileProviderAdapterTestCase { parentID: XCTUnwrap(folderMetadata.id), lastModifiedDate: nil, statusCode: .isUploaded, - cloudPath: CloudPath("/Directory 1/Directory 2"), isPlaceholderItem: false) let subFolderMetadataID = try XCTUnwrap(subFolderMetadata.id) metadataManagerMock.cachedMetadata[subFolderMetadataID] = subFolderMetadata @@ -70,43 +67,20 @@ class FileProviderAdapterGetItemIdentifierTests: FileProviderAdapterTestCase { XCTAssertEqual(expectedItemIdentifier, itemIdentifier) } - func testGetItemIdentifierForCachedItemMissingInCloud() throws { - let cloudPath = CloudPath("/Directory 1/Directory 3") - let folderMetadata = ItemMetadata(id: 2, - name: "Directory 1", - type: .folder, - size: nil, - parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, - lastModifiedDate: nil, - statusCode: .isUploaded, - cloudPath: CloudPath("/Directory 1"), - isPlaceholderItem: false) - - let folderMetadataID = try XCTUnwrap(folderMetadata.id) - metadataManagerMock.cachedMetadata[folderMetadataID] = folderMetadata - - let subFolderMetadata = try ItemMetadata(id: 3, - name: "Directory 3", - type: .folder, - size: nil, - parentID: XCTUnwrap(folderMetadata.id), - lastModifiedDate: nil, - statusCode: .isUploaded, - cloudPath: CloudPath("/Directory 1/Directory 3"), - isPlaceholderItem: false) - let subFolderMetadataID = try XCTUnwrap(subFolderMetadata.id) - metadataManagerMock.cachedMetadata[subFolderMetadataID] = subFolderMetadata - - let getItemIdentifierPromise = adapter.getItemIdentifier(for: cloudPath) - XCTAssertRejects(getItemIdentifierPromise, with: NSFileProviderError(.noSuchItem)._nsError) - } - - func testGetItemIdentifierForItemNotYetCached() throws { + func testGetItemIdentifierFallsBackToEnumerationForColdPath() throws { + // Not cached, but exists in mock cloud; enumeration populates the cache, then lookup succeeds. let cloudPath = CloudPath("/Directory 1/Directory 2") let getItemIdentifierPromise = adapter.getItemIdentifier(for: cloudPath) wait(for: getItemIdentifierPromise, timeout: 5.0) let itemIdentifier = try XCTUnwrap(getItemIdentifierPromise.value) - let expectedItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 7) - XCTAssertEqual(expectedItemIdentifier, itemIdentifier) + let resolvedMetadata = try XCTUnwrap(metadataManagerMock.getCachedMetadata(for: cloudPath)) + XCTAssertEqual(try NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: XCTUnwrap(resolvedMetadata.id)), itemIdentifier) + } + + func testGetItemIdentifierForNonexistentItem() { + // Neither cached nor in mock cloud; enumeration finds nothing, lookup rejects. + let cloudPath = CloudPath("/Directory 1/Does Not Exist") + let getItemIdentifierPromise = adapter.getItemIdentifier(for: cloudPath) + XCTAssertRejects(getItemIdentifierPromise, with: NSFileProviderError(.noSuchItem)._nsError) } } diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift index 9d87bd029..4a8f91b4d 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift @@ -26,7 +26,7 @@ class FileProviderAdapterGetItemTests: FileProviderAdapterTestCase { func testGetFileProviderItem() throws { let id: Int64 = 2 - let itemMetadata = ItemMetadata(id: id, name: "TestItem", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestItem"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: id, name: "TestItem", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) metadataManagerMock.cachedMetadata[id] = itemMetadata let item = try adapter.item(for: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: id)) @@ -39,7 +39,7 @@ class FileProviderAdapterGetItemTests: FileProviderAdapterTestCase { func testGetFileProviderItemWithUploadError() throws { let id: Int64 = 2 - let itemMetadata = ItemMetadata(id: id, name: "TestItem", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: CloudPath("/TestItem"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(id: id, name: "TestItem", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: true) metadataManagerMock.cachedMetadata[id] = itemMetadata let uploadTask = UploadTaskRecord(correspondingItem: id, lastFailedUploadDate: Date(), uploadErrorCode: NSFileProviderError(.insufficientQuota).errorCode, uploadErrorDomain: NSFileProviderErrorDomain, uploadStartedAt: nil) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift index 55019c1b2..22717894f 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift @@ -35,7 +35,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let fileURL = tmpDirectory.appendingPathComponent("ItemToBeImported.txt", isDirectory: false) let fileContent = "TestContent" try fileContent.write(to: fileURL, atomically: true, encoding: .utf8) - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let result = try adapter.localItemImport(fileURL: fileURL, parentIdentifier: .rootContainer) @@ -65,7 +65,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let fileContent = "TestContent" try fileContent.write(to: fileURL, atomically: true, encoding: .utf8) - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) XCTAssertThrowsError(try adapter.localItemImport(fileURL: fileURL, parentIdentifier: .rootContainer)) { error in @@ -86,7 +86,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { try FileManager.default.createDirectory(at: expectedFileURL.deletingLastPathComponent(), withIntermediateDirectories: false) let existingFileContent = "ExistingFileContent" try existingFileContent.write(to: expectedFileURL, atomically: true, encoding: .utf8) - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) XCTAssertThrowsError(try adapter.localItemImport(fileURL: fileURL, parentIdentifier: .rootContainer)) { error in @@ -113,7 +113,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { func testImportDocument() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let fileURL = tmpDirectory.appendingPathComponent("ItemToBeImported.txt", isDirectory: false) @@ -158,7 +158,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { func testItemChanged() throws { let cloudPath = CloudPath("/Item.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "Item.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Item.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) metadataManagerMock.cachedMetadata[itemID] = itemMetadata let adapter = createFullyMockedAdapter() @@ -204,7 +204,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { cloudProviderMock.deleteFileAtReturnValue = deleteItemPromise cloudProviderMock.uploadFileFromToReplaceExistingReturnValue = uploadItemPromise - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let itemFolderURL = tmpDirectory.appendingPathComponent("\(itemID)", isDirectory: true) @@ -212,7 +212,7 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let expectedFileURL = itemFolderURL.appendingPathComponent("File 1", isDirectory: false) let existingFileContent = "Existing Content" try existingFileContent.write(to: expectedFileURL, atomically: true, encoding: .utf8) - let existingItemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 1"), isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let existingItemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) try metadataManagerMock.cacheMetadata(existingItemMetadata) try cachedFileManagerMock.cacheLocalFileInfo(for: itemID, localURL: expectedFileURL, lastModifiedDate: Date(timeIntervalSince1970: 0)) @@ -282,16 +282,13 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let fileURL = tmpDirectory.appendingPathComponent(nfdName, isDirectory: false) try "test".write(to: fileURL, atomically: true, encoding: .utf8) - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let result = try adapter.localItemImport(fileURL: fileURL, parentIdentifier: .rootContainer) let storedName = result.item.filename XCTAssertEqual(Array(storedName.utf8), Array(nfcName.utf8), "Filename should be stored in NFC form") - - let storedCloudPath = result.item.metadata.cloudPath.path - XCTAssertEqual(Array(storedCloudPath.utf8), Array("/\(nfcName)".utf8), "CloudPath should be stored in NFC form") } } diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift index d24d6bea0..f804418f6 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift @@ -15,7 +15,7 @@ import XCTest class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { func testMoveItemLocally() throws { - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let parentItemID: Int64 = 2 @@ -23,9 +23,9 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Folder/RenamedTest.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let targetParentCloudPath = CloudPath("/Folder/") - let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: targetParentCloudPath, isPlaceholderItem: false) + let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([itemMetadata, newParentItemMetadata]) let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) @@ -37,7 +37,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(parentItemIdentifier, item.parentItemIdentifier) XCTAssertEqual(itemIdentifier, item.itemIdentifier) XCTAssertEqual(ItemStatus.isUploading, item.metadata.statusCode) - XCTAssertEqual(targetCloudPath, item.metadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, item.metadata.name) XCTAssertEqual(3, metadataManagerMock.cachedMetadata.count) XCTAssertEqual(itemMetadata, metadataManagerMock.cachedMetadata[itemID]) @@ -45,7 +45,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(newName, itemMetadata.name) XCTAssertEqual(newParentItemMetadata.id, itemMetadata.parentID) XCTAssertEqual(ItemStatus.isUploading, itemMetadata.statusCode) - XCTAssertEqual(targetCloudPath, itemMetadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, itemMetadata.name) let reparentTaskRecord = result.reparentTaskRecord XCTAssertEqual(itemID, reparentTaskRecord.correspondingItem) @@ -56,13 +56,13 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { } func testMoveItemLocallyOnlyNameChanged() throws { - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/RenamedTest.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) metadataManagerMock.cachedMetadata[itemID] = itemMetadata let itemIdentifier = try NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: XCTUnwrap(itemMetadata.id)) let newName = "RenamedTest.txt" @@ -72,7 +72,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) XCTAssertEqual(itemIdentifier, item.itemIdentifier) XCTAssertEqual(ItemStatus.isUploading, item.metadata.statusCode) - XCTAssertEqual(targetCloudPath, item.metadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, item.metadata.name) XCTAssertEqual(2, metadataManagerMock.cachedMetadata.count) XCTAssertEqual(itemMetadata, metadataManagerMock.cachedMetadata[itemID]) @@ -80,7 +80,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(newName, itemMetadata.name) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainerDatabaseValue, itemMetadata.parentID) XCTAssertEqual(ItemStatus.isUploading, itemMetadata.statusCode) - XCTAssertEqual(targetCloudPath, itemMetadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, itemMetadata.name) let reparentTaskRecord = result.reparentTaskRecord XCTAssertEqual(itemID, reparentTaskRecord.correspondingItem) @@ -89,7 +89,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { } func testMoveItemLocallyOnlyParentChanged() throws { - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let parentItemID: Int64 = 2 @@ -97,9 +97,9 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Folder/Test.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let targetParentCloudPath = CloudPath("/Folder/") - let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: targetParentCloudPath, isPlaceholderItem: false) + let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([itemMetadata, newParentItemMetadata]) let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) @@ -110,7 +110,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(parentItemIdentifier, item.parentItemIdentifier) XCTAssertEqual(itemIdentifier, item.itemIdentifier) XCTAssertEqual(ItemStatus.isUploading, item.metadata.statusCode) - XCTAssertEqual(targetCloudPath, item.metadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, item.metadata.name) XCTAssertEqual(3, metadataManagerMock.cachedMetadata.count) XCTAssertEqual(itemMetadata, metadataManagerMock.cachedMetadata[itemID]) @@ -118,7 +118,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual("Test.txt", itemMetadata.name) XCTAssertEqual(parentItemID, itemMetadata.parentID) XCTAssertEqual(ItemStatus.isUploading, itemMetadata.statusCode) - XCTAssertEqual(targetCloudPath, itemMetadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, itemMetadata.name) let reparentTaskRecord = result.reparentTaskRecord XCTAssertEqual(itemID, reparentTaskRecord.correspondingItem) @@ -131,13 +131,13 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { func testRenameItem() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/RenamedTest.txt") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) metadataManagerMock.cachedMetadata[itemID] = itemMetadata let newName = "RenamedTest.txt" let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) @@ -152,7 +152,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, fileProviderItem.parentItemIdentifier) XCTAssertEqual(itemIdentifier, fileProviderItem.itemIdentifier) XCTAssertEqual(ItemStatus.isUploading, fileProviderItem.metadata.statusCode) - XCTAssertEqual(targetCloudPath, fileProviderItem.metadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, fileProviderItem.metadata.name) XCTAssertEqual(2, self.metadataManagerMock.cachedMetadata.count) XCTAssertEqual(itemMetadata, self.metadataManagerMock.cachedMetadata[itemID]) @@ -160,7 +160,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(newName, itemMetadata.name) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainerDatabaseValue, itemMetadata.parentID) XCTAssertEqual(ItemStatus.isUploading, itemMetadata.statusCode) - XCTAssertEqual(targetCloudPath, itemMetadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, itemMetadata.name) guard let reparentTaskRecord = self.reparentTaskManagerMock.reparentTasks[itemID] else { XCTFail("reparentTaskRecord is nil") @@ -177,7 +177,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { func testReparentItem() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let parentItemID: Int64 = 2 @@ -185,9 +185,9 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Folder/Test.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let targetParentCloudPath = CloudPath("/Folder/") - let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: targetParentCloudPath, isPlaceholderItem: false) + let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([itemMetadata, newParentItemMetadata]) let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) @@ -202,7 +202,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(parentItemIdentifier, fileProviderItem.parentItemIdentifier) XCTAssertEqual(itemIdentifier, fileProviderItem.itemIdentifier) XCTAssertEqual(ItemStatus.isUploading, fileProviderItem.metadata.statusCode) - XCTAssertEqual(targetCloudPath, fileProviderItem.metadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, fileProviderItem.metadata.name) XCTAssertEqual(3, self.metadataManagerMock.cachedMetadata.count) XCTAssertEqual(itemMetadata, self.metadataManagerMock.cachedMetadata[itemID]) @@ -210,7 +210,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual("Test.txt", itemMetadata.name) XCTAssertEqual(parentItemID, itemMetadata.parentID) XCTAssertEqual(ItemStatus.isUploading, itemMetadata.statusCode) - XCTAssertEqual(targetCloudPath, itemMetadata.cloudPath) + XCTAssertEqual(targetCloudPath.lastPathComponent, itemMetadata.name) guard let reparentTaskRecord = self.reparentTaskManagerMock.reparentTasks[itemID] else { XCTFail("reparentTaskRecord is nil") diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterRecoverUploadsTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterRecoverUploadsTests.swift index 8e2547cff..725f63ef6 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterRecoverUploadsTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterRecoverUploadsTests.swift @@ -42,7 +42,7 @@ class FileProviderAdapterRecoverUploadsTests: FileProviderAdapterTestCase { func testRecoverStuckUploads_marksErrorWhenLocalFileMissing() throws { let cloudPath = CloudPath("/File.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "File.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "File.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let taskRecord = UploadTaskRecord(correspondingItem: itemID, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: Date()) @@ -60,7 +60,7 @@ class FileProviderAdapterRecoverUploadsTests: FileProviderAdapterTestCase { func testRecoverStuckUploads_reschedulesWhenLocalFileExists() throws { let cloudPath = CloudPath("/File.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "File.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "File.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) // Create a local file @@ -83,7 +83,7 @@ class FileProviderAdapterRecoverUploadsTests: FileProviderAdapterTestCase { func testRecoverStuckUploads_retriesConnectivityFailedUploads() throws { let cloudPath = CloudPath("/OfflineFile.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "OfflineFile.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: true) + let itemMetadata = ItemMetadata(id: itemID, name: "OfflineFile.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: true) try metadataManagerMock.cacheMetadata(itemMetadata) // Create a local file @@ -131,7 +131,7 @@ class FileProviderAdapterRecoverUploadsTests: FileProviderAdapterTestCase { func testRecoverStuckUploads_marksErrorForRetryableUploadWithMissingLocalFile() throws { let cloudPath = CloudPath("/OfflineFile.txt") - let itemMetadata = ItemMetadata(id: itemID, name: "OfflineFile.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: true) + let itemMetadata = ItemMetadata(id: itemID, name: "OfflineFile.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: true) try metadataManagerMock.cacheMetadata(itemMetadata) // No cached file info exists diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift index 935951271..b583bca0d 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift @@ -13,7 +13,7 @@ import XCTest class FileProviderAdapterSetFavoriteRankTests: FileProviderAdapterTestCase { func testSetFavoriteRank() { let expectation = XCTestExpectation() - metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) + metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) let favoriteRank: NSNumber = 100 let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) adapter.setFavoriteRank(favoriteRank, forItemIdentifier: itemIdentifier) { item, error in diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift index 0e5dba773..a0a52d91c 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift @@ -15,7 +15,7 @@ class FileProviderAdapterSetTagDataTests: FileProviderAdapterTestCase { let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) func testSetTagData() { let expectation = XCTestExpectation() - metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) + metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) let tagData = Data("Foo".utf8) adapter.setTagData(tagData, forItemIdentifier: itemIdentifier) { item, error in @@ -29,7 +29,7 @@ class FileProviderAdapterSetTagDataTests: FileProviderAdapterTestCase { func testSetEmptyTagData() { let expectation = XCTestExpectation() - metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) + metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) let emptyTagData = Data() adapter.setTagData(emptyTagData, forItemIdentifier: itemIdentifier) { item, error in XCTAssertNil(error) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift index ccdacbbdf..b1e4b7f5a 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift @@ -14,7 +14,7 @@ import XCTest class FileProviderAdapterStartProvidingItemTests: FileProviderAdapterTestCase { private let itemID: Int64 = 2 private let cloudPath = CloudPath("/File 1") - private lazy var itemMetadata = ItemMetadata(id: 2, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + private lazy var itemMetadata = ItemMetadata(id: 2, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) private lazy var itemDirectory = tmpDirectory.appendingPathComponent("/\(itemID)") private lazy var url = itemDirectory.appendingPathComponent("File 1") @@ -70,7 +70,7 @@ class FileProviderAdapterStartProvidingItemTests: FileProviderAdapterTestCase { func testStartProvidingItemWithConflictingLocalVersion() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) // Create local folder for conflicting item diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift index c15555f00..a58f7a953 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import Dependencies import Foundation import Promises @@ -53,7 +54,8 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { uploadTaskManagerMock.getTaskForOnURLSessionTaskCreationClosure = { let id = $0.correspondingItem let metadata = try XCTUnwrap(self.metadataManagerMock.cachedMetadata[id]) - return UploadTask(taskRecord: $0, itemMetadata: metadata, onURLSessionTaskCreation: $1) + let cloudPath = (try? self.metadataManagerMock.getCloudPath(for: id)) ?? CloudPath("/") + return UploadTask(taskRecord: $0, itemMetadata: metadata, cloudPath: cloudPath, onURLSessionTaskCreation: $1) } } diff --git a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift index 22a6cfadc..0d696eac7 100644 --- a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift @@ -26,8 +26,8 @@ class FileProviderEnumeratorTestCase: XCTestCase { let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(vaultUID: "VaultUID-12345", displayName: "Test Vault") let items: [FileProviderItem] = [ - .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false), domainIdentifier: .test), - .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false), domainIdentifier: .test) + .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test) ] let deleteItemIdentifiers = [1, 2, 3].map { NSFileProviderItemIdentifier("\($0)") } diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index 17cc4a4a6..de2b5e51c 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -16,7 +16,7 @@ import XCTest class FileProviderItemTests: XCTestCase { func testRootItem() { let cloudPath = CloudPath("/") - let metadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "root", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "root", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) @@ -25,7 +25,7 @@ class FileProviderItemTests: XCTestCase { func testFileItem() { let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) @@ -40,7 +40,7 @@ class FileProviderItemTests: XCTestCase { func testFolderItem() { let cloudPath = CloudPath("/test Folder/") - let metadata = ItemMetadata(id: 2, name: "test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) @@ -55,7 +55,7 @@ class FileProviderItemTests: XCTestCase { func testUploadError() { let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) let lastFailedUploadDate = Date(timeIntervalSinceReferenceDate: 0) let failedUploadTask = UploadTaskRecord(correspondingItem: 2, lastFailedUploadDate: lastFailedUploadDate, uploadErrorCode: NSFileProviderError.insufficientQuota.rawValue, uploadErrorDomain: NSFileProviderErrorDomain, uploadStartedAt: nil) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, error: failedUploadTask.failedWithError) @@ -75,7 +75,7 @@ class FileProviderItemTests: XCTestCase { func testIsDownloadedOnlyForLocallyExistingFile() throws { let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false) @@ -114,7 +114,7 @@ class FileProviderItemTests: XCTestCase { $0.permissionProvider = permissionProviderMock } operation: { let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) let capabilities: [NSFileProviderItemCapabilities] = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsDeleting, .allowsReading, .allowsReparenting, .allowsWriting] @@ -147,7 +147,7 @@ class FileProviderItemTests: XCTestCase { func testEvictFileFromCacheActionDisabledForNotCachedFile() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isDownloading, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) let userInfo = try XCTUnwrap(item.userInfo) XCTAssertFalse(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? true) @@ -155,7 +155,7 @@ class FileProviderItemTests: XCTestCase { func testEvictFileFromCacheActionDisabledForFolder() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isDownloading, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) let userInfo = try XCTUnwrap(item.userInfo) XCTAssertFalse(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? true) @@ -165,7 +165,7 @@ class FileProviderItemTests: XCTestCase { func testRetryFailedUploadActionEnabled() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, error: NSFileProviderError(.insufficientQuota)._nsError) let userInfo = try XCTUnwrap(item.userInfo) XCTAssertTrue(userInfo["enableRetryFailedUploadAction"] as? Bool ?? false) @@ -173,7 +173,7 @@ class FileProviderItemTests: XCTestCase { func testRetryFailedUploadActionDisabled() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) let userInfo = try XCTUnwrap(item.userInfo) XCTAssertFalse(userInfo["enableRetryFailedUploadAction"] as? Bool ?? true) @@ -183,7 +183,7 @@ class FileProviderItemTests: XCTestCase { func testRetryWaitingUploadActionEnabled() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) let userInfo = try XCTUnwrap(item.userInfo) XCTAssert(userInfo["enableRetryWaitingUploadAction"] as? Bool ?? false) @@ -191,7 +191,7 @@ class FileProviderItemTests: XCTestCase { func testRetryWaitingUploadActionDisabledForFolder() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) let userInfo = try XCTUnwrap(item.userInfo) XCTAssertFalse(userInfo["enableRetryWaitingUploadAction"] as? Bool ?? true) @@ -199,7 +199,7 @@ class FileProviderItemTests: XCTestCase { func testRetryWaitingUploadActionDisabledForUploadError() throws { let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, error: NSFileProviderError(.insufficientQuota)._nsError) let userInfo = try XCTUnwrap(item.userInfo) XCTAssertFalse(userInfo["enableRetryWaitingUploadAction"] as? Bool ?? true) @@ -212,7 +212,7 @@ class FileProviderItemTests: XCTestCase { try "Foo".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: statusCode, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: statusCode, isPlaceholderItem: false) return FileProviderItem(metadata: metadata, domainIdentifier: .test, localURL: localURL) } } diff --git a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift index e72a9e982..dfbee9a51 100644 --- a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift @@ -18,7 +18,7 @@ class FileProviderNotificatorTests: XCTestCase { let updatedMetadataIDs: [Int64] = [2, 3, 4] lazy var updatedItemIdentifiers = updatedMetadataIDs.map { NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: $0) } lazy var updatedItems: [FileProviderItem] = updatedMetadataIDs.map { - FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: CloudPath("/\($0)"), isPlaceholderItem: false), domainIdentifier: .test) + FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, isPlaceholderItem: false), domainIdentifier: .test) } override func setUpWithError() throws { diff --git a/CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift b/CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift index 4fc3174ea..d1f7a2c2c 100644 --- a/CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift +++ b/CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift @@ -54,5 +54,9 @@ class ErrorMapperTests: XCTestCase { var itemMetadata: ItemMetadata { fatalError("not implemented") } + + var cloudPath: CloudPath { + fatalError("not implemented") + } } } diff --git a/CryptomatorFileProviderTests/Middleware/OnlineItemNameCollisionHandlerTests.swift b/CryptomatorFileProviderTests/Middleware/OnlineItemNameCollisionHandlerTests.swift index d3517b90e..0a189d88c 100644 --- a/CryptomatorFileProviderTests/Middleware/OnlineItemNameCollisionHandlerTests.swift +++ b/CryptomatorFileProviderTests/Middleware/OnlineItemNameCollisionHandlerTests.swift @@ -39,15 +39,15 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { let expectation = XCTestExpectation() let originalCloudPath = CloudPath("/foo.txt") let workflowMock = WorkflowMiddlewareMock { task in - if task.itemMetadata.cloudPath == originalCloudPath { + if task.cloudPath == originalCloudPath { return Promise(CloudProviderError.itemAlreadyExists) } return Promise(()) } - let itemMetadata = ItemMetadata(name: "foo.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: originalCloudPath, isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "foo.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) middleware.setNext(AnyWorkflowMiddleware(workflowMock)) - middleware.execute(task: SampleCloudTask(itemMetadata: itemMetadata)).then { + middleware.execute(task: FolderCreationTask(itemMetadata: itemMetadata, cloudPath: originalCloudPath)).then { guard let itemID = itemMetadata.id, let cachedItemMetadata = try self.itemMetadataManager.getCachedMetadata(for: itemID) else { XCTFail("ItemMetadata not found in DB") return @@ -59,9 +59,10 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { XCTAssert(cachedItemMetadata.name.hasSuffix(").txt")) XCTAssertEqual(15, cachedItemMetadata.name.count) - XCTAssert(cachedItemMetadata.cloudPath.path.hasPrefix("/foo (")) - XCTAssert(cachedItemMetadata.cloudPath.path.hasSuffix(").txt")) - XCTAssertEqual(16, cachedItemMetadata.cloudPath.path.count) + let cachedCloudPath = (try? self.itemMetadataManager.getCloudPath(for: itemID)) ?? CloudPath("/") + XCTAssert(cachedCloudPath.path.hasPrefix("/foo (")) + XCTAssert(cachedCloudPath.path.hasSuffix(").txt")) + XCTAssertEqual(16, cachedCloudPath.path.count) // Check that the remaining item metadata properties have not changed. @@ -85,10 +86,11 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { let workflowMock = WorkflowMiddlewareMock { _ in return Promise(CloudProviderError.itemAlreadyExists) } - let itemMetadata = ItemMetadata(name: "foo.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo.txt"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "foo.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) middleware.setNext(AnyWorkflowMiddleware(workflowMock)) - middleware.execute(task: SampleCloudTask(itemMetadata: itemMetadata)).then { + let sampleCloudPath = CloudPath("/foo.txt") + middleware.execute(task: FolderCreationTask(itemMetadata: itemMetadata, cloudPath: sampleCloudPath)).then { XCTFail("Promise fulfilled") }.catch { error in guard case CloudProviderError.itemAlreadyExists = error else { @@ -105,9 +107,10 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { XCTAssert(cachedItemMetadata.name.hasSuffix(").txt")) XCTAssertEqual(15, cachedItemMetadata.name.count) - XCTAssert(cachedItemMetadata.cloudPath.path.hasPrefix("/foo (")) - XCTAssert(cachedItemMetadata.cloudPath.path.hasSuffix(").txt")) - XCTAssertEqual(16, cachedItemMetadata.cloudPath.path.count) + let cachedCloudPath = (try? self.itemMetadataManager.getCloudPath(for: itemID)) ?? CloudPath("/") + XCTAssert(cachedCloudPath.path.hasPrefix("/foo (")) + XCTAssert(cachedCloudPath.path.hasSuffix(").txt")) + XCTAssertEqual(16, cachedCloudPath.path.count) // Check that the remaining item metadata properties have not changed. XCTAssertEqual(CloudItemType.file, cachedItemMetadata.type) @@ -126,10 +129,11 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { let workflowMock = WorkflowMiddlewareMock { _ in return Promise(CloudProviderError.itemNotFound) } - let itemMetadata = ItemMetadata(name: "foo.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo.txt"), isPlaceholderItem: true) + let itemMetadata = ItemMetadata(name: "foo.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) middleware.setNext(AnyWorkflowMiddleware(workflowMock)) - middleware.execute(task: SampleCloudTask(itemMetadata: itemMetadata)).then { + let sampleCloudPath = CloudPath("/foo.txt") + middleware.execute(task: FolderCreationTask(itemMetadata: itemMetadata, cloudPath: sampleCloudPath)).then { XCTFail("Promise fulfilled") }.catch { error in guard case CloudProviderError.itemNotFound = error else { @@ -143,7 +147,6 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { // Check that the item metadata properties have not changed. XCTAssertEqual("foo.txt", cachedItemMetadata.name) - XCTAssertEqual(CloudPath("/foo.txt"), cachedItemMetadata.cloudPath) XCTAssertEqual(CloudItemType.file, cachedItemMetadata.type) XCTAssertNil(cachedItemMetadata.size) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainerDatabaseValue, cachedItemMetadata.parentID) @@ -154,8 +157,4 @@ class OnlineItemNameCollisionHandlerTests: XCTestCase { } wait(for: [expectation], timeout: 5.0) } - - private struct SampleCloudTask: CloudTask { - let itemMetadata: ItemMetadata - } } diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift index 7626bf4d2..8a67e346a 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift @@ -32,7 +32,9 @@ class CloudTaskExecutorTestCase: XCTestCase { reparentTaskManagerMock = ReparentTaskManagerMock() deletionTaskManagerMock = DeletionTaskManagerMock() itemEnumerationTaskManagerMock = ItemEnumerationTaskManagerMock() + itemEnumerationTaskManagerMock.itemMetadataManager = metadataManagerMock downloadTaskManagerMock = DownloadTaskManagerMock() + downloadTaskManagerMock.itemMetadataManager = metadataManagerMock deleteItemHelper = DeleteItemHelper(itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) tmpDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tmpDirectory, withIntermediateDirectories: false, attributes: nil) @@ -51,7 +53,7 @@ class CloudTaskExecutorTestCase: XCTestCase { var setFavoriteRank = [Int64: Int64?]() func cacheMetadata(_ metadata: ItemMetadata) throws { - if let cachedItemMetadata = try getCachedMetadata(for: metadata.cloudPath) { + if let cachedItemMetadata = try childOfFolder(parentID: metadata.parentID, name: metadata.name), cachedItemMetadata.id != NSFileProviderItemIdentifier.rootContainerDatabaseValue { metadata.id = cachedItemMetadata.id metadata.statusCode = cachedItemMetadata.statusCode metadata.tagData = cachedItemMetadata.tagData @@ -75,6 +77,9 @@ class CloudTaskExecutorTestCase: XCTestCase { func updateMetadata(_ metadata: ItemMetadata) throws { updatedMetadata.append(metadata) + if let id = metadata.id { + cachedMetadata[id] = metadata + } } func cacheMetadata(_ itemMetadataList: [ItemMetadata]) throws { @@ -83,8 +88,49 @@ class CloudTaskExecutorTestCase: XCTestCase { } } + func getCloudPath(for id: Int64) throws -> CloudPath { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + if id == rootID { + return CloudPath("/") + } + var components = [String]() + var currentID = id + var depth = 0 + while currentID != rootID { + guard let metadata = cachedMetadata[currentID] else { + throw FileProviderAdapterError.itemNotFound + } + components.insert(metadata.name, at: 0) + currentID = metadata.parentID + depth += 1 + if depth > 1024 { + throw FileProviderAdapterError.unresolvableParentChain + } + } + return components.reduce(CloudPath("/")) { $0.appendingPathComponent($1) } + } + func getCachedMetadata(for cloudPath: CloudPath) throws -> ItemMetadata? { - cachedMetadata.first(where: { $1.cloudPath == cloudPath })?.value + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + if cloudPath == CloudPath("/") { + return cachedMetadata[rootID] + } + var currentID = rootID + var current: ItemMetadata? + let components = cloudPath.pathComponents.dropFirst() + for component in components { + guard let child = try childOfFolder(parentID: currentID, name: String(component)) else { + return nil + } + currentID = child.id! + current = child + } + return current + } + + private func childOfFolder(parentID: Int64, name: String) throws -> ItemMetadata? { + let lowercasedName = name.lowercased() + return cachedMetadata.values.first(where: { $0.parentID == parentID && $0.name.lowercased() == lowercasedName && $0.id != NSFileProviderItemIdentifier.rootContainerDatabaseValue }) } func getPlaceholderMetadata(withParentID parentID: Int64) throws -> [ItemMetadata] { @@ -133,15 +179,17 @@ class CloudTaskExecutorTestCase: XCTestCase { } func getAllCachedMetadata(inside parent: ItemMetadata) throws -> [ItemMetadata] { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + if parent.id == rootID { + return cachedMetadata.values.filter { $0.id != rootID } + } var result = [ItemMetadata]() - for metadata in cachedMetadata.values { - if metadata.id == parent.id { - continue - } - if metadata.parentID == parent.id { - result.append(metadata) - } else if metadata.cloudPath.path.hasPrefix(parent.cloudPath.path) { + var queue: [Int64] = [parent.id!] + while let current = queue.first { + queue.removeFirst() + for metadata in cachedMetadata.values where metadata.parentID == current && metadata.id != current { result.append(metadata) + queue.append(metadata.id!) } } return result @@ -260,8 +308,8 @@ class CloudTaskExecutorTestCase: XCTestCase { var deletionTasks = [Int64: DeletionTaskRecord]() var associatedItemMetadata = [Int64: ItemMetadata]() - func createTaskRecord(for item: ItemMetadata) throws -> DeletionTaskRecord { - let deletionTask = DeletionTaskRecord(correspondingItem: item.id!, cloudPath: item.cloudPath, parentID: item.parentID, itemType: item.type) + func createTaskRecord(for item: ItemMetadata, cloudPath: CloudPath) throws -> DeletionTaskRecord { + let deletionTask = DeletionTaskRecord(correspondingItem: item.id!, cloudPath: cloudPath, parentID: item.parentID, itemType: item.type) deletionTasks[item.id!] = deletionTask associatedItemMetadata[item.id!] = item return deletionTask @@ -290,7 +338,7 @@ class CloudTaskExecutorTestCase: XCTestCase { guard let itemMetadata = associatedItemMetadata[deletionTask.correspondingItem] else { throw DBManagerError.missingItemMetadata } - return DeletionTask(taskRecord: deletionTask, itemMetadata: itemMetadata) + return DeletionTask(taskRecord: deletionTask, itemMetadata: itemMetadata, cloudPath: deletionTask.cloudPath) } } @@ -299,13 +347,20 @@ class CloudTaskExecutorTestCase: XCTestCase { var reparentTasks = [Int64: ReparentTaskRecord]() var removedReparentTasks = [ReparentTaskRecord]() - func createTaskRecord(for itemMetadata: ItemMetadata, targetCloudPath: CloudPath, newParentID: Int64) throws -> ReparentTaskRecord { - let taskRecord = ReparentTaskRecord(correspondingItem: itemMetadata.id!, sourceCloudPath: itemMetadata.cloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: newParentID) + func createTaskRecord(for itemMetadata: ItemMetadata, sourceCloudPath: CloudPath, targetCloudPath: CloudPath, newParentID: Int64) throws -> ReparentTaskRecord { + let taskRecord = ReparentTaskRecord(correspondingItem: itemMetadata.id!, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: newParentID) reparentTasks[itemMetadata.id!] = taskRecord associatedItemMetadata[itemMetadata.id!] = itemMetadata return taskRecord } + func renameItemAndUpdateReparentTask(itemMetadata: ItemMetadata, newName: String, targetCloudPath: CloudPath, reparentRecord: ReparentTaskRecord) throws -> ReparentTaskRecord { + itemMetadata.name = newName + let refreshed = ReparentTaskRecord(correspondingItem: reparentRecord.correspondingItem, sourceCloudPath: reparentRecord.sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: reparentRecord.oldParentID, newParentID: reparentRecord.newParentID) + reparentTasks[reparentRecord.correspondingItem] = refreshed + return refreshed + } + func removeTaskRecord(_ task: ReparentTaskRecord) throws { removedReparentTasks.append(task) } @@ -330,7 +385,7 @@ class CloudTaskExecutorTestCase: XCTestCase { guard let itemMetadata = associatedItemMetadata[reparentTask.correspondingItem] else { throw DBManagerError.missingItemMetadata } - return ReparentTask(taskRecord: reparentTask, itemMetadata: itemMetadata) + return ReparentTask(taskRecord: reparentTask, itemMetadata: itemMetadata, cloudPath: reparentTask.sourceCloudPath) } } @@ -338,9 +393,13 @@ class CloudTaskExecutorTestCase: XCTestCase { var removedTaskRecords = [ItemEnumerationTaskRecord]() var createdTasks = [ItemEnumerationTask]() + var itemMetadataManager: ItemMetadataManager? + var stubCloudPath: CloudPath? + func createTask(for item: ItemMetadata, pageToken: String?) throws -> ItemEnumerationTask { let taskRecord = ItemEnumerationTaskRecord(correspondingItem: item.id!, pageToken: pageToken) - let task = ItemEnumerationTask(taskRecord: taskRecord, itemMetadata: item) + let cloudPath = try stubCloudPath ?? itemMetadataManager?.getCloudPath(for: item.id!) ?? CloudPath("/") + let task = ItemEnumerationTask(taskRecord: taskRecord, itemMetadata: item, cloudPath: cloudPath) createdTasks.append(task) return task } @@ -353,9 +412,13 @@ class CloudTaskExecutorTestCase: XCTestCase { class DownloadTaskManagerMock: DownloadTaskManager { var removedTasks = [DownloadTaskRecord]() + var itemMetadataManager: ItemMetadataManager? + var stubCloudPath: CloudPath? + func createTask(for item: ItemMetadata, replaceExisting: Bool, localURL: URL, onURLSessionTaskCreation: CryptomatorFileProvider.URLSessionTaskCreationClosure?) throws -> DownloadTask { let taskRecord = DownloadTaskRecord(correspondingItem: item.id!, replaceExisting: replaceExisting, localURL: localURL) - return DownloadTask(taskRecord: taskRecord, itemMetadata: item, onURLSessionTaskCreation: onURLSessionTaskCreation) + let cloudPath = try stubCloudPath ?? itemMetadataManager?.getCloudPath(for: item.id!) ?? CloudPath("/") + return DownloadTask(taskRecord: taskRecord, itemMetadata: item, cloudPath: cloudPath, onURLSessionTaskCreation: onURLSessionTaskCreation) } func removeTaskRecord(_ task: DownloadTaskRecord) throws { diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/DeletionTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/DeletionTaskExecutorTests.swift index e741fc2e8..07763fe83 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/DeletionTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/DeletionTaskExecutorTests.swift @@ -20,8 +20,8 @@ class DeletionTaskExecutorTests: CloudTaskExecutorTestCase { func testDeleteFile() throws { let expectation = XCTestExpectation() - let itemMetadata = ItemMetadata(id: 2, name: "TestFile", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFile"), isPlaceholderItem: false, isCandidateForCacheCleanup: false) - let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata) + let itemMetadata = ItemMetadata(id: 2, name: "TestFile", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let deletionTask = try deletionTaskManagerMock.getTask(for: taskRecord) taskExecutor.execute(task: deletionTask).then { XCTAssertEqual(1, self.cloudProviderMock.deletedFiles.count) @@ -39,8 +39,8 @@ class DeletionTaskExecutorTests: CloudTaskExecutorTestCase { func testDeleteFolder() throws { let expectation = XCTestExpectation() - let itemMetadata = ItemMetadata(id: 2, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false) - let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata) + let itemMetadata = ItemMetadata(id: 2, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let deletionTask = try deletionTaskManagerMock.getTask(for: taskRecord) taskExecutor.execute(task: deletionTask).then { XCTAssertEqual(1, self.cloudProviderMock.deletedFolders.count) @@ -63,8 +63,8 @@ class DeletionTaskExecutorTests: CloudTaskExecutorTestCase { } let expectation = XCTestExpectation() - let itemMetadata = ItemMetadata(id: 2, name: "TestFile", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFile"), isPlaceholderItem: false, isCandidateForCacheCleanup: false) - let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata) + let itemMetadata = ItemMetadata(id: 2, name: "TestFile", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let deletionTask = try deletionTaskManagerMock.getTask(for: taskRecord) let taskExecutor = DeletionTaskExecutor(provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock) taskExecutor.execute(task: deletionTask).then { @@ -88,8 +88,8 @@ class DeletionTaskExecutorTests: CloudTaskExecutorTestCase { } let expectation = XCTestExpectation() - let itemMetadata = ItemMetadata(id: 2, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false) - let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata) + let itemMetadata = ItemMetadata(id: 2, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false) + let taskRecord = try deletionTaskManagerMock.createTaskRecord(for: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let deletionTask = try deletionTaskManagerMock.getTask(for: taskRecord) let taskExecutor = DeletionTaskExecutor(provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock) taskExecutor.execute(task: deletionTask).then { diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift index 7a7fb3283..1a6f1f49f 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift @@ -19,10 +19,10 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let cloudPath = CloudPath("/File 1") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let downloadTaskRecord = try DownloadTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), replaceExisting: false, localURL: localURL) - let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) @@ -51,7 +51,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let localURL = tmpDirectory.appendingPathComponent("itemNotFound.txt", isDirectory: false) let cloudPath = CloudPath("/itemNotFound.txt") - let itemMetadata = ItemMetadata(id: 3, name: "itemNotFound.txt", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: 3, name: "itemNotFound.txt", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemMetadataResponse = { _ in Promise(CloudTaskTestError.correctPassthrough) @@ -61,7 +61,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { } let downloadTaskRecord = try DownloadTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), replaceExisting: false, localURL: localURL) - let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) @@ -89,10 +89,10 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let existingLocalContentData = try Data(contentsOf: localURL) let cloudPath = CloudPath("/File 1") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let downloadTaskRecord = try DownloadTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), replaceExisting: true, localURL: localURL) - let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) @@ -121,7 +121,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { func testDownloadPostProcessingForReplaceExisting() throws { let cloudPath = CloudPath("/File 1") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let localURL = tmpDirectory.appendingPathComponent("localItem.txt", isDirectory: false) let existingLocalContent = "Old Local FileContent" @@ -155,7 +155,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { func testDownloadPostProcessingForNewFile() throws { let cloudPath = CloudPath("/File 1") let itemID: Int64 = 2 - let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: itemID, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let localURL = tmpDirectory.appendingPathComponent("localItem.txt", isDirectory: false) let downloadedContent = "Downloaded FileContent" diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift index 454dd3386..9cb40360d 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift @@ -16,9 +16,9 @@ class FolderCreationTaskExecutorTests: CloudTaskExecutorTestCase { func testCreateFolder() { let expectation = XCTestExpectation() let cloudPath = CloudPath("/NewFolder") - let itemMetadata = ItemMetadata(id: 2, name: "NewFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: 2, name: "NewFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true, isCandidateForCacheCleanup: false) - let task = FolderCreationTask(itemMetadata: itemMetadata) + let task = FolderCreationTask(itemMetadata: itemMetadata, cloudPath: cloudPath) let taskExecutor = FolderCreationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock) taskExecutor.execute(task: task).then { item in XCTAssertEqual(1, self.metadataManagerMock.updatedMetadata.count) @@ -26,7 +26,7 @@ class FolderCreationTaskExecutorTests: CloudTaskExecutorTestCase { XCTAssertEqual(ItemStatus.isUploaded, item.metadata.statusCode) XCTAssertFalse(item.metadata.isPlaceholderItem) XCTAssertEqual(1, self.cloudProviderMock.createdFolders.count) - XCTAssertEqual(item.metadata.cloudPath.path, self.cloudProviderMock.createdFolders[0]) + XCTAssertEqual(cloudPath.path, self.cloudProviderMock.createdFolders[0]) }.catch { error in XCTFail("Promise failed with error: \(error)") }.always { @@ -38,7 +38,7 @@ class FolderCreationTaskExecutorTests: CloudTaskExecutorTestCase { func testCreateFolderFailWithSameErrorAsProvider() { let expectation = XCTestExpectation() let cloudPath = CloudPath("/NewFolder") - let itemMetadata = ItemMetadata(id: 2, name: "NewFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: 2, name: "NewFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true, isCandidateForCacheCleanup: false) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.createFolderResponse = { folderPath in @@ -46,7 +46,7 @@ class FolderCreationTaskExecutorTests: CloudTaskExecutorTestCase { return Promise(CloudTaskTestError.correctPassthrough) } - let task = FolderCreationTask(itemMetadata: itemMetadata) + let task = FolderCreationTask(itemMetadata: itemMetadata, cloudPath: cloudPath) let taskExecutor = FolderCreationTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock) taskExecutor.execute(task: task).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift index ed4469068..46fc4925e 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift @@ -25,11 +25,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFileEnumeration() throws { let expectation = XCTestExpectation() let path = CloudPath("/File 1") - let fileMetadata = ItemMetadata(id: 2, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: path, isPlaceholderItem: false) + let fileMetadata = ItemMetadata(id: 2, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(fileMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(fileMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata, cloudPath: CloudPath("/").appendingPathComponent(fileMetadata.name)) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -46,7 +46,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { XCTAssertEqual(NSFileProviderItemIdentifier.rootContainerDatabaseValue, fetchedItemMetadata.parentID) XCTAssertNotNil(fetchedItemMetadata.lastModifiedDate) XCTAssertEqual(ItemStatus.isUploaded, fetchedItemMetadata.statusCode) - XCTAssertEqual(CloudPath("/File 1"), fetchedItemMetadata.cloudPath) + XCTAssertEqual("File 1", fetchedItemMetadata.name) XCTAssertFalse(fetchedItemMetadata.isPlaceholderItem) XCTAssertFalse(fetchedItemMetadata.isMaybeOutdated) @@ -69,7 +69,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectation = XCTestExpectation() let path = CloudPath("/File 1") let id: Int64 = 2 - let fileMetadata = ItemMetadata(id: id, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: path, isPlaceholderItem: false) + let fileMetadata = ItemMetadata(id: id, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(fileMetadata) uploadTaskManagerMock.getTaskRecordForClosure = { @@ -80,7 +80,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { } let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(fileMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata, cloudPath: CloudPath("/").appendingPathComponent(fileMetadata.name)) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -117,7 +117,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectation = XCTestExpectation() let path = CloudPath("/File 1") let id: Int64 = 2 - let fileMetadata = ItemMetadata(id: id, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: path, isPlaceholderItem: false) + let fileMetadata = ItemMetadata(id: id, name: "File 1", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(fileMetadata) let localURL = URL(fileURLWithPath: "/LocalFile 1") @@ -125,7 +125,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { try cachedFileManagerMock.cacheLocalFileInfo(for: id, localURL: localURL, lastModifiedDate: lastModifiedDate) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(fileMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata, cloudPath: CloudPath("/").appendingPathComponent(fileMetadata.name)) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -149,7 +149,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { XCTAssertEqual(NSFileProviderItemIdentifier.rootContainerDatabaseValue, fetchedItemMetadata.parentID) XCTAssertNotNil(fetchedItemMetadata.lastModifiedDate) XCTAssertEqual(ItemStatus.isUploaded, fetchedItemMetadata.statusCode) - XCTAssertEqual(CloudPath("/File 1"), fetchedItemMetadata.cloudPath) + XCTAssertEqual("File 1", fetchedItemMetadata.name) XCTAssertFalse(fetchedItemMetadata.isPlaceholderItem) XCTAssertFalse(fetchedItemMetadata.isMaybeOutdated) @@ -171,11 +171,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectation = XCTestExpectation() let cloudPath = CloudPath("/Test") let id: Int64 = 2 - let itemMetadata = ItemMetadata(id: id, name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true) + let itemMetadata = ItemMetadata(id: id, name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) try metadataManagerMock.cacheMetadata(itemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: itemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemMetadataResponse = { _ in @@ -203,20 +203,20 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { // MARK: Folder func testFolderEnumeration() async throws { - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) - let expectedItemMetadataInsideRootFolder = [ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false), - ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 1"), isPlaceholderItem: false), - ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 2"), isPlaceholderItem: false), - ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), - ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false)] + let expectedItemMetadataInsideRootFolder = [ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), + ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), + ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), + ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), + ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false)] - let expectedItemMetadataInsideSubFolder = [ItemMetadata(id: 7, name: "Directory 2", type: .folder, size: 0, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/Directory 2"), isPlaceholderItem: false), - ItemMetadata(id: 8, name: "File 5", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/File 5"), isPlaceholderItem: false)] + let expectedItemMetadataInsideSubFolder = [ItemMetadata(id: 7, name: "Directory 2", type: .folder, size: 0, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), + ItemMetadata(id: 8, name: "File 5", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false)] let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) let permissionProviderMock = PermissionProviderMock() @@ -230,14 +230,14 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let rootFolderItemList = try await taskExecutor.execute(task: enumerationTask).getValue() XCTAssertEqual(5, rootFolderItemList.items.count) XCTAssertEqual(expectedRootFolderFileProviderItems, rootFolderItemList.items) - XCTAssertEqual(expectedRootFolderFileProviderItems.map(\.metadata.cloudPath), rootFolderItemList.items.map(\.metadata.cloudPath)) + XCTAssertEqual(expectedRootFolderFileProviderItems.map(\.metadata.name), rootFolderItemList.items.map(\.metadata.name)) XCTAssertEqual(6, self.metadataManagerMock.cachedMetadata.count) // Check cached metadata equals expected metadata, except the last modified date let expectedItemMetadata = [rootItemMetadata] + expectedItemMetadataInsideRootFolder XCTAssert(expectedItemMetadata.allSatisfy { expectedMetadata in self.metadataManagerMock.cachedMetadata.contains(where: { key, value in - key == expectedMetadata.id && value.name == expectedMetadata.name && value.type == expectedMetadata.type && value.size == expectedMetadata.size && value.parentID == expectedMetadata.parentID && value.statusCode == expectedMetadata.statusCode && value.cloudPath == expectedMetadata.cloudPath && value.isPlaceholderItem == expectedMetadata.isPlaceholderItem + key == expectedMetadata.id && value.name == expectedMetadata.name && value.type == expectedMetadata.type && value.size == expectedMetadata.size && value.parentID == expectedMetadata.parentID && value.statusCode == expectedMetadata.statusCode && value.parentID == expectedMetadata.parentID && value.isPlaceholderItem == expectedMetadata.isPlaceholderItem }) }) XCTAssertEqual(1, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) @@ -245,31 +245,31 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let subfolderMetadata = rootFolderItemList.items[0].metadata let subfolderEnumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(subfolderMetadata.id), pageToken: nil) - let subfolderEnumerationTask = ItemEnumerationTask(taskRecord: subfolderEnumerationTaskRecord, itemMetadata: subfolderMetadata) + let subfolderEnumerationTask = ItemEnumerationTask(taskRecord: subfolderEnumerationTaskRecord, itemMetadata: subfolderMetadata, cloudPath: CloudPath("/").appendingPathComponent(subfolderMetadata.name)) let subfolderItemList = try await taskExecutor.execute(task: subfolderEnumerationTask).getValue() XCTAssertEqual(2, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) XCTAssert(self.itemEnumerationTaskManagerMock.removedTaskRecords.contains(where: { $0 == subfolderEnumerationTaskRecord })) XCTAssertEqual(2, subfolderItemList.items.count) XCTAssertEqual(expectedSubFolderFileProviderItems, subfolderItemList.items) - XCTAssertEqual(expectedSubFolderFileProviderItems.map(\.metadata.cloudPath), subfolderItemList.items.map(\.metadata.cloudPath)) + XCTAssertEqual(expectedSubFolderFileProviderItems.map(\.metadata.name), subfolderItemList.items.map(\.metadata.name)) XCTAssertEqual(8, self.metadataManagerMock.cachedMetadata.count) // Check cached metadata equals expected metadata, except the last modified date XCTAssert(expectedItemMetadataInsideSubFolder.allSatisfy { expectedMetadata in self.metadataManagerMock.cachedMetadata.contains(where: { key, value in - key == expectedMetadata.id && value.name == expectedMetadata.name && value.type == expectedMetadata.type && value.size == expectedMetadata.size && value.parentID == expectedMetadata.parentID && value.statusCode == expectedMetadata.statusCode && value.cloudPath == expectedMetadata.cloudPath && value.isPlaceholderItem == expectedMetadata.isPlaceholderItem + key == expectedMetadata.id && value.name == expectedMetadata.name && value.type == expectedMetadata.type && value.size == expectedMetadata.size && value.parentID == expectedMetadata.parentID && value.statusCode == expectedMetadata.statusCode && value.parentID == expectedMetadata.parentID && value.isPlaceholderItem == expectedMetadata.isPlaceholderItem }) }) }) } func testFolderEnumerationSameFolderTwice() async throws { - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let permissionProviderMock = PermissionProviderMock() let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -278,46 +278,46 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { $0.permissionProvider = permissionProviderMock }, operation: { permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading - let expectedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 1"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 2"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test)] - let expectedChangedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 2"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test), - FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/NewFileFromCloud"), isPlaceholderItem: false), domainIdentifier: .test)] + let expectedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test)] + let expectedChangedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test)] let firstEnumerationItemList = try await taskExecutor.execute(task: enumerationTask).getValue() XCTAssertEqual(5, firstEnumerationItemList.items.count) XCTAssertEqual(expectedRootFolderFileProviderItems, firstEnumerationItemList.items) - XCTAssertEqual(expectedRootFolderFileProviderItems.map(\.metadata.cloudPath), firstEnumerationItemList.items.map(\.metadata.cloudPath)) + XCTAssertEqual(expectedRootFolderFileProviderItems.map(\.metadata.name), firstEnumerationItemList.items.map(\.metadata.name)) XCTAssertEqual(1, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) XCTAssert(self.itemEnumerationTaskManagerMock.removedTaskRecords.contains(where: { $0 == enumerationTaskRecord })) self.cloudProviderMock.files["/File 1"] = nil self.cloudProviderMock.files["/NewFileFromCloud"] = Data("NewFileFromCloud content".utf8) let secondEnumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) - let secondEnumerationTask = ItemEnumerationTask(taskRecord: secondEnumerationTaskRecord, itemMetadata: rootItemMetadata) + let secondEnumerationTask = ItemEnumerationTask(taskRecord: secondEnumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let secondEnumerationItemList = try await taskExecutor.execute(task: secondEnumerationTask).getValue() XCTAssertEqual(2, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) XCTAssert(self.itemEnumerationTaskManagerMock.removedTaskRecords.contains(where: { $0 == secondEnumerationTaskRecord })) XCTAssertEqual(5, secondEnumerationItemList.items.count) XCTAssertEqual(expectedChangedRootFolderFileProviderItems, secondEnumerationItemList.items) - XCTAssertEqual(expectedChangedRootFolderFileProviderItems.map(\.metadata.cloudPath), secondEnumerationItemList.items.map(\.metadata.cloudPath)) + XCTAssertEqual(expectedChangedRootFolderFileProviderItems.map(\.metadata.name), secondEnumerationItemList.items.map(\.metadata.name)) }) } func testFolderEnumerationPreservesUploadError() throws { let expectation = XCTestExpectation() let id: Int64 = 3 - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -336,7 +336,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { } }.then { _ -> Promise in let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) - let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) return taskExecutor.execute(task: secondEnumerationTask) }.then { fileProviderItemList in XCTAssertEqual(2, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) @@ -364,15 +364,15 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectation = XCTestExpectation() let paginatedMockedProvider = CloudProviderPaginationMock() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: paginatedMockedProvider, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) let id: Int64 = 2 - let itemMetadata = ItemMetadata(id: 2, name: "TestItem", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestItem"), isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: 2, name: "TestItem", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cachedMetadata[XCTUnwrap(itemMetadata.id)] = itemMetadata taskExecutor.execute(task: enumerationTask).then { fileProviderItemList in @@ -396,15 +396,15 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectation = XCTestExpectation() let paginatedMockedProvider = CloudProviderPaginationMock() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) // Add some items to the ItemMetadataManager to simulate a previous folder enumeration on the root folder - try metadataManagerMock.cacheMetadata(ItemMetadata(id: 2, name: "OutdatedFile", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/OutdatedFile"), isPlaceholderItem: false, isCandidateForCacheCleanup: false)) - try metadataManagerMock.cacheMetadata(ItemMetadata(id: 3, name: "OutdatedFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/OutdatedFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false)) + try metadataManagerMock.cacheMetadata(ItemMetadata(id: 2, name: "OutdatedFile", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false)) + try metadataManagerMock.cacheMetadata(ItemMetadata(id: 3, name: "OutdatedFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, isCandidateForCacheCleanup: false)) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: paginatedMockedProvider, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -423,7 +423,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { XCTAssert(self.itemEnumerationTaskManagerMock.removedTaskRecords.contains(where: { $0 == enumerationTaskRecord })) let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nextPageToken) - let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) return taskExecutor.execute(task: secondEnumerationTask) }.then { fileProviderItemList in XCTAssertEqual(2, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) @@ -448,11 +448,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationDidNotOverwriteReparentTask() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -464,14 +464,14 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { // Simulate a local rename, i.e. update name, status code and create a reparent task, which renames the item with filename File 1 to RenamedItem let item = fileProviderItemList.items[1] XCTAssertEqual("File 1", item.filename) - self.reparentTaskManagerMock.reparentTasks[item.metadata.id!] = ReparentTaskRecord(correspondingItem: item.metadata.id!, sourceCloudPath: item.metadata.cloudPath, targetCloudPath: newCloudPath, oldParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, newParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) + self.reparentTaskManagerMock.reparentTasks[item.metadata.id!] = ReparentTaskRecord(correspondingItem: item.metadata.id!, sourceCloudPath: CloudPath("/").appendingPathComponent(item.metadata.name), targetCloudPath: newCloudPath, oldParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, newParentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue) self.metadataManagerMock.cachedMetadata[item.metadata.id!]?.name = "RenamedItem" self.metadataManagerMock.cachedMetadata[item.metadata.id!]?.statusCode = .isUploading - self.metadataManagerMock.cachedMetadata[item.metadata.id!]?.cloudPath = newCloudPath + // Enumerate the Root folder again, File 1 has not been renamed in the cloud yet let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) - let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) return taskExecutor.execute(task: secondEnumerationTask) }.then { fileProviderItemList in XCTAssertEqual(2, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) @@ -482,7 +482,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { XCTAssertNil(oldItem) XCTAssertNotNil(renamedItem) XCTAssertEqual(ItemStatus.isUploading, renamedItem?.metadata.statusCode) - XCTAssertEqual(newCloudPath, renamedItem?.metadata.cloudPath) + XCTAssertEqual(newCloudPath.lastPathComponent, renamedItem?.metadata.name) }.catch { error in XCTFail("Error in promise: \(error)") }.always { @@ -494,11 +494,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationDidNotOverwriteDeletionTask() throws { let expectation = XCTestExpectation() - let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(rootItemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) @@ -511,12 +511,12 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { // Simulate a local item deletion, i.e. update the status code and create a deletion task. let item = fileProviderItemList.items[1] XCTAssertEqual("File 1", item.filename) - self.deletionTaskManagerMock.deletionTasks[item.metadata.id!] = DeletionTaskRecord(correspondingItem: item.metadata.id!, cloudPath: item.metadata.cloudPath, parentID: item.metadata.parentID, itemType: item.metadata.type) + self.deletionTaskManagerMock.deletionTasks[item.metadata.id!] = DeletionTaskRecord(correspondingItem: item.metadata.id!, cloudPath: CloudPath("/").appendingPathComponent(item.metadata.name), parentID: item.metadata.parentID, itemType: item.metadata.type) self.metadataManagerMock.cachedMetadata[item.metadata.id!]?.statusCode = .isUploading // Enumerate the Root folder again, File 1 has not been deleted in the cloud yet let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) - let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) + let secondEnumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata, cloudPath: CloudPath("/")) return taskExecutor.execute(task: secondEnumerationTask) }.then { fileProviderItemList in XCTAssertEqual(2, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) @@ -535,16 +535,16 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationOfflineFallbackWithCachedItems() throws { let expectation = XCTestExpectation() - let folderMetadata = ItemMetadata(id: 2, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder"), isPlaceholderItem: false, lastEnumeratedAt: Date()) + let folderMetadata = ItemMetadata(id: 2, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, lastEnumeratedAt: Date()) try metadataManagerMock.cacheMetadata(folderMetadata) - let child1 = ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder/File 1"), isPlaceholderItem: false) - let child2 = ItemMetadata(id: 4, name: "Directory 1", type: .folder, size: 0, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder/Directory 1/"), isPlaceholderItem: false) + let child1 = ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let child2 = ItemMetadata(id: 4, name: "Directory 1", type: .folder, size: 0, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(child1) try metadataManagerMock.cacheMetadata(child2) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(folderMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata, cloudPath: CloudPath("/").appendingPathComponent(folderMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemListResponse = { _, _ in @@ -566,11 +566,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationOfflineFallbackWithNoCachedItems() throws { let expectation = XCTestExpectation() - let folderMetadata = ItemMetadata(id: 2, name: "EmptyFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/EmptyFolder"), isPlaceholderItem: false) + let folderMetadata = ItemMetadata(id: 2, name: "EmptyFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(folderMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(folderMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata, cloudPath: CloudPath("/").appendingPathComponent(folderMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemListResponse = { _, _ in @@ -594,11 +594,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationOfflineFallbackWithEmptyEnumeratedFolder() throws { let expectation = XCTestExpectation() - let folderMetadata = ItemMetadata(id: 2, name: "EmptyFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/EmptyFolder"), isPlaceholderItem: false, lastEnumeratedAt: Date()) + let folderMetadata = ItemMetadata(id: 2, name: "EmptyFolder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, lastEnumeratedAt: Date()) try metadataManagerMock.cacheMetadata(folderMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(folderMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata, cloudPath: CloudPath("/").appendingPathComponent(folderMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemListResponse = { _, _ in @@ -620,14 +620,14 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationOfflineFallbackWithPageToken() throws { let expectation = XCTestExpectation() - let folderMetadata = ItemMetadata(id: 2, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder"), isPlaceholderItem: false) + let folderMetadata = ItemMetadata(id: 2, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(folderMetadata) - let child1 = ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder/File 1"), isPlaceholderItem: false) + let child1 = ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(child1) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(folderMetadata.id), pageToken: "somePageToken") - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata, cloudPath: CloudPath("/").appendingPathComponent(folderMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemListResponse = { _, _ in @@ -651,12 +651,12 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { func testFolderEnumerationOfflineFallbackPreservesCachedFileInfo() throws { let expectation = XCTestExpectation() - let folderMetadata = ItemMetadata(id: 2, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder"), isPlaceholderItem: false, lastEnumeratedAt: Date()) + let folderMetadata = ItemMetadata(id: 2, name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false, lastEnumeratedAt: Date()) try metadataManagerMock.cacheMetadata(folderMetadata) let lastModifiedDate = Date(timeIntervalSince1970: 0) - let child1 = ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: 2, lastModifiedDate: lastModifiedDate, statusCode: .isUploaded, cloudPath: CloudPath("/Folder/File 1"), isPlaceholderItem: false) - let child2 = ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Folder/File 2"), isPlaceholderItem: false) + let child1 = ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: 2, lastModifiedDate: lastModifiedDate, statusCode: .isUploaded, isPlaceholderItem: false) + let child2 = ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(child1) try metadataManagerMock.cacheMetadata(child2) @@ -664,7 +664,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { try cachedFileManagerMock.cacheLocalFileInfo(for: 3, localURL: localURL, lastModifiedDate: lastModifiedDate) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(folderMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: folderMetadata, cloudPath: CloudPath("/").appendingPathComponent(folderMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemListResponse = { _, _ in @@ -700,11 +700,11 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectation = XCTestExpectation() let cloudPath = CloudPath("/Test") let id: Int64 = 2 - let itemMetadata = ItemMetadata(id: id, name: "Test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true) + let itemMetadata = ItemMetadata(id: id, name: "Test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) try metadataManagerMock.cacheMetadata(itemMetadata) let enumerationTaskRecord = try ItemEnumerationTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), pageToken: nil) - let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: itemMetadata) + let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: itemMetadata, cloudPath: CloudPath("/").appendingPathComponent(itemMetadata.name)) let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.fetchItemListResponse = { _, _ in diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift index 151251160..e1e2d58e2 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift @@ -17,11 +17,11 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Folder/Test.txt") - let itemMetadata = ItemMetadata(id: 2, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: 2, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let reparentTaskRecord = try ReparentTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) - let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) + let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata, cloudPath: sourceCloudPath) let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { item in @@ -49,11 +49,11 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let sourceCloudPath = CloudPath("/Test") let targetCloudPath = CloudPath("/Folder/Test") - let itemMetadata = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let reparentTaskRecord = try ReparentTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) - let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) + let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata, cloudPath: sourceCloudPath) let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { item in @@ -86,11 +86,11 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let sourceCloudPath = CloudPath("/Test.txt") let targetCloudPath = CloudPath("/Folder/Test.txt") - let itemMetadata = ItemMetadata(id: 2, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: 2, name: "Test.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let reparentTaskRecord = try ReparentTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) - let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) + let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata, cloudPath: sourceCloudPath) let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") @@ -116,11 +116,11 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let sourceCloudPath = CloudPath("/Test") let targetCloudPath = CloudPath("/Folder/Test") - let itemMetadata = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: sourceCloudPath, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let reparentTaskRecord = try ReparentTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) - let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) + let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata, cloudPath: sourceCloudPath) let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift index 8710e8452..f16ba2cd4 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift @@ -19,15 +19,15 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { try "TestContent".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/FileToBeUploaded") let progressManagerMock = ProgressManagerMock() - let itemMetadata = ItemMetadata(id: itemID, name: "FileToBeUploaded", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: itemID, name: "FileToBeUploaded", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true, isCandidateForCacheCleanup: false) cachedFileManagerMock.cachedLocalFileInfo[itemID] = LocalCachedFileInfo(lastModifiedDate: nil, correspondingItem: itemID, localLastModifiedDate: Date(), localURL: localURL) let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock, progressManager: progressManagerMock) let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) - cloudProviderMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate + cloudProviderMock.lastModifiedDate[cloudPath.path] = mockedCloudDate let uploadTaskRecord = UploadTaskRecord(correspondingItem: itemID, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: nil) - let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) uploadTaskExecutor.execute(task: uploadTask).then { _ in XCTAssertEqual(Data("TestContent".utf8), self.cloudProviderMock.createdFiles["/FileToBeUploaded"]) @@ -39,7 +39,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let cachedFileInfo = self.cachedFileManagerMock.cachedLocalFileInfo[itemID] let lastModifiedDate = cachedFileInfo?.lastModifiedDate XCTAssertNotNil(lastModifiedDate) - XCTAssertEqual(self.cloudProviderMock.lastModifiedDate[itemMetadata.cloudPath.path], lastModifiedDate) + XCTAssertEqual(self.cloudProviderMock.lastModifiedDate[cloudPath.path], lastModifiedDate) // Verify that the upload task has been removed XCTAssertEqual([itemMetadata.id], self.uploadTaskManagerMock.removeTaskRecordForReceivedInvocations) @@ -61,15 +61,15 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let localURL = tmpDirectory.appendingPathComponent("FileToBeUploaded", isDirectory: false) try "TestContent".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/FileToBeUploaded") - let itemMetadata = ItemMetadata(id: 2, name: "FileToBeUploaded", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: 2, name: "FileToBeUploaded", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true, isCandidateForCacheCleanup: false) let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) - cloudProviderMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate + cloudProviderMock.lastModifiedDate[cloudPath.path] = mockedCloudDate let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let uploadTaskRecord = try UploadTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: nil) - let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) uploadTaskExecutor.execute(task: uploadTask).then { _ in XCTFail("Promise should not fulfill for missing local cached file info") @@ -90,17 +90,17 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let localURL = tmpDirectory.appendingPathComponent("FileToBeUploaded", isDirectory: false) try "TestContent".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/FileToBeUploaded") - let itemMetadata = ItemMetadata(id: 2, name: "FileToBeUploaded", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: 2, name: "FileToBeUploaded", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true, isCandidateForCacheCleanup: false) cachedFileManagerMock.cachedLocalFileInfo[2] = LocalCachedFileInfo(lastModifiedDate: nil, correspondingItem: 2, localLastModifiedDate: Date(), localURL: localURL) let cloudProviderUploadInconsistencyMock = CloudProviderUploadInconsistencyMock() let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) - cloudProviderUploadInconsistencyMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate + cloudProviderUploadInconsistencyMock.lastModifiedDate[cloudPath.path] = mockedCloudDate let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderUploadInconsistencyMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let uploadTaskRecord = try UploadTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: nil) - let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) uploadTaskExecutor.execute(task: uploadTask).then { item in // Verify that the file has been modified since the upload began. @@ -133,7 +133,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let localURL = tmpDirectory.appendingPathComponent("itemNotFound.txt", isDirectory: false) try "".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/itemNotFound.txt") - let itemMetadata = ItemMetadata(id: 2, name: "itemNotFound.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) + let itemMetadata = ItemMetadata(id: 2, name: "itemNotFound.txt", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true, isCandidateForCacheCleanup: false) cachedFileManagerMock.cachedLocalFileInfo[2] = LocalCachedFileInfo(lastModifiedDate: nil, correspondingItem: 2, localLastModifiedDate: Date(), localURL: localURL) let errorCloudProviderMock = CloudProviderErrorMock() @@ -144,7 +144,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let uploadTaskRecord = try UploadTaskRecord(correspondingItem: XCTUnwrap(itemMetadata.id), lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: nil) - let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, onURLSessionTaskCreation: nil) + let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata, cloudPath: cloudPath, onURLSessionTaskCreation: nil) let promise = uploadTaskExecutor.execute(task: uploadTask) wait(for: promise) diff --git a/CryptomatorFileProviderTests/PermissionProviderImplTests.swift b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift index 8416bccc4..83a974a89 100644 --- a/CryptomatorFileProviderTests/PermissionProviderImplTests.swift +++ b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift @@ -32,7 +32,7 @@ final class PermissionProviderImplTests: XCTestCase { self.fullVersionCheckerMock.isFullVersion = true let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) } @@ -46,7 +46,7 @@ final class PermissionProviderImplTests: XCTestCase { self.fullVersionCheckerMock.isFullVersion = true let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) } @@ -60,7 +60,7 @@ final class PermissionProviderImplTests: XCTestCase { self.fullVersionCheckerMock.isFullVersion = false let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) } @@ -74,7 +74,7 @@ final class PermissionProviderImplTests: XCTestCase { self.fullVersionCheckerMock.isFullVersion = false let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) } @@ -88,7 +88,7 @@ final class PermissionProviderImplTests: XCTestCase { self.fullVersionCheckerMock.isFullVersion = false let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) } @@ -103,7 +103,7 @@ final class PermissionProviderImplTests: XCTestCase { self.hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) } @@ -120,7 +120,7 @@ final class PermissionProviderImplTests: XCTestCase { self.hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) } @@ -135,7 +135,7 @@ final class PermissionProviderImplTests: XCTestCase { self.hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) } @@ -150,7 +150,7 @@ final class PermissionProviderImplTests: XCTestCase { self.hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) } @@ -165,7 +165,7 @@ final class PermissionProviderImplTests: XCTestCase { self.hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) } @@ -180,7 +180,7 @@ final class PermissionProviderImplTests: XCTestCase { self.hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let actualCapabilities = PermissionProviderImpl().getPermissions(for: metadata, at: .test) XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) } diff --git a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift index 94e0c2b0a..9fcd6a05c 100644 --- a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift @@ -67,7 +67,7 @@ class CacheManagingServiceSourceTests: XCTestCase { $0.permissionProvider = permissionProviderMock } operation: { permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading - testItem = FileProviderItem(metadata: ItemMetadata(id: itemID, name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false), domainIdentifier: .test) + testItem = FileProviderItem(metadata: ItemMetadata(id: itemID, name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false), domainIdentifier: .test) serviceSource.getItem = { receivedItemIdentifier in guard itemIdentifier == receivedItemIdentifier else { return nil diff --git a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift index b4c6951af..fbe33f15c 100644 --- a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift +++ b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift @@ -17,7 +17,7 @@ class WorkingSetObserverTests: XCTestCase { var notificatorMock: FileProviderNotificatorTypeMock! let updatedMetadataIDs: [Int64] = [1, 2, 3] lazy var updatedItems: [FileProviderItem] = updatedMetadataIDs.map { - FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: CloudPath("/\($0)"), isPlaceholderItem: false), domainIdentifier: .test) + FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, isPlaceholderItem: false), domainIdentifier: .test) } override func setUpWithError() throws { From 725c59d68c20dad7201f82cbdde60129ab6cf765 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Sun, 17 May 2026 15:21:50 +0200 Subject: [PATCH 2/6] Add regression tests for descendant cloudPath after folder move --- .../FileProviderAdapterMoveItemTests.swift | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift index f804418f6..c75953bd9 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift @@ -128,6 +128,84 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { XCTAssertEqual(targetCloudPath, reparentTaskRecord.targetCloudPath) } + func testMoveFolderLocallyUpdatesDescendantCloudPaths() throws { + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try metadataManagerMock.cacheMetadata(rootItemMetadata) + + let sourceParentID: Int64 = 2 + let movedFolderID: Int64 = 3 + let childFileID: Int64 = 4 + let targetParentID: Int64 = 5 + + // Initial tree: + // / + // |- A/ + // | |- B/ + // | |- C.txt + // |- Target/ + let sourceParent = ItemMetadata(id: sourceParentID, name: "A", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let movedFolder = ItemMetadata(id: movedFolderID, name: "B", type: .folder, size: nil, parentID: sourceParentID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let childFile = ItemMetadata(id: childFileID, name: "C.txt", type: .file, size: 100, parentID: movedFolderID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let targetParent = ItemMetadata(id: targetParentID, name: "Target", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try metadataManagerMock.cacheMetadata([sourceParent, movedFolder, childFile, targetParent]) + + let movedFolderIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: movedFolderID) + let targetParentIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: targetParentID) + + // Move B from /A/B/ to /Target/B/. + // Because cloudPath is derived from the parentID chain, the descendant must follow the move automatically. + _ = try adapter.moveItemLocally(withIdentifier: movedFolderIdentifier, toParentItemWithIdentifier: targetParentIdentifier, newName: nil) + + // Sanity check for the folder row itself. + XCTAssertEqual(targetParentID, movedFolder.parentID) + XCTAssertEqual(CloudPath("/Target/B"), try metadataManagerMock.getCloudPath(for: movedFolderID)) + + // Regression check for #450: + // The child's parentID is unchanged, but its derived cloudPath must reflect the new chain. + // Otherwise path-based metadata lookups, subtree queries, enumeration, deletion bookkeeping, + // and follow-up remote operations can use the stale location. + XCTAssertEqual(movedFolderID, childFile.parentID) + XCTAssertEqual(CloudPath("/Target/B/C.txt"), try metadataManagerMock.getCloudPath(for: childFileID)) + XCTAssertEqual(childFileID, try metadataManagerMock.getCachedMetadata(for: CloudPath("/Target/B/C.txt"))?.id) + XCTAssertNil(try metadataManagerMock.getCachedMetadata(for: CloudPath("/A/B/C.txt"))) + } + + func testMoveFolderLocallyUpdatesDeepDescendantCloudPaths() throws { + let rootItemMetadata = ItemMetadata(id: NSFileProviderItemIdentifier.rootContainerDatabaseValue, name: "Home", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try metadataManagerMock.cacheMetadata(rootItemMetadata) + + let sourceParentID: Int64 = 2 + let movedFolderID: Int64 = 3 + let middleFolderID: Int64 = 4 + let deepFileID: Int64 = 5 + let targetParentID: Int64 = 6 + + // Initial tree: + // / + // |- A/ + // | |- B/ + // | |- C/ + // | |- D.txt + // |- Target/ + let sourceParent = ItemMetadata(id: sourceParentID, name: "A", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let movedFolder = ItemMetadata(id: movedFolderID, name: "B", type: .folder, size: nil, parentID: sourceParentID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let middleFolder = ItemMetadata(id: middleFolderID, name: "C", type: .folder, size: nil, parentID: movedFolderID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let deepFile = ItemMetadata(id: deepFileID, name: "D.txt", type: .file, size: 100, parentID: middleFolderID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let targetParent = ItemMetadata(id: targetParentID, name: "Target", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try metadataManagerMock.cacheMetadata([sourceParent, movedFolder, middleFolder, deepFile, targetParent]) + + let movedFolderIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: movedFolderID) + let targetParentIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: targetParentID) + _ = try adapter.moveItemLocally(withIdentifier: movedFolderIdentifier, toParentItemWithIdentifier: targetParentIdentifier, newName: nil) + + XCTAssertEqual(targetParentID, movedFolder.parentID) + XCTAssertEqual(CloudPath("/Target/B"), try metadataManagerMock.getCloudPath(for: movedFolderID)) + XCTAssertEqual(movedFolderID, middleFolder.parentID) + XCTAssertEqual(CloudPath("/Target/B/C"), try metadataManagerMock.getCloudPath(for: middleFolderID)) + XCTAssertEqual(middleFolderID, deepFile.parentID) + XCTAssertEqual(CloudPath("/Target/B/C/D.txt"), try metadataManagerMock.getCloudPath(for: deepFileID)) + } + func testRenameItem() throws { let expectation = XCTestExpectation() From 0b522f2f1a15a7b7eaa6c427d472a008685b19ab Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 26 May 2026 18:16:02 +0200 Subject: [PATCH 3/6] Apply review feedback for cloudPath parentID-chain derivation --- .../DB/ItemMetadataDBManager.swift | 13 +++- .../DB/UploadTaskDBManager.swift | 5 +- .../DB/MetadataManagerTests.swift | 76 +++++++++++++++++++ .../DB/UploadTaskManagerTests.swift | 10 +++ .../FileProviderAdapterTestCase.swift | 2 +- 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift b/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift index 14e6e7ea2..c8c4b4c65 100644 --- a/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift +++ b/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift @@ -142,9 +142,12 @@ class ItemMetadataDBManager: ItemMetadataManager { func getAllCachedMetadata(inside parent: ItemMetadata) throws -> [ItemMetadata] { precondition(parent.type == .folder) + guard let parentID = parent.id else { + throw DBManagerError.nonSavedItemMetadata + } return try database.read { db in let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue - if parent.id == rootID { + if parentID == rootID { return try ItemMetadata.filter(ItemMetadata.Columns.id != rootID).fetchAll(db) } return try ItemMetadata.fetchAll(db, sql: """ @@ -157,7 +160,7 @@ class ItemMetadataDBManager: ItemMetadataManager { WHERE d.depth < 1024 ) SELECT * FROM itemMetadata WHERE id IN (SELECT id FROM descendants) - """, arguments: [parent.id!]) + """, arguments: [parentID]) } } @@ -198,8 +201,10 @@ class ItemMetadataDBManager: ItemMetadataManager { private func childOfFolder(parentID: Int64, name: String, database: Database) throws -> ItemMetadata? { let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue let lowercasedName = name.lowercased() + // Deterministic order so case-only duplicate siblings resolve to a stable row (SQLite leaves row order unspecified without ORDER BY). let siblings = try ItemMetadata .filter(ItemMetadata.Columns.parentID == parentID && ItemMetadata.Columns.id != rootID) + .order(ItemMetadata.Columns.id) .fetchAll(database) return siblings.first { $0.name.lowercased() == lowercasedName } } @@ -234,10 +239,10 @@ class ItemMetadataDBManager: ItemMetadataManager { SELECT m.id, m.parentID, m.name, a.depth + 1 FROM itemMetadata m JOIN ancestors a ON m.id = a.parentID - WHERE a.id != 1 AND a.depth < 1024 + WHERE a.id != ? AND a.depth < 1024 ) SELECT id, name, depth FROM ancestors ORDER BY depth DESC - """, arguments: [id]) + """, arguments: [id, rootID]) guard !rows.isEmpty else { throw FileProviderAdapterError.itemNotFound } diff --git a/CryptomatorFileProvider/DB/UploadTaskDBManager.swift b/CryptomatorFileProvider/DB/UploadTaskDBManager.swift index ad20c8783..42f5eb98c 100644 --- a/CryptomatorFileProvider/DB/UploadTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/UploadTaskDBManager.swift @@ -66,8 +66,11 @@ class UploadTaskDBManager: UploadTaskManager { } func createNewTaskRecord(for itemMetadata: ItemMetadata) throws -> UploadTaskRecord { + guard let id = itemMetadata.id else { + throw DBManagerError.nonSavedItemMetadata + } return try database.write { db in - let task = UploadTaskRecord(correspondingItem: itemMetadata.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: Date()) + let task = UploadTaskRecord(correspondingItem: id, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil, uploadStartedAt: Date()) try task.save(db) return task } diff --git a/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift b/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift index e81a5006e..95b588531 100644 --- a/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift @@ -212,6 +212,16 @@ class MetadataManagerTests: XCTestCase { XCTAssertTrue(cachedMetadata.contains(where: { $0.id == itemMetadataForFileInSubFolder.id! })) } + func testGetAllCachedMetadataInsideUnsavedFolderThrowsNonSavedItemMetadata() throws { + let unsavedFolder = ItemMetadata(name: "Unsaved Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + XCTAssertThrowsError(try manager.getAllCachedMetadata(inside: unsavedFolder)) { error in + guard case DBManagerError.nonSavedItemMetadata = error else { + XCTFail("Throws the wrong error: \(error)") + return + } + } + } + func testGetMetadataWithCaseMismatchPath() throws { let cloudPath = CloudPath("/File.txt") let itemMetadataForFile = ItemMetadata(name: "File.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) @@ -230,6 +240,72 @@ class MetadataManagerTests: XCTestCase { XCTAssertEqual("File.txt", fetchedMetadataForInSensitivePath.name) } + func testGetCloudPathResolvesParentChain() throws { + let itemMetadataForFolder = ItemMetadata(name: "Test Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(itemMetadataForFolder) + let folderId = try XCTUnwrap(itemMetadataForFolder.id) + let itemMetadataForSubFolder = ItemMetadata(name: "SecondFolder", type: .folder, size: nil, parentID: folderId, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(itemMetadataForSubFolder) + let subFolderId = try XCTUnwrap(itemMetadataForSubFolder.id) + let itemMetadataForFile = ItemMetadata(name: "Test File.txt", type: .file, size: 100, parentID: subFolderId, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(itemMetadataForFile) + let fileId = try XCTUnwrap(itemMetadataForFile.id) + + XCTAssertEqual(CloudPath("/Test Folder/SecondFolder/Test File.txt"), try manager.getCloudPath(for: fileId)) + XCTAssertEqual(CloudPath("/"), try manager.getCloudPath(for: NSFileProviderItemIdentifier.rootContainerDatabaseValue)) + } + + func testGetCloudPathForMissingItemThrowsItemNotFound() throws { + XCTAssertThrowsError(try manager.getCloudPath(for: 999)) { error in + guard case FileProviderAdapterError.itemNotFound = error else { + XCTFail("Throws the wrong error: \(error)") + return + } + } + } + + func testGetCloudPathForUnresolvableParentChainThrows() throws { + let db = try DatabaseQueue() + try DatabaseHelper.migrate(db) + let orphan = ItemMetadata(name: "Orphan", type: .folder, size: nil, parentID: 999, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + // Foreign keys must be off to insert a row whose parent does not exist; PRAGMA can't change inside a transaction, hence writeWithoutTransaction. + try db.writeWithoutTransaction { db in + try db.execute(sql: "PRAGMA foreign_keys = OFF") + try orphan.insert(db) + } + let manager = ItemMetadataDBManager(database: db) + let orphanID = try XCTUnwrap(orphan.id) + XCTAssertThrowsError(try manager.getCloudPath(for: orphanID)) { error in + guard case FileProviderAdapterError.unresolvableParentChain = error else { + XCTFail("Throws the wrong error: \(error)") + return + } + } + } + + func testGetCachedMetadataForCaseOnlyDuplicateSiblingsResolvesDeterministically() throws { + let db = try DatabaseQueue() + try DatabaseHelper.migrate(db) + // Reverse the order of unordered SELECTs so this test fails unless childOfFolder explicitly orders by id. + try db.writeWithoutTransaction { db in + try db.execute(sql: "PRAGMA reverse_unordered_selects = ON") + } + // Bypass cacheMetadata's case-insensitive dedup to force two case-only siblings into the same parent. + let firstSibling = ItemMetadata(name: "Foo", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let secondSibling = ItemMetadata(name: "foo", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try db.write { db in + try firstSibling.insert(db) + try secondSibling.insert(db) + } + let lowerId = try XCTUnwrap(firstSibling.id) + XCTAssertLessThan(lowerId, try XCTUnwrap(secondSibling.id)) + + let manager = ItemMetadataDBManager(database: db) + let resolved = try XCTUnwrap(manager.getCachedMetadata(for: CloudPath("/foo"))) + XCTAssertEqual(lowerId, resolved.id) + XCTAssertEqual("Foo", resolved.name) + } + // MARK: Set Tag Data func testSetTagData() throws { diff --git a/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift index a69f4e7c9..e54a38e38 100644 --- a/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift @@ -38,6 +38,16 @@ class UploadTaskManagerTests: XCTestCase { XCTAssertNil(fetchedUploadTask.uploadErrorDomain) } + func testCreateNewTaskRecordForUnsavedMetadataThrowsNonSavedItemMetadata() throws { + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) + XCTAssertThrowsError(try manager.createNewTaskRecord(for: itemMetadata)) { error in + guard case DBManagerError.nonSavedItemMetadata = error else { + XCTFail("Throws the wrong error: \(error)") + return + } + } + } + func testUpdateTaskRecord() throws { let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(itemMetadata) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift index a58f7a953..c352f13ab 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift @@ -54,7 +54,7 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { uploadTaskManagerMock.getTaskForOnURLSessionTaskCreationClosure = { let id = $0.correspondingItem let metadata = try XCTUnwrap(self.metadataManagerMock.cachedMetadata[id]) - let cloudPath = (try? self.metadataManagerMock.getCloudPath(for: id)) ?? CloudPath("/") + let cloudPath = try self.metadataManagerMock.getCloudPath(for: id) return UploadTask(taskRecord: $0, itemMetadata: metadata, cloudPath: cloudPath, onURLSessionTaskCreation: $1) } } From 604e7f41679b9025577561d9de00ae7d56d8a6f4 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Wed, 24 Jun 2026 17:08:34 +0200 Subject: [PATCH 4/6] Drop removed cloudPath argument from resume ordering tests after develop merge --- .../FileProviderAdapterResumeOrderingTests.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterResumeOrderingTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterResumeOrderingTests.swift index e967c3e4e..1c66b4438 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterResumeOrderingTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterResumeOrderingTests.swift @@ -6,7 +6,6 @@ // Copyright © 2026 Skymatic GmbH. All rights reserved. // -import CryptomatorCloudAccessCore import FileProvider import Foundation import XCTest @@ -23,8 +22,8 @@ final class FileProviderAdapterResumeOrderingTests: FileProviderAdapterTestCase override func setUpWithError() throws { try super.setUpWithError() - metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "download.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/download.txt"), isPlaceholderItem: false) - metadataManagerMock.cachedMetadata[3] = ItemMetadata(id: 3, name: "upload.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/upload.txt"), isPlaceholderItem: false) + metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "download.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + metadataManagerMock.cachedMetadata[3] = ItemMetadata(id: 3, name: "upload.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: false) } // MARK: - Download From a258d2cf59a889b1b5b153a7b5dcbd81bd2b4525 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 31 Jul 2026 07:09:29 +0200 Subject: [PATCH 5/6] Add tests pinning cloud path resolution at task construction and move-task lock ordering --- .../DB/DownloadTaskManagerTests.swift | 4 ++- .../DB/ItemEnumerationTaskManagerTests.swift | 1 + .../DB/UploadTaskManagerTests.swift | 14 +++++++- .../WorkflowDependencyFactoryTests.swift | 34 +++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift index 7d8db1564..7d907ed32 100644 --- a/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift @@ -71,7 +71,9 @@ class DownloadTaskManagerTests: XCTestCase { let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) let localURL = URL(string: "/Test")! - return try manager.createTask(for: itemMetadata, replaceExisting: true, localURL: localURL, onURLSessionTaskCreation: nil) + let createdTask = try manager.createTask(for: itemMetadata, replaceExisting: true, localURL: localURL, onURLSessionTaskCreation: nil) + XCTAssertEqual(cloudPath, createdTask.cloudPath) + return createdTask } private func getTaskRecord(for id: Int64) throws -> DownloadTaskRecord { diff --git a/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift index 0c378c158..128b6464d 100644 --- a/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift @@ -33,6 +33,7 @@ class ItemEnumerationTaskManagerTests: XCTestCase { XCTAssertEqual(fetchedTask, createdTask.taskRecord) XCTAssertEqual(itemMetadata.id, fetchedTask.correspondingItem) XCTAssertEqual(pageToken, fetchedTask.pageToken) + XCTAssertEqual(cloudPath, createdTask.cloudPath) } private func getTaskRecord(for id: Int64) throws -> ItemEnumerationTaskRecord { diff --git a/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift b/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift index e54a38e38..eb1a5e0a8 100644 --- a/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift @@ -100,13 +100,25 @@ class UploadTaskManagerTests: XCTestCase { func testGetTask() throws { let cloudPath = CloudPath("/Test") - let itemMetadataManager = ItemMetadataDBManager(database: inMemoryDB) let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) try itemMetadataManager.cacheMetadata(itemMetadata) let taskRecord = try manager.createNewTaskRecord(for: itemMetadata) let fetchedTask = try manager.getTask(for: taskRecord, onURLSessionTaskCreation: nil) XCTAssertEqual(itemMetadata, fetchedTask.itemMetadata) XCTAssertEqual(itemMetadata.id, fetchedTask.taskRecord.correspondingItem) + XCTAssertEqual(cloudPath, fetchedTask.cloudPath) + } + + func testGetTaskResolvesCloudPathAfterLocalMoveOfItem() throws { + let folderMetadata = ItemMetadata(name: "Folder", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let itemMetadata = ItemMetadata(name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try itemMetadataManager.cacheMetadata([folderMetadata, itemMetadata]) + let taskRecord = try manager.createNewTaskRecord(for: itemMetadata) + itemMetadata.name = "RenamedTest" + itemMetadata.parentID = try XCTUnwrap(folderMetadata.id) + try itemMetadataManager.updateMetadata(itemMetadata) + let fetchedTask = try manager.getTask(for: taskRecord, onURLSessionTaskCreation: nil) + XCTAssertEqual(CloudPath("/Folder/RenamedTest"), fetchedTask.cloudPath) } // MARK: - getRetryableUploadTaskRecords diff --git a/CryptomatorFileProviderTests/WorkflowDependencyFactoryTests.swift b/CryptomatorFileProviderTests/WorkflowDependencyFactoryTests.swift index 4506abe11..c32d62ab7 100644 --- a/CryptomatorFileProviderTests/WorkflowDependencyFactoryTests.swift +++ b/CryptomatorFileProviderTests/WorkflowDependencyFactoryTests.swift @@ -239,6 +239,40 @@ class WorkflowDependencyFactoryTests: XCTestCase { XCTAssertEqual(expectedWriteTasksPaths, actualWriteTasksPaths) } + func testWriteTaskDependentOnExistingMoveTask() { + let dependenciesForMoveTask = factory.createDependencies(paths: [CloudPath("/a/b"), CloudPath("/c/b")], lockType: .write) + let dependenciesForWriteTask = factory.createDependencies(for: CloudPath("/c/b"), lockType: .write) + // check if move task starts immediately (but isn't completed yet, blocking the write task on the move target) + wait(for: dependenciesForMoveTask.awaitPreconditions()) + XCTAssertGetsNotExecuted(dependenciesForWriteTask.awaitPreconditions()) + + // when the move task completes, check if the dependent write task starts (but doesn't finish yet) + dependenciesForMoveTask.workflowCompleted.fulfill(()) + wait(for: dependenciesForWriteTask.awaitPreconditions()) + XCTAssertGetsNotExecuted(dependenciesForWriteTask.workflowCompleted) + + // when the write task completes its completion promise is fulfilled + dependenciesForWriteTask.workflowCompleted.fulfill(()) + wait(for: dependenciesForWriteTask.workflowCompleted) + } + + func testWriteTaskOnDescendantDependentOnExistingMoveTask() { + let dependenciesForMoveTask = factory.createDependencies(paths: [CloudPath("/a"), CloudPath("/c")], lockType: .write) + let dependenciesForWriteTask = factory.createDependencies(for: CloudPath("/c/d"), lockType: .write) + // check if move task starts immediately (but isn't completed yet, blocking the write task below the move target) + wait(for: dependenciesForMoveTask.awaitPreconditions()) + XCTAssertGetsNotExecuted(dependenciesForWriteTask.awaitPreconditions()) + + // when the move task completes, check if the dependent write task starts (but doesn't finish yet) + dependenciesForMoveTask.workflowCompleted.fulfill(()) + wait(for: dependenciesForWriteTask.awaitPreconditions()) + XCTAssertGetsNotExecuted(dependenciesForWriteTask.workflowCompleted) + + // when the write task completes its completion promise is fulfilled + dependenciesForWriteTask.workflowCompleted.fulfill(()) + wait(for: dependenciesForWriteTask.workflowCompleted) + } + // MARK: Error propagation func testErrorDoesNotPropagateBetweenDependentWorkflows() { From 3cd978915a9d47640414ccdf9fe36c984ddb03b1 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 31 Jul 2026 12:09:37 +0200 Subject: [PATCH 6/6] Keep case-only siblings as separate rows and reconcile enumeration batches order-independently --- .../DB/ItemMetadataDBManager.swift | 86 ++++++++-- .../DB/MetadataManagerTests.swift | 161 ++++++++++++++++-- 2 files changed, 220 insertions(+), 27 deletions(-) diff --git a/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift b/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift index c8c4b4c65..0b5d51710 100644 --- a/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift +++ b/CryptomatorFileProvider/DB/ItemMetadataDBManager.swift @@ -20,7 +20,7 @@ protocol ItemMetadataManager { /** Returns the item metadata that has the same path. - The path is case-insensitively checked for equality. + An exactly matching item wins; otherwise the path is case-insensitively checked for equality. However, it is stored and returned case-preserving in the database, because this is important for the `VaultDecorator` since the two cleartext paths "/foo" and "/Foo" lead to different ciphertext paths. */ @@ -52,9 +52,7 @@ class ItemMetadataDBManager: ItemMetadataManager { } func cacheMetadata(_ metadata: ItemMetadata) throws { - try database.write { db in - try cacheMetadata(metadata, database: db) - } + try cacheMetadata([metadata]) } func updateMetadata(_ metadata: ItemMetadata) throws { @@ -64,9 +62,11 @@ class ItemMetadataDBManager: ItemMetadataManager { } func cacheMetadata(_ itemMetadataList: [ItemMetadata]) throws { + // Sorted because `Dictionary` iteration order is unspecified, and it decides which folder's rows get the lower ids. + let itemMetadataListByParentID = Dictionary(grouping: itemMetadataList, by: { $0.parentID }).sorted { $0.key < $1.key } try database.write { db in - for metadata in itemMetadataList { - try cacheMetadata(metadata, database: db) + for (parentID, siblings) in itemMetadataListByParentID { + try reconcile(siblings, inFolder: parentID, database: db) } } } @@ -198,15 +198,28 @@ class ItemMetadataDBManager: ItemMetadataManager { return try ItemMetadata.fetchOne(database, key: id) } - private func childOfFolder(parentID: Int64, name: String, database: Database) throws -> ItemMetadata? { + /** + Returns every child of the folder, ordered by id so that case-only duplicate siblings resolve to a stable row. + + Callers match names in Swift rather than SQL: `==` on `String` treats canonically equivalent (NFC/NFD) spellings as equal, whereas SQLite compares bytes and its `NOCASE` collation folds ASCII only. Narrowing this to a `WHERE name = ?` would break that equivalence. + */ + private func childrenOfFolder(parentID: Int64, database: Database) throws -> [ItemMetadata] { let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue - let lowercasedName = name.lowercased() - // Deterministic order so case-only duplicate siblings resolve to a stable row (SQLite leaves row order unspecified without ORDER BY). - let siblings = try ItemMetadata + return try ItemMetadata .filter(ItemMetadata.Columns.parentID == parentID && ItemMetadata.Columns.id != rootID) .order(ItemMetadata.Columns.id) .fetchAll(database) - return siblings.first { $0.name.lowercased() == lowercasedName } + } + + private func childOfFolder(parentID: Int64, name: String, database: Database) throws -> ItemMetadata? { + let children = try childrenOfFolder(parentID: parentID, database: database) + // Exact spelling wins, so both case-only siblings stay reachable. + if let exactMatch = children.first(where: { $0.name == name }) { + return exactMatch + } + // Unlike the reconciler's fallback, this one is not gated on `isMaybeOutdated`: lookups resolve any case, they do not decide identity. + let lowercasedName = name.lowercased() + return children.first { $0.name.lowercased() == lowercasedName } } private func resolveMetadata(for cloudPath: CloudPath, database db: Database) throws -> ItemMetadata? { @@ -256,17 +269,58 @@ class ItemMetadataDBManager: ItemMetadataManager { return names.reduce(CloudPath("/")) { $0.appendingPathComponent($1) } } - private func cacheMetadata(_ metadata: ItemMetadata, database: Database) throws { - let cached = try childOfFolder(parentID: metadata.parentID, name: metadata.name, database: database) - if let cached = cached { + /** + Reconciles freshly reported items against the folder's cached rows. + + Names are compared case-sensitively: `Cryptor.encryptFileName(_:dirId:encoding:)` derives a different ciphertext name per case, so case-only siblings are two distinct cloud items. It precomposes to NFC first, so canonically equivalent (NFC/NFD) spellings share one ciphertext name and must resolve to a single item — including two spellings carried by the same listing, which is why freshly inserted rows join the pool. + + A row that no reported item matches exactly may instead be claimed by a case-insensitive match, but only while it is still flagged as maybe outdated. That flag separates a case-only rename, where the old spelling is gone and the row should keep its id, tag data and favorite rank, from two case variants coexisting, where each is its own item. Placeholders never claim by case, because an item the user is creating right now must not adopt the identity of an existing row. + + Every exact match resolves before any case-insensitive claim, so which row a given spelling ends up on never depends on where the cloud provider listed it. In a single pass the first-listed spelling of a newly added case variant would claim the row belonging to the other spelling, and the two would swap identities along with tags, favorite rank and cached content. Where both spellings are new to the cache, the listing order still decides which of them inherits the outdated row; both survive either way. + */ + private func reconcile(_ itemMetadataList: [ItemMetadata], inFolder parentID: Int64, database: Database) throws { + assert(itemMetadataList.allSatisfy { $0.parentID == parentID }) + var cachedChildren = try childrenOfFolder(parentID: parentID, database: database) + + func overwrite(_ cached: ItemMetadata, with metadata: ItemMetadata) throws { + assert(cachedChildren.contains { $0 === cached }) metadata.id = cached.id metadata.statusCode = cached.statusCode metadata.tagData = cached.tagData metadata.favoriteRank = cached.favoriteRank metadata.lastEnumeratedAt = cached.lastEnumeratedAt + // Clearing the flag is what stops the row being claimed by case again, here and in the remaining pages of the enumeration. + metadata.isMaybeOutdated = false try metadata.update(database) - } else { - try metadata.insert(database) + if let index = cachedChildren.firstIndex(where: { $0 === cached }) { + cachedChildren[index] = metadata + } + } + + var unmatchedMetadata = [ItemMetadata]() + for metadata in itemMetadataList { + if let sameName = cachedChildren.first(where: { $0.name == metadata.name }) { + try overwrite(sameName, with: metadata) + } else { + unmatchedMetadata.append(metadata) + } + } + + for metadata in unmatchedMetadata { + // Rows inserted earlier in this pass are in the pool, so a canonically equivalent spelling later in the same listing merges instead of duplicating. + if let sameName = cachedChildren.first(where: { $0.name == metadata.name }) { + try overwrite(sameName, with: metadata) + } else if !metadata.isPlaceholderItem, let claimed = firstClaimableByCase(named: metadata.name, in: cachedChildren) { + try overwrite(claimed, with: metadata) + } else { + try metadata.insert(database) + cachedChildren.append(metadata) + } } } + + private func firstClaimableByCase(named name: String, in cachedChildren: [ItemMetadata]) -> ItemMetadata? { + let lowercasedName = name.lowercased() + return cachedChildren.first { $0.isMaybeOutdated && $0.name.lowercased() == lowercasedName } + } } diff --git a/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift b/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift index 95b588531..c83c99ec1 100644 --- a/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift +++ b/CryptomatorFileProviderTests/DB/MetadataManagerTests.swift @@ -286,24 +286,163 @@ class MetadataManagerTests: XCTestCase { func testGetCachedMetadataForCaseOnlyDuplicateSiblingsResolvesDeterministically() throws { let db = try DatabaseQueue() try DatabaseHelper.migrate(db) - // Reverse the order of unordered SELECTs so this test fails unless childOfFolder explicitly orders by id. + // Reverse the order of unordered SELECTs so this test fails unless the sibling fetch explicitly orders by id. try db.writeWithoutTransaction { db in try db.execute(sql: "PRAGMA reverse_unordered_selects = ON") } - // Bypass cacheMetadata's case-insensitive dedup to force two case-only siblings into the same parent. + let manager = ItemMetadataDBManager(database: db) let firstSibling = ItemMetadata(name: "Foo", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) let secondSibling = ItemMetadata(name: "foo", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) - try db.write { db in - try firstSibling.insert(db) - try secondSibling.insert(db) + try manager.cacheMetadata(firstSibling) + try manager.cacheMetadata(secondSibling) + let lowerID = try XCTUnwrap(firstSibling.id) + let higherID = try XCTUnwrap(secondSibling.id) + XCTAssertLessThan(lowerID, higherID) + + let exactlyMatchingLowercase = try XCTUnwrap(manager.getCachedMetadata(for: CloudPath("/foo"))) + XCTAssertEqual(higherID, exactlyMatchingLowercase.id) + XCTAssertEqual("foo", exactlyMatchingLowercase.name) + + let exactlyMatchingUppercase = try XCTUnwrap(manager.getCachedMetadata(for: CloudPath("/Foo"))) + XCTAssertEqual(lowerID, exactlyMatchingUppercase.id) + XCTAssertEqual("Foo", exactlyMatchingUppercase.name) + + let caseMismatch = try XCTUnwrap(manager.getCachedMetadata(for: CloudPath("/FOO"))) + XCTAssertEqual(lowerID, caseMismatch.id) + } + + func testCacheMetadataKeepsCaseOnlySiblingsSeparate() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let uppercase = ItemMetadata(name: "CaseTest.txt", type: .file, size: 15, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let lowercase = ItemMetadata(name: "casetest.txt", type: .file, size: 15, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(uppercase) + try manager.cacheMetadata(lowercase) + + XCTAssertNotEqual(uppercase.id, lowercase.id) + let children = try manager.getCachedMetadata(withParentID: rootID) + XCTAssertEqual(["CaseTest.txt", "casetest.txt"], children.map { $0.name }.sorted()) + XCTAssertEqual(CloudPath("/CaseTest.txt"), try manager.getCloudPath(for: XCTUnwrap(uppercase.id))) + XCTAssertEqual(CloudPath("/casetest.txt"), try manager.getCloudPath(for: XCTUnwrap(lowercase.id))) + } + + func testCacheMetadataMergesCanonicallyEquivalentNames() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let precomposed = ItemMetadata(name: "Cafe\u{0301}.txt".precomposedStringWithCanonicalMapping, type: .file, size: 10, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let decomposed = ItemMetadata(name: "Cafe\u{0301}.txt".decomposedStringWithCanonicalMapping, type: .file, size: 20, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(precomposed) + try manager.cacheMetadata(decomposed) + + // Both spellings encrypt to the same ciphertext name, so they are one item. + XCTAssertEqual(precomposed.id, decomposed.id) + let children = try manager.getCachedMetadata(withParentID: rootID) + XCTAssertEqual(1, children.count) + XCTAssertEqual(decomposed.name, children.first?.name) + } + + func testCacheMetadataUpdatesCaseOnlyRenameInPlace() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let original = ItemMetadata(name: "foo.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(original) + let originalID = try XCTUnwrap(original.id) + let tagData = Data("Tag".utf8) + try manager.setTagData(to: tagData, forItemWithID: originalID) + try manager.setFavoriteRank(to: 42, forItemWithID: originalID) + + // Simulates an enumeration that reports only the renamed spelling. + try manager.flagAllItemsAsMaybeOutdated(withParentID: rootID) + let renamed = ItemMetadata(name: "Foo.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(renamed) + + XCTAssertEqual(originalID, renamed.id) + let children = try manager.getCachedMetadata(withParentID: rootID) + XCTAssertEqual(["Foo.txt"], children.map { $0.name }) + let reloaded = try XCTUnwrap(manager.getCachedMetadata(for: originalID)) + XCTAssertEqual(tagData, reloaded.tagData) + XCTAssertEqual(42, reloaded.favoriteRank) + } + + func testCacheMetadataKeepsIdentityWhenCaseVariantIsAddedBeforeTheKnownSpelling() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let known = ItemMetadata(name: "report.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(known) + let knownID = try XCTUnwrap(known.id) + let tagData = Data("Tag".utf8) + try manager.setTagData(to: tagData, forItemWithID: knownID) + try manager.flagAllItemsAsMaybeOutdated(withParentID: rootID) + + // Listed before the known spelling: the order a single-pass reconciler would get wrong. + let addedVariant = ItemMetadata(name: "Report.txt", type: .file, size: 5, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let knownAgain = ItemMetadata(name: "report.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata([addedVariant, knownAgain]) + + XCTAssertEqual(knownID, knownAgain.id) + XCTAssertNotEqual(knownID, addedVariant.id) + let reloaded = try XCTUnwrap(manager.getCachedMetadata(for: knownID)) + XCTAssertEqual("report.txt", reloaded.name) + XCTAssertEqual(tagData, reloaded.tagData) + } + + func testCacheMetadataReconcilesCaseVariantsIndependentlyOfListingOrder() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + func idsAfterCaching(_ names: [String]) throws -> (ids: [String: Int64], knownID: Int64) { + let db = try DatabaseQueue() + try DatabaseHelper.migrate(db) + let manager = ItemMetadataDBManager(database: db) + let known = ItemMetadata(name: "report.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(known) + try manager.flagAllItemsAsMaybeOutdated(withParentID: rootID) + let batch = names.map { ItemMetadata(name: $0, type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) } + try manager.cacheMetadata(batch) + let ids: [String: Int64] = try manager.getCachedMetadata(withParentID: rootID).reduce(into: [:]) { $0[$1.name] = $1.id } + return try (ids, XCTUnwrap(known.id)) } - let lowerId = try XCTUnwrap(firstSibling.id) - XCTAssertLessThan(lowerId, try XCTUnwrap(secondSibling.id)) + let variantFirst = try idsAfterCaching(["Report.txt", "report.txt"]) + let knownFirst = try idsAfterCaching(["report.txt", "Report.txt"]) + XCTAssertEqual(variantFirst.ids, knownFirst.ids) + // Agreement alone would also hold if both orders were wrong in the same way. + XCTAssertEqual(variantFirst.knownID, variantFirst.ids["report.txt"]) + } - let manager = ItemMetadataDBManager(database: db) - let resolved = try XCTUnwrap(manager.getCachedMetadata(for: CloudPath("/foo"))) - XCTAssertEqual(lowerId, resolved.id) - XCTAssertEqual("Foo", resolved.name) + func testCacheMetadataClaimsAnOutdatedRowOnlyOnce() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let known = ItemMetadata(name: "REPORT.TXT", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(known) + let knownID = try XCTUnwrap(known.id) + try manager.flagAllItemsAsMaybeOutdated(withParentID: rootID) + + // Neither reported spelling matches exactly, so both are candidates for the one outdated row. + let first = ItemMetadata(name: "Report.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let second = ItemMetadata(name: "report.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata([first, second]) + + XCTAssertNotEqual(first.id, second.id) + let children = try manager.getCachedMetadata(withParentID: rootID) + XCTAssertEqual(["Report.txt", "report.txt"], children.map { $0.name }.sorted()) + XCTAssertTrue([first.id, second.id].contains(knownID)) + } + + func testCacheMetadataMergesCanonicallyEquivalentNamesWithinOneBatch() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let precomposed = ItemMetadata(name: "Cafe\u{0301}.txt".precomposedStringWithCanonicalMapping, type: .file, size: 10, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + let decomposed = ItemMetadata(name: "Cafe\u{0301}.txt".decomposedStringWithCanonicalMapping, type: .file, size: 20, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata([precomposed, decomposed]) + + XCTAssertEqual(precomposed.id, decomposed.id) + XCTAssertEqual(1, try manager.getCachedMetadata(withParentID: rootID).count) + } + + func testCacheMetadataForPlaceholderDoesNotAdoptCaseOnlySibling() throws { + let rootID = NSFileProviderItemIdentifier.rootContainerDatabaseValue + let existing = ItemMetadata(name: "foo.txt", type: .file, size: 100, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploaded, isPlaceholderItem: false) + try manager.cacheMetadata(existing) + let existingID = try XCTUnwrap(existing.id) + try manager.flagAllItemsAsMaybeOutdated(withParentID: rootID) + + let placeholder = ItemMetadata(name: "Foo.txt", type: .file, size: 5, parentID: rootID, lastModifiedDate: nil, statusCode: .isUploading, isPlaceholderItem: true) + try manager.cacheMetadata(placeholder) + + XCTAssertNotEqual(existingID, placeholder.id) + XCTAssertEqual(2, try manager.getCachedMetadata(withParentID: rootID).count) } // MARK: Set Tag Data