diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift index 8a30b5f11ceb6..e23199a989f2e 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift @@ -5,6 +5,7 @@ import Foundation import NextcloudCapabilitiesKit import NextcloudKit +import UniformTypeIdentifiers public extension Item { /// @@ -190,11 +191,17 @@ public extension Item { ) } + let contentType: String = if itemTemplate.contentType == .aliasFile { + UTType.aliasFile.identifier + } else { + itemTemplate.contentType?.preferredMIMEType ?? "" + } + let newMetadata = SendableItemMetadata( ocId: ocId, account: account.ncKitAccount, classFile: "", // Placeholder as not set in original code - contentType: itemTemplate.contentType?.preferredMIMEType ?? "", + contentType: contentType, creationDate: Date(), // Default as not set in original code date: date ?? Date(), directory: false, diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift index b324a2ba3c5fe..59dbcedf871b0 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift @@ -4,6 +4,7 @@ @preconcurrency import FileProvider import Foundation import NextcloudKit +import UniformTypeIdentifiers public extension Item { private func fetchDirectoryContents( @@ -199,6 +200,18 @@ public extension Item { logger.debug("Acquired contents of item.", [.item: ocId, .name: updatedMetadata.fileName]) + if !isDirectory, updatedMetadata.contentType != UTType.aliasFile.identifier { + if let fileHandle = try? FileHandle(forReadingFrom: localPath) { + let magic = fileHandle.readData(ofLength: 4) + try? fileHandle.close() + // Apple Bookmark/Alias format magic bytes: "book" (0x62 0x6F 0x6F 0x6B) + if magic == Data([0x62, 0x6F, 0x6F, 0x6B]) { + logger.debug("Detected macOS alias file by magic number.", [.name: updatedMetadata.fileName]) + updatedMetadata.contentType = UTType.aliasFile.identifier + } + } + } + updatedMetadata.status = Status.normal.rawValue updatedMetadata.downloaded = true // HACK: We were previously failing to correctly set the uploaded state to true for diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemFetchTests.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemFetchTests.swift index f228df5859364..56bc61a3c8c85 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemFetchTests.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemFetchTests.swift @@ -7,6 +7,7 @@ import NextcloudFileProviderKitMocks import NextcloudKit import RealmSwift import TestInterface +import UniformTypeIdentifiers import XCTest final class ItemFetchTests: NextcloudFileProviderKitTestCase { @@ -72,6 +73,51 @@ final class ItemFetchTests: NextcloudFileProviderKitTestCase { XCTAssertEqual(fetchedItem.creationDate, item.creationDate) } + func testFetchAliasFileContentsDetectsAliasType() async throws { + let remoteInterface = MockRemoteInterface(account: Self.account, rootItem: rootItem) + remoteInterface.injectMock(Self.account) + + // Construct minimal data starting with the Apple Bookmark magic "book" (0x62 0x6F 0x6F 0x6B) + var aliasData = Data([0x62, 0x6F, 0x6F, 0x6B]) + aliasData.append(contentsOf: [UInt8](repeating: 0, count: 60)) + + let remoteItem = MockRemoteItem( + identifier: "aliasItem", + versionIdentifier: "0", + name: "My Alias", // no extension, no MIME type — as the server would present it + remotePath: Self.account.davFilesUrl + "/My Alias", + data: aliasData, + account: Self.account.ncKitAccount, + username: Self.account.username, + userId: Self.account.id, + serverUrl: Self.account.serverUrl + ) + rootItem.children = [remoteItem] + remoteItem.parent = rootItem + + // contentType is empty — as it arrives from the server with no MIME type + let itemMetadata = remoteItem.toItemMetadata(account: Self.account) + XCTAssertTrue(itemMetadata.contentType.isEmpty) + Self.dbManager.addItemMetadata(itemMetadata) + + let item = Item( + metadata: itemMetadata, + parentItemIdentifier: .rootContainer, + account: Self.account, + remoteInterface: remoteInterface, + dbManager: Self.dbManager + ) + + let (_, fetchedItemMaybe, error) = await item.fetchContents(dbManager: Self.dbManager) + XCTAssertNil(error) + let fetchedItem = try XCTUnwrap(fetchedItemMaybe) + + XCTAssertEqual(fetchedItem.contentType, UTType.aliasFile) + + let storedMetadata = Self.dbManager.itemMetadata(ocId: itemMetadata.ocId) + XCTAssertEqual(storedMetadata?.contentType, UTType.aliasFile.identifier) + } + func testFetchDirectoryContents() async throws { let remoteInterface = MockRemoteInterface(account: Self.account, rootItem: rootItem) remoteInterface.injectMock(Self.account) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift index 35bce1526acda..99c51263dd0db 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift @@ -187,12 +187,7 @@ import OSLog return progress } - func fetchContents( - for itemIdentifier: NSFileProviderItemIdentifier, - version requestedVersion: NSFileProviderItemVersion?, - request: NSFileProviderRequest, - completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void - ) -> Progress { + func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress { let actionId = UUID() insertSyncAction(actionId) logger.debug("Received request to fetch contents of item.", [.item: itemIdentifier, .request: request]) @@ -222,33 +217,21 @@ import OSLog return Progress() } - let progress = Progress() + Task { - guard let item = await Item.storedItem( - identifier: itemIdentifier, - account: ncAccount, - remoteInterface: ncKit, - dbManager: dbManager, - log: log - ) else { + guard let item = await Item.storedItem(identifier: itemIdentifier, account: ncAccount, remoteInterface: ncKit, dbManager: dbManager, log: log) else { logger.error("Not fetching contents for item because item was not found.", [.item: itemIdentifier]) - - completionHandler( - nil, - nil, - NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier) - ) + completionHandler(nil, nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier)) insertErrorAction(actionId) return } - let (localUrl, updatedItem, error) = await item.fetchContents( - domain: self.domain, progress: progress, dbManager: dbManager - ) + let (localUrl, updatedItem, error) = await item.fetchContents(domain: self.domain, progress: progress, dbManager: dbManager) removeSyncAction(actionId) completionHandler(localUrl, updatedItem, error) } + return progress }