Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 8 additions & 98 deletions Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -46,45 +37,6 @@ public final class FilesDatabaseManager: Sendable {

var itemMetadatas: Results<RealmItemMetadata> { 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.
///
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
16 changes: 0 additions & 16 deletions Sources/NextcloudFileProviderKit/Utilities/LocalFiles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Loading