-
-
Save ole/e113a716158e26c1089a1d74b468deed to your computer and use it in GitHub Desktop.
| 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) | |
| } | |
| } |
How about importing this backup file though?
How about restore this backup file though?
To restore the backup check WWDC session https://developer.apple.com/videos/play/wwdc2015/220/ 9:50 you can use replacePersistentStore
if destination doesn`t exist, this does a copy
func restoreBackup() {
let backupDir = FileManager().temporaryDirectory.appendingPathComponent("backup", isDirectory: true)
let backupFile = backupDir.appendingPathComponent("Backup-20200714T065045.sqlite")
try! persistentContainer.persistentStoreCoordinator.replacePersistentStore(at: self.privateConfiguration.url!,
destinationOptions: [:],
withPersistentStoreFrom: backupFile,
sourceOptions: [:],
ofType: NSSQLiteStoreType)
persistentContainer.persistentStoreCoordinator.addPersistentStore(with: privateConfiguration) { config, error in
print("Restore complete")
//completion(config, error)
}
}
Hi, I have experience with Core Data but hardly any when backing up and restoring. Would you have a more detailed description of this? For instance, in the "restoreBackup()" func you reference a "privateConfiguration"; can you provide more information on this?
Works perfect! Any help with how to export this file via the fileExporter?
It's just NSPersistentStoreDescription
lazy var storePath: URL = { FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! }()
lazy var privateConfiguration: NSPersistentStoreDescription = {
let privateConfig = NSPersistentStoreDescription(url: storePath.appendingPathComponent("MainFile.sqlite"))
return privateConfig
}()
public lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: self.momName, managedObjectModel: mom!)
container.persistentStoreDescriptions = [self.privateConfiguration]
container.loadPersistentStores(completionHandler: ....etc) {}
}
What do you mean with fileExporter? SwiftUI's ViewModifier?
What do you mean with fileExporter? SwiftUI's ViewModifier?
Yes, i mean the view modifier. Already found a solution though. dont know if its the best way
struct SettingsView: View {
let sqliteFile = UTType(exportedAs: "....", conformingTo: .database)
@State private var isExportingDatabasePickerOpen: Bool = false
@State private var isExportingDatabase: Bool = false
@State private var exportedDatabaseFile: TemporaryFile? = nil
@State private var somethingWentWrong: Bool = false
@State private var somethingWentWrongTitle: String = ""
@State private var somethingWentWrongMessage: String = ""
var body: some View {
List {
Section(header: Text("Backup")) {
Button(action: exportData) {
HStack {
Image(systemName: "square.and.arrow.up")
Text("Export backup")
}
}
}
}
.listStyle(InsetGroupedListStyle())
.fileExporter(
isPresented: $isExportingDatabasePickerOpen,
document: SqlDocument(url: exportedDatabaseFile?.fileURL ?? URL(fileURLWithPath: "")),
contentType: sqliteFile,
onCompletion: { result in
exportingDatabase(result: result)
}
)
.alert(isPresented: $somethingWentWrong) {
Alert(title: Text("Oops something went wrong"))
}
.navigationTitle("Settings")
}
func exportData() {
let persistenceController = PersistenceController.shared
let storeCoordinator: NSPersistentStoreCoordinator = persistenceController.container.persistentStoreCoordinator
do {
let backupFile = try storeCoordinator.backupPersistentStore(atIndex: 0)
exportedDatabaseFile = backupFile
isExportingDatabasePickerOpen.toggle()
} catch {
openSomethingWentWrongAlert(title: "Oops something went wrong!", message: error.localizedDescription)
}
}
func exportingDatabase(result: Result<URL, Error>) {
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
openSomethingWentWrongAlert(title: "Oops something went wrong!", message: error.localizedDescription)
}
}
struct SqlDocument: FileDocument {
static var readableContentTypes: [UTType] { [.database] }
var url: URL
init(url: URL) {
self.url = url
}
init(configuration: ReadConfiguration) throws {
self.url = URL(fileURLWithPath: "")
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let file = try! FileWrapper(url: url, options: .immediate)
return file
}
}
}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!
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.
I'm curious why do you choose to return a
deleteDirectoryfunction fromurlForUniqueTemporaryDirectory, instead of justing defining thedeleteDirectorymethod inTemporaryFile.