Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save atomicbird/25fed73657be4b9d3642981a4892fea4 to your computer and use it in GitHub Desktop.
Save atomicbird/25fed73657be4b9d3642981a4892fea4 to your computer and use it in GitHub Desktop.
Back up and restore Core Data persistent stores
//
// 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)")
}
}
}
}
@tmmeito
Copy link

tmmeito commented Jul 15, 2020

Dear Tom! Thank you very much for your article on atomicbird and your code! This is exactly what I was looking for. I'm not very experienced in programming and trying around to build an app for my use and really would like to backup Core Data like you describe here. I implemented your code and it works without problems to create a copy. But I can't reload a backup in my project. It always skips the part "for persistentStoreDescription in persistentStoreDescriptions". Interestingly, perstistentStoreDescriptions is always empty. Do you have any idea what could be the problem? I'm trying and searching around nearly one week and have no idea whats going wrong... :)

I would appreciate every suggestion. Thank you very much!!

@atomicbird
Copy link
Author

Interestingly, perstistentStoreDescriptions is always empty. Do you have any idea what could be the problem? I'm trying and searching around nearly one week and have no idea whats going wrong... :)

That sounds like you haven't loaded any persistent stores yet. The code above assumes that you already loaded the current persistent store, and that you're using the restore function to replace that data with the old copy you're restoring. Sorry if that wasn't clear.

@tmmeito
Copy link

tmmeito commented Jul 21, 2020

Thank you so much! Now it works!! Its great.

@tmmeito
Copy link

tmmeito commented Jul 26, 2020

Sorry for bodering you again 😅. This approach wouldn‘t allow to use a backup on a different device, wouldn‘ it? It works in my case only on the same device. Is there a work around without having the data stored in a cloud?

@atomicbird
Copy link
Author

This code makes and restores backups on the local device. It doesn't do any kind of cloud sync.

@tmmeito
Copy link

tmmeito commented Jul 27, 2020

What approach would you take to sync with other devices but WITHOUT cloud services? Via export and import json files? Thank you!

@atomicbird
Copy link
Author

JSON could be part of a solution but you still need some way to transfer the files. If you're trying to sync directly from one device to another, you might consider the multipeer framework for local-only networking. If you don't want to use the built-in CloudKit support, also maybe consider Ensembles (http://www.ensembles.io/) which will sync Core Data over a variety of services (still cloud, but more choices and not a server you need to own).

@bcyng
Copy link

bcyng commented Apr 15, 2022

you need to wrap both functions in

if destinationURL.startAccessingSecurityScopedResource() {
            defer { destinationURL.stopAccessingSecurityScopedResource() }
//copyPersistentStores code goes here
}

if backupURL.startAccessingSecurityScopedResource() {
            defer { backupURL.startAccessingSecurityScopedResource() }
// restorePersistentStore code goes here
}

otherwise you won't get access to the folders or files.

@atomicbird
Copy link
Author

@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.

@bcyng
Copy link

bcyng commented Apr 15, 2022

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.

@atomicbird
Copy link
Author

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.

@TuxedoCatGames
Copy link

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?

@atomicbird
Copy link
Author

@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.

@AlwaysBee
Copy link

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

@atomicbird
Copy link
Author

@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
Copy link

@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!

@sgflamel
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment