ios_app_v2/yobble/CoreData/PersistenceController.swift
2025-10-21 17:56:12 +03:00

102 lines
3.7 KiB
Swift

import CoreData
enum PersistenceControllerError: Error {
case encryptionKeyUnavailable
case persistentStoreMissing
case rekeyingUnavailable
}
final class PersistenceController {
static let shared: PersistenceController = {
do {
return try PersistenceController()
} catch {
fatalError("Failed to initialize PersistenceController: \(error)")
}
}()
let container: NSPersistentContainer
private let keyManager: DatabaseEncryptionKeyManager
var viewContext: NSManagedObjectContext {
container.viewContext
}
init(
inMemory: Bool = false,
keyManager: DatabaseEncryptionKeyManager = .shared,
fileManager: FileManager = .default
) throws {
self.keyManager = keyManager
let modelName = "YobbleDataModel"
guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd"),
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Unable to load Core Data model \(modelName)")
}
container = NSPersistentContainer(name: modelName, managedObjectModel: managedObjectModel)
let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
description.type = NSSQLiteStoreType
if inMemory {
description.url = URL(fileURLWithPath: "/dev/null")
} else {
description.url = try Self.makeStoreURL(fileManager: fileManager, fileName: "\(modelName).sqlite")
}
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
let key: String
do {
key = try keyManager.currentKey()
} catch {
throw PersistenceControllerError.encryptionKeyUnavailable
}
let pragmas: [String: String] = [
"journal_mode": "WAL",
"cipher_page_size": "4096",
"key": key
]
description.setOption(pragmas as NSDictionary, forKey: NSSQLitePragmasOption)
description.setOption(FileProtectionType.completeUntilFirstUserAuthentication as NSObject, forKey: NSPersistentStoreFileProtectionKey)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
if let error {
fatalError("Unresolved error \(error)")
}
}
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = true
}
func newBackgroundContext() -> NSManagedObjectContext {
container.newBackgroundContext()
}
/// Placeholder for a future rekey flow once a password-based key is available.
///
/// On iOS 16 with SQLCipher you typically re-encrypt by running `PRAGMA rekey` via a raw
/// SQLite handle and then persisting the new key to the key manager. This helper keeps
/// the signature in place for when that flow is implemented.
func rekeyStore(to newKey: String) throws {
throw PersistenceControllerError.rekeyingUnavailable
}
private static func makeStoreURL(fileManager: FileManager, fileName: String) throws -> URL {
guard let baseURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
fatalError("Unable to resolve Application Support directory")
}
if !fileManager.fileExists(atPath: baseURL.path) {
try fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
return baseURL.appendingPathComponent(fileName)
}
}