Skip to content

Commit 1682310

Browse files
authored
Adding a way to safely back up your database to disk - CoreDataPlusStore.backupToFile(fileName: completion) (#9)
1 parent 94e45d9 commit 1682310

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import Foundation
2+
import CoreData
3+
4+
/// A wrapper around a temporary file in a temporary directory. The directory
5+
/// has been especially created for the file, so it's safe to delete when you're
6+
/// done working with the file.
7+
///
8+
/// Call `deleteDirectory` when you no longer need the file.
9+
struct TemporaryFile {
10+
let directoryURL: URL
11+
let fileURL: URL
12+
/// Deletes the temporary directory and all files in it.
13+
let deleteDirectory: () throws -> Void
14+
15+
/// Creates a temporary directory with a unique name and initializes the
16+
/// receiver with a `fileURL` representing a file named `filename` in that
17+
/// directory.
18+
///
19+
/// - Note: This doesn't create the file!
20+
init(creatingTempDirectoryForFilename filename: String) throws {
21+
let (directory, deleteDirectory) = try FileManager.default
22+
.urlForUniqueTemporaryDirectory()
23+
self.directoryURL = directory
24+
self.fileURL = directory.appendingPathComponent(filename)
25+
self.deleteDirectory = deleteDirectory
26+
}
27+
}
28+
29+
extension FileManager {
30+
/// Creates a temporary directory with a unique name and returns its URL.
31+
///
32+
/// - Returns: A tuple of the directory's URL and a delete function.
33+
/// Call the function to delete the directory after you're done with it.
34+
///
35+
/// - Note: You should not rely on the existence of the temporary directory
36+
/// after the app is exited.
37+
func urlForUniqueTemporaryDirectory(preferredName: String? = nil) throws
38+
-> (url: URL, deleteDirectory: () throws -> Void)
39+
{
40+
let basename = preferredName ?? UUID().uuidString
41+
42+
var counter = 0
43+
var createdSubdirectory: URL? = nil
44+
repeat {
45+
do {
46+
let subdirName = counter == 0 ? basename : "\(basename)-\(counter)"
47+
let subdirectory = temporaryDirectory
48+
.appendingPathComponent(subdirName, isDirectory: true)
49+
try createDirectory(at: subdirectory, withIntermediateDirectories: false)
50+
createdSubdirectory = subdirectory
51+
} catch CocoaError.fileWriteFileExists {
52+
// Catch file exists error and try again with another name.
53+
// Other errors propagate to the caller.
54+
counter += 1
55+
}
56+
} while createdSubdirectory == nil
57+
58+
let directory = createdSubdirectory!
59+
let deleteDirectory: () throws -> Void = {
60+
try self.removeItem(at: directory)
61+
}
62+
return (directory, deleteDirectory)
63+
}
64+
}
65+
66+
67+
/// Safely copies the specified `NSPersistentStore` to a temporary file.
68+
/// Useful for backups.
69+
///
70+
/// - Parameter index: The index of the persistent store in the coordinator's
71+
/// `persistentStores` array. Passing an index that doesn't exist will trap.
72+
///
73+
/// - Returns: The URL of the backup file, wrapped in a TemporaryFile instance
74+
/// for easy deletion.
75+
extension NSPersistentStoreCoordinator {
76+
func backupPersistentStore(atIndex index: Int) throws -> TemporaryFile {
77+
// Inspiration: https://stackoverflow.com/a/22672386
78+
// Documentation for NSPersistentStoreCoordinate.migratePersistentStore:
79+
// "After invocation of this method, the specified [source] store is
80+
// removed from the coordinator and thus no longer a useful reference."
81+
// => Strategy:
82+
// 1. Create a new "intermediate" NSPersistentStoreCoordinator and add
83+
// the original store file.
84+
// 2. Use this new PSC to migrate to a new file URL.
85+
// 3. Drop all reference to the intermediate PSC.
86+
precondition(persistentStores.indices.contains(index), "Index \(index) doesn't exist in persistentStores array")
87+
let sourceStore = persistentStores[index]
88+
let backupCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
89+
90+
let intermediateStoreOptions = (sourceStore.options ?? [:])
91+
.merging([NSReadOnlyPersistentStoreOption: true],
92+
uniquingKeysWith: { $1 })
93+
let intermediateStore = try backupCoordinator.addPersistentStore(
94+
ofType: sourceStore.type,
95+
configurationName: sourceStore.configurationName,
96+
at: sourceStore.url,
97+
options: intermediateStoreOptions
98+
)
99+
100+
let backupStoreOptions: [AnyHashable: Any] = [
101+
NSReadOnlyPersistentStoreOption: true,
102+
// Disable write-ahead logging. Benefit: the entire store will be
103+
// contained in a single file. No need to handle -wal/-shm files.
104+
// https://developer.apple.com/library/content/qa/qa1809/_index.html
105+
NSSQLitePragmasOption: ["journal_mode": "DELETE"],
106+
// Minimize file size
107+
NSSQLiteManualVacuumOption: true,
108+
]
109+
110+
// Filename format: basename-date.sqlite
111+
// E.g. "MyStore-20180221T200731.sqlite" (time is in UTC)
112+
func makeFilename() -> String {
113+
let basename = sourceStore.url?.deletingPathExtension().lastPathComponent ?? "store-backup"
114+
let dateFormatter = ISO8601DateFormatter()
115+
dateFormatter.formatOptions = [.withYear, .withMonth, .withDay, .withTime]
116+
let dateString = dateFormatter.string(from: Date())
117+
return "\(basename)-\(dateString).sqlite"
118+
}
119+
120+
let backupFilename = makeFilename()
121+
let backupFile = try TemporaryFile(creatingTempDirectoryForFilename: backupFilename)
122+
try backupCoordinator.migratePersistentStore(intermediateStore, to: backupFile.fileURL, options: backupStoreOptions, withType: NSSQLiteStoreType)
123+
return backupFile
124+
}
125+
}
126+
127+
extension CoreDataPlusStore {
128+
129+
130+
/// Backs up your database to a .sqlite file. Safely copies the specified `NSPersistentStore` to a temporary file, so this works even if your database is in use.
131+
///
132+
/// - Parameters:
133+
/// - fileName: Name of the file to save to
134+
/// - completion: The URL on the filesystem of the backup is permanent. If you want to remove it, use `FileManager` to delete the file once you're finished using it.
135+
public func backupToFile(fileName: String, completion: (URL?)->Void ){
136+
var result: URL?
137+
let storeCoordinator: NSPersistentStoreCoordinator = persistentContainer.persistentStoreCoordinator
138+
do {
139+
let backupFile = try storeCoordinator.backupPersistentStore(atIndex: 0)
140+
defer {
141+
// Delete temporary directory when done
142+
try! backupFile.deleteDirectory()
143+
}
144+
print("The backup is at \"\(backupFile.fileURL.path)\"")
145+
146+
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
147+
let destURL = documentsURL.appendingPathComponent("\(fileName).sqlite")
148+
149+
if FileManager.default.fileExists(atPath: destURL.path) {
150+
try! FileManager.default.removeItem(at: destURL)
151+
}
152+
try! FileManager.default.copyItem(at: backupFile.fileURL, to: destURL)
153+
154+
completion(destURL)
155+
} catch {
156+
completion(nil)
157+
print("Error backing up Core Data store: \(error)")
158+
}
159+
160+
}
161+
}

0 commit comments

Comments
 (0)