add ProfileDatabase class to read and write profiles to disk

This commit is contained in:
Bryan Montz
2023-05-12 07:21:25 -05:00
parent 7027b7016c
commit 4646f0e23c
2 changed files with 114 additions and 0 deletions

View File

@@ -261,6 +261,7 @@
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; };
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; };
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
@@ -686,6 +687,7 @@
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; };
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; };
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
@@ -1000,6 +1002,7 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
@@ -1651,6 +1654,7 @@
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,

View File

@@ -0,0 +1,110 @@
//
// ProfileDatabase.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
enum ProfileDatabaseError: Error {
case missing_context
case outdated_input
}
final class ProfileDatabase {
private let entity_name = "PersistedProfile"
private var persistent_container: NSPersistentContainer?
private var background_context: NSManagedObjectContext?
init() {
set_up()
}
private var profile_cache_url: URL {
(FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("profiles"))!
}
private var persistent_store_description: NSPersistentStoreDescription {
let description = NSPersistentStoreDescription(url: profile_cache_url)
description.type = NSSQLiteStoreType
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSSQLiteManualVacuumOption)
return description
}
private var object_model: NSManagedObjectModel? {
guard let url = Bundle.main.url(forResource: "Damus", withExtension: "momd") else {
return nil
}
return NSManagedObjectModel(contentsOf: url)
}
private func set_up() {
guard let object_model else {
print("⚠️ Warning: ProfileDatabase failed to load its object model")
return
}
persistent_container = NSPersistentContainer(name: "Damus", managedObjectModel: object_model)
persistent_container?.persistentStoreDescriptions = [persistent_store_description]
persistent_container?.loadPersistentStores { _, error in
if let error {
print("WARNING: ProfileDatabase failed to load: \(error)")
}
}
persistent_container?.viewContext.automaticallyMergesChangesFromParent = true
persistent_container?.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
background_context = persistent_container?.newBackgroundContext()
background_context?.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
}
private func get_persisted(id: String) -> PersistedProfile? {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
return try? persistent_container?.viewContext.fetch(request).first
}
// MARK: - Public
/// Updates or inserts a new Profile into the local database. Rejects profiles whose update date
/// is older than one we already have. Database writes occur on a background context for best performance.
/// - Parameters:
/// - id: Profile id (pubkey)
/// - profile: Profile object to be stored
/// - last_update: Date that the Profile was updated
func upsert(id: String, profile: Profile, last_update: Date) throws {
guard let context = background_context else {
throw ProfileDatabaseError.missing_context
}
var persisted_profile: PersistedProfile?
if let profile = get_persisted(id: id) {
if let existing_last_update = profile.last_update, last_update < existing_last_update {
throw ProfileDatabaseError.outdated_input
} else {
persisted_profile = profile
}
} else {
persisted_profile = NSEntityDescription.insertNewObject(forEntityName: entity_name, into: context) as? PersistedProfile
persisted_profile?.id = id
}
persisted_profile?.copyValues(from: profile)
persisted_profile?.last_update = last_update
try context.save()
}
func get(id: String) -> Profile? {
guard let profile = get_persisted(id: id) else {
return nil
}
return Profile(persisted_profile: profile)
}
}