diff --git a/kDriveCore/DI/FactoryService.swift b/kDriveCore/DI/FactoryService.swift
index cf10e9340..6663b920d 100644
--- a/kDriveCore/DI/FactoryService.swift
+++ b/kDriveCore/DI/FactoryService.swift
@@ -158,7 +158,8 @@ public enum FactoryService {
(loggerFactory, "BGTaskScheduling"),
(loggerFactory, "PhotoLibraryUploader"),
(loggerFactory, "AppDelegate"),
- (loggerFactory, "FileProvider")
+ (loggerFactory, "FileProvider"),
+ (loggerFactory, "DriveInfosManager")
]
return services
} else {
diff --git a/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager+FileProvider.swift b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager+FileProvider.swift
new file mode 100644
index 000000000..a97febd5d
--- /dev/null
+++ b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager+FileProvider.swift
@@ -0,0 +1,218 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 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 .
+ */
+
+import FileProvider
+import Foundation
+import InfomaniakConcurrency
+import InfomaniakCore
+
+public extension DriveInfosManager {
+ private typealias FilteredDomain = (new: NSFileProviderDomain, existing: NSFileProviderDomain?)
+
+ internal func initFileProviderDomains(drives: [Drive], user: InfomaniakCore.UserProfile) {
+ // Clean file provider storage if needed
+ if UserDefaults.shared.fpStorageVersion < currentFpStorageVersion {
+ do {
+ let fileURLs = try FileManager.default.contentsOfDirectory(
+ at: NSFileProviderManager.default.documentStorageURL,
+ includingPropertiesForKeys: nil
+ )
+ for url in fileURLs {
+ try FileManager.default.removeItem(at: url)
+ }
+ UserDefaults.shared.fpStorageVersion = currentFpStorageVersion
+ } catch {
+ Log.driveInfosManager("FileManager issue :\(error)", level: .error)
+ }
+ }
+
+ updateFileManagerDomains(drives: drives, user: user)
+ }
+
+ internal func deleteFileProviderDomains(for userId: Int) {
+ NSFileProviderManager.getDomainsWithCompletionHandler { allDomains, error in
+ if let error {
+ Log.driveInfosManager("Error while getting domains: \(error)", level: .error)
+ }
+
+ let domainsForCurrentUser = allDomains.filter { $0.identifier.rawValue.hasSuffix("_\(userId)") }
+ for domain in domainsForCurrentUser {
+ NSFileProviderManager.remove(domain) { error in
+ guard let error else {
+ return
+ }
+ Log.driveInfosManager("Error while removing domain \(domain.displayName): \(error)", level: .error)
+ }
+ }
+ }
+ }
+
+ func deleteAllFileProviderDomains() {
+ NSFileProviderManager.removeAllDomains { error in
+ guard let error else {
+ Log.driveInfosManager("Did remove all domains")
+ return
+ }
+ Log.driveInfosManager("Error while removing domains: \(error)", level: .error)
+ }
+ }
+
+ func getFileProviderManager(for drive: Drive, completion: @escaping (NSFileProviderManager) -> Void) {
+ getFileProviderManager(for: drive.objectId, completion: completion)
+ }
+
+ func getFileProviderManager(driveId: Int, userId: Int, completion: @escaping (NSFileProviderManager) -> Void) {
+ let objectId = DriveInfosManager.getObjectId(driveId: driveId, userId: userId)
+ getFileProviderManager(for: objectId, completion: completion)
+ }
+
+ func getFileProviderManager(for driveId: String, completion: @escaping (NSFileProviderManager) -> Void) {
+ getFileProviderDomain(for: driveId) { domain in
+ if let domain {
+ completion(NSFileProviderManager(for: domain) ?? .default)
+ } else {
+ completion(.default)
+ }
+ }
+ }
+
+ private func getFileProviderDomain(for driveId: String, completion: @escaping (NSFileProviderDomain?) -> Void) {
+ NSFileProviderManager.getDomainsWithCompletionHandler { domains, error in
+ if let error {
+ Log.driveInfosManager("Error while getting domains: \(error)", level: .error)
+ completion(nil)
+ } else {
+ completion(domains.first { $0.identifier.rawValue == driveId })
+ }
+ }
+ }
+
+ // MARK: Update FileManager
+
+ /// Diffing __NSFileProviderDomain__ for drives of a specified user, and propagate changes to the __NSFileProviderManager__
+ private func updateFileManagerDomains(drives: [Drive], user: InfomaniakCore.UserProfile) {
+ let expiringActivity = ExpiringActivity(id: "\(#function)_\(UUID().uuidString)", delegate: nil)
+ expiringActivity.start()
+ Task {
+ let updatedDomains = drives.map {
+ NSFileProviderDomain(
+ identifier: NSFileProviderDomainIdentifier($0.objectId),
+ displayName: "\($0.name) (\(user.email))",
+ pathRelativeToDocumentStorage: "\($0.objectId)"
+ )
+ }
+
+ Log.driveInfosManager("Updated domains \(updatedDomains.count) for user :\(user.displayName) \(user.id)")
+ do {
+ try await updateDomainsIfNecessary(updatedDomains: updatedDomains, userId: user.id)
+ try await deleteDomainsIfNecessary(updatedDomains: updatedDomains, userId: user.id)
+ } catch {
+ Log.driveInfosManager("Error while updating file provider domains: \(error)", level: .error)
+ }
+
+ expiringActivity.endAll()
+ }
+ }
+
+ /// Insert or update Domains if necessary
+ private func updateDomainsIfNecessary(updatedDomains: [NSFileProviderDomain], userId: Int) async throws {
+ let existingDomainsForCurrentUser = try await existingDomains(for: userId)
+
+ let updatedDomainsForCurrentUser: [FilteredDomain] = updatedDomains.map { newDomain in
+ let existingDomain = existingDomainsForCurrentUser.first { $0.identifier == newDomain.identifier }
+ return (newDomain, existingDomain)
+ }
+
+ try await updatedDomainsForCurrentUser.concurrentForEach(customConcurrency: 1) { domain in
+ // Simply add domain if new
+ let newDomain = domain.new
+ guard let existingDomain = domain.existing else {
+ Log.driveInfosManager("Inserting new domain:\(newDomain.identifier)")
+ try await NSFileProviderManager.add(newDomain)
+ self.signalChanges(for: newDomain)
+ return
+ }
+
+ // Update existing accounts if necessary
+ if existingDomain.displayName != newDomain.displayName {
+ Log.driveInfosManager("Updating domain:\(newDomain.identifier)")
+ try await NSFileProviderManager.remove(existingDomain)
+ try await NSFileProviderManager.add(newDomain)
+ self.signalChanges(for: newDomain)
+ }
+ }
+ }
+
+ /// Delete Domains if necessary
+ private func deleteDomainsIfNecessary(updatedDomains: [NSFileProviderDomain], userId: Int) async throws {
+ // We need to fetch a fresh copy of the domains after the update
+ let existingDomainsForCurrentUser = try await existingDomains(for: userId)
+
+ // Remove domains no longer present for current user
+ let removedDomainsForCurrentUser = updatedDomains.filter { updatedDomain in
+ guard existingDomainsForCurrentUser.contains(where: { $0.identifier == updatedDomain.identifier }) else {
+ return true
+ }
+
+ return false
+ }
+
+ try await removedDomainsForCurrentUser.concurrentForEach(customConcurrency: 1) { oldDomain in
+ Log.driveInfosManager("Removing domain:\(oldDomain.identifier)")
+ try await NSFileProviderManager.remove(oldDomain)
+ }
+ }
+
+ /// Fetch a fresh list of registered domains for a specified user
+ private func existingDomains(for userId: Int) async throws -> [NSFileProviderDomain] {
+ let allDomains = try await NSFileProviderManager.domains()
+ let existingDomains = allDomains.filter { $0.identifier.rawValue.hasSuffix("_\(userId)") }
+ return existingDomains
+ }
+
+ // MARK: Signal FileManager
+
+ /// Signal changes on this Drive to the File Provider Extension
+ private func signalChanges(for domain: NSFileProviderDomain) {
+ guard let driveId = domain.driveId, let userId = domain.userId else {
+ Log.driveInfosManager(
+ "Unable to read: driveId:\(String(describing: domain.driveId)) userId:\(String(describing: domain.userId))",
+ level: .error
+ )
+ return
+ }
+
+ DriveInfosManager.instance.getFileProviderManager(driveId: driveId, userId: userId) { manager in
+ manager.signalEnumerator(for: .workingSet) { error in
+ guard let error else {
+ Log.driveInfosManager("did signal .workingSet")
+ return
+ }
+
+ Log.driveInfosManager("failed to signal .workingSet \(error)", level: .error)
+ }
+ manager.signalEnumerator(for: .rootContainer) { error in
+ guard let error else {
+ Log.driveInfosManager("did signal .rootContainer")
+ return
+ }
+ Log.driveInfosManager("failed to signal .rootContainer \(error)", level: .error)
+ }
+ }
+ }
+}
diff --git a/kDriveCore/Data/Cache/DriveInfosManager.swift b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift
similarity index 61%
rename from kDriveCore/Data/Cache/DriveInfosManager.swift
rename to kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift
index e55a32249..60984ed0d 100644
--- a/kDriveCore/Data/Cache/DriveInfosManager.swift
+++ b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift
@@ -16,21 +16,23 @@
along with this program. If not, see .
*/
-import CocoaLumberjackSwift
-import FileProvider
import Foundation
import InfomaniakCore
import Realm
import RealmSwift
import Sentry
-public class DriveInfosManager {
- public static let instance = DriveInfosManager()
+public final class DriveInfosManager {
+ private static let dbName = "DrivesInfos.realm"
+
private static let currentDbVersion: UInt64 = 9
- private let currentFpStorageVersion = 1
+
+ let currentFpStorageVersion = 1
+
public let realmConfiguration: Realm.Configuration
- private let dbName = "DrivesInfos.realm"
- private var fileProviderManagers: [String: NSFileProviderManager] = [:]
+
+ // TODO: use DI
+ public static let instance = DriveInfosManager()
private class func removeDanglingObjects(ofType type: RLMObjectBase.Type, migration: Migration, ids: Set) {
migration.enumerateObjects(ofType: type.className()) { oldObject, newObject in
@@ -43,7 +45,7 @@ public class DriveInfosManager {
private init() {
realmConfiguration = Realm.Configuration(
- fileURL: DriveFileManager.constants.rootDocumentsURL.appendingPathComponent(dbName),
+ fileURL: DriveFileManager.constants.rootDocumentsURL.appendingPathComponent(Self.dbName),
schemaVersion: DriveInfosManager.currentDbVersion,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < DriveInfosManager.currentDbVersion {
@@ -116,125 +118,6 @@ public class DriveInfosManager {
drive.sharedWithMe = sharedWithMe
}
- private func initFileProviderDomains(drives: [Drive], user: InfomaniakCore.UserProfile) {
- // Clean file provider storage if needed
- if UserDefaults.shared.fpStorageVersion < currentFpStorageVersion {
- do {
- let fileURLs = try FileManager.default.contentsOfDirectory(
- at: NSFileProviderManager.default.documentStorageURL,
- includingPropertiesForKeys: nil
- )
- for url in fileURLs {
- try FileManager.default.removeItem(at: url)
- }
- UserDefaults.shared.fpStorageVersion = currentFpStorageVersion
- } catch {
- // Silently handle error
- }
- }
-
- let updatedDomains = drives.map {
- NSFileProviderDomain(
- identifier: NSFileProviderDomainIdentifier($0.objectId),
- displayName: "\($0.name) (\(user.email))",
- pathRelativeToDocumentStorage: "\($0.objectId)"
- )
- }
- Task {
- do {
- let allDomains = try await NSFileProviderManager.domains()
- var domainsForCurrentUser = allDomains.filter { $0.identifier.rawValue.hasSuffix("_\(user.id)") }
- await withThrowingTaskGroup(of: Void.self) { group in
- for newDomain in updatedDomains {
- // Check if domain already added
- if let existingDomainIndex = domainsForCurrentUser
- .firstIndex(where: { $0.identifier == newDomain.identifier }) {
- let existingDomain = domainsForCurrentUser.remove(at: existingDomainIndex)
- // Domain exists but its name could have changed
- if existingDomain.displayName != newDomain.displayName {
- group.addTask {
- try await NSFileProviderManager.remove(existingDomain)
- try await NSFileProviderManager.add(newDomain)
- }
- }
- } else {
- // Domain didn't exist we have to add it
- group.addTask {
- try await NSFileProviderManager.add(newDomain)
- }
- }
- }
- }
-
- // Remove left domains
- await withThrowingTaskGroup(of: Void.self) { group in
- for domain in domainsForCurrentUser {
- group.addTask {
- try await NSFileProviderManager.remove(domain)
- }
- }
- }
- } catch {
- DDLogError("Error while updating file provider domains: \(error)")
- }
- }
- }
-
- func deleteFileProviderDomains(for userId: Int) {
- NSFileProviderManager.getDomainsWithCompletionHandler { allDomains, error in
- if let error {
- DDLogError("Error while getting domains: \(error)")
- }
-
- let domainsForCurrentUser = allDomains.filter { $0.identifier.rawValue.hasSuffix("_\(userId)") }
- for domain in domainsForCurrentUser {
- NSFileProviderManager.remove(domain) { error in
- if let error {
- DDLogError("Error while removing domain \(domain.displayName): \(error)")
- }
- }
- }
- }
- }
-
- public func deleteAllFileProviderDomains() {
- NSFileProviderManager.removeAllDomains { error in
- if let error {
- DDLogError("Error while removing domains: \(error)")
- }
- }
- }
-
- func getFileProviderDomain(for driveId: String, completion: @escaping (NSFileProviderDomain?) -> Void) {
- NSFileProviderManager.getDomainsWithCompletionHandler { domains, error in
- if let error {
- DDLogError("Error while getting domains: \(error)")
- completion(nil)
- } else {
- completion(domains.first { $0.identifier.rawValue == driveId })
- }
- }
- }
-
- public func getFileProviderManager(for drive: Drive, completion: @escaping (NSFileProviderManager) -> Void) {
- getFileProviderManager(for: drive.objectId, completion: completion)
- }
-
- public func getFileProviderManager(driveId: Int, userId: Int, completion: @escaping (NSFileProviderManager) -> Void) {
- let objectId = DriveInfosManager.getObjectId(driveId: driveId, userId: userId)
- getFileProviderManager(for: objectId, completion: completion)
- }
-
- public func getFileProviderManager(for driveId: String, completion: @escaping (NSFileProviderManager) -> Void) {
- getFileProviderDomain(for: driveId) { domain in
- if let domain {
- completion(NSFileProviderManager(for: domain) ?? .default)
- } else {
- completion(.default)
- }
- }
- }
-
@discardableResult
func storeDriveResponse(user: InfomaniakCore.UserProfile, driveResponse: DriveResponse) -> [Drive] {
var driveList = [Drive]()
diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Notifications.swift b/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Notifications.swift
index 51451cbed..3ccc113c7 100644
--- a/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Notifications.swift
+++ b/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Notifications.swift
@@ -42,6 +42,11 @@ extension UploadQueue: UploadNotifiable {
public func sendPausedNotificationIfNeeded() {
Log.uploadQueue("sendPausedNotificationIfNeeded")
+ guard appContextService.context != .fileProviderExtension else {
+ Log.uploadQueue("\(#function) disabled in FileProviderExtension", level: .error)
+ return
+ }
+
serialQueue.async { [weak self] in
guard let self else { return }
if !pausedNotificationSent {
diff --git a/kDriveCore/Utils/AbstractLog+Category.swift b/kDriveCore/Utils/AbstractLog+Category.swift
index 7cafd1bed..8b1b4323b 100644
--- a/kDriveCore/Utils/AbstractLog+Category.swift
+++ b/kDriveCore/Utils/AbstractLog+Category.swift
@@ -175,6 +175,39 @@ public enum Log {
tag: tag)
}
+ /// Shorthand for ABLog, with "DriveInfosManager" category.
+ ///
+ /// Sentry tracking enabled when level == .error
+ public static func driveInfosManager(_ message: @autoclosure () -> Any,
+ level: AbstractLogLevel = .debug,
+ context: Int = 0,
+ file: StaticString = #file,
+ function: StaticString = #function,
+ line: UInt = #line,
+ tag: Any? = nil) {
+ let category = "DriveInfosManager"
+ let messageAny = message()
+ guard let messageString = messageAny as? String else {
+ assertionFailure("This should always cast to a String")
+ return
+ }
+
+ // All errors are tracked on Sentry
+ if level == .error {
+ SentryDebug.addBreadcrumb(message: messageString, category: .DriveInfosManager, level: .error)
+ SentryDebug.capture(message: messageString, level: .error, extras: ["function": "\(function)", "line": "\(line)"])
+ }
+
+ ABLog(messageAny,
+ category: category,
+ level: level,
+ context: context,
+ file: file,
+ function: function,
+ line: line,
+ tag: tag)
+ }
+
private static func defaultLogHandler(_ message: @autoclosure () -> Any,
category: String,
level: AbstractLogLevel,
diff --git a/kDriveCore/Utils/FileProvider/FileProviderDomain+Identifier.swift b/kDriveCore/Utils/FileProvider/FileProviderDomain+Identifier.swift
new file mode 100644
index 000000000..0c76b70f5
--- /dev/null
+++ b/kDriveCore/Utils/FileProvider/FileProviderDomain+Identifier.swift
@@ -0,0 +1,45 @@
+/*
+ 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 .
+ */
+
+import FileProvider
+import Foundation
+
+public extension NSFileProviderDomain {
+ private typealias DriveIdAndUserId = (driveId: Int, userId: Int)
+
+ private var userAndDrive: DriveIdAndUserId? {
+ let identifiers = identifier.rawValue.components(separatedBy: "_")
+
+ guard let driveIdString = identifiers[safe: 0],
+ let usedIdString = identifiers[safe: 1],
+ let driveId = Int(driveIdString),
+ let usedId = Int(usedIdString) else {
+ return nil
+ }
+
+ return (driveId, usedId)
+ }
+
+ var userId: Int? {
+ userAndDrive?.userId
+ }
+
+ var driveId: Int? {
+ userAndDrive?.driveId
+ }
+}
diff --git a/kDriveCore/Utils/Sentry/SentryDebug.swift b/kDriveCore/Utils/Sentry/SentryDebug.swift
index 20b4bc3e4..2d1decb03 100644
--- a/kDriveCore/Utils/Sentry/SentryDebug.swift
+++ b/kDriveCore/Utils/Sentry/SentryDebug.swift
@@ -38,6 +38,8 @@ public enum SentryDebug {
case realmMigration = "RealmMigration"
/// Photo library assets
case PHAsset
+ /// DriveInfosManager, and communication with FileProvider APIs
+ case DriveInfosManager
}
public enum EventNames {
@@ -136,6 +138,7 @@ public enum SentryDebug {
message: String,
context: [String: Any]? = nil,
contextKey: String? = nil,
+ level: SentryLevel? = nil,
extras: [String: Any]? = nil
) {
Task {
@@ -144,6 +147,10 @@ public enum SentryDebug {
scope.setContext(value: context, key: contextKey)
}
+ if let level {
+ scope.setLevel(level)
+ }
+
if let extras {
scope.setExtras(extras)
}
diff --git a/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift b/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift
index f9cbab131..460c8bd21 100644
--- a/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift
+++ b/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift
@@ -43,6 +43,10 @@ final class ITAppLaunchTest: XCTestCase {
SimpleResolver.register(FactoryService.debugServices)
let services = [
+ Factory(type: AppContextServiceable.self) { _, _ in
+ // We fake the main app context
+ return AppContextService(context: .app)
+ },
Factory(type: InfomaniakNetworkLogin.self) { _, _ in
return InfomaniakNetworkLogin(config: self.loginConfig)
},
diff --git a/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift b/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift
index e915f0080..f66a4f11c 100644
--- a/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift
+++ b/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift
@@ -42,6 +42,10 @@ final class UTRootViewControllerState: XCTestCase {
SimpleResolver.sharedResolver.removeAll()
let services = [
+ Factory(type: AppContextServiceable.self) { _, _ in
+ // We fake the main app context
+ return AppContextService(context: .app)
+ },
Factory(type: InfomaniakNetworkLogin.self) { _, _ in
InfomaniakNetworkLogin(config: self.loginConfig)
},