@@ -7,60 +7,60 @@ import CoreData
77///
88/// Call `deleteDirectory` when you no longer need the file.
99struct 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- }
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+ }
2727}
2828
2929extension 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)
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)
6163 }
62- return ( directory, deleteDirectory)
63- }
6464}
6565
6666
@@ -73,55 +73,55 @@ extension FileManager {
7373/// - Returns: The URL of the backup file, wrapped in a TemporaryFile instance
7474/// for easy deletion.
7575extension 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 "
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
118124 }
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- }
125125}
126126
127127extension ActiveCoreDataStore {
@@ -133,29 +133,29 @@ extension ActiveCoreDataStore {
133133 /// - fileName: Name of the file to save to
134134 /// - 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.
135135 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)
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) " )
151158 }
152- try ! FileManager . default. copyItem ( at: backupFile. fileURL, to: destURL)
153159
154- completion ( destURL)
155- } catch {
156- completion ( nil )
157- print ( " Error backing up Core Data store: \( error) " )
158- }
159-
160160 }
161161}
0 commit comments