-
-
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) | |
} | |
} |
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.
Works perfect! Any help with how to export this file via the fileExporter?