-
-
Save atomicbird/25fed73657be4b9d3642981a4892fea4 to your computer and use it in GitHub Desktop.
// | |
// NSPersistentContainer+extension.swift | |
// CDMoveDemo | |
// | |
// Created by Tom Harrington on 5/12/20. | |
// Copyright © 2020 Atomic Bird LLC. All rights reserved. | |
// | |
import Foundation | |
import CoreData | |
extension NSPersistentContainer { | |
enum CopyPersistentStoreErrors: Error { | |
case invalidDestination(String) | |
case destinationError(String) | |
case destinationNotRemoved(String) | |
case copyStoreError(String) | |
case invalidSource(String) | |
} | |
/// 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 persistentStore in persistentStoreCoordinator.persistentStores { | |
guard let loadedStoreURL = persistentStore.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 { | |
// Remove the existing persistent store first | |
try persistentStoreCoordinator.remove(persistentStore) | |
} catch { | |
print("Error removing store: \(error)") | |
throw CopyPersistentStoreErrors.copyStoreError("Could not remove persistent store before restore") | |
} | |
do { | |
// Clear out the existing persistent store so that we'll have a clean slate for restoring. | |
try persistentStoreCoordinator.destroyPersistentStore(at: loadedStoreURL, ofType: persistentStore.type, options: persistentStore.options) | |
// Add the backup store at its current location | |
let backupStore = try persistentStoreCoordinator.addPersistentStore(ofType: persistentStore.type, configurationName: persistentStore.configurationName, at: backupStoreURL, options: persistentStore.options) | |
// Migrate the backup store to the non-backup location. This leaves the backup copy in place in case it's needed in the future, but backupStore won't be useful anymore. | |
let restoredTemporaryStore = try persistentStoreCoordinator.migratePersistentStore(backupStore, to: loadedStoreURL, options: persistentStore.options, withType: persistentStore.type) | |
print("Restored temp store: \(restoredTemporaryStore)") | |
} 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 { | |
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) | |
} catch { | |
throw CopyPersistentStoreErrors.copyStoreError("\(error.localizedDescription)") | |
} | |
} | |
} | |
} |
@bcyng That was not the case when I tested the code. It shouldn’t be necessary unless you’re already using security-scoped bookmarks in other places. If that’s true then they apply here too but if you’re not using them in the rest of the app then you don’t need this here.
I think since iOS14 it’s been required for all file or folder access outside the apps bundle. Which is the case for all real world backups since you won’t want to have the backup deleted when the user deletes the app.
This code sample doesn't attempt to deal with files outside the app bundle. If you want to add that feature then of course you would need to deal with the additional requirements. This gist is only about the Core Data details of backing up-- it's not intended as a full drop-in solution for any backup situation.
Great code thank you! A few questions.. How does creating/restoring backups work with data protection level? For example, say I am setting protection level to "Complete"
//set data protection level to complete
container.persistentStoreDescriptions.first!.setOption(FileProtectionType.complete as NSObject, forKey: "NSPersistentStoreFileProtectionKey")
Will backup/restore maintain this setting? What if I had originally not set the protection level, created a backup, then updated the app to use "Complete" protection level? Would restoring the previous backup revert back to default protection level?
@TuxedoCatGames I think you would need to set the file protection key as an option when adding a store to temporaryPSC
, and also use it when calling migratePersistentStore
. If you didn't do it in the first case, you couldn't load the store. If you didn't do it in the second case, the backup would probably not be protected. I haven't verified this, though.
Hello, Thank you for your work, it almost works for me. But there is a problem after restored, the App crashed with "persistent store is not reachable from this NSManagedObjectContext's coordinator". But when relaunch the App, everything is ok, the data is also correct.
I am trying to fix this issue, but not worked( reset the viewContext before destroy the persistentStore). Do you have any idea about this? Thank you!
ps: My project is based on SwiftUI
@AlwaysBee That means that you restored the persistent store file but you still have at least one managed object context that uses the old store file, from before you did the restore. When you restore a persistent store file, you need to get rid of (or replace) every Core Data object you have— all managed objects, all contexts, everything. All of them are linked to the store file, so when you change the store file you need to replace all of them.
@AlwaysBee That means that you restored the persistent store file but you still have at least one managed object context that uses the old store file, from before you did the restore. When you restore a persistent store file, you need to get rid of (or replace) every Core Data object you have— all managed objects, all contexts, everything. All of them are linked to the store file, so when you change the store file you need to replace all of them.
You are right! When I updated the viewContext from the root View after migration, the App DID NOT crash anymore! Though some deeper pages still crash, but the leading cause is clear now :)
Thank you for your sharing!
Hello, great work! Thanks a lot. However, I am facing some bugs here. I want to back up the current persistent store before migrating it to a new App Group store. I think the backup succeeded, but when I try to restore the data into the newly created App Group store (not the original one), it seems to work, and the data is exactly what I have backed up. However, when I try to load it from the widget, it's not there, and after the app reboots, all the data disappears. I have tried several times, and it's always the same. Do you know what my problem is?
you need to wrap both functions in
otherwise you won't get access to the folders or files.