Last active
January 1, 2024 16:52
-
-
Save ole/e113a716158e26c1089a1d74b468deed to your computer and use it in GitHub Desktop.
How to make a copy of a Core Data SQLite database. See https://oleb.net/blog/2018/03/core-data-sqlite-backup/ for more.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import CoreData | |
import Foundation | |
/// Safely copies the specified `NSPersistentStore` to a temporary file. | |
/// Useful for backups. | |
/// | |
/// - Parameter index: The index of the persistent store in the coordinator's | |
/// `persistentStores` array. Passing an index that doesn't exist will trap. | |
/// | |
/// - Returns: The URL of the backup file, wrapped in a TemporaryFile instance | |
/// for easy deletion. | |
extension NSPersistentStoreCoordinator { | |
func backupPersistentStore(atIndex index: Int) throws -> TemporaryFile { | |
// Inspiration: https://stackoverflow.com/a/22672386 | |
// Documentation for NSPersistentStoreCoordinate.migratePersistentStore: | |
// "After invocation of this method, the specified [source] store is | |
// removed from the coordinator and thus no longer a useful reference." | |
// => Strategy: | |
// 1. Create a new "intermediate" NSPersistentStoreCoordinator and add | |
// the original store file. | |
// 2. Use this new PSC to migrate to a new file URL. | |
// 3. Drop all reference to the intermediate PSC. | |
precondition(persistentStores.indices.contains(index), "Index \(index) doesn't exist in persistentStores array") | |
let sourceStore = persistentStores[index] | |
let backupCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) | |
let intermediateStoreOptions = (sourceStore.options ?? [:]) | |
.merging([NSReadOnlyPersistentStoreOption: true], | |
uniquingKeysWith: { $1 }) | |
let intermediateStore = try backupCoordinator.addPersistentStore( | |
ofType: sourceStore.type, | |
configurationName: sourceStore.configurationName, | |
at: sourceStore.url, | |
options: intermediateStoreOptions | |
) | |
let backupStoreOptions: [AnyHashable: Any] = [ | |
NSReadOnlyPersistentStoreOption: true, | |
// Disable write-ahead logging. Benefit: the entire store will be | |
// contained in a single file. No need to handle -wal/-shm files. | |
// https://developer.apple.com/library/content/qa/qa1809/_index.html | |
NSSQLitePragmasOption: ["journal_mode": "DELETE"], | |
// Minimize file size | |
NSSQLiteManualVacuumOption: true, | |
] | |
// Filename format: basename-date.sqlite | |
// E.g. "MyStore-20180221T200731.sqlite" (time is in UTC) | |
func makeFilename() -> String { | |
let basename = sourceStore.url?.deletingPathExtension().lastPathComponent ?? "store-backup" | |
let dateFormatter = ISO8601DateFormatter() | |
dateFormatter.formatOptions = [.withYear, .withMonth, .withDay, .withTime] | |
let dateString = dateFormatter.string(from: Date()) | |
return "\(basename)-\(dateString).sqlite" | |
} | |
let backupFilename = makeFilename() | |
let backupFile = try TemporaryFile(creatingTempDirectoryForFilename: backupFilename) | |
try backupCoordinator.migratePersistentStore(intermediateStore, to: backupFile.fileURL, options: backupStoreOptions, withType: NSSQLiteStoreType) | |
return backupFile | |
} | |
} | |
/// A wrapper around a temporary file in a temporary directory. The directory | |
/// has been especially created for the file, so it's safe to delete when you're | |
/// done working with the file. | |
/// | |
/// Call `deleteDirectory` when you no longer need the file. | |
struct TemporaryFile { | |
let directoryURL: URL | |
let fileURL: URL | |
/// Deletes the temporary directory and all files in it. | |
let deleteDirectory: () throws -> Void | |
/// Creates a temporary directory with a unique name and initializes the | |
/// receiver with a `fileURL` representing a file named `filename` in that | |
/// directory. | |
/// | |
/// - Note: This doesn't create the file! | |
init(creatingTempDirectoryForFilename filename: String) throws { | |
let (directory, deleteDirectory) = try FileManager.default.urlForUniqueTemporaryDirectory() | |
self.directoryURL = directory | |
self.fileURL = directory.appendingPathComponent(filename) | |
self.deleteDirectory = deleteDirectory | |
} | |
} | |
extension FileManager { | |
/// Creates a temporary directory with a unique name and returns its URL. | |
/// | |
/// - Returns: A tuple of the directory's URL and a delete function. | |
/// Call the function to delete the directory after you're done with it. | |
/// | |
/// - Note: You should not rely on the existence of the temporary directory | |
/// after the app is exited. | |
func urlForUniqueTemporaryDirectory(preferredName: String? = nil) throws -> (url: URL, deleteDirectory: () throws -> Void) { | |
let basename = preferredName ?? UUID().uuidString | |
var counter = 0 | |
var createdSubdirectory: URL? = nil | |
repeat { | |
do { | |
let subdirName = counter == 0 ? basename : "\(basename)-\(counter)" | |
let subdirectory = temporaryDirectory.appendingPathComponent(subdirName, isDirectory: true) | |
try createDirectory(at: subdirectory, withIntermediateDirectories: false) | |
createdSubdirectory = subdirectory | |
} catch CocoaError.fileWriteFileExists { | |
// Catch file exists error and try again with another name. | |
// Other errors propagate to the caller. | |
counter += 1 | |
} | |
} while createdSubdirectory == nil | |
let directory = createdSubdirectory! | |
let deleteDirectory: () throws -> Void = { | |
try self.removeItem(at: directory) | |
} | |
return (directory, deleteDirectory) | |
} | |
} |
I have this working, but for the life of me, I can't figure out what I'm missing here...
When I restore the backup by replacing the persistent store, all of my entities have suddenly lost all reference to their /_EXTERNAL_DATA
binary data. The binary data files (and the whole _SUPPORT
directory) are carried over and intact, but suddenly I've lost all the images that were allowed as external storage.
Any ideas? I could really use some direction. Thank you so much for this helpful gist.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for the solution! Any idea how the info.plist looks with respect to Document Types / Exported Type. That whole area really confuses me. Thanks!