diff --git a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift index 47622e1e..c421ad5d 100644 --- a/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift +++ b/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift @@ -9,15 +9,6 @@ import RealmSwift /// Realm database abstraction and management. /// public final class FilesDatabaseManager: Sendable { - /// - /// File name suffix for Realm database files. - /// - /// In the past, before account-specific databases, there was a single and shared database which had this file name. - /// - /// > Important: The value must not change, as it is used to migrate from the old unified database to the new per-account databases. - /// - static let databaseFilename = "fileproviderextdatabase.realm" - public enum ErrorCode: Int { case metadataNotFound = -1000 case parentMetadataNotFound = -1001 @@ -46,45 +37,6 @@ public final class FilesDatabaseManager: Sendable { var itemMetadatas: Results { ncDatabase().objects(RealmItemMetadata.self) } - /// - /// Check for the existence of the directory where to place database files and return it. - /// - /// - Returns: The location of the database files directory. - /// - private func assertDatabaseDirectory(for identifier: NSFileProviderDomainIdentifier) -> URL { - logger.debug("Asserting existence of database directory...") - - let manager = FileManager.default - - guard let fileProviderExtensionDataDirectory = manager.fileProviderDomainSupportDirectory(for: identifier) else { - logger.fault("Failed to resolve the file provider extension data directory!") - assertionFailure("Failed to resolve the file provider extension data directory!") - return manager.temporaryDirectory // Only to satisfy the non-optional return type. The extension is unusable at this point anyway. - } - - let databaseDirectory = fileProviderExtensionDataDirectory.appendingPathComponent("Database", isDirectory: true) - let exists = manager.fileExists(atPath: databaseDirectory.path) - - if exists { - logger.info("Database directory exists at: \(databaseDirectory.path)") - } else { - logger.info("Due to nonexistent \"Database\" directory, assume it is not a legacy location and returning file provider extension data directory at: \(fileProviderExtensionDataDirectory.path)") - return fileProviderExtensionDataDirectory - } - - // Disable file protection for database directory. - // See: https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/ - - do { - try FileManager.default.setAttributes([.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: databaseDirectory.path) - logger.info("Set protectionKey attribute for database directory to FileProtectionType.completeUntilFirstUserAuthentication.") - } catch { - logger.error("Could not set protectionKey attribute to FileProtectionType.completeUntilFirstUserAuthentication for database directory: \(error)") - } - - return databaseDirectory - } - /// /// Convenience initializer which defines a default configuration for Realm. /// @@ -95,18 +47,18 @@ public final class FilesDatabaseManager: Sendable { /// public init(realmConfiguration customConfiguration: Realm.Configuration? = nil, account: Account, databaseDirectory customDatabaseDirectory: URL? = nil, fileProviderDomainIdentifier: NSFileProviderDomainIdentifier, log: any FileProviderLogging) { self.account = account - logger = FileProviderLogger(category: "FilesDatabaseManager", log: log) - logger.info("Initializing for account: \(account.ncKitAccount)") - let databaseDirectory = customDatabaseDirectory ?? assertDatabaseDirectory(for: fileProviderDomainIdentifier) - let accountDatabaseFilename: String = if UUID(uuidString: fileProviderDomainIdentifier.rawValue) != nil { - "\(fileProviderDomainIdentifier.rawValue).realm" - } else { - account.fileName + "-" + Self.databaseFilename + let defaultDatabaseDirectory = FileManager.default.fileProviderDomainSupportDirectory(for: fileProviderDomainIdentifier) + + guard let databaseDirectory = customDatabaseDirectory ?? defaultDatabaseDirectory else { + logger.fault("Neither custom nor default database directory defined!") + return } - let databaseLocation = databaseDirectory.appendingPathComponent(accountDatabaseFilename) + let databaseLocation = databaseDirectory + .appendingPathComponent(fileProviderDomainIdentifier.rawValue) + .appendingPathExtension("realm") let configuration = customConfiguration ?? Realm.Configuration( fileURL: databaseLocation, @@ -140,54 +92,12 @@ public final class FilesDatabaseManager: Sendable { Realm.Configuration.defaultConfiguration = configuration - let fileManager = FileManager.default - let databasePathFromRealmConfiguration = configuration.fileURL?.path - let migrate = databasePathFromRealmConfiguration != nil && fileManager.fileExists(atPath: databasePathFromRealmConfiguration!) == false - do { _ = try Realm() logger.info("Successfully created Realm.") } catch { logger.fault("Error creating Realm: \(error)") } - - // Migrate from old unified database to new per-account DB - guard migrate else { - logger.debug("No migration needed for \(account.ncKitAccount)") - return - } - - let sharedDatabaseURL = databaseDirectory.appendingPathComponent(Self.databaseFilename) - - guard FileManager.default.fileExists(atPath: sharedDatabaseURL.path) == true else { - logger.debug("No shared legacy database found at \"\(sharedDatabaseURL.path)\", skipping migration.") - return - } - - logger.info("Migrating shared legacy database to new database for \(account.ncKitAccount)") - - let legacyConfiguration = Realm.Configuration(fileURL: sharedDatabaseURL, schemaVersion: SchemaVersion.deletedLocalFileMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]) - - do { - let legacyRealm = try Realm(configuration: legacyConfiguration) - - let itemMetadatas = legacyRealm - .objects(RealmItemMetadata.self) - .filter { $0.account == account.ncKitAccount } - - let remoteFileChunks = legacyRealm.objects(RemoteFileChunk.self) - - logger.info("Migrating \(itemMetadatas.count) metadatas and \(remoteFileChunks.count) chunks.") - - let currentRealm = try Realm() - - try currentRealm.write { - itemMetadatas.forEach { currentRealm.create(RealmItemMetadata.self, value: $0) } - remoteFileChunks.forEach { currentRealm.create(RemoteFileChunk.self, value: $0) } - } - } catch { - logger.error("Error migrating shared legacy database to account-specific database for: \(account.ncKitAccount) because of error: \(error)") - } } func ncDatabase() -> Realm { diff --git a/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainLogDirectory.swift b/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainLogDirectory.swift index 2ce0d85d..686b4ce1 100644 --- a/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainLogDirectory.swift +++ b/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainLogDirectory.swift @@ -6,7 +6,7 @@ import Foundation public extension FileManager { /// - /// Return the sandboxed log directory specific to the file provider domain distinguished by the given identifier. + /// Return log directory specific to the file provider domain distinguished by the given identifier. /// /// If such directory does not exist yet, this attempts to create it implicitly. /// @@ -16,21 +16,23 @@ public extension FileManager { /// - Returns: A directory based on what the system returns for looking up standard directories. Likely in the sandbox containers of the file provider extension. Very unlikely to fail by returning `nil`. /// func fileProviderDomainLogDirectory(for identifier: NSFileProviderDomainIdentifier) -> URL? { - guard let libraryDirectory = try? url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + guard let applicationGroupContainer = applicationGroupContainer() else { return nil } - let logsDirectory = libraryDirectory.appendingPathComponent("Logs") - let fileProviderDomainLogDirectory = logsDirectory.appendingPathComponent(identifier.rawValue) + let logsDirectory = applicationGroupContainer + .appendingPathComponent("File Provider Domains", isDirectory: true) + .appendingPathComponent(identifier.rawValue, isDirectory: true) + .appendingPathComponent("Logs", isDirectory: true) - if fileExists(atPath: fileProviderDomainLogDirectory.path) == false { + if fileExists(atPath: logsDirectory.path) == false { do { - try createDirectory(at: fileProviderDomainLogDirectory, withIntermediateDirectories: true) + try createDirectory(at: logsDirectory, withIntermediateDirectories: true) } catch { return nil } } - return fileProviderDomainLogDirectory + return logsDirectory } } diff --git a/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainSupportDirectory.swift b/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainSupportDirectory.swift index 3e3275a5..007fc1b9 100644 --- a/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainSupportDirectory.swift +++ b/Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainSupportDirectory.swift @@ -6,46 +6,32 @@ import Foundation public extension FileManager { /// - /// Return the sandboxed application support directory specific to the file provider domain distinguished by the given identifier. + /// Return the application support directory specific to the file provider domain distinguished by the given identifier. /// /// If such directory does not exist yet, this attempts to create it implicitly. /// - /// > Legacy Support: In the past, a subdirectory in the application group container was used for everything. - /// This caused crashes due to violations of sandbox restrictions. - /// If already existent, the legacy location will be used. - /// Otherwise the data will be stored in a new location. - /// /// - Parameters: /// - identifier: File provider domain identifier which is used to isolate application support data for different file provider domains of the same extension. /// /// - Returns: A directory based on what the system returns for looking up standard directories. Likely in the sandbox containers of the file provider extension. Very unlikely to fail by returning `nil`. /// func fileProviderDomainSupportDirectory(for identifier: NSFileProviderDomainIdentifier) -> URL? { - // Legacy directory support. - if let containerUrl = pathForAppGroupContainer() { - let legacyLocation = containerUrl.appendingPathComponent("FileProviderExt") - - if FileManager.default.fileExists(atPath: legacyLocation.path) { - return legacyLocation - } - } - - // Designated file provider domain directories. - guard let applicationSupportDirectory = try? url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { + guard let containerUrl = applicationGroupContainer() else { return nil } - let domainsSupportDirectory = applicationSupportDirectory.appendingPathComponent("File Provider Domains") - let fileProviderDomainSupportDirectory = domainsSupportDirectory.appendingPathComponent(identifier.rawValue) + let supportDirectory = containerUrl + .appendingPathComponent("File Provider Domains", isDirectory: true) + .appendingPathComponent(identifier.rawValue, isDirectory: true) - if fileExists(atPath: fileProviderDomainSupportDirectory.path) == false { + if fileExists(atPath: supportDirectory.path) == false { do { - try createDirectory(at: fileProviderDomainSupportDirectory, withIntermediateDirectories: true) + try createDirectory(at: supportDirectory, withIntermediateDirectories: true) } catch { return nil } } - return fileProviderDomainSupportDirectory + return supportDirectory } } diff --git a/Sources/NextcloudFileProviderKit/Extensions/FileManager+applicationGroupContainer.swift b/Sources/NextcloudFileProviderKit/Extensions/FileManager+applicationGroupContainer.swift new file mode 100644 index 00000000..75111e54 --- /dev/null +++ b/Sources/NextcloudFileProviderKit/Extensions/FileManager+applicationGroupContainer.swift @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public extension FileManager { + /// + /// Resolve the location of the shared container for the app group of the file provider extension. + /// + /// - Returns: Container URL for the extension's app group or `nil`, if it could not be found. + /// + func applicationGroupContainer() -> URL? { + guard let infoDictionary = Bundle.main.infoDictionary else { + return nil + } + + guard let extensionDictionary = infoDictionary["NSExtension"] as? [String: Any] else { + return nil + } + + guard let appGroupIdentifier = extensionDictionary["NSExtensionFileProviderDocumentGroup"] as? String else { + return nil + } + + return containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) + } +} diff --git a/Sources/NextcloudFileProviderKit/Utilities/LocalFiles.swift b/Sources/NextcloudFileProviderKit/Utilities/LocalFiles.swift index 6f81ec6c..99904a78 100644 --- a/Sources/NextcloudFileProviderKit/Utilities/LocalFiles.swift +++ b/Sources/NextcloudFileProviderKit/Utilities/LocalFiles.swift @@ -5,22 +5,6 @@ import Foundation import OSLog -private let lfuLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "localfileutils") - -/// -/// Resolve the path of the shared container for the app group of the file provider extension. -/// -/// - Returns: Container URL for the extension's app group or `nil`, if it could not be found. -/// -public func pathForAppGroupContainer() -> URL? { - guard let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "NCFPKAppGroupIdentifier") as? String else { - lfuLogger.error("Could not get app group container URL due to missing value for NCFPKAppGroupIdentifier key in Info.plist!") - return nil - } - - return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) -} - /// /// Determine whether the given filename is a lock file as created by certain applications like Microsoft Office or LibreOffice. ///