Merge pull request #723 from meshtastic/update_coredata_backup

Update coredata backup
This commit is contained in:
Garth Vander Houwen 2024-06-26 19:20:30 -07:00 committed by GitHub
commit 28e10b0b4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89 additions and 49 deletions

View file

@ -645,10 +645,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if FileManager.default.fileExists(atPath: databasePath.path) {
do {
disconnectPeripheral(reconnect: false)
clearCoreDataDatabase(context: context!, includeRoutes: true)
try container.restorePersistentStore(from: databasePath)
let request = MyInfoEntity.fetchRequest()
try context?.fetch(request)
UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0)
context!.reset()
connectTo(peripheral: peripheral)
Logger.data.notice("🗂️ Restored Core data for /\(UserDefaults.preferredPeripheralNum, privacy: .public)")
} catch {

View file

@ -108,33 +108,76 @@ extension NSPersistentContainer {
/// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores.
/// - Throws: `CopyPersistentStoreError` in various situations.
/// - Returns: Nothing. If no errors are thrown, the restore is complete.
func restorePersistentStore(from backupURL: URL) throws -> Void {
guard backupURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL")
}
// func restorePersistentStore(from backupURL: URL) throws -> Void {
// guard backupURL.isFileURL else {
// throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL")
// }
//
// for persistentStoreDescription in persistentStoreDescriptions {
// guard let loadedStoreURL = persistentStoreDescription.url else {
// continue
// }
// guard FileManager.default.fileExists(atPath: backupURL.path) else {
// throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupURL)")
// }
// do {
// let storeOptions = persistentStoreDescription.options
// let configurationName = persistentStoreDescription.configuration
// let storeType = persistentStoreDescription.type
//
// // Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack.
// // When restoring, it's necessary to use the current persistent store coordinator.
// try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupURL, sourceOptions: storeOptions, ofType: storeType)
// // Add the persistent store at the same location we've been using, because it was removed in the previous step.
// try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions)
// } catch {
// throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)")
// }
// }
// }
//
/// Restore backup persistent stores located in the directory referenced by `backupURL`.
///
/// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app.
/// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores.
/// - Throws: `CopyPersistentStoreError` in various situations.
/// - Returns: Nothing. If no errors are thrown, the restore is complete.
func restorePersistentStore(from backupURL: URL) throws -> Void {
guard backupURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL")
}
var isDirectory: ObjCBool = false
if FileManager.default.fileExists(atPath: backupURL.path, isDirectory: &isDirectory) {
if !isDirectory.boolValue {
throw CopyPersistentStoreErrors.invalidSource("Source URL must be a directory")
}
} else {
throw CopyPersistentStoreErrors.invalidSource("Source URL must exist")
}
for persistentStoreDescription in persistentStoreDescriptions {
guard let loadedStoreURL = persistentStoreDescription.url else {
continue
}
guard FileManager.default.fileExists(atPath: backupURL.path) else {
throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupURL)")
}
do {
let storeOptions = persistentStoreDescription.options
let configurationName = persistentStoreDescription.configuration
let storeType = persistentStoreDescription.type
// Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack.
// When restoring, it's necessary to use the current persistent store coordinator.
try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupURL, sourceOptions: storeOptions, ofType: storeType)
// Add the persistent store at the same location we've been using, because it was removed in the previous step.
try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions)
} catch {
throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)")
}
}
}
for persistentStoreDescription in persistentStoreDescriptions {
guard let loadedStoreURL = persistentStoreDescription.url else {
continue
}
let backupStoreURL = backupURL.appendingPathComponent(loadedStoreURL.lastPathComponent)
guard FileManager.default.fileExists(atPath: backupStoreURL.path) else {
throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupStoreURL)")
}
do {
let storeOptions = persistentStoreDescription.options
let configurationName = persistentStoreDescription.configuration
let storeType = persistentStoreDescription.type
// Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack.
// When restoring, it's necessary to use the current persistent store coordinator.
try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupStoreURL, sourceOptions: storeOptions, ofType: storeType)
// Add the persistent store at the same location we've been using, because it was removed in the previous step.
try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions)
} catch {
throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)")
}
}
}
/// Copy all loaded persistent stores to a new directory. Each currently loaded file-based persistent store will be copied (including journal files, external binary storage, and anything else Core Data needs) into the destination directory to a persistent store with the same name and type as the existing store. In-memory stores, if any, are skipped.
/// - Parameters:
@ -151,7 +194,6 @@ extension NSPersistentContainer {
guard destinationURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidDestination("Destination URL must be a file URL")
}
// If the destination exists and we aren't overwriting it, then it must be a directory. (If we are overwriting, we'll remove it anyway, so it doesn't matter whether it's a directory).
var isDirectory: ObjCBool = false
if !overwriting && FileManager.default.fileExists(atPath: destinationURL.path, isDirectory: &isDirectory) {
@ -168,7 +210,6 @@ extension NSPersistentContainer {
throw CopyPersistentStoreErrors.destinationNotRemoved("Can't overwrite destination at \(destinationURL)")
}
}
// Create the destination directory
do {
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
@ -183,16 +224,17 @@ extension NSPersistentContainer {
guard persistentStoreDescription.type != NSInMemoryStoreType else {
continue
}
let temporaryPSC = NSPersistentStoreCoordinator(managedObjectModel: persistentStoreCoordinator.managedObjectModel)
let destinationStoreURL = destinationURL.appendingPathComponent(storeURL.lastPathComponent)
if !overwriting && FileManager.default.fileExists(atPath: destinationStoreURL.path) {
// If the destination exists, the migratePersistentStore call will update it in place. That's fine unless we're not overwriting.
// If the destination exists, the replacePersistentStore call will update it in place. That's fine unless we're not overwriting.
throw CopyPersistentStoreErrors.destinationError("Destination already exists at \(destinationStoreURL)")
}
do {
let newStore = try temporaryPSC.addPersistentStore(ofType: persistentStoreDescription.type, configurationName: persistentStoreDescription.configuration, at: persistentStoreDescription.url, options: persistentStoreDescription.options)
let _ = try temporaryPSC.migratePersistentStore(newStore, to: destinationStoreURL, options: persistentStoreDescription.options, withType: persistentStoreDescription.type)
// Replace an existing backup, if any, with a new one with the same options and type. This doesn't affect the current Core Data stack.
// The function name says "replace", but it works if there's nothing at the destination yet. In that case it creates a new persistent store.
// Note that for backup, it doesn't matter if the persistent store coordinator is the one currently in use or a different one. It could be a class function, for this use.
try persistentStoreCoordinator.replacePersistentStore(at: destinationStoreURL, destinationOptions: persistentStoreDescription.options, withPersistentStoreFrom: storeURL, sourceOptions: persistentStoreDescription.options, ofType: persistentStoreDescription.type)
/// Cleanup extra files
let directory = destinationStoreURL.deletingLastPathComponent()
/// Delete -wal file

View file

@ -200,7 +200,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
} else {
if packet.from > Int16.max {
let newUser = createUser(num: Int64(packet.from), context: context)
fetchedNode[0].user = newUser
newNode.user = newUser
}
}

View file

@ -214,7 +214,6 @@ struct Connect: View {
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
}
// clearCoreDataDatabase(context: context, includeRoutes: false)
let container = NSPersistentContainer(name: "Meshtastic")
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
Logger.data.error("nil File path for back")
@ -222,18 +221,15 @@ struct Connect: View {
}
do {
try container.copyPersistentStores(to: url.appendingPathComponent("backup").appendingPathComponent("\(UserDefaults.preferredPeripheralNum)"), overwriting: true)
clearCoreDataDatabase(context: context, includeRoutes: true)
Logger.data.notice("🗂️ Made a core data backup to backup/\(UserDefaults.preferredPeripheralNum)")
} catch {
Logger.data.error("🗂️ Core data backup copy error: \(error, privacy: .public)")
}
UserDefaults.preferredPeripheralId = selectedPeripherialId
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId })
if radio != nil {
bleManager.connectTo(peripheral: radio!.peripheral)
}
} else {
self.bleManager.connectTo(peripheral: peripheral.peripheral)
}
UserDefaults.preferredPeripheralId = selectedPeripherialId
self.bleManager.connectTo(peripheral: peripheral.peripheral)
}) {
Text(peripheral.name).font(.callout)
}

View file

@ -53,16 +53,18 @@ struct AppData: View {
VStack(alignment: .leading ) {
if file.pathExtension.contains("sqlite") {
Label {
Text("Node Core Data Backup \(file.pathComponents[9])/\(file.lastPathComponent) - \(file.creationDate?.formatted() ?? "") - \(file.fileSizeString)")
Text("Node Core Data Backup \(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10])/\(file.lastPathComponent) - \(file.creationDate?.formatted() ?? "") - \(file.fileSizeString)")
.swipeActions {
Button(role: .none) {
bleManager.disconnectPeripheral(reconnect: false)
let container = NSPersistentContainer(name: "Meshtastic")
do {
context.reset()
clearCoreDataDatabase(context: context, includeRoutes: true)
try container.restorePersistentStore(from: file.absoluteURL)
let request = MyInfoEntity.fetchRequest()
try context.fetch(request)
UserDefaults.preferredPeripheralId = ""
UserDefaults.preferredPeripheralNum = Int(file.pathComponents[10]) ?? 0
UserDefaults.preferredPeripheralNum = Int(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10]) ?? 0
Logger.data.notice("🗂️ Restored a core data backup to backup/\(UserDefaults.preferredPeripheralNum, privacy: .public)")
} catch {
Logger.data.error("🗂️ Core data restore copy error: \(error, privacy: .public)")

View file

@ -19,8 +19,8 @@ struct AppLog: View {
@State private var selectedLog: OSLogEntryLog?
@State private var presentingErrorDetails: Bool = false
@State private var searchText = ""
@State private var categories: Set<Int> = [0, 1, 2, 3, 4, 5, 6]
@State private var levels: Set<Int> = [0, 1, 2, 3, 4]
@State private var categories: Set<Int> = []
@State private var levels: Set<Int> = []
@State var isExporting = false
@State var exportString = ""
@State var isEditingFilters = false