diff --git a/Sources/NextcloudKit/TypeIdentifiers/NKFilePropertyResolver.swift b/Sources/NextcloudKit/TypeIdentifiers/NKFilePropertyResolver.swift index cb1b1bb8..39ea3b32 100644 --- a/Sources/NextcloudKit/TypeIdentifiers/NKFilePropertyResolver.swift +++ b/Sources/NextcloudKit/TypeIdentifiers/NKFilePropertyResolver.swift @@ -29,6 +29,7 @@ public enum NKTypeIconFile: String { case compress = "compress" case directory = "directory" case document = "document" + case draw = "draw" case image = "image" case video = "video" case pdf = "pdf" @@ -44,119 +45,85 @@ public final class NKFilePropertyResolver { public init() {} - public func resolve(inUTI: String, capabilities: NKCapabilities.Capabilities) -> NKFileProperty { + public func resolve( + mimeType: String, + fileExtension: String, + typeIdentifier: String, + capabilities: NKCapabilities.Capabilities + ) -> NKFileProperty { + let fileProperty = NKFileProperty() - let typeIdentifier = inUTI as String - let utiString = inUTI as String - // Preferred extension - if let type = UTType(utiString), - let ext = type.preferredFilenameExtension { - fileProperty.ext = ext + // MARK: - Custom MIME types + + switch mimeType { + + case "application/vnd.excalidraw+json": + fileProperty.classFile = .document + fileProperty.iconName = .draw + fileProperty.name = "whiteboard" + fileProperty.ext = "whiteboard" + return fileProperty + + default: + break } - // Collabora Nextcloud Text Office - if capabilities.richDocumentsMimetypes.contains(typeIdentifier) { + // MARK: - Collabora / Office + + if capabilities.richDocumentsMimetypes.contains(mimeType) { fileProperty.classFile = .document fileProperty.iconName = .document fileProperty.name = "document" + return fileProperty + } + + // MARK: - Resolve UTType + guard let type = + UTType(mimeType: mimeType) ?? + UTType(typeIdentifier) + else { + fileProperty.classFile = .unknow + fileProperty.iconName = .unknow + fileProperty.name = "file" return fileProperty } - // Special-case identifiers - switch typeIdentifier { - case "text/plain", "text/html", "net.daringfireball.markdown", "text/x-markdown": + // MARK: - Type conformance + + if type.conforms(to: .image) { + + fileProperty.classFile = .image + fileProperty.iconName = .image + fileProperty.name = "image" + + } else if type.conforms(to: .movie) { + + fileProperty.classFile = .video + fileProperty.iconName = .video + fileProperty.name = "movie" + + } else if type.conforms(to: .audio) { + + fileProperty.classFile = .audio + fileProperty.iconName = .audio + fileProperty.name = "audio" + + } else if type.conforms(to: .text) { + fileProperty.classFile = .document - fileProperty.iconName = .document - fileProperty.name = "markdown" - return fileProperty - case "com.microsoft.word.doc": + fileProperty.iconName = .txt + fileProperty.name = "text" + + } else if type.conforms(to: .content) { + fileProperty.classFile = .document fileProperty.iconName = .document fileProperty.name = "document" - return fileProperty - case "com.apple.iwork.keynote.key": - fileProperty.classFile = .document - fileProperty.iconName = .ppt - fileProperty.name = "keynote" - return fileProperty - case "com.microsoft.excel.xls": - fileProperty.classFile = .document - fileProperty.iconName = .xls - fileProperty.name = "sheet" - return fileProperty - case "com.apple.iwork.numbers.numbers": - fileProperty.classFile = .document - fileProperty.iconName = .xls - fileProperty.name = "numbers" - return fileProperty - case "com.microsoft.powerpoint.ppt": - fileProperty.classFile = .document - fileProperty.iconName = .ppt - fileProperty.name = "presentation" - default: - break - } - // Well-known UTI type classifications - if let type = UTType(utiString) { - if type.conforms(to: .image) { - fileProperty.classFile = .image - fileProperty.iconName = .image - fileProperty.name = "image" - - } else if type.conforms(to: .movie) { - fileProperty.classFile = .video - fileProperty.iconName = .video - fileProperty.name = "movie" - - } else if type.conforms(to: .audio) { - fileProperty.classFile = .audio - fileProperty.iconName = .audio - fileProperty.name = "audio" - - } else if type.conforms(to: .zip) { - fileProperty.classFile = .compress - fileProperty.iconName = .compress - fileProperty.name = "archive" - - } else if type.conforms(to: .html) { - fileProperty.classFile = .document - fileProperty.iconName = .code - fileProperty.name = "code" - - } else if type.conforms(to: .pdf) { - fileProperty.classFile = .document - fileProperty.iconName = .pdf - fileProperty.name = "document" - - } else if type.conforms(to: .rtf) { - fileProperty.classFile = .document - fileProperty.iconName = .txt - fileProperty.name = "document" - - } else if type.conforms(to: .text) { - // Default to .txt if extension is empty - if fileProperty.ext.isEmpty { - fileProperty.ext = "txt" - } - fileProperty.classFile = .document - fileProperty.iconName = .txt - fileProperty.name = "text" - - } else if type.conforms(to: .content) { - fileProperty.classFile = .document - fileProperty.iconName = .document - fileProperty.name = "document" - - } else { - fileProperty.classFile = .unknow - fileProperty.iconName = .unknow - fileProperty.name = "file" - } } else { - // tipo UTI non valido + fileProperty.classFile = .unknow fileProperty.iconName = .unknow fileProperty.name = "file" diff --git a/Sources/NextcloudKit/TypeIdentifiers/NKTypeIdentifiers.swift b/Sources/NextcloudKit/TypeIdentifiers/NKTypeIdentifiers.swift index 1a2003c0..9140451e 100644 --- a/Sources/NextcloudKit/TypeIdentifiers/NKTypeIdentifiers.swift +++ b/Sources/NextcloudKit/TypeIdentifiers/NKTypeIdentifiers.swift @@ -5,7 +5,7 @@ import Foundation import UniformTypeIdentifiers -/// Resolved file type metadata, used for cache and classification +/// Resolved file type metadata, used for cache and classification. public struct NKTypeIdentifierCache: Sendable { public let mimeType: String public let classFile: String @@ -15,134 +15,254 @@ public struct NKTypeIdentifierCache: Sendable { public let ext: String } -/// Actor responsible for resolving file type metadata (UTI, MIME type, icon, class file, etc.) +/// Actor responsible for resolving file type metadata +/// (UTType, MIME type, icon, class file, etc.). public actor NKTypeIdentifiers { + public static let shared = NKTypeIdentifiers() - // Cache: extension → resolved type info + + // Cache key: + // "|" private var filePropertyCache: [String: NKTypeIdentifierCache] = [:] + // Internal resolver private let resolver = NKFilePropertyResolver() private init() {} - // Resolves type info from file name and optional MIME type - public func getInternalType(fileName: String, mimeType inputMimeType: String, directory: Bool, account: String) async -> NKTypeIdentifierCache { - var ext = (fileName as NSString).pathExtension.lowercased() - var mimeType = inputMimeType - var classFile = "" - var iconName = "" - var typeIdentifier = "" - var fileNameWithoutExt = (fileName as NSString).deletingPathExtension - - // Use full name if no extension - if ext.isEmpty { - fileNameWithoutExt = fileName + // MARK: - Public + + /// Resolves internal file type information. + /// + /// MIME type is considered the primary source of truth. + /// + /// - Parameters: + /// - fileName: Original file name. + /// - inputMimeType: MIME type provided by backend/server. + /// - directory: Indicates whether the item is a directory. + /// - account: Current account identifier. + /// + /// - Returns: Fully resolved file type metadata. + public func getInternalType( + fileName: String, + mimeType inputMimeType: String, + directory: Bool, + account: String + ) async -> NKTypeIdentifierCache { + + var fileExtension = + (fileName as NSString) + .pathExtension + .lowercased() + + let fileNameWithoutExtension = + fileExtension.isEmpty + ? fileName + : (fileName as NSString).deletingPathExtension + + // MARK: - Directory special case + + if directory { + + return NKTypeIdentifierCache( + mimeType: "httpd/unix-directory", + classFile: NKTypeClassFile.directory.rawValue, + iconName: NKTypeIconFile.directory.rawValue, + typeIdentifier: UTType.folder.identifier, + fileNameWithoutExt: fileName, + ext: "" + ) } - // Check cache first - if let cached = filePropertyCache[ext] { - return cached + // MARK: - Resolve MIME type + + let resolvedMimeType: String + + if !inputMimeType.isEmpty { + + resolvedMimeType = inputMimeType + + } else if let mime = + UTType(filenameExtension: fileExtension)? + .preferredMIMEType { + + resolvedMimeType = mime + + } else { + + resolvedMimeType = "application/octet-stream" } - // Resolve UTType - let type = UTType(filenameExtension: ext) ?? .data - typeIdentifier = type.identifier + // MARK: - Cache + + let cacheKey = "\(resolvedMimeType)|\(fileExtension)" - // Resolve MIME type - if mimeType.isEmpty { - mimeType = type.preferredMIMEType ?? "application/octet-stream" + if let cached = filePropertyCache[cacheKey] { + return cached } - // Handle folder case - if directory { - mimeType = "httpd/unix-directory" - classFile = NKTypeClassFile.directory.rawValue - iconName = NKTypeIconFile.directory.rawValue - typeIdentifier = UTType.folder.identifier - fileNameWithoutExt = fileName - ext = "" - } else { - let capabilities = await NKCapabilities.shared.getCapabilities(for: account) - let props = resolver.resolve(inUTI: typeIdentifier, capabilities: capabilities) - classFile = props.classFile.rawValue - iconName = props.iconName.rawValue + // MARK: - Resolve UTType + + // Resolve from MIME type first. + // Fallback to extension if needed. + let resolvedType = + UTType(mimeType: resolvedMimeType) ?? + UTType(filenameExtension: fileExtension) ?? + .data + + let typeIdentifier = resolvedType.identifier + + // Fill extension if missing + if fileExtension.isEmpty { + fileExtension = + resolvedType.preferredFilenameExtension ?? + "" } - // Construct result + // MARK: - Resolve properties + + let capabilities = + await NKCapabilities.shared.getCapabilities(for: account) + + let properties = resolver.resolve( + mimeType: resolvedMimeType, + fileExtension: fileExtension, + typeIdentifier: typeIdentifier, + capabilities: capabilities + ) + + // MARK: - Result + let result = NKTypeIdentifierCache( - mimeType: mimeType, - classFile: classFile, - iconName: iconName, + mimeType: resolvedMimeType, + classFile: properties.classFile.rawValue, + iconName: properties.iconName.rawValue, typeIdentifier: typeIdentifier, - fileNameWithoutExt: fileNameWithoutExt, - ext: ext + fileNameWithoutExt: fileNameWithoutExtension, + ext: fileExtension ) - // Cache it - if !ext.isEmpty { - filePropertyCache[ext] = result - } + // Cache result + filePropertyCache[cacheKey] = result return result } - // Clears the internal cache (used for testing or reset) + // MARK: - Cache + + /// Clears the internal cache. public func clearCache() { filePropertyCache.removeAll() } } -/// Helper class to access NKTypeIdentifiers from sync contexts (e.g. in legacy code or libraries). +/// Helper class used to access NKTypeIdentifiers +/// from synchronous contexts (legacy code, libraries, etc.). public final class NKTypeIdentifiersHelper { + public static let shared = NKTypeIdentifiersHelper() - // Resolves type info from file name and optional MIME type - public func getInternalType(fileName: String, mimeType inputMimeType: String, directory: Bool, capabilities: NKCapabilities.Capabilities) -> NKTypeIdentifierCache { - var ext = (fileName as NSString).pathExtension.lowercased() - var mimeType = inputMimeType - var classFile = "" - var iconName = "" - var typeIdentifier = "" - var fileNameWithoutExt = (fileName as NSString).deletingPathExtension - - // Use full name if no extension - if ext.isEmpty { - fileNameWithoutExt = fileName - } + private let resolver = NKFilePropertyResolver() + + private init() {} - // Resolve UTType - let type = UTType(filenameExtension: ext) ?? .data - typeIdentifier = type.identifier + // MARK: - Public - // Resolve MIME type - if mimeType.isEmpty { - mimeType = type.preferredMIMEType ?? "application/octet-stream" - } + /// Resolves internal type information synchronously. + /// + /// MIME type is considered the primary source of truth. + /// + /// - Parameters: + /// - fileName: Original file name. + /// - inputMimeType: MIME type provided by backend/server. + /// - directory: Indicates whether the item is a directory. + /// - capabilities: Current server capabilities. + /// + /// - Returns: Fully resolved file type metadata. + public func getInternalType( + fileName: String, + mimeType inputMimeType: String, + directory: Bool, + capabilities: NKCapabilities.Capabilities + ) -> NKTypeIdentifierCache { + + var fileExtension = + (fileName as NSString) + .pathExtension + .lowercased() + + let fileNameWithoutExtension = + fileExtension.isEmpty + ? fileName + : (fileName as NSString).deletingPathExtension + + // MARK: - Directory special case - // Handle folder case if directory { - mimeType = "httpd/unix-directory" - classFile = NKTypeClassFile.directory.rawValue - iconName = NKTypeIconFile.directory.rawValue - typeIdentifier = UTType.folder.identifier - fileNameWithoutExt = fileName - ext = "" + + return NKTypeIdentifierCache( + mimeType: "httpd/unix-directory", + classFile: NKTypeClassFile.directory.rawValue, + iconName: NKTypeIconFile.directory.rawValue, + typeIdentifier: UTType.folder.identifier, + fileNameWithoutExt: fileName, + ext: "" + ) + } + + // MARK: - Resolve MIME type + + let resolvedMimeType: String + + if !inputMimeType.isEmpty { + + resolvedMimeType = inputMimeType + + } else if let mime = + UTType(filenameExtension: fileExtension)? + .preferredMIMEType { + + resolvedMimeType = mime + } else { - let props = NKFilePropertyResolver().resolve(inUTI: typeIdentifier, capabilities: capabilities) - classFile = props.classFile.rawValue - iconName = props.iconName.rawValue + + resolvedMimeType = "application/octet-stream" } - // Construct result - let result = NKTypeIdentifierCache( - mimeType: mimeType, - classFile: classFile, - iconName: iconName, + // MARK: - Resolve UTType + + let resolvedType = + UTType(mimeType: resolvedMimeType) ?? + UTType(filenameExtension: fileExtension) ?? + .data + + let typeIdentifier = resolvedType.identifier + + // Fill extension if missing + if fileExtension.isEmpty { + fileExtension = + resolvedType.preferredFilenameExtension ?? + "" + } + + // MARK: - Resolve properties + + let properties = resolver.resolve( + mimeType: resolvedMimeType, + fileExtension: fileExtension, typeIdentifier: typeIdentifier, - fileNameWithoutExt: fileNameWithoutExt, - ext: ext + capabilities: capabilities ) - return result + // MARK: - Result + + return NKTypeIdentifierCache( + mimeType: resolvedMimeType, + classFile: properties.classFile.rawValue, + iconName: properties.iconName.rawValue, + typeIdentifier: typeIdentifier, + fileNameWithoutExt: fileNameWithoutExtension, + ext: fileExtension + ) } }