diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index f9577c0d..a5dec13b 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -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 { diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index f085b5d7..b00f00a9 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -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 diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c5be865b..db6d4cf4 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -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 } } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index b831c544..673e6a8d 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -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) } diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index 237fc197..dee18d9f 100644 --- a/Meshtastic/Views/Settings/AppData.swift +++ b/Meshtastic/Views/Settings/AppData.swift @@ -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)") diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index 3348a62d..a3dcd59d 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -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 = [0, 1, 2, 3, 4, 5, 6] - @State private var levels: Set = [0, 1, 2, 3, 4] + @State private var categories: Set = [] + @State private var levels: Set = [] @State var isExporting = false @State var exportString = "" @State var isEditingFilters = false