diff --git a/Sources/InfomaniakCore/FileManagement/AppGroupPathProvider.swift b/Sources/InfomaniakCore/FileManagement/AppGroupPathProvider.swift new file mode 100644 index 0000000..35dac24 --- /dev/null +++ b/Sources/InfomaniakCore/FileManagement/AppGroupPathProvider.swift @@ -0,0 +1,129 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#if canImport(MobileCoreServices) + +import CocoaLumberjackSwift +import Foundation + +/// Something that can provide a set of common URLs within the app group +/// +/// This is shared between all apps of the group initialised with +public protocol AppGroupPathProvidable: AnyObject { + /// Failable init if app group is not found + init?(realmRootPath: String, appGroupIdentifier: String) + + /// The directory of the current app group, exists in FS. + /// Uses the `.completeUntilFirstUserAuthentication` protection policy + var groupDirectoryURL: URL { get } + + /// The root directory to store the App's Realm files, exists in FS + var realmRootURL: URL { get } + + /// The import directory, exists in FS + var importDirectoryURL: URL { get } + + /// The cache directory within the app group, exists in FS + var cacheDirectoryURL: URL { get } + + /// The temporary directory within the app group, exists in FS + var tmpDirectoryURL: URL { get } + + /// Open In Place directory if available + var openInPlaceDirectoryURL: URL? { get } +} + +public final class AppGroupPathProvider: AppGroupPathProvidable { + private let fileManager = FileManager.default + + private let realmRootPath: String + + // MARK: public var + + public let groupDirectoryURL: URL + + public lazy var realmRootURL: URL = { + let drivesURL = groupDirectoryURL.appendingPathComponent(self.realmRootPath, isDirectory: true) + try? fileManager.createDirectory( + atPath: drivesURL.path, + withIntermediateDirectories: true, + attributes: nil + ) + return drivesURL + }() + + public lazy var importDirectoryURL: URL = { + let importURL = groupDirectoryURL.appendingPathComponent("import", isDirectory: true) + try? fileManager.createDirectory( + atPath: importURL.path, + withIntermediateDirectories: true, + attributes: nil + ) + return importURL + }() + + public lazy var cacheDirectoryURL: URL = { + let cacheURL = groupDirectoryURL.appendingPathComponent("Library/Caches", isDirectory: true) + try? fileManager.createDirectory( + atPath: cacheURL.path, + withIntermediateDirectories: true, + attributes: nil + ) + return cacheURL + }() + + public lazy var tmpDirectoryURL: URL = { + let tmpURL = groupDirectoryURL.appendingPathComponent("tmp", isDirectory: true) + try? fileManager.createDirectory( + atPath: tmpURL.path, + withIntermediateDirectories: true, + attributes: nil + ) + return tmpURL + }() + + public lazy var openInPlaceDirectoryURL: URL? = { + let openInPlaceURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first? + .appendingPathComponent(".shared", isDirectory: true) + return openInPlaceURL + }() + + // MARK: init + + public init?(realmRootPath: String, appGroupIdentifier: String) { + guard let groupDirectoryURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) + else { + return nil + } + + do { + try fileManager.setAttributes( + [FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], + ofItemAtPath: groupDirectoryURL.path + ) + } catch { + DDLogError("[AppGroupPathProvider] failed to protect mandatory path :\(error)") + return nil + } + + self.groupDirectoryURL = groupDirectoryURL + self.realmRootPath = realmRootPath + } +} + +#endif diff --git a/Sources/InfomaniakCore/FileManagement/NSItemProvider+Detect.swift b/Sources/InfomaniakCore/FileManagement/NSItemProvider+Detect.swift new file mode 100644 index 0000000..9c1b2fb --- /dev/null +++ b/Sources/InfomaniakCore/FileManagement/NSItemProvider+Detect.swift @@ -0,0 +1,124 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#if canImport(MobileCoreServices) + +import Foundation +import InfomaniakDI + +/// Extending NSItemProvider for detecting file type, business logic. +extension NSItemProvider { + public enum ItemUnderlyingType: Equatable { + /// The item is an URL + case isURL + /// The item is Text + case isText + /// The item is an UIImage + case isUIImage + /// The item is image Data (heic or jpg) + case isImageData + /// The item is a Directory + case isDirectory + /// The item is a compressed file + case isCompressedData(identifier: String) + /// The item is of a miscellaneous type + case isMiscellaneous(identifier: String) + /// This should not happen, no type identifier was found + case none + } + + /// Wrapping business logic of supported types by the apps. + public var underlyingType: ItemUnderlyingType { + if hasItemConformingToTypeIdentifier(UTI.url.identifier) && registeredTypeIdentifiers.count == 1 { + return .isURL + } else if hasItemConformingToTypeIdentifier(UTI.plainText.identifier) + && !hasItemConformingToTypeIdentifier(UTI.fileURL.identifier) + && canLoadObject(ofClass: String.self) { + return .isText + } else if hasItemConformingToTypeIdentifier(UTI.directory.identifier) + || hasItemConformingToTypeIdentifier(UTI.folder.identifier) + || hasItemConformingToTypeIdentifier(UTI.filesAppFolder.identifier) { + return .isDirectory + } else if hasItemConformingToTypeIdentifier(UTI.zip.identifier) + || hasItemConformingToTypeIdentifier(UTI.bz2.identifier) + || hasItemConformingToTypeIdentifier(UTI.gzip.identifier) + || hasItemConformingToTypeIdentifier(UTI.archive.identifier), + let typeIdentifier = registeredTypeIdentifiers.first { + return .isCompressedData(identifier: typeIdentifier) + } else if registeredTypeIdentifiers.count == 1 && + registeredTypeIdentifiers.first == UTI.image.identifier { + return .isUIImage + } else if hasItemConformingToTypeIdentifier(UTI.heic.identifier) || + hasItemConformingToTypeIdentifier(UTI.jpeg.identifier) { + return .isImageData + } else if let typeIdentifier = registeredTypeIdentifiers.first { + return .isMiscellaneous(identifier: typeIdentifier) + } else { + return .none + } + } +} + +public extension NSItemProvider { + enum ErrorDomain: Error { + case unableToLoadURLForObject + case notADirectory + case wrapping(error: Error) + } + + /// Provide a zip representation of the item, if the`ItemUnderlyingType` is `.isDirectory` + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public var zippedRepresentation: Result { + get async { + guard underlyingType == ItemUnderlyingType.isDirectory else { + return .failure(ErrorDomain.notADirectory) + } + + @InjectService var pathProvider: AppGroupPathProvidable + + let fileManager = FileManager.default + let coordinator = NSFileCoordinator() + let tmpDirectoryURL = pathProvider.tmpDirectoryURL + let tempURL = tmpDirectoryURL.appendingPathComponent("\(UUID().uuidString).zip") + + let result: Result = await withCheckedContinuation { continuation in + _ = loadObject(ofClass: URL.self) { path, error in + guard error == nil, let path: URL = path else { + continuation.resume(returning: .failure(ErrorDomain.unableToLoadURLForObject)) + return + } + + // compress content of folder and move it somewhere we can safely store it for upload + var error: NSError? + coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { zipURL in + do { + try fileManager.moveItem(at: zipURL, to: tempURL) + continuation.resume(returning: .success(tempURL)) + } catch { + continuation.resume(returning: .failure(ErrorDomain.wrapping(error: error))) + } + } + } + } + + return result + } + } +} + +#endif diff --git a/Sources/InfomaniakCore/FileManagement/UTI.swift b/Sources/InfomaniakCore/FileManagement/UTI.swift new file mode 100644 index 0000000..145256f --- /dev/null +++ b/Sources/InfomaniakCore/FileManagement/UTI.swift @@ -0,0 +1,470 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2021 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#if canImport(MobileCoreServices) + +import Foundation +import MobileCoreServices + +public struct UTI: RawRepresentable { + public var rawValue: CFString + + public var identifier: String { + return rawValue as String + } + + public var preferredFilenameExtension: String? { + return UTTypeCopyPreferredTagWithClass(rawValue, kUTTagClassFilenameExtension)?.takeRetainedValue() as String? + } + + public var preferredMIMEType: String? { + return UTTypeCopyPreferredTagWithClass(rawValue, kUTTagClassMIMEType)?.takeRetainedValue() as String? + } + + public var localizedDescription: String? { + return UTTypeCopyDescription(rawValue)?.takeRetainedValue() as String? + } + + public var isDynamic: Bool { + return UTTypeIsDynamic(rawValue) + } + + public var isDeclared: Bool { + return UTTypeIsDeclared(rawValue) + } + + public init?(_ identifier: String) { + let allUTIs: [UTI] = [ + .item, + .content, + .compositeContent, + .diskImage, + .data, + .directory, + .resolvable, + .symbolicLink, + .executable, + .mountPoint, + .aliasFile, + .urlBookmarkData, + .url, + .fileURL, + .text, + .plainText, + .utf8PlainText, + .utf16ExternalPlainText, + .utf16PlainText, + .delimitedText, + .commaSeparatedText, + .tabSeparatedText, + .utf8TabSeparatedText, + .rtf, + .html, + .xml, + .sourceCode, + .assemblyLanguageSource, + .cSource, + .objectiveCSource, + .swiftSource, + .cPlusPlusSource, + .objectiveCPlusPlusSource, + .cHeader, + .cPlusPlusHeader, + .script, + .appleScript, + .osaScript, + .osaScriptBundle, + .javaScript, + .shellScript, + .perlScript, + .pythonScript, + .rubyScript, + .phpScript, + .json, + .propertyList, + .xmlPropertyList, + .binaryPropertyList, + .pdf, + .rtfd, + .flatRTFD, + .webArchive, + .image, + .jpeg, + .tiff, + .gif, + .png, + .icns, + .bmp, + .ico, + .rawImage, + .svg, + .livePhoto, + .heic, + .threeDContent, + .audiovisualContent, + .movie, + .video, + .audio, + .quickTimeMovie, + .mpeg, + .mpeg2Video, + .mpeg2TransportStream, + .mp3, + .mpeg4Movie, + .mpeg4Audio, + .appleProtectedMPEG4Audio, + .appleProtectedMPEG4Video, + .avi, + .aiff, + .wav, + .midi, + .playlist, + .m3uPlaylist, + .folder, + .volume, + .package, + .bundle, + .pluginBundle, + .spotlightImporter, + .quickLookGenerator, + .xpcService, + .framework, + .application, + .applicationBundle, + .unixExecutable, + .exe, + .systemPreferencesPane, + .archive, + .gzip, + .bz2, + .zip, + .spreadsheet, + .presentation, + .database, + .message, + .contact, + .vCard, + .toDoItem, + .calendarEvent, + .emailMessage, + .internetLocation, + .internetShortcut, + .font, + .bookmark, + .pkcs12, + .x509Certificate, + .epub, + .log + ] + guard let rawValue = allUTIs.first(where: { $0.identifier == identifier })?.rawValue else { + return nil + } + self.rawValue = rawValue + } + + public init?(filenameExtension: String, conformingTo supertype: UTI = .data) { + guard let rawValue = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, + filenameExtension as CFString, + supertype.identifier as CFString + )?.takeRetainedValue() else { + return nil + } + self.rawValue = rawValue + } + + public init?(mimeType: String, conformingTo supertype: UTI = .data) { + guard let rawValue = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassMIMEType, + mimeType as CFString, + supertype.identifier as CFString + )?.takeRetainedValue() else { + return nil + } + self.rawValue = rawValue + } + + public init(rawValue: CFString) { + self.rawValue = rawValue + } + + public func conforms(to type: UTI) -> Bool { + return UTTypeConformsTo(rawValue, type.rawValue) + } + + public static let item = UTI(rawValue: kUTTypeItem) + + public static let content = UTI(rawValue: kUTTypeContent) + + public static let compositeContent = UTI(rawValue: kUTTypeCompositeContent) + + public static let diskImage = UTI(rawValue: kUTTypeDiskImage) + + public static let data = UTI(rawValue: kUTTypeData) + + public static let directory = UTI(rawValue: kUTTypeDirectory) + + public static let resolvable = UTI(rawValue: kUTTypeResolvable) + + public static let symbolicLink = UTI(rawValue: kUTTypeSymLink) + + public static let executable = UTI(rawValue: kUTTypeExecutable) + + public static let mountPoint = UTI(rawValue: kUTTypeMountPoint) + + public static let aliasFile = UTI(rawValue: kUTTypeAliasFile) + + public static let urlBookmarkData = UTI(rawValue: kUTTypeURLBookmarkData) + + public static let url = UTI(rawValue: kUTTypeURL) + + public static let fileURL = UTI(rawValue: kUTTypeFileURL) + + public static let text = UTI(rawValue: kUTTypeText) + + public static let plainText = UTI(rawValue: kUTTypePlainText) + + public static let utf8PlainText = UTI(rawValue: kUTTypeUTF8PlainText) + + public static let utf16ExternalPlainText = UTI(rawValue: kUTTypeUTF16ExternalPlainText) + + public static let utf16PlainText = UTI(rawValue: kUTTypeUTF16PlainText) + + public static let delimitedText = UTI(rawValue: kUTTypeDelimitedText) + + public static let commaSeparatedText = UTI(rawValue: kUTTypeCommaSeparatedText) + + public static let tabSeparatedText = UTI(rawValue: kUTTypeTabSeparatedText) + + public static let utf8TabSeparatedText = UTI(rawValue: kUTTypeUTF8TabSeparatedText) + + public static let rtf = UTI(rawValue: kUTTypeRTF) + + public static let html = UTI(rawValue: kUTTypeHTML) + + public static let xml = UTI(rawValue: kUTTypeXML) + + public static let sourceCode = UTI(rawValue: kUTTypeSourceCode) + + public static let assemblyLanguageSource = UTI(rawValue: kUTTypeAssemblyLanguageSource) + + public static let cSource = UTI(rawValue: kUTTypeCSource) + + public static let objectiveCSource = UTI(rawValue: kUTTypeObjectiveCSource) + + public static let swiftSource = UTI(rawValue: kUTTypeSwiftSource) + + public static let cPlusPlusSource = UTI(rawValue: kUTTypeCPlusPlusSource) + + public static let objectiveCPlusPlusSource = UTI(rawValue: kUTTypeObjectiveCPlusPlusSource) + + public static let cHeader = UTI(rawValue: kUTTypeCHeader) + + public static let cPlusPlusHeader = UTI(rawValue: kUTTypeCPlusPlusHeader) + + public static let script = UTI(rawValue: kUTTypeScript) + + public static let appleScript = UTI(rawValue: kUTTypeAppleScript) + + public static let osaScript = UTI(rawValue: kUTTypeOSAScript) + + public static let osaScriptBundle = UTI(rawValue: kUTTypeOSAScriptBundle) + + public static let javaScript = UTI(rawValue: kUTTypeJavaScript) + + public static let shellScript = UTI(rawValue: kUTTypeShellScript) + + public static let perlScript = UTI(rawValue: kUTTypePerlScript) + + public static let pythonScript = UTI(rawValue: kUTTypePythonScript) + + public static let rubyScript = UTI(rawValue: kUTTypeRubyScript) + + public static let phpScript = UTI(rawValue: kUTTypePHPScript) + + public static let json = UTI(rawValue: kUTTypeJSON) + + public static let propertyList = UTI(rawValue: kUTTypePropertyList) + + public static let xmlPropertyList = UTI(rawValue: kUTTypeXMLPropertyList) + + public static let binaryPropertyList = UTI(rawValue: kUTTypeBinaryPropertyList) + + public static let pdf = UTI(rawValue: kUTTypePDF) + + public static let rtfd = UTI(rawValue: kUTTypeRTFD) + + public static let flatRTFD = UTI(rawValue: kUTTypeFlatRTFD) + + public static let webArchive = UTI(rawValue: kUTTypeWebArchive) + + public static let image = UTI(rawValue: kUTTypeImage) + + public static let jpeg = UTI(rawValue: kUTTypeJPEG) + + public static let tiff = UTI(rawValue: kUTTypeTIFF) + + public static let gif = UTI(rawValue: kUTTypeGIF) + + public static let png = UTI(rawValue: kUTTypePNG) + + public static let icns = UTI(rawValue: kUTTypeAppleICNS) + + public static let bmp = UTI(rawValue: kUTTypeBMP) + + public static let ico = UTI(rawValue: kUTTypeICO) + + public static let rawImage = UTI(rawValue: kUTTypeRawImage) + + public static let svg = UTI(rawValue: kUTTypeScalableVectorGraphics) + + public static let livePhoto = UTI(rawValue: kUTTypeLivePhoto) + + public static let heic = UTI(rawValue: "public.heic" as CFString) + + public static let threeDContent = UTI(rawValue: kUTType3DContent) + + public static let audiovisualContent = UTI(rawValue: kUTTypeAudiovisualContent) + + public static let movie = UTI(rawValue: kUTTypeMovie) + + public static let video = UTI(rawValue: kUTTypeVideo) + + public static let audio = UTI(rawValue: kUTTypeAudio) + + public static let quickTimeMovie = UTI(rawValue: kUTTypeQuickTimeMovie) + + public static let mpeg = UTI(rawValue: kUTTypeMPEG) + + public static let mpeg2Video = UTI(rawValue: kUTTypeMPEG2Video) + + public static let mpeg2TransportStream = UTI(rawValue: kUTTypeMPEG2TransportStream) + + public static let mp3 = UTI(rawValue: kUTTypeMP3) + + public static let mpeg4Movie = UTI(rawValue: kUTTypeMPEG4) + + public static let mpeg4Audio = UTI(rawValue: kUTTypeMPEG4Audio) + + public static let appleProtectedMPEG4Audio = UTI(rawValue: kUTTypeAppleProtectedMPEG4Audio) + + public static let appleProtectedMPEG4Video = UTI(rawValue: kUTTypeAppleProtectedMPEG4Video) + + public static let avi = UTI(rawValue: kUTTypeAVIMovie) + + public static let aiff = UTI(rawValue: kUTTypeAudioInterchangeFileFormat) + + public static let wav = UTI(rawValue: kUTTypeWaveformAudio) + + public static let midi = UTI(rawValue: kUTTypeMIDIAudio) + + public static let playlist = UTI(rawValue: kUTTypePlaylist) + + public static let m3uPlaylist = UTI(rawValue: kUTTypeM3UPlaylist) + + public static let folder = UTI(rawValue: kUTTypeFolder) + + /// Files.app folder type + public static let filesAppFolder = UTI(rawValue: "dyn.age8u" as CFString) + + public static let volume = UTI(rawValue: kUTTypeVolume) + + public static let package = UTI(rawValue: kUTTypePackage) + + public static let bundle = UTI(rawValue: kUTTypeBundle) + + public static let pluginBundle = UTI(rawValue: kUTTypePluginBundle) + + public static let spotlightImporter = UTI(rawValue: kUTTypeSpotlightImporter) + + public static let quickLookGenerator = UTI(rawValue: kUTTypeQuickLookGenerator) + + public static let xpcService = UTI(rawValue: kUTTypeXPCService) + + public static let framework = UTI(rawValue: kUTTypeFramework) + + public static let application = UTI(rawValue: kUTTypeApplication) + + public static let applicationBundle = UTI(rawValue: kUTTypeApplicationBundle) + + public static let unixExecutable = UTI(rawValue: kUTTypeUnixExecutable) + + public static let exe = UTI(rawValue: kUTTypeWindowsExecutable) + + public static let systemPreferencesPane = UTI(rawValue: kUTTypeSystemPreferencesPane) + + public static let archive = UTI(rawValue: kUTTypeArchive) + + public static let gzip = UTI(rawValue: kUTTypeGNUZipArchive) + + public static let bz2 = UTI(rawValue: kUTTypeBzip2Archive) + + public static let zip = UTI(rawValue: kUTTypeZipArchive) + + public static let spreadsheet = UTI(rawValue: kUTTypeSpreadsheet) + + public static let presentation = UTI(rawValue: kUTTypePresentation) + + public static let database = UTI(rawValue: kUTTypeDatabase) + + public static let message = UTI(rawValue: kUTTypeMessage) + + public static let contact = UTI(rawValue: kUTTypeContact) + + public static let vCard = UTI(rawValue: kUTTypeVCard) + + public static let toDoItem = UTI(rawValue: kUTTypeToDoItem) + + public static let calendarEvent = UTI(rawValue: kUTTypeCalendarEvent) + + public static let emailMessage = UTI(rawValue: kUTTypeEmailMessage) + + public static let internetLocation = UTI(rawValue: kUTTypeInternetLocation) + + public static let internetShortcut = UTI(rawValue: "com.microsoft.internet-shortcut" as CFString) + + public static let font = UTI(rawValue: kUTTypeFont) + + public static let bookmark = UTI(rawValue: kUTTypeBookmark) + + public static let pkcs12 = UTI(rawValue: kUTTypePKCS12) + + public static let x509Certificate = UTI(rawValue: kUTTypeX509Certificate) + + public static let epub = UTI(rawValue: kUTTypeElectronicPublication) + + public static let log = UTI(rawValue: kUTTypeLog) +} + +extension UTI: Equatable, Hashable { + public static func == (lhs: UTI, rhs: UTI) -> Bool { + return UTTypeEqual(lhs.rawValue, rhs.rawValue) + } +} + +extension UTI: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + return localizedDescription ?? identifier + } + + public var debugDescription: String { + return identifier + } +} + +#endif