Skip to content

Commit 8330844

Browse files
committed
fix: Proper sandbox support.
- These changes are related to making the main app bundle of the macOS build adopt the app sandbox entitlement which imposes certain restrictions on its file system access (nextcloud/desktop#9023). - Logs are now written to the app group container because they are not accessible by the main app for creating debug archives otherwise. - Databases are now written to the app group container because they are not accessible by the main app for creating debug archives otherwise. - Removed legacy database migration code because it cannot work with the new app group identifier of the now sandboxed app anymore. - Simplified database location assembly. Signed-off-by: Iva Horn <[email protected]>
1 parent 3aed8ec commit 8330844

File tree

5 files changed

+35
-134
lines changed

5 files changed

+35
-134
lines changed

Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift

Lines changed: 9 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,6 @@ import RealmSwift
99
/// Realm database abstraction and management.
1010
///
1111
public final class FilesDatabaseManager: Sendable {
12-
///
13-
/// File name suffix for Realm database files.
14-
///
15-
/// In the past, before account-specific databases, there was a single and shared database which had this file name.
16-
///
17-
/// > Important: The value must not change, as it is used to migrate from the old unified database to the new per-account databases.
18-
///
19-
static let databaseFilename = "fileproviderextdatabase.realm"
20-
2112
public enum ErrorCode: Int {
2213
case metadataNotFound = -1000
2314
case parentMetadataNotFound = -1001
@@ -46,45 +37,6 @@ public final class FilesDatabaseManager: Sendable {
4637

4738
var itemMetadatas: Results<RealmItemMetadata> { ncDatabase().objects(RealmItemMetadata.self) }
4839

49-
///
50-
/// Check for the existence of the directory where to place database files and return it.
51-
///
52-
/// - Returns: The location of the database files directory.
53-
///
54-
private func assertDatabaseDirectory(for identifier: NSFileProviderDomainIdentifier) -> URL {
55-
logger.debug("Asserting existence of database directory...")
56-
57-
let manager = FileManager.default
58-
59-
guard let fileProviderExtensionDataDirectory = manager.fileProviderDomainSupportDirectory(for: identifier) else {
60-
logger.fault("Failed to resolve the file provider extension data directory!")
61-
assertionFailure("Failed to resolve the file provider extension data directory!")
62-
return manager.temporaryDirectory // Only to satisfy the non-optional return type. The extension is unusable at this point anyway.
63-
}
64-
65-
let databaseDirectory = fileProviderExtensionDataDirectory.appendingPathComponent("Database", isDirectory: true)
66-
let exists = manager.fileExists(atPath: databaseDirectory.path)
67-
68-
if exists {
69-
logger.info("Database directory exists at: \(databaseDirectory.path)")
70-
} else {
71-
logger.info("Due to nonexistent \"Database\" directory, assume it is not a legacy location and returning file provider extension data directory at: \(fileProviderExtensionDataDirectory.path)")
72-
return fileProviderExtensionDataDirectory
73-
}
74-
75-
// Disable file protection for database directory.
76-
// See: https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/
77-
78-
do {
79-
try FileManager.default.setAttributes([.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: databaseDirectory.path)
80-
logger.info("Set protectionKey attribute for database directory to FileProtectionType.completeUntilFirstUserAuthentication.")
81-
} catch {
82-
logger.error("Could not set protectionKey attribute to FileProtectionType.completeUntilFirstUserAuthentication for database directory: \(error)")
83-
}
84-
85-
return databaseDirectory
86-
}
87-
8840
///
8941
/// Convenience initializer which defines a default configuration for Realm.
9042
///
@@ -95,18 +47,19 @@ public final class FilesDatabaseManager: Sendable {
9547
///
9648
public init(realmConfiguration customConfiguration: Realm.Configuration? = nil, account: Account, databaseDirectory customDatabaseDirectory: URL? = nil, fileProviderDomainIdentifier: NSFileProviderDomainIdentifier, log: any FileProviderLogging) {
9749
self.account = account
98-
9950
logger = FileProviderLogger(category: "FilesDatabaseManager", log: log)
100-
logger.info("Initializing for account: \(account.ncKitAccount)")
10151

102-
let databaseDirectory = customDatabaseDirectory ?? assertDatabaseDirectory(for: fileProviderDomainIdentifier)
103-
let accountDatabaseFilename: String = if UUID(uuidString: fileProviderDomainIdentifier.rawValue) != nil {
104-
"\(fileProviderDomainIdentifier.rawValue).realm"
105-
} else {
106-
account.fileName + "-" + Self.databaseFilename
52+
let defaultDatabaseDirectory = FileManager.default.fileProviderDomainSupportDirectory(for: fileProviderDomainIdentifier)
53+
54+
guard let databaseDirectory = customDatabaseDirectory ?? defaultDatabaseDirectory else {
55+
logger.fault("Neither custom nor default database directory defined!")
56+
return
10757
}
10858

109-
let databaseLocation = databaseDirectory.appendingPathComponent(accountDatabaseFilename)
59+
let databaseLocation = databaseDirectory
60+
.appendingPathComponent("\(fileProviderDomainIdentifier.rawValue).realm")
61+
.appendingPathComponent(fileProviderDomainIdentifier.rawValue)
62+
.appendingPathExtension("realm")
11063

11164
let configuration = customConfiguration ?? Realm.Configuration(
11265
fileURL: databaseLocation,
@@ -139,55 +92,14 @@ public final class FilesDatabaseManager: Sendable {
13992
)
14093

14194
Realm.Configuration.defaultConfiguration = configuration
142-
14395
let fileManager = FileManager.default
144-
let databasePathFromRealmConfiguration = configuration.fileURL?.path
145-
let migrate = databasePathFromRealmConfiguration != nil && fileManager.fileExists(atPath: databasePathFromRealmConfiguration!) == false
14696

14797
do {
14898
_ = try Realm()
14999
logger.info("Successfully created Realm.")
150100
} catch {
151101
logger.fault("Error creating Realm: \(error)")
152102
}
153-
154-
// Migrate from old unified database to new per-account DB
155-
guard migrate else {
156-
logger.debug("No migration needed for \(account.ncKitAccount)")
157-
return
158-
}
159-
160-
let sharedDatabaseURL = databaseDirectory.appendingPathComponent(Self.databaseFilename)
161-
162-
guard FileManager.default.fileExists(atPath: sharedDatabaseURL.path) == true else {
163-
logger.debug("No shared legacy database found at \"\(sharedDatabaseURL.path)\", skipping migration.")
164-
return
165-
}
166-
167-
logger.info("Migrating shared legacy database to new database for \(account.ncKitAccount)")
168-
169-
let legacyConfiguration = Realm.Configuration(fileURL: sharedDatabaseURL, schemaVersion: SchemaVersion.deletedLocalFileMetadata.rawValue, objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self])
170-
171-
do {
172-
let legacyRealm = try Realm(configuration: legacyConfiguration)
173-
174-
let itemMetadatas = legacyRealm
175-
.objects(RealmItemMetadata.self)
176-
.filter { $0.account == account.ncKitAccount }
177-
178-
let remoteFileChunks = legacyRealm.objects(RemoteFileChunk.self)
179-
180-
logger.info("Migrating \(itemMetadatas.count) metadatas and \(remoteFileChunks.count) chunks.")
181-
182-
let currentRealm = try Realm()
183-
184-
try currentRealm.write {
185-
itemMetadatas.forEach { currentRealm.create(RealmItemMetadata.self, value: $0) }
186-
remoteFileChunks.forEach { currentRealm.create(RemoteFileChunk.self, value: $0) }
187-
}
188-
} catch {
189-
logger.error("Error migrating shared legacy database to account-specific database for: \(account.ncKitAccount) because of error: \(error)")
190-
}
191103
}
192104

193105
func ncDatabase() -> Realm {

Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainLogDirectory.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Foundation
66

77
public extension FileManager {
88
///
9-
/// Return the sandboxed log directory specific to the file provider domain distinguished by the given identifier.
9+
/// Return log directory specific to the file provider domain distinguished by the given identifier.
1010
///
1111
/// If such directory does not exist yet, this attempts to create it implicitly.
1212
///
@@ -16,11 +16,11 @@ public extension FileManager {
1616
/// - 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`.
1717
///
1818
func fileProviderDomainLogDirectory(for identifier: NSFileProviderDomainIdentifier) -> URL? {
19-
guard let libraryDirectory = try? url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
19+
guard let applicationGroupContainer = applicationGroupContainer() else {
2020
return nil
2121
}
2222

23-
let logsDirectory = libraryDirectory.appendingPathComponent("Logs")
23+
let logsDirectory = applicationGroupContainer.appendingPathComponent("Logs")
2424
let fileProviderDomainLogDirectory = logsDirectory.appendingPathComponent(identifier.rawValue)
2525

2626
if fileExists(atPath: fileProviderDomainLogDirectory.path) == false {

Sources/NextcloudFileProviderKit/Extensions/FileManager+FileProviderDomainSupportDirectory.swift

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,21 @@ import Foundation
66

77
public extension FileManager {
88
///
9-
/// Return the sandboxed application support directory specific to the file provider domain distinguished by the given identifier.
9+
/// Return the application support directory specific to the file provider domain distinguished by the given identifier.
1010
///
1111
/// If such directory does not exist yet, this attempts to create it implicitly.
1212
///
13-
/// > Legacy Support: In the past, a subdirectory in the application group container was used for everything.
14-
/// This caused crashes due to violations of sandbox restrictions.
15-
/// If already existent, the legacy location will be used.
16-
/// Otherwise the data will be stored in a new location.
17-
///
1813
/// - Parameters:
1914
/// - identifier: File provider domain identifier which is used to isolate application support data for different file provider domains of the same extension.
2015
///
2116
/// - 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`.
2217
///
2318
func fileProviderDomainSupportDirectory(for identifier: NSFileProviderDomainIdentifier) -> URL? {
24-
// Legacy directory support.
25-
if let containerUrl = pathForAppGroupContainer() {
26-
let legacyLocation = containerUrl.appendingPathComponent("FileProviderExt")
27-
28-
if FileManager.default.fileExists(atPath: legacyLocation.path) {
29-
return legacyLocation
30-
}
31-
}
32-
33-
// Designated file provider domain directories.
34-
guard let applicationSupportDirectory = try? url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
19+
guard let containerUrl = applicationGroupContainer() else {
3520
return nil
3621
}
3722

38-
let domainsSupportDirectory = applicationSupportDirectory.appendingPathComponent("File Provider Domains")
23+
let domainsSupportDirectory = containerUrl.appendingPathComponent("File Provider Domains")
3924
let fileProviderDomainSupportDirectory = domainsSupportDirectory.appendingPathComponent(identifier.rawValue)
4025

4126
if fileExists(atPath: fileProviderDomainSupportDirectory.path) == false {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
// SPDX-License-Identifier: GPL-2.0-or-later
3+
4+
import Foundation
5+
6+
public extension FileManager {
7+
///
8+
/// Resolve the location of the shared container for the app group of the file provider extension.
9+
///
10+
/// - Returns: Container URL for the extension's app group or `nil`, if it could not be found.
11+
/// This can only fail because the `NCFPKAppGroupIdentifier` in the Info.plist could not be retrieved as a `String` or the container URL call to the file manager failed.
12+
///
13+
func applicationGroupContainer() -> URL? {
14+
guard let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "NCFPKAppGroupIdentifier") as? String else {
15+
return nil
16+
}
17+
18+
return containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
19+
}
20+
}

Sources/NextcloudFileProviderKit/Utilities/LocalFiles.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,6 @@ import FileProvider
55
import Foundation
66
import OSLog
77

8-
private let lfuLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "localfileutils")
9-
10-
///
11-
/// Resolve the path of the shared container for the app group of the file provider extension.
12-
///
13-
/// - Returns: Container URL for the extension's app group or `nil`, if it could not be found.
14-
///
15-
public func pathForAppGroupContainer() -> URL? {
16-
guard let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "NCFPKAppGroupIdentifier") as? String else {
17-
lfuLogger.error("Could not get app group container URL due to missing value for NCFPKAppGroupIdentifier key in Info.plist!")
18-
return nil
19-
}
20-
21-
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
22-
}
23-
248
///
259
/// Determine whether the given filename is a lock file as created by certain applications like Microsoft Office or LibreOffice.
2610
///

0 commit comments

Comments
 (0)