2021-11-29 19:58:06 -08:00
//
// P e r s i s t e n c e . s w i f t
2023-03-26 09:08:08 -07:00
// M e s h t a s t i c
2021-11-29 19:58:06 -08:00
//
2023-03-26 09:08:08 -07:00
// C o p y r i g h t ( c ) G a r t h V a n d e r H o u w e n 1 1 / 2 8 / 2 1 .
2021-11-29 19:58:06 -08:00
//
import CoreData
2024-06-03 02:17:55 -07:00
import OSLog
2021-11-29 19:58:06 -08:00
2024-07-10 16:03:38 -05:00
class PersistenceController : ObservableObject {
2021-11-29 19:58:06 -08:00
2021-12-12 17:17:46 -08:00
static var preview : PersistenceController = {
let result = PersistenceController ( inMemory : false )
let viewContext = result . container . viewContext
for _ in 0. . < 10 {
let newItem = NodeInfoEntity ( context : viewContext )
2021-12-16 22:45:18 -08:00
newItem . lastHeard = Date ( )
2021-12-12 17:17:46 -08:00
}
do {
try viewContext . save ( )
} catch {
// R e p l a c e t h i s i m p l e m e n t a t i o n w i t h c o d e t o h a n d l e t h e e r r o r a p p r o p r i a t e l y .
// f a t a l E r r o r ( ) c a u s e s t h e a p p l i c a t i o n t o g e n e r a t e a c r a s h l o g a n d t e r m i n a t e . Y o u s h o u l d n o t u s e t h i s f u n c t i o n i n a s h i p p i n g a p p l i c a t i o n , a l t h o u g h i t m a y b e u s e f u l d u r i n g d e v e l o p m e n t .
let nsError = error as NSError
fatalError ( " Unresolved error \( nsError ) , \( nsError . userInfo ) " )
}
return result
} ( )
2021-11-29 19:58:06 -08:00
2021-12-12 17:17:46 -08:00
let container : NSPersistentContainer
2021-11-29 19:58:06 -08:00
2021-12-12 17:17:46 -08:00
init ( inMemory : Bool = false ) {
2023-03-06 10:33:18 -08:00
2021-12-12 17:17:46 -08:00
container = NSPersistentContainer ( name : " Meshtastic " )
2023-03-06 10:33:18 -08:00
2021-12-12 17:17:46 -08:00
if inMemory {
container . persistentStoreDescriptions . first ! . url = URL ( fileURLWithPath : " /dev/null " )
}
2023-03-06 10:33:18 -08:00
2021-12-25 23:48:12 -08:00
container . loadPersistentStores ( completionHandler : { ( _ , error ) in
2023-03-06 10:33:18 -08:00
2021-12-12 17:17:46 -08:00
// M e r g e p o l i c y t h a t f a v o r s i n m e m o r y d a t a o v e r d a t a i n t h e d b
self . container . viewContext . mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
2022-01-01 08:03:46 -08:00
self . container . viewContext . automaticallyMergesChangesFromParent = true
2023-03-06 10:33:18 -08:00
2021-12-12 17:17:46 -08:00
if let error = error as NSError ? {
2021-11-29 19:58:06 -08:00
2024-06-03 02:17:55 -07:00
Logger . data . error ( " CoreData Error: \( error . localizedDescription ) . Now attempting to truncate CoreData database. All app data will be lost. " )
2021-12-29 16:13:17 -08:00
self . clearDatabase ( )
2021-12-12 17:17:46 -08:00
}
} )
}
2023-03-06 10:33:18 -08:00
2021-12-29 16:13:17 -08:00
public func clearDatabase ( ) {
guard let url = self . container . persistentStoreDescriptions . first ? . url else { return }
let persistentStoreCoordinator = self . container . persistentStoreCoordinator
do {
2023-03-06 10:33:18 -08:00
try persistentStoreCoordinator . destroyPersistentStore ( at : url , ofType : NSSQLiteStoreType , options : nil )
2024-06-03 02:17:55 -07:00
Logger . data . error ( " CoreData database truncated. All app data has been erased. " )
2024-05-29 16:40:07 -05:00
2024-02-04 21:59:54 -08:00
do {
try persistentStoreCoordinator . addPersistentStore ( ofType : NSSQLiteStoreType , configurationName : nil , at : url , options : nil )
} catch let error {
2024-06-03 02:17:55 -07:00
Logger . data . error ( " Failed to re-create CoreData database: \( error . localizedDescription ) " )
2024-02-04 21:59:54 -08:00
}
2023-03-06 10:33:18 -08:00
2021-12-29 16:13:17 -08:00
} catch let error {
2024-06-03 02:17:55 -07:00
Logger . data . error ( " Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: \( error . localizedDescription ) " )
2021-12-29 16:13:17 -08:00
}
}
2024-07-10 16:03:38 -05:00
public func clearCoreDataDatabase ( context : NSManagedObjectContext , includeRoutes : Bool ) {
for i in 0. . . container . managedObjectModel . entities . count - 1 {
let entity = container . managedObjectModel . entities [ i ]
let query = NSFetchRequest < NSFetchRequestResult > ( entityName : entity . name ! )
var deleteRequest = NSBatchDeleteRequest ( fetchRequest : query )
let entityName = entity . name ? ? " UNK "
if includeRoutes {
deleteRequest = NSBatchDeleteRequest ( fetchRequest : query )
} else if ! includeRoutes {
if ! ( entityName . contains ( " RouteEntity " ) || entityName . contains ( " LocationEntity " ) ) {
deleteRequest = NSBatchDeleteRequest ( fetchRequest : query )
}
}
do {
try context . executeAndMergeChanges ( using : deleteRequest )
} catch {
Logger . data . error ( " \( error . localizedDescription ) " )
}
}
}
2021-11-29 19:58:06 -08:00
}
2022-10-06 08:56:15 -07:00
extension NSManagedObjectContext {
2023-03-06 10:33:18 -08:00
2022-10-06 08:56:15 -07:00
// / E x e c u t e s t h e g i v e n ` N S B a t c h D e l e t e R e q u e s t ` a n d d i r e c t l y m e r g e s t h e c h a n g e s t o b r i n g t h e g i v e n m a n a g e d o b j e c t c o n t e x t u p t o d a t e .
// /
// / - P a r a m e t e r b a t c h D e l e t e R e q u e s t : T h e ` N S B a t c h D e l e t e R e q u e s t ` t o e x e c u t e .
// / - T h r o w s : A n e r r o r i f a n y t h i n g w e n t w r o n g e x e c u t i n g t h e b a t c h d e l e t i o n .
public func executeAndMergeChanges ( using batchDeleteRequest : NSBatchDeleteRequest ) throws {
batchDeleteRequest . resultType = . resultTypeObjectIDs
2023-03-06 10:33:18 -08:00
2022-10-06 08:56:15 -07:00
let result = try execute ( batchDeleteRequest ) as ? NSBatchDeleteResult
let changes : [ AnyHashable : Any ] = [ NSDeletedObjectsKey : result ? . result as ? [ NSManagedObjectID ] ? ? [ ] ]
2023-03-06 10:33:18 -08:00
2022-10-06 08:56:15 -07:00
NSManagedObjectContext . mergeChanges ( fromRemoteContextSave : changes , into : [ self ] )
}
}
2024-06-23 13:00:20 -07:00
// C r e a t e d b y T o m H a r r i n g t o n o n 5 / 1 2 / 2 0 .
// C o p y r i g h t © 2 0 2 0 A t o m i c B i r d L L C . A l l r i g h t s r e s e r v e d .
// G i s t f r o m h t t p s : / / a t o m i c b i r d . c o m / b l o g / c o r e - d a t a - b a c k - u p - s t o r e /
//
extension NSPersistentContainer {
enum CopyPersistentStoreErrors : Error {
case invalidDestination ( String )
case destinationError ( String )
case destinationNotRemoved ( String )
case copyStoreError ( String )
case invalidSource ( String )
}
2024-06-23 16:11:02 -07:00
2024-07-10 16:03:38 -05:00
// / R e s t o r e b a c k u p p e r s i s t e n t s t o r e s l o c a t e d i n t h e d i r e c t o r y r e f e r e n c e d b y ` b a c k u p U R L ` .
2024-06-26 09:31:37 -07:00
// /
// / * * B e v e r y c a r e f u l w i t h t h i s * * . T o r e s t o r e a p e r s i s t e n t s t o r e , t h e c u r r e n t p e r s i s t e n t s t o r e m u s t b e r e m o v e d f r o m t h e c o n t a i n e r . W h e n t h a t h a p p e n s , * * a l l c u r r e n t l y l o a d e d C o r e D a t a o b j e c t s * * w i l l b e c o m e i n v a l i d . U s i n g t h e m a f t e r r e s t o r i n g w i l l c a u s e y o u r a p p t o c r a s h . W h e n c a l l i n g t h i s m e t h o d y o u * * m u s t * * e n s u r e t h a t y o u d o n o t c o n t i n u e t o u s e a n y p r e v i o u s l y f e t c h e d m a n a g e d o b j e c t s o r e x i s t i n g f e t c h e d r e s u l t s c o n t r o l l e r s . * * I f t h i s m e t h o d d o e s n o t t h r o w , t h a t d o e s n o t m e a n y o u r a p p i s s a f e . * * Y o u n e e d t o t a k e e x t r a s t e p s t o p r e v e n t c r a s h e s . T h e d e t a i l s v a r y d e p e n d i n g o n t h e n a t u r e o f y o u r a p p .
// / - P a r a m e t e r b a c k u p U R L : A f i l e U R L c o n t a i n i n g b a c k u p c o p i e s o f a l l c u r r e n t l y l o a d e d p e r s i s t e n t s t o r e s .
// / - T h r o w s : ` C o p y P e r s i s t e n t S t o r e E r r o r ` i n v a r i o u s s i t u a t i o n s .
// / - R e t u r n s : N o t h i n g . I f n o e r r o r s a r e t h r o w n , t h e r e s t o r e i s c o m p l e t e .
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 " )
}
2024-06-23 16:11:02 -07:00
2024-06-26 09:31:37 -07:00
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
// R e p l a c e t h e c u r r e n t s t o r e w i t h t h e b a c k u p c o p y . T h i s h a s a s i d e e f f e c t o f r e m o v i n g t h e c u r r e n t s t o r e f r o m t h e C o r e D a t a s t a c k .
// W h e n r e s t o r i n g , i t ' s n e c e s s a r y t o u s e t h e c u r r e n t p e r s i s t e n t s t o r e c o o r d i n a t o r .
try persistentStoreCoordinator . replacePersistentStore ( at : loadedStoreURL , destinationOptions : storeOptions , withPersistentStoreFrom : backupStoreURL , sourceOptions : storeOptions , ofType : storeType )
// A d d t h e p e r s i s t e n t s t o r e a t t h e s a m e l o c a t i o n w e ' v e b e e n u s i n g , b e c a u s e i t w a s r e m o v e d i n t h e p r e v i o u s s t e p .
try persistentStoreCoordinator . addPersistentStore ( ofType : storeType , configurationName : configurationName , at : loadedStoreURL , options : storeOptions )
} catch {
throw CopyPersistentStoreErrors . copyStoreError ( " Could not restore: \( error . localizedDescription ) " )
}
}
}
2024-06-23 16:11:02 -07:00
2024-06-23 13:00:20 -07:00
// / C o p y a l l l o a d e d p e r s i s t e n t s t o r e s t o a n e w d i r e c t o r y . E a c h c u r r e n t l y l o a d e d f i l e - b a s e d p e r s i s t e n t s t o r e w i l l b e c o p i e d ( i n c l u d i n g j o u r n a l f i l e s , e x t e r n a l b i n a r y s t o r a g e , a n d a n y t h i n g e l s e C o r e D a t a n e e d s ) i n t o t h e d e s t i n a t i o n d i r e c t o r y t o a p e r s i s t e n t s t o r e w i t h t h e s a m e n a m e a n d t y p e a s t h e e x i s t i n g s t o r e . I n - m e m o r y s t o r e s , i f a n y , a r e s k i p p e d .
// / - P a r a m e t e r s :
// / - d e s t i n a t i o n U R L : D e s t i n a t i o n f o r n e w p e r s i s t e n t s t o r e f i l e s . M u s t b e a f i l e U R L . I f ` o v e r w r i t i n g ` i s ` f a l s e ` a n d ` d e s t i n a t i o n U R L ` e x i s t s , i t m u s t b e a d i r e c t o r y .
// / - o v e r w r i t i n g : I f ` t r u e ` , a n y e x i s t i n g c o p i e s o f t h e p e r s i s t e n t s t o r e w i l l b e r e p l a c e d o r u p d a t e d . I f ` f a l s e ` , e x i s t i n g c o p i e s w i l l n o t b e c h a n g e d o r r e m o t e d . W h e n t h i s i s ` f a l s e ` , t h e d e s t i n a t i o n p e r s i s t e n t s t o r e f i l e m u s t n o t a l r e a d y e x i s t .
// / - T h r o w s : ` C o p y P e r s i s t e n t S t o r e E r r o r `
// / - R e t u r n s : N o t h i n g . I f n o e r r o r s a r e t h r o w n , a l l l o a d e d p e r s i s t e n t s t o r e s w i l l b e c o p i e d t o t h e d e s t i n a t i o n d i r e c t o r y .
func copyPersistentStores ( to destinationURL : URL , overwriting : Bool = false ) throws -> Void {
2024-06-23 16:11:02 -07:00
2024-06-23 13:00:20 -07:00
guard ! destinationURL . relativeString . contains ( " /0/ " ) else {
throw CopyPersistentStoreErrors . invalidDestination ( " Invalid 0 Node Id " )
}
2024-06-23 16:11:02 -07:00
2024-06-23 13:00:20 -07:00
guard destinationURL . isFileURL else {
throw CopyPersistentStoreErrors . invalidDestination ( " Destination URL must be a file URL " )
}
// I f t h e d e s t i n a t i o n e x i s t s a n d w e a r e n ' t o v e r w r i t i n g i t , t h e n i t m u s t b e a d i r e c t o r y . ( I f w e a r e o v e r w r i t i n g , w e ' l l r e m o v e i t a n y w a y , s o i t d o e s n ' t m a t t e r w h e t h e r i t ' s a d i r e c t o r y ) .
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 " )
}
// D o n ' t c h e c k i f d e s t i n a t i o n s t o r e s e x i s t i n t h e d e s t i n a t i o n d i r , t h a t c o m e s l a t e r o n a p e r - s t o r e b a s i s .
}
// I f w e ' r e o v e r w r i t i n g , r e m o v e t h e d e s t i n a t i o n .
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 ) " )
}
}
// C r e a t e t h e d e s t i n a t i o n d i r e c t o r y
do {
try FileManager . default . createDirectory ( at : destinationURL , withIntermediateDirectories : true , attributes : nil )
} catch {
throw CopyPersistentStoreErrors . destinationError ( " Could not create destination directory at \( destinationURL ) " )
}
2024-06-23 16:11:02 -07:00
2024-06-23 13:00:20 -07:00
for persistentStoreDescription in persistentStoreDescriptions {
guard let storeURL = persistentStoreDescription . url else {
continue
}
guard persistentStoreDescription . type != NSInMemoryStoreType else {
continue
}
let destinationStoreURL = destinationURL . appendingPathComponent ( storeURL . lastPathComponent )
2024-06-26 09:31:37 -07:00
2024-06-23 13:00:20 -07:00
if ! overwriting && FileManager . default . fileExists ( atPath : destinationStoreURL . path ) {
2024-06-26 09:31:37 -07:00
// I f t h e d e s t i n a t i o n e x i s t s , t h e r e p l a c e P e r s i s t e n t S t o r e c a l l w i l l u p d a t e i t i n p l a c e . T h a t ' s f i n e u n l e s s w e ' r e n o t o v e r w r i t i n g .
2024-06-23 13:00:20 -07:00
throw CopyPersistentStoreErrors . destinationError ( " Destination already exists at \( destinationStoreURL ) " )
}
do {
2024-06-26 09:31:37 -07:00
// R e p l a c e a n e x i s t i n g b a c k u p , i f a n y , w i t h a n e w o n e w i t h t h e s a m e o p t i o n s a n d t y p e . T h i s d o e s n ' t a f f e c t t h e c u r r e n t C o r e D a t a s t a c k .
// T h e f u n c t i o n n a m e s a y s " r e p l a c e " , b u t i t w o r k s i f t h e r e ' s n o t h i n g a t t h e d e s t i n a t i o n y e t . I n t h a t c a s e i t c r e a t e s a n e w p e r s i s t e n t s t o r e .
// N o t e t h a t f o r b a c k u p , i t d o e s n ' t m a t t e r i f t h e p e r s i s t e n t s t o r e c o o r d i n a t o r i s t h e o n e c u r r e n t l y i n u s e o r a d i f f e r e n t o n e . I t c o u l d b e a c l a s s f u n c t i o n , f o r t h i s u s e .
try persistentStoreCoordinator . replacePersistentStore ( at : destinationStoreURL , destinationOptions : persistentStoreDescription . options , withPersistentStoreFrom : storeURL , sourceOptions : persistentStoreDescription . options , ofType : persistentStoreDescription . type )
2024-06-23 13:00:20 -07:00
// / C l e a n u p e x t r a f i l e s
let directory = destinationStoreURL . deletingLastPathComponent ( )
// / D e l e t e - w a l f i l e
do {
try FileManager . default . removeItem ( at : directory . appendingPathComponent ( " Meshtastic.sqlite-wal " ) )
// / D e l e t e - s h m f i l e
do {
try FileManager . default . removeItem ( at : directory . appendingPathComponent ( " Meshtastic.sqlite-shm " ) )
} catch {
2024-06-29 11:05:29 -07:00
Logger . services . error ( " 🗄 Error Deleting Meshtastic.sqlite-shm file \( error , privacy : . public ) " )
2024-06-23 13:00:20 -07:00
}
} catch {
2024-06-29 11:05:29 -07:00
Logger . services . error ( " 🗄 Error Deleting Meshtastic.sqlite-wal file \( error , privacy : . public ) " )
2024-06-23 13:00:20 -07:00
}
} catch {
2024-06-29 11:05:29 -07:00
Logger . services . error ( " 🗄 Error Deleting Meshtastic.sqlite file \( error , privacy : . public ) " )
2024-06-23 13:00:20 -07:00
throw CopyPersistentStoreErrors . copyStoreError ( " \( error . localizedDescription ) " )
}
}
}
}