mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
- Multiple fixes and CI setup
This commit is contained in:
604
AltStoreCore/Model/DatabaseManager/DatabaseManager.swift
Normal file
604
AltStoreCore/Model/DatabaseManager/DatabaseManager.swift
Normal file
@@ -0,0 +1,604 @@
|
||||
//
|
||||
// DatabaseManager.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
extension CFNotificationName
|
||||
{
|
||||
fileprivate static let willMigrateDatabase = CFNotificationName("com.rileytestut.AltStore.WillMigrateDatabase" as CFString)
|
||||
}
|
||||
|
||||
private let ReceivedWillMigrateDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
|
||||
DatabaseManager.shared.receivedWillMigrateDatabaseNotification()
|
||||
}
|
||||
|
||||
fileprivate class PersistentContainer: RSTPersistentContainer
|
||||
{
|
||||
override class func defaultDirectoryURL() -> URL
|
||||
{
|
||||
guard let sharedDirectoryURL = FileManager.default.altstoreSharedDirectory else { return super.defaultDirectoryURL() }
|
||||
|
||||
let databaseDirectoryURL = sharedDirectoryURL.appendingPathComponent("Database")
|
||||
try? FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
return databaseDirectoryURL
|
||||
}
|
||||
|
||||
class func legacyDirectoryURL() -> URL
|
||||
{
|
||||
return super.defaultDirectoryURL()
|
||||
}
|
||||
}
|
||||
|
||||
public class DatabaseManager
|
||||
{
|
||||
public static private(set) var shared = DatabaseManager()
|
||||
|
||||
public let persistentContainer: RSTPersistentContainer
|
||||
|
||||
public private(set) var isStarted = false
|
||||
|
||||
private var startCompletionHandlers = [(Error?) -> Void]()
|
||||
private let dispatchQueue = DispatchQueue(label: "io.sidestore.DatabaseManager")
|
||||
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
private var ignoreWillMigrateDatabaseNotification = false
|
||||
|
||||
private init()
|
||||
{
|
||||
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
||||
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
||||
|
||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillMigrateDatabaseNotification, CFNotificationName.willMigrateDatabase.rawValue, nil, .deliverImmediately)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
private class func loadPersistentStoresSync() {
|
||||
let container = Self.shared.persistentContainer
|
||||
let semaphore = DispatchSemaphore(value: 0) // Semaphore to wait for async completion
|
||||
|
||||
container.loadPersistentStores { description, error in
|
||||
if let error = error {
|
||||
print("Failed to load store: \(error)")
|
||||
} else {
|
||||
print("Store URL: \(description.url ?? URL(string: "unknown")!)")
|
||||
}
|
||||
|
||||
semaphore.signal() // Signal the semaphore to unblock the thread
|
||||
}
|
||||
|
||||
semaphore.wait() // Wait for the semaphore signal to unblock the thread
|
||||
print("Persistent store loading complete.")
|
||||
}
|
||||
|
||||
class func deleteDatabase() -> Bool
|
||||
{
|
||||
// delete existing database and start fresh if required
|
||||
do {
|
||||
let container = Self.shared.persistentContainer
|
||||
|
||||
var databaseStore = container.persistentStoreCoordinator.persistentStores.first
|
||||
if databaseStore == nil{
|
||||
// perform a load before acquiring the databaseStoreURL
|
||||
Self.loadPersistentStoresSync()
|
||||
databaseStore = container.persistentStoreCoordinator.persistentStores.first
|
||||
}
|
||||
|
||||
|
||||
guard let databaseStore else
|
||||
{
|
||||
print("\nDatabase Delete request FAILED: databaseStore = nil\n")
|
||||
return false
|
||||
}
|
||||
|
||||
guard let databaseStoreURL = databaseStore.url else
|
||||
{
|
||||
print("\nDatabase Delete request FAILED: databaseStoreURL = nil\n")
|
||||
return false
|
||||
}
|
||||
|
||||
// Reset the managed object context
|
||||
Self.shared.persistentContainer.viewContext.reset()
|
||||
|
||||
// Remove all existing persistent stores
|
||||
for store in Self.shared.persistentContainer.persistentStoreCoordinator.persistentStores {
|
||||
try? Self.shared.persistentContainer.persistentStoreCoordinator.remove(store)
|
||||
}
|
||||
|
||||
// Now destroy the persistent store
|
||||
try Self.shared.persistentContainer.persistentStoreCoordinator.destroyPersistentStore(
|
||||
at: databaseStoreURL,
|
||||
ofType: NSSQLiteStoreType,
|
||||
options: nil
|
||||
)
|
||||
|
||||
// just be sure
|
||||
try? FileManager.default.removeItem(at: databaseStoreURL)
|
||||
|
||||
print("\nDatabase Delete: SUCCEEDED\n")
|
||||
|
||||
return true
|
||||
}catch{
|
||||
print("\nDatabase Delete request FAILED: \(error)\n")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class func recreateDatabase() {
|
||||
// Try to perform delete if one exists
|
||||
_ = Self.deleteDatabase()
|
||||
|
||||
// create new instance and load persistence store
|
||||
Self.shared = DatabaseManager()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
func start(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
|
||||
func finish(_ error: Error?)
|
||||
{
|
||||
self.dispatchQueue.async {
|
||||
if error == nil
|
||||
{
|
||||
self.isStarted = true
|
||||
}
|
||||
|
||||
self.startCompletionHandlers.forEach { $0(error) }
|
||||
self.startCompletionHandlers.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
self.dispatchQueue.async {
|
||||
self.startCompletionHandlers.append(completionHandler)
|
||||
guard self.startCompletionHandlers.count == 1 else { return }
|
||||
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
|
||||
// In simulator, when previews are generated, it initializes the db, in doing so this removal may be required
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
// Wrap in #if DEBUG to *ensure* we never accidentally delete production databases.
|
||||
if ProcessInfo.processInfo.isPreview
|
||||
{
|
||||
do
|
||||
{
|
||||
print("!!! Purging database for preview...")
|
||||
try FileManager.default.removeItem(at: PersistentContainer.defaultDirectoryURL())
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove database directory for preview.", error)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if self.persistentContainer.isMigrationRequired
|
||||
{
|
||||
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
||||
self.ignoreWillMigrateDatabaseNotification = true
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
|
||||
}
|
||||
|
||||
self.migrateDatabaseToAppGroupIfNeeded { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error)
|
||||
case .success:
|
||||
self.persistentContainer.loadPersistentStores { (description, error) in
|
||||
guard error == nil else { return finish(error!) }
|
||||
|
||||
self.prepareDatabase() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error)
|
||||
case .success: finish(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signOut(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
self.persistentContainer.performBackgroundTask { (context) in
|
||||
if let account = self.activeAccount(in: context)
|
||||
{
|
||||
account.isActiveAccount = false
|
||||
}
|
||||
|
||||
if let team = self.activeTeam(in: context)
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
Keychain.shared.reset()
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to save when signing out.", error)
|
||||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.persistentContainer.performBackgroundTask { context in
|
||||
do
|
||||
{
|
||||
let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) }
|
||||
|
||||
let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true])
|
||||
loggedErrors.forEach { context.delete($0) }
|
||||
|
||||
try context.save()
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeaturedSortIDs() async
|
||||
{
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // DON'T use our custom merge policy, because that one ignores changes to featuredSortID.
|
||||
await context.performAsync {
|
||||
do
|
||||
{
|
||||
// Randomize source order
|
||||
let fetchRequest = Source.fetchRequest()
|
||||
let sources = try context.fetch(fetchRequest)
|
||||
|
||||
for source in sources
|
||||
{
|
||||
source.featuredSortID = UUID().uuidString
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.main.error("Failed to update source order. \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Randomize app order
|
||||
let fetchRequest = StoreApp.fetchRequest()
|
||||
let apps = try context.fetch(fetchRequest)
|
||||
|
||||
for app in apps
|
||||
{
|
||||
app.featuredSortID = UUID().uuidString
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.main.error("Failed to update app order. \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
func startForPreview()
|
||||
{
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
self.dispatchQueue.async {
|
||||
self.startCompletionHandlers.append { error in
|
||||
semaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
_ = semaphore.wait(timeout: .now() + 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
var viewContext: NSManagedObjectContext {
|
||||
return self.persistentContainer.viewContext
|
||||
}
|
||||
|
||||
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
|
||||
|
||||
let activeAccount = Account.first(satisfying: predicate, in: context)
|
||||
return activeAccount
|
||||
}
|
||||
|
||||
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
|
||||
|
||||
let activeTeam = Team.first(satisfying: predicate, in: context)
|
||||
return activeTeam
|
||||
}
|
||||
|
||||
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
|
||||
{
|
||||
guard let patreonAccountID = Keychain.shared.patreonAccountID else { return nil }
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(PatreonAccount.identifier), patreonAccountID)
|
||||
|
||||
let patreonAccount = PatreonAccount.first(satisfying: predicate, in: context, requestProperties: [\.relationshipKeyPathsForPrefetching: [#keyPath(PatreonAccount._pledges)]])
|
||||
return patreonAccount
|
||||
}
|
||||
}
|
||||
|
||||
private extension DatabaseManager
|
||||
{
|
||||
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard !Bundle.isAppExtension() else { return completionHandler(.success(())) }
|
||||
|
||||
let context = self.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
|
||||
|
||||
let altStoreSource: Source
|
||||
|
||||
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||
{
|
||||
altStoreSource = source
|
||||
}
|
||||
else
|
||||
{
|
||||
altStoreSource = Source.makeAltStoreSource(in: context)
|
||||
}
|
||||
|
||||
// Make sure to always update source URL to be current.
|
||||
try! altStoreSource.setSourceURL(Source.altStoreSourceURL)
|
||||
|
||||
let storeApp: StoreApp
|
||||
|
||||
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
|
||||
{
|
||||
storeApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: nil, in: context)
|
||||
storeApp.source = altStoreSource
|
||||
}
|
||||
|
||||
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
|
||||
let installedApp: InstalledApp
|
||||
|
||||
if let app = storeApp.installedApp
|
||||
{
|
||||
installedApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: Support build versions.
|
||||
// For backwards compatibility reasons, we cannot use localApp's buildVersion as storeBuildVersion,
|
||||
// or else the latest update will _always_ be considered new because we don't use buildVersions in our source (yet).
|
||||
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, storeBuildVersion: nil, context: context)
|
||||
installedApp.storeApp = storeApp
|
||||
}
|
||||
|
||||
/* App Extensions */
|
||||
var installedExtensions = Set<InstalledExtension>()
|
||||
|
||||
for appExtension in localApp.appExtensions
|
||||
{
|
||||
let resignedBundleID = appExtension.bundleIdentifier
|
||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: localApp.bundleIdentifier, with: StoreApp.altstoreAppID)
|
||||
|
||||
let installedExtension: InstalledExtension
|
||||
|
||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||
{
|
||||
installedExtension = appExtension
|
||||
}
|
||||
else
|
||||
{
|
||||
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: context)
|
||||
}
|
||||
|
||||
installedExtension.update(resignedAppExtension: appExtension)
|
||||
|
||||
installedExtensions.insert(installedExtension)
|
||||
}
|
||||
|
||||
installedApp.appExtensions = installedExtensions
|
||||
|
||||
let fileURL = installedApp.fileURL
|
||||
|
||||
// @mahee96: it shouldn't matter if it is debug/release, the file is expected to be in its place (except for simulator probably coz it doesn't suppor app installs anyway)
|
||||
#if DEBUG && targetEnvironment(simulator)
|
||||
let replaceCachedApp = true
|
||||
#else
|
||||
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version || installedApp.buildVersion != localApp.buildVersion
|
||||
#endif
|
||||
|
||||
if replaceCachedApp
|
||||
{
|
||||
func update(_ bundle: Bundle, bundleID: String) throws
|
||||
{
|
||||
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
|
||||
|
||||
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
|
||||
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
||||
}
|
||||
|
||||
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
|
||||
|
||||
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
|
||||
try update(appBundle, bundleID: StoreApp.altstoreAppID)
|
||||
|
||||
if let tempApp = ALTApplication(fileURL: temporaryFileURL)
|
||||
{
|
||||
for appExtension in tempApp.appExtensions
|
||||
{
|
||||
guard let extensionBundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.invalidApp) }
|
||||
guard let installedExtension = installedExtensions.first(where: { $0.resignedBundleIdentifier == appExtension.bundleIdentifier }) else { throw ALTError(.invalidApp) }
|
||||
try update(extensionBundle, bundleID: installedExtension.bundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to copy SideStore app bundle to its proper location.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cachedRefreshedDate = installedApp.refreshedDate
|
||||
let cachedExpirationDate = installedApp.expirationDate
|
||||
|
||||
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
||||
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber, storeBuildVersion: nil)
|
||||
|
||||
if installedApp.refreshedDate < cachedRefreshedDate
|
||||
{
|
||||
// Embedded provisioning profile has a creation date older than our refreshed date.
|
||||
// This most likely means we've refreshed the app since then, and profile is now outdated,
|
||||
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
|
||||
|
||||
installedApp.refreshedDate = cachedRefreshedDate
|
||||
installedApp.expirationDate = cachedExpirationDate
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
Task(priority: .high) {
|
||||
await self.updateFeaturedSortIDs()
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDatabaseToAppGroupIfNeeded(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
// Only migrate if we haven't migrated yet and there's a valid AltStore app group.
|
||||
guard UserDefaults.shared.requiresAppGroupMigration && Bundle.main.altstoreAppGroup != nil else { return completion(.success(())) }
|
||||
|
||||
func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success:
|
||||
UserDefaults.shared.requiresAppGroupMigration = false
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
let previousDatabaseURL = PersistentContainer.legacyDirectoryURL().appendingPathComponent("AltStore.sqlite")
|
||||
let databaseURL = PersistentContainer.defaultDirectoryURL().appendingPathComponent("AltStore.sqlite")
|
||||
|
||||
let previousAppsDirectoryURL = InstalledApp.legacyAppsDirectoryURL
|
||||
let appsDirectoryURL = InstalledApp.appsDirectoryURL
|
||||
|
||||
let databaseIntent = NSFileAccessIntent.writingIntent(with: databaseURL, options: [.forReplacing])
|
||||
let appsIntent = NSFileAccessIntent.writingIntent(with: appsDirectoryURL, options: [.forReplacing])
|
||||
|
||||
self.coordinator.coordinate(with: [databaseIntent, appsIntent], queue: self.coordinatorQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let description = NSPersistentStoreDescription(url: previousDatabaseURL)
|
||||
|
||||
// Disable WAL to remove extra files automatically during migration.
|
||||
description.setOption(["journal_mode": "DELETE"] as NSDictionary, forKey: NSSQLitePragmasOption)
|
||||
|
||||
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.persistentContainer.managedObjectModel)
|
||||
|
||||
// Migrate database
|
||||
if FileManager.default.fileExists(atPath: previousDatabaseURL.path)
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: databaseURL.path, isDirectory: nil)
|
||||
{
|
||||
try FileManager.default.removeItem(at: databaseURL)
|
||||
}
|
||||
|
||||
let previousDatabase = try persistentStoreCoordinator.addPersistentStore(ofType: description.type, configurationName: description.configuration, at: description.url, options: description.options)
|
||||
|
||||
// Pass nil options to prevent later error due to self.persistentContainer using WAL.
|
||||
try persistentStoreCoordinator.migratePersistentStore(previousDatabase, to: databaseURL, options: nil, withType: NSSQLiteStoreType)
|
||||
|
||||
try FileManager.default.removeItem(at: previousDatabaseURL)
|
||||
}
|
||||
|
||||
// Migrate apps
|
||||
if FileManager.default.fileExists(atPath: previousAppsDirectoryURL.path, isDirectory: nil)
|
||||
{
|
||||
if(previousAppsDirectoryURL.path != appsDirectoryURL.path)
|
||||
{
|
||||
_ = try FileManager.default.replaceItemAt(appsDirectoryURL, withItemAt: previousAppsDirectoryURL)
|
||||
}
|
||||
}
|
||||
|
||||
finish(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to migrate database to app group:", error)
|
||||
finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivedWillMigrateDatabaseNotification()
|
||||
{
|
||||
defer { self.ignoreWillMigrateDatabaseNotification = false }
|
||||
|
||||
// Ignore notifications sent by the current process.
|
||||
guard !self.ignoreWillMigrateDatabaseNotification else { return }
|
||||
|
||||
exit(104)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user