Refactor core data classes (#671)

This commit is contained in:
Mingshen Sun 2025-01-25 15:40:12 -08:00 committed by GitHub
parent ab453580ad
commit d1de81d919
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 605 additions and 433 deletions

View file

@ -6,56 +6,208 @@
// Copyright © 2017 Bob Sun. All rights reserved.
//
import CoreData
import Foundation
import ObjectiveGit
import SwiftyUserDefaults
public extension PasswordEntity {
var nameWithCategory: String {
if let path {
if path.hasSuffix(".gpg") {
return String(path.prefix(upTo: path.index(path.endIndex, offsetBy: -4)))
}
return path
}
return ""
public final class PasswordEntity: NSManagedObject, Identifiable {
/// Name of the password, i.e., filename without extension.
@NSManaged public var name: String
/// A Boolean value indicating whether the entity is a directory.
@NSManaged public var isDir: Bool
/// A Boolean value indicating whether the entity is synced with remote repository.
@NSManaged public var isSynced: Bool
/// The relative file path of the password or directory.
@NSManaged public var path: String
/// The thumbnail image of the password if there is a url entry in the password.
@NSManaged public var image: Data?
/// The parent password entity.
@NSManaged public var parent: PasswordEntity?
/// A set of child password entities.
@NSManaged public var children: Set<PasswordEntity>
@nonobjc
public static func fetchRequest() -> NSFetchRequest<PasswordEntity> {
NSFetchRequest<PasswordEntity>(entityName: "PasswordEntity")
}
func getCategoryText() -> String {
getCategoryArray().joined(separator: " > ")
/// A String value with password directory and name, i.e., path without extension.
public var nameWithDir: String {
(path as NSString).deletingPathExtension
}
func getCategoryArray() -> [String] {
public var dirText: String {
getDirArray().joined(separator: " > ")
}
public func getDirArray() -> [String] {
var parentEntity = parent
var passwordCategoryArray: [String] = []
while parentEntity != nil {
passwordCategoryArray.append(parentEntity!.name!)
parentEntity = parentEntity!.parent
while let current = parentEntity {
passwordCategoryArray.append(current.name)
parentEntity = current.parent
}
passwordCategoryArray.reverse()
return passwordCategoryArray
}
func getURL() throws -> URL {
if let path = getPath().stringByAddingPercentEncodingForRFC3986(), let url = URL(string: path) {
return url
public static func fetchAll(in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func fetchAllPassword(in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "isDir = false")
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func totalNumber(in context: NSManagedObjectContext) -> Int {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "isDir = false")
return (try? context.count(for: request)) ?? 0
}
public static func fetchUnsynced(in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "isSynced = false")
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func fetch(by path: String, in context: NSManagedObjectContext) -> PasswordEntity? {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "path = %@", path)
return try? context.fetch(request).first as? Self
}
public static func fetch(by path: String, isDir: Bool, in context: NSManagedObjectContext) -> PasswordEntity? {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "path = %@ and isDir = %@", path, isDir as NSNumber)
return try? context.fetch(request).first as? Self
}
public static func fetch(by parent: PasswordEntity?, in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "parent = %@", parent ?? 0)
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func updateAllToSynced(in context: NSManagedObjectContext) -> Int {
let request = NSBatchUpdateRequest(entity: Self.entity())
request.resultType = .updatedObjectsCountResultType
request.predicate = NSPredicate(format: "isSynced = false")
request.propertiesToUpdate = ["isSynced": true]
let result = try? context.execute(request) as? NSBatchUpdateResult
return result?.result as? Int ?? 0
}
public static func deleteRecursively(entity: PasswordEntity, in context: NSManagedObjectContext) {
var currentEntity: PasswordEntity? = entity
while let node = currentEntity, node.children.isEmpty {
let parent = node.parent
context.delete(node)
try? context.save()
currentEntity = parent
}
throw AppError.other(message: "cannot decode URL")
}
// XXX: define some getters to get core data, we need to consider
// manually write models instead auto generation.
func getImage() -> Data? {
image
public static func deleteAll(in context: NSManagedObjectContext) {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Self.fetchRequest())
_ = try? context.execute(deleteRequest)
}
func getName() -> String {
// unwrap non-optional core data
name ?? ""
public static func exists(password: Password, in context: NSManagedObjectContext) -> Bool {
let request = fetchRequest()
request.predicate = NSPredicate(format: "name = %@ and path = %@ and isDir = false", password.name, password.path)
if let count = try? context.count(for: request) {
return count > 0
}
return false
}
func getPath() -> String {
// unwrap non-optional core data
path ?? ""
@discardableResult
public static func insert(name: String, path: String, isDir: Bool, into context: NSManagedObjectContext) -> PasswordEntity {
let entity = PasswordEntity(context: context)
entity.name = name
entity.path = path
entity.isDir = isDir
entity.isSynced = false
return entity
}
public static func initPasswordEntityCoreData(url: URL, in context: NSManagedObjectContext) {
let localFileManager = FileManager.default
let root = {
let entity = PasswordEntity(context: context)
entity.name = "root"
entity.isDir = true
entity.path = ""
return entity
}()
var queue = [root]
while !queue.isEmpty {
let current = queue.removeFirst()
let resourceKeys = Set<URLResourceKey>([.nameKey, .isDirectoryKey])
let options = FileManager.DirectoryEnumerationOptions([.skipsHiddenFiles, .skipsSubdirectoryDescendants])
let currentURL = url.appendingPathComponent(current.path)
guard let directoryEnumerator = localFileManager.enumerator(at: currentURL, includingPropertiesForKeys: Array(resourceKeys), options: options) else {
continue
}
for case let fileURL as URL in directoryEnumerator {
guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys),
let isDirectory = resourceValues.isDirectory,
let name = resourceValues.name
else {
continue
}
let passwordEntity = PasswordEntity(context: context)
passwordEntity.isDir = isDirectory
if isDirectory {
passwordEntity.name = name
queue.append(passwordEntity)
} else {
if (name as NSString).pathExtension == "gpg" {
passwordEntity.name = (name as NSString).deletingPathExtension
} else {
passwordEntity.name = name
}
}
passwordEntity.parent = current
let path = String(fileURL.path.replacingOccurrences(of: url.path, with: "").drop(while: { $0 == "/" }))
passwordEntity.path = path
}
}
context.delete(root)
}
}
public extension PasswordEntity {
@objc(addChildrenObject:)
@NSManaged
func addToChildren(_ value: PasswordEntity)
@objc(removeChildrenObject:)
@NSManaged
func removeFromChildren(_ value: PasswordEntity)
@objc(addChildren:)
@NSManaged
func addToChildren(_ values: NSSet)
@objc(removeChildren:)
@NSManaged
func removeFromChildren(_ values: NSSet)
}