Use app group container

This commit is contained in:
Justin Mazzocchi 2020-11-08 19:07:23 -08:00
parent 567fc9eeda
commit 095abbeea9
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
11 changed files with 91 additions and 33 deletions

View file

@ -17,21 +17,19 @@ public struct ContentDatabase {
useHomeTimelineLastReadId: Bool, useHomeTimelineLastReadId: Bool,
useNotificationsLastReadId: Bool, useNotificationsLastReadId: Bool,
inMemory: Bool, inMemory: Bool,
appGroup: String,
keychain: Keychain.Type) throws { keychain: Keychain.Type) throws {
if inMemory { if inMemory {
databaseWriter = DatabaseQueue() databaseWriter = DatabaseQueue()
try Self.migrator.migrate(databaseWriter)
} else { } else {
let path = try Self.fileURL(id: id).path databaseWriter = try DatabasePool.withFileCoordinator(
var configuration = Configuration() url: Self.fileURL(id: id, appGroup: appGroup),
migrator: Self.migrator) {
configuration.prepareDatabase { try Secrets.databaseKey(identityId: id, keychain: keychain)
try $0.usePassphrase(Secrets.databaseKey(identityId: id, keychain: keychain))
} }
databaseWriter = try DatabasePool(path: path, configuration: configuration)
} }
try Self.migrator.migrate(databaseWriter)
try Self.clean( try Self.clean(
databaseWriter, databaseWriter,
useHomeTimelineLastReadId: useHomeTimelineLastReadId, useHomeTimelineLastReadId: useHomeTimelineLastReadId,
@ -47,8 +45,8 @@ public struct ContentDatabase {
} }
public extension ContentDatabase { public extension ContentDatabase {
static func delete(id: Identity.Id) throws { static func delete(id: Identity.Id, appGroup: String) throws {
try FileManager.default.removeItem(at: fileURL(id: id)) try FileManager.default.removeItem(at: fileURL(id: id, appGroup: appGroup))
} }
func insert(status: Status) -> AnyPublisher<Never, Error> { func insert(status: Status) -> AnyPublisher<Never, Error> {
@ -411,8 +409,8 @@ public extension ContentDatabase {
private extension ContentDatabase { private extension ContentDatabase {
static let cleanAfterLastReadIdCount = 40 static let cleanAfterLastReadIdCount = 40
static func fileURL(id: Identity.Id) throws -> URL { static func fileURL(id: Identity.Id, appGroup: String) throws -> URL {
try FileManager.default.databaseDirectoryURL(name: id.uuidString) try FileManager.default.databaseDirectoryURL(name: id.uuidString, appGroup: appGroup)
} }
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length

View file

@ -0,0 +1,51 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
// https://github.com/groue/GRDB.swift/blob/master/Documentation/SharingADatabase.md
extension DatabasePool {
class func withFileCoordinator(url: URL,
migrator: DatabaseMigrator,
passphrase: @escaping (() throws -> String)) throws -> Self {
let coordinator = NSFileCoordinator(filePresenter: nil)
var coordinatorError: NSError?
var dbPool: Self?
var dbError: Error?
coordinator.coordinate(writingItemAt: url, options: .forMerging, error: &coordinatorError) { coordinatedURL in
do {
var configuration = Configuration()
configuration.prepareDatabase { db in
try db.usePassphrase(passphrase())
try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
if !db.configuration.readonly {
var flag: CInt = 1
let code = withUnsafeMutablePointer(to: &flag) {
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, $0)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: ResultCode(rawValue: code))
}
}
}
dbPool = try Self(path: coordinatedURL.path, configuration: configuration)
try migrator.migrate(dbPool!)
} catch {
dbError = error
}
}
if let error = dbError ?? coordinatorError {
throw error
}
return dbPool!
}
}

View file

@ -3,12 +3,17 @@
import Foundation import Foundation
extension FileManager { extension FileManager {
func databaseDirectoryURL(name: String) throws -> URL { enum DatabaseDirectoryError: Error {
let databaseDirectoryURL = try url(for: .applicationSupportDirectory, case containerURLNotFound
in: .userDomainMask, case unexpectedFileExistsWithDBDirectoryName
appropriateFor: nil, }
create: true)
.appendingPathComponent("DB") func databaseDirectoryURL(name: String, appGroup: String) throws -> URL {
guard let containerURL = containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw DatabaseDirectoryError.containerURLNotFound
}
let databaseDirectoryURL = containerURL.appendingPathComponent("DB")
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
if !fileExists(atPath: databaseDirectoryURL.path, isDirectory: &isDirectory) { if !fileExists(atPath: databaseDirectoryURL.path, isDirectory: &isDirectory) {
@ -16,7 +21,7 @@ extension FileManager {
withIntermediateDirectories: false, withIntermediateDirectories: false,
attributes: [.protectionKey: FileProtectionType.complete]) attributes: [.protectionKey: FileProtectionType.complete])
} else if !isDirectory.boolValue { } else if !isDirectory.boolValue {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileWriteFileExistsError, userInfo: nil) throw DatabaseDirectoryError.unexpectedFileExistsWithDBDirectoryName
} }
return databaseDirectoryURL.appendingPathComponent(name) return databaseDirectoryURL.appendingPathComponent(name)

View file

@ -3,7 +3,7 @@
import GRDB import GRDB
extension IdentityDatabase { extension IdentityDatabase {
var migrator: DatabaseMigrator { static var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator() var migrator = DatabaseMigrator()
migrator.registerMigration("0.1.0") { db in migrator.registerMigration("0.1.0") { db in

View file

@ -14,21 +14,17 @@ public enum IdentityDatabaseError: Error {
public struct IdentityDatabase { public struct IdentityDatabase {
private let databaseWriter: DatabaseWriter private let databaseWriter: DatabaseWriter
public init(inMemory: Bool, keychain: Keychain.Type) throws { public init(inMemory: Bool, appGroup: String, keychain: Keychain.Type) throws {
if inMemory { if inMemory {
databaseWriter = DatabaseQueue() databaseWriter = DatabaseQueue()
try Self.migrator.migrate(databaseWriter)
} else { } else {
let path = try FileManager.default.databaseDirectoryURL(name: Self.name).path let url = try FileManager.default.databaseDirectoryURL(name: Self.name, appGroup: appGroup)
var configuration = Configuration()
configuration.prepareDatabase { databaseWriter = try DatabasePool.withFileCoordinator(url: url, migrator: Self.migrator) {
try $0.usePassphrase(Secrets.databaseKey(identityId: nil, keychain: keychain)) try Secrets.databaseKey(identityId: nil, keychain: keychain)
} }
databaseWriter = try DatabasePool(path: path, configuration: configuration)
} }
try migrator.migrate(databaseWriter)
} }
} }

View file

@ -172,7 +172,7 @@ public extension Secrets {
private extension Secrets { private extension Secrets {
static let keychainServiceName = "com.metabolist.metatext" static let keychainServiceName = "com.metabolist.metatext"
static let databaseKeyLength = 32 static let databaseKeyLength = 48
private static func set(_ data: SecretsStorable, forAccount account: String, keychain: Keychain.Type) throws { private static func set(_ data: SecretsStorable, forAccount account: String, keychain: Keychain.Type) throws {
try keychain.setGenericPassword( try keychain.setGenericPassword(

View file

@ -40,12 +40,14 @@ public struct AppEnvironment {
} }
public extension AppEnvironment { public extension AppEnvironment {
static let appGroup = "group.metabolist.metatext"
static func live(userNotificationCenter: UNUserNotificationCenter, reduceMotion: @escaping () -> Bool) -> Self { static func live(userNotificationCenter: UNUserNotificationCenter, reduceMotion: @escaping () -> Bool) -> Self {
Self( Self(
session: URLSession.shared, session: URLSession.shared,
webAuthSessionType: LiveWebAuthSession.self, webAuthSessionType: LiveWebAuthSession.self,
keychain: LiveKeychain.self, keychain: LiveKeychain.self,
userDefaults: .standard, userDefaults: UserDefaults(suiteName: appGroup)!,
userNotificationClient: .live(userNotificationCenter), userNotificationClient: .live(userNotificationCenter),
reduceMotion: reduceMotion, reduceMotion: reduceMotion,
uuid: UUID.init, uuid: UUID.init,

View file

@ -18,6 +18,7 @@ public struct AllIdentitiesService {
self.environment = environment self.environment = environment
self.database = try environment.fixtureDatabase ?? IdentityDatabase( self.database = try environment.fixtureDatabase ?? IdentityDatabase(
inMemory: environment.inMemoryContent, inMemory: environment.inMemoryContent,
appGroup: AppEnvironment.appGroup,
keychain: environment.keychain) keychain: environment.keychain)
identitiesCreated = identitiesCreatedSubject.eraseToAnyPublisher() identitiesCreated = identitiesCreatedSubject.eraseToAnyPublisher()
} }
@ -88,7 +89,7 @@ public extension AllIdentitiesService {
database.deleteIdentity(id: id) database.deleteIdentity(id: id)
.collect() .collect()
.tryMap { _ -> AnyPublisher<Never, Error> in .tryMap { _ -> AnyPublisher<Never, Error> in
try ContentDatabase.delete(id: id) try ContentDatabase.delete(id: id, appGroup: AppEnvironment.appGroup)
let secrets = Secrets(identityId: id, keychain: environment.keychain) let secrets = Secrets(identityId: id, keychain: environment.keychain)

View file

@ -34,6 +34,7 @@ public struct IdentityService {
useHomeTimelineLastReadId: appPreferences.homeTimelineBehavior == .rememberPosition, useHomeTimelineLastReadId: appPreferences.homeTimelineBehavior == .rememberPosition,
useNotificationsLastReadId: appPreferences.notificationsTabBehavior == .rememberPosition, useNotificationsLastReadId: appPreferences.notificationsTabBehavior == .rememberPosition,
inMemory: environment.inMemoryContent, inMemory: environment.inMemoryContent,
appGroup: AppEnvironment.appGroup,
keychain: environment.keychain) keychain: environment.keychain)
} }
} }

View file

@ -11,6 +11,10 @@
</array> </array>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.metabolist.metatext</string>
</array>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>

View file

@ -15,7 +15,7 @@ import ViewModels
let db: IdentityDatabase = { let db: IdentityDatabase = {
let id = Identity.Id() let id = Identity.Id()
let db = try! IdentityDatabase(inMemory: true, keychain: MockKeychain.self) let db = try! IdentityDatabase(inMemory: true, appGroup: "", keychain: MockKeychain.self)
let secrets = Secrets(identityId: id, keychain: MockKeychain.self) let secrets = Secrets(identityId: id, keychain: MockKeychain.self)
try! secrets.setInstanceURL(.previewInstanceURL) try! secrets.setInstanceURL(.previewInstanceURL)