mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Backup database functionality
This commit is contained in:
parent
bf7d57a1eb
commit
12e090059d
8 changed files with 415 additions and 45 deletions
|
|
@ -170,6 +170,7 @@
|
|||
DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0A2C285E45007E03CA /* LogDetail.swift */; };
|
||||
DDD5BB0D2C285F00007E03CA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0C2C285F00007E03CA /* Logger.swift */; };
|
||||
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */; };
|
||||
DDD5BB162C28B1E4007E03CA /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB152C28B1E4007E03CA /* AppData.swift */; };
|
||||
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6EEAE29BC024700383354 /* Firmware.swift */; };
|
||||
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; };
|
||||
DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; };
|
||||
|
|
@ -448,6 +449,7 @@
|
|||
DDD5BB0C2C285F00007E03CA /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogFilter.swift; sourceTree = "<group>"; };
|
||||
DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 38.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DDD5BB152C28B1E4007E03CA /* AppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppData.swift; sourceTree = "<group>"; };
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -614,6 +616,8 @@
|
|||
DDD5BB0E2C285F92007E03CA /* Logs */,
|
||||
DD93800C2BA74CE3008BEC06 /* Channels */,
|
||||
DD97E96728EFE9A00056DDA4 /* About.swift */,
|
||||
DDD5BB152C28B1E4007E03CA /* AppData.swift */,
|
||||
DDD5BB082C285DDC007E03CA /* AppLog.swift */,
|
||||
DD4A911D2708C65400501B7E /* AppSettings.swift */,
|
||||
DDAB580C2B0DAA9E00147258 /* Routes.swift */,
|
||||
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */,
|
||||
|
|
@ -627,7 +631,6 @@
|
|||
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */,
|
||||
DD61937A2863876A00E59241 /* Config */,
|
||||
DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */,
|
||||
DDD5BB082C285DDC007E03CA /* AppLog.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1318,6 +1321,7 @@
|
|||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */,
|
||||
DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */,
|
||||
DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */,
|
||||
DDD5BB162C28B1E4007E03CA /* AppData.swift in Sources */,
|
||||
DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */,
|
||||
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
|
||||
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -30,4 +30,24 @@ extension URL {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
var attributes: [FileAttributeKey: Any]? {
|
||||
do {
|
||||
return try FileManager.default.attributesOfItem(atPath: path)
|
||||
} catch let error as NSError {
|
||||
print("FileAttribute error: \(error)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var fileSize: UInt64 {
|
||||
return attributes?[.size] as? UInt64 ?? UInt64(0)
|
||||
}
|
||||
|
||||
var fileSizeString: String {
|
||||
return ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
|
||||
}
|
||||
|
||||
var creationDate: Date? {
|
||||
return attributes?[.creationDate] as? Date
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -637,6 +637,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
connectedPeripheral.num = myInfo?.myNodeNum ?? 0
|
||||
connectedPeripheral.name = myInfo?.bleName ?? "unknown".localized
|
||||
connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized
|
||||
let newConnection = Int64(UserDefaults.preferredPeripheralNum) != Int64(decodedInfo.myInfo.myNodeNum)
|
||||
if newConnection {
|
||||
let container = NSPersistentContainer(name: "Meshtastic")
|
||||
if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
let databasePath = url.appendingPathComponent("backup")
|
||||
.appendingPathComponent("\(UserDefaults.preferredPeripheralNum)")
|
||||
.appendingPathComponent("Meshtastic.sqlite")
|
||||
if FileManager.default.fileExists(atPath: databasePath.path) {
|
||||
do {
|
||||
disconnectPeripheral(reconnect: false)
|
||||
try container.restorePersistentStore(from: databasePath)
|
||||
UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0)
|
||||
context!.reset()
|
||||
connectTo(peripheral: peripheral)
|
||||
Logger.data.notice("🗂️ Restored Core data for /\(UserDefaults.preferredPeripheralNum)")
|
||||
} catch {
|
||||
Logger.data.error("Copy error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tryClearExistingChannels()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,3 +89,131 @@ extension NSManagedObjectContext {
|
|||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Created by Tom Harrington on 5/12/20.
|
||||
// Copyright © 2020 Atomic Bird LLC. All rights reserved.
|
||||
// Gist from https://atomicbird.com/blog/core-data-back-up-store/
|
||||
//
|
||||
extension NSPersistentContainer {
|
||||
|
||||
enum CopyPersistentStoreErrors: Error {
|
||||
case invalidDestination(String)
|
||||
case destinationError(String)
|
||||
case destinationNotRemoved(String)
|
||||
case copyStoreError(String)
|
||||
case invalidSource(String)
|
||||
}
|
||||
|
||||
/// Restore a persistent store for a URL `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")
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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:
|
||||
/// - destinationURL: Destination for new persistent store files. Must be a file URL. If `overwriting` is `false` and `destinationURL` exists, it must be a directory.
|
||||
/// - overwriting: If `true`, any existing copies of the persistent store will be replaced or updated. If `false`, existing copies will not be changed or remoted. When this is `false`, the destination persistent store file must not already exist.
|
||||
/// - Throws: `CopyPersistentStoreError`
|
||||
/// - Returns: Nothing. If no errors are thrown, all loaded persistent stores will be copied to the destination directory.
|
||||
func copyPersistentStores(to destinationURL: URL, overwriting: Bool = false) throws -> Void {
|
||||
print(destinationURL)
|
||||
guard !destinationURL.relativeString.contains("/0/") else {
|
||||
throw CopyPersistentStoreErrors.invalidDestination("Invalid 0 Node Id")
|
||||
}
|
||||
|
||||
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) {
|
||||
if !isDirectory.boolValue {
|
||||
throw CopyPersistentStoreErrors.invalidDestination("Destination URL must be a directory")
|
||||
}
|
||||
// Don't check if destination stores exist in the destination dir, that comes later on a per-store basis.
|
||||
}
|
||||
// If we're overwriting, remove the destination.
|
||||
if overwriting && FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
} catch {
|
||||
throw CopyPersistentStoreErrors.destinationNotRemoved("Can't overwrite destination at \(destinationURL)")
|
||||
}
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
throw CopyPersistentStoreErrors.destinationError("Could not create destination directory at \(destinationURL)")
|
||||
}
|
||||
|
||||
for persistentStoreDescription in persistentStoreDescriptions {
|
||||
guard let storeURL = persistentStoreDescription.url else {
|
||||
continue
|
||||
}
|
||||
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.
|
||||
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)
|
||||
|
||||
/// Cleanup extra files
|
||||
let directory = destinationStoreURL.deletingLastPathComponent()
|
||||
/// Delete -wal file
|
||||
do {
|
||||
try FileManager.default.removeItem(at: directory.appendingPathComponent("Meshtastic.sqlite-wal"))
|
||||
/// Delete -shm file
|
||||
do {
|
||||
try FileManager.default.removeItem(at: directory.appendingPathComponent("Meshtastic.sqlite-shm"))
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
|
||||
} catch {
|
||||
throw CopyPersistentStoreErrors.copyStoreError("\(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ struct Connect: View {
|
|||
@State var isUnsetRegion = false
|
||||
@State var invalidFirmwareVersion = false
|
||||
@State var liveActivityStarted = false
|
||||
@State var presentingSwitchPreferredPeripheral = false
|
||||
@State var selectedPeripherialId = ""
|
||||
|
||||
init () {
|
||||
|
|
@ -49,7 +48,7 @@ struct Connect: View {
|
|||
List {
|
||||
if bleManager.isSwitchedOn {
|
||||
Section(header: Text("connected.radio").font(.title)) {
|
||||
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
|
||||
}
|
||||
|
|
@ -60,9 +59,9 @@ struct Connect: View {
|
|||
.padding(.trailing)
|
||||
VStack(alignment: .leading) {
|
||||
if node != nil {
|
||||
Text(bleManager.connectedPeripheral.longName).font(.title2)
|
||||
Text(connectedPeripheral.longName).font(.title2)
|
||||
}
|
||||
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral.peripheral.name ?? "unknown".localized)")
|
||||
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)")
|
||||
.font(.callout).foregroundColor(Color.gray)
|
||||
if node != nil {
|
||||
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
|
||||
|
|
@ -91,7 +90,8 @@ struct Connect: View {
|
|||
.padding([.top, .bottom])
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral,
|
||||
connectedPeripheral.peripheral.state == .connected {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
}
|
||||
} label: {
|
||||
|
|
@ -121,7 +121,8 @@ struct Connect: View {
|
|||
Text("Num: \(String(node!.num))")
|
||||
Text("Short Name: \(node?.user?.shortName ?? "?")")
|
||||
Text("Long Name: \(node?.user?.longName ?? "unknown".localized)")
|
||||
Text("BLE RSSI: \(bleManager.connectedPeripheral.rssi)")
|
||||
Text("BLE RSSI: \(connectedPeripheral.rssi)")
|
||||
|
||||
Button {
|
||||
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) {
|
||||
Logger.mesh.error("Shutdown Failed")
|
||||
|
|
@ -210,8 +211,27 @@ struct Connect: View {
|
|||
}
|
||||
Button(action: {
|
||||
if UserDefaults.preferredPeripheralId.count > 0 && peripheral.peripheral.identifier.uuidString != UserDefaults.preferredPeripheralId {
|
||||
presentingSwitchPreferredPeripheral = true
|
||||
selectedPeripherialId = peripheral.peripheral.identifier.uuidString
|
||||
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")
|
||||
return
|
||||
}
|
||||
do {
|
||||
try container.copyPersistentStores(to: url.appendingPathComponent("backup").appendingPathComponent("\(UserDefaults.preferredPeripheralNum)"), overwriting: true)
|
||||
|
||||
Logger.data.notice("🗂️ Made a core data backup to backup/\(UserDefaults.preferredPeripheralNum)")
|
||||
} catch {
|
||||
print("Copy error: \(error)")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
@ -225,23 +245,6 @@ struct Connect: View {
|
|||
}.padding([.bottom, .top])
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Connecting to a new radio will clear all local app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
|
||||
|
||||
Button("Connect to new radio?", role: .destructive) {
|
||||
UserDefaults.preferredPeripheralId = selectedPeripherialId
|
||||
UserDefaults.preferredPeripheralNum = 0
|
||||
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
|
||||
bleManager.disconnectPeripheral()
|
||||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
|
||||
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId })
|
||||
if radio != nil {
|
||||
bleManager.connectTo(peripheral: radio!.peripheral)
|
||||
}
|
||||
}
|
||||
}
|
||||
.textCase(nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
@ -254,9 +257,9 @@ struct Connect: View {
|
|||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
#if targetEnvironment(macCatalyst)
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral {
|
||||
Button(role: .destructive, action: {
|
||||
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
|
||||
if connectedPeripheral.peripheral.state == CBPeripheralState.connected {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
}
|
||||
}) {
|
||||
|
|
@ -285,10 +288,18 @@ struct Connect: View {
|
|||
.padding(.bottom, 10)
|
||||
}
|
||||
.navigationTitle("bluetooth")
|
||||
.navigationBarItems(leading: MeshtasticLogo(), trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected, mqttTopic: bleManager.mqttManager.topic)
|
||||
})
|
||||
.navigationBarItems(
|
||||
leading: MeshtasticLogo(),
|
||||
trailing: ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: bleManager.connectedPeripheral?.shortName ?? "?",
|
||||
mqttProxyConnected: bleManager.mqttProxyConnected,
|
||||
mqttTopic: bleManager.mqttManager.topic
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) {
|
||||
InvalidVersion(minimumVersion: self.bleManager.minimumVersion, version: self.bleManager.connectedVersion)
|
||||
|
|
@ -302,24 +313,18 @@ struct Connect: View {
|
|||
|
||||
if UserDefaults.preferredPeripheralId.count > 0 && sub {
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1))
|
||||
|
||||
do {
|
||||
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
|
||||
return
|
||||
}
|
||||
// Found a node, check it for a region
|
||||
if !fetchedNode.isEmpty {
|
||||
node = fetchedNode[0]
|
||||
if node!.loRaConfig != nil && node!.loRaConfig?.regionCode ?? 0 == RegionCodes.unset.rawValue {
|
||||
isUnsetRegion = true
|
||||
} else {
|
||||
isUnsetRegion = false
|
||||
}
|
||||
node = try context.fetch(fetchNodeInfoRequest).first
|
||||
if let loRaConfig = node?.loRaConfig, loRaConfig.regionCode == RegionCodes.unset.rawValue {
|
||||
isUnsetRegion = true
|
||||
} else {
|
||||
isUnsetRegion = false
|
||||
}
|
||||
} catch {
|
||||
|
||||
Logger.data.error("💥 Error fetching node info: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
Meshtastic/Views/Settings/AppData.swift
Normal file
157
Meshtastic/Views/Settings/AppData.swift
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//
|
||||
// BackupData.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/8/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
struct AppData: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@State private var files = [URL]()
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
VStack {
|
||||
Button(action: {
|
||||
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")
|
||||
return
|
||||
}
|
||||
do {
|
||||
try container.copyPersistentStores(to: url.appendingPathComponent("backup").appendingPathComponent("\(UserDefaults.preferredPeripheralNum)"), overwriting: true)
|
||||
loadFiles()
|
||||
Logger.data.notice("🗂️ Made a core data backup to backup/\(UserDefaults.preferredPeripheralNum)")
|
||||
} catch {
|
||||
print("Copy error: \(error)")
|
||||
}
|
||||
|
||||
}) {
|
||||
Label {
|
||||
Text("Backup Database")
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
} icon: {
|
||||
Image(systemName: "cylinder.split.1x2")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
|
||||
}
|
||||
List(files, id: \.self) { file in
|
||||
HStack {
|
||||
VStack (alignment: .leading ) {
|
||||
if file.pathExtension.contains("sqlite") { //} == "sqlite" {
|
||||
Label {
|
||||
Text("Node Core Data Backup \(file.pathComponents[9])/\(file.lastPathComponent) - \(file.creationDate?.formatted() ?? "") - \(file.fileSizeString)")
|
||||
.swipeActions {
|
||||
Button(role: .none) {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
let container = NSPersistentContainer(name : "Meshtastic")
|
||||
do {
|
||||
context.reset()
|
||||
try container.restorePersistentStore(from: file.absoluteURL)
|
||||
UserDefaults.preferredPeripheralId = ""
|
||||
UserDefaults.preferredPeripheralNum = Int(file.pathComponents[10]) ?? 0
|
||||
Logger.data.notice("🗂️ Restored a core data backup to backup/\(UserDefaults.preferredPeripheralNum)")
|
||||
} catch {
|
||||
print("Copy error: \(error)")
|
||||
}
|
||||
} label: {
|
||||
Label("restore", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: file)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "cylinder.split.1x2")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
}
|
||||
else {
|
||||
Label {
|
||||
Text("\(file.lastPathComponent) - \(file.creationDate?.formatted() ?? "") - \(file.fileSizeString)")
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: file)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
} icon: {
|
||||
Image(systemName: "doc.text")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
VStack (alignment: .trailing) {
|
||||
Button() {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: file)
|
||||
loadFiles()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
Label("", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("File Storage", displayMode: .inline)
|
||||
.onAppear(perform: {
|
||||
loadFiles()
|
||||
})
|
||||
.listStyle(.inset)
|
||||
}
|
||||
|
||||
private func loadFiles() {
|
||||
files = []
|
||||
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
Logger.data.error("🗂️ nil default document directory path for backup, core data backup failed.")
|
||||
return
|
||||
}
|
||||
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey])
|
||||
if fileAttributes.isRegularFile! {
|
||||
files.append(fileURL)
|
||||
}
|
||||
} catch { print(error, fileURL) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,26 @@ struct AppSettings: View {
|
|||
) {
|
||||
Button("Erase all app data?", role: .destructive) {
|
||||
bleManager.disconnectPeripheral()
|
||||
/// Delete any database backups too
|
||||
if var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
url = url.appendingPathComponent("backup").appendingPathComponent(String(UserDefaults.preferredPeripheralNum))
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite"))
|
||||
/// Delete -shm file
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-wal"))
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-shm"))
|
||||
} catch {
|
||||
Logger.services.error("Error Deleting Meshtastic.sqlite-shm file \(error)")
|
||||
}
|
||||
} catch {
|
||||
Logger.services.error("Error Deleting Meshtastic.sqlite-wal file \(error)")
|
||||
}
|
||||
} catch {
|
||||
Logger.services.error("Error Deleting Meshtastic.sqlite file \(error)")
|
||||
}
|
||||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: true)
|
||||
context.refreshAllObjects()
|
||||
UserDefaults.standard.reset()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ struct Settings: View {
|
|||
case adminMessageLog
|
||||
case about
|
||||
case appLog
|
||||
case appData
|
||||
}
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
|
|
@ -426,6 +427,20 @@ struct Settings: View {
|
|||
.tag(SettingsSidebar.appLog)
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
Section(header: Text("Developers")) {
|
||||
NavigationLink {
|
||||
AppData()
|
||||
} label: {
|
||||
Label {
|
||||
Text("App Files")
|
||||
} icon: {
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.appData)
|
||||
}
|
||||
#endif
|
||||
Section(header: Text("Firmware")) {
|
||||
NavigationLink {
|
||||
Firmware(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue