2019-05-20 21:24:53 +02:00
|
|
|
//
|
|
|
|
|
// DatabaseManager.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 5/20/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import CoreData
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
import AltSign
|
2019-05-20 21:24:53 +02:00
|
|
|
import Roxas
|
|
|
|
|
|
2022-09-22 14:16:01 -05:00
|
|
|
extension CFNotificationName
|
|
|
|
|
{
|
2024-08-06 10:43:52 +09:00
|
|
|
fileprivate static let willMigrateDatabase = CFNotificationName("com.rileytestut.AltStore.WillMigrateDatabase" as CFString)
|
2022-09-22 14:16:01 -05:00
|
|
|
}
|
|
|
|
|
|
2024-08-06 10:43:52 +09:00
|
|
|
private let ReceivedWillMigrateDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { (center, observer, name, object, userInfo) in
|
|
|
|
|
DatabaseManager.shared.receivedWillMigrateDatabaseNotification()
|
2022-09-22 14:16:01 -05:00
|
|
|
}
|
|
|
|
|
|
2020-09-16 12:09:12 -07:00
|
|
|
fileprivate class PersistentContainer: RSTPersistentContainer
|
2020-09-14 14:31:46 -07:00
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
}
|
2020-09-16 12:09:12 -07:00
|
|
|
|
|
|
|
|
class func legacyDirectoryURL() -> URL
|
|
|
|
|
{
|
|
|
|
|
return super.defaultDirectoryURL()
|
|
|
|
|
}
|
2020-09-14 14:31:46 -07:00
|
|
|
}
|
|
|
|
|
|
2019-05-20 21:24:53 +02:00
|
|
|
public class DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
public static let shared = DatabaseManager()
|
|
|
|
|
|
|
|
|
|
public let persistentContainer: RSTPersistentContainer
|
|
|
|
|
|
|
|
|
|
public private(set) var isStarted = false
|
|
|
|
|
|
2019-07-31 13:24:59 -07:00
|
|
|
private var startCompletionHandlers = [(Error?) -> Void]()
|
2020-07-13 17:59:52 -07:00
|
|
|
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
|
2019-07-31 13:24:59 -07:00
|
|
|
|
2020-09-16 12:09:12 -07:00
|
|
|
private let coordinator = NSFileCoordinator()
|
|
|
|
|
private let coordinatorQueue = OperationQueue()
|
|
|
|
|
|
2024-08-06 10:43:52 +09:00
|
|
|
private var ignoreWillMigrateDatabaseNotification = false
|
2019-05-20 21:24:53 +02:00
|
|
|
private init()
|
|
|
|
|
{
|
2020-09-14 14:31:46 -07:00
|
|
|
self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
2019-07-24 12:23:54 -07:00
|
|
|
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
2022-09-22 14:16:01 -05:00
|
|
|
|
|
|
|
|
let observer = Unmanaged.passUnretained(self).toOpaque()
|
2024-08-06 10:43:52 +09:00
|
|
|
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillMigrateDatabaseNotification, CFNotificationName.willMigrateDatabase.rawValue, nil, .deliverImmediately)
|
2019-05-20 21:24:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
func start(completionHandler: @escaping (Error?) -> Void)
|
|
|
|
|
{
|
2019-07-31 13:24:59 -07:00
|
|
|
func finish(_ error: Error?)
|
|
|
|
|
{
|
2020-07-13 17:59:52 -07:00
|
|
|
self.dispatchQueue.async {
|
|
|
|
|
if error == nil
|
|
|
|
|
{
|
|
|
|
|
self.isStarted = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.startCompletionHandlers.forEach { $0(error) }
|
|
|
|
|
self.startCompletionHandlers.removeAll()
|
|
|
|
|
}
|
2019-07-31 13:24:59 -07:00
|
|
|
}
|
|
|
|
|
|
2020-07-13 17:59:52 -07:00
|
|
|
self.dispatchQueue.async {
|
|
|
|
|
self.startCompletionHandlers.append(completionHandler)
|
|
|
|
|
guard self.startCompletionHandlers.count == 1 else { return }
|
2019-05-20 21:24:53 +02:00
|
|
|
|
2020-07-13 17:59:52 -07:00
|
|
|
guard !self.isStarted else { return finish(nil) }
|
|
|
|
|
|
2023-10-11 17:59:01 -05:00
|
|
|
#if DEBUG
|
|
|
|
|
// 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
|
|
|
|
|
{
|
2024-08-06 10:43:52 +09:00
|
|
|
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
|
|
|
|
|
self.ignoreWillMigrateDatabaseNotification = true
|
|
|
|
|
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willMigrateDatabase, nil, nil, true)
|
|
|
|
|
}
|
2020-09-16 12:09:12 -07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-07-13 17:59:52 -07:00
|
|
|
}
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2019-05-20 21:24:53 +02:00
|
|
|
}
|
|
|
|
|
}
|
2019-06-06 14:46:23 -07:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-09 17:44:15 -05:00
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-20 21:24:53 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-11 17:59:01 -05:00
|
|
|
public extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
func startForPreview()
|
|
|
|
|
{
|
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
|
|
|
|
|
|
|
|
self.dispatchQueue.async {
|
|
|
|
|
self.startCompletionHandlers.append { error in
|
|
|
|
|
semaphore.signal()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-19 14:13:12 -05:00
|
|
|
_ = semaphore.wait(timeout: .now() + 2.0)
|
2023-10-11 17:59:01 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 21:24:53 +02:00
|
|
|
public extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
var viewContext: NSManagedObjectContext {
|
|
|
|
|
return self.persistentContainer.viewContext
|
|
|
|
|
}
|
2022-09-09 17:44:15 -05:00
|
|
|
|
2019-06-06 14:46:23 -07:00
|
|
|
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
|
|
|
|
|
}
|
2019-08-28 11:13:22 -07:00
|
|
|
|
|
|
|
|
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
|
|
|
|
|
{
|
2022-04-14 16:37:29 -07:00
|
|
|
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)
|
|
|
|
|
return patreonAccount
|
2019-08-28 11:13:22 -07:00
|
|
|
}
|
2019-06-06 14:46:23 -07:00
|
|
|
}
|
2019-06-17 16:31:10 -07:00
|
|
|
|
|
|
|
|
private extension DatabaseManager
|
|
|
|
|
{
|
|
|
|
|
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
|
|
|
{
|
2020-09-15 13:51:29 -07:00
|
|
|
guard !Bundle.isAppExtension() else { return completionHandler(.success(())) }
|
|
|
|
|
|
2019-07-31 13:24:59 -07:00
|
|
|
let context = self.persistentContainer.newBackgroundContext()
|
|
|
|
|
context.performAndWait {
|
2019-07-28 15:08:13 -07:00
|
|
|
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
|
2019-06-17 16:31:10 -07:00
|
|
|
|
2019-09-14 12:38:53 -07:00
|
|
|
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.
|
2023-10-10 17:39:20 -05:00
|
|
|
try! altStoreSource.setSourceURL(Source.altStoreSourceURL)
|
2019-09-14 12:38:53 -07:00
|
|
|
|
2019-07-31 14:07:00 -07:00
|
|
|
let storeApp: StoreApp
|
2019-06-17 16:31:10 -07:00
|
|
|
|
2023-02-02 08:09:15 -08:00
|
|
|
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
|
2019-06-17 16:31:10 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
storeApp = app
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2023-05-26 19:12:13 -05:00
|
|
|
storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: nil, in: context)
|
2019-09-14 12:38:53 -07:00
|
|
|
storeApp.source = altStoreSource
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
2019-09-14 12:38:53 -07:00
|
|
|
|
2020-03-06 17:08:35 -08:00
|
|
|
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
|
2019-06-21 11:20:03 -07:00
|
|
|
let installedApp: InstalledApp
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
if let app = storeApp.installedApp
|
2019-06-17 16:31:10 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
installedApp = app
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2023-05-26 19:12:13 -05:00
|
|
|
//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)
|
2019-07-28 15:08:13 -07:00
|
|
|
installedApp.storeApp = storeApp
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
|
2020-07-20 14:57:13 -07:00
|
|
|
/* 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
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
let fileURL = installedApp.fileURL
|
2020-06-07 09:49:29 -07:00
|
|
|
|
|
|
|
|
#if DEBUG
|
|
|
|
|
let replaceCachedApp = true
|
|
|
|
|
#else
|
2023-05-18 14:51:26 -05:00
|
|
|
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version || installedApp.buildVersion != localApp.buildVersion
|
2020-06-07 09:49:29 -07:00
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
if replaceCachedApp
|
2019-07-28 15:08:13 -07:00
|
|
|
{
|
2020-07-20 14:57:13 -07:00
|
|
|
func update(_ bundle: Bundle, bundleID: String) throws
|
|
|
|
|
{
|
|
|
|
|
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
|
|
|
|
|
|
2020-06-10 14:58:25 -07:00
|
|
|
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
2020-07-20 14:57:13 -07:00
|
|
|
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
|
|
|
|
|
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-21 16:35:08 -07:00
|
|
|
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
|
|
|
|
|
|
2020-07-20 14:57:13 -07:00
|
|
|
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
|
|
|
|
|
try update(appBundle, bundleID: StoreApp.altstoreAppID)
|
2019-09-21 16:35:08 -07:00
|
|
|
|
2020-07-20 14:57:13 -07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-21 16:35:08 -07:00
|
|
|
|
|
|
|
|
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
print("Failed to copy AltStore app bundle to its proper location.", error)
|
|
|
|
|
}
|
2019-07-28 15:08:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 14:43:19 -07:00
|
|
|
let cachedRefreshedDate = installedApp.refreshedDate
|
|
|
|
|
let cachedExpirationDate = installedApp.expirationDate
|
|
|
|
|
|
2019-09-21 16:35:08 -07:00
|
|
|
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
2023-05-26 19:12:13 -05:00
|
|
|
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber, storeBuildVersion: nil)
|
2019-06-17 16:31:10 -07:00
|
|
|
|
2020-03-11 14:43:19 -07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-17 16:31:10 -07:00
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try context.save()
|
|
|
|
|
completionHandler(.success(()))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-16 12:09:12 -07:00
|
|
|
|
|
|
|
|
func migrateDatabaseToAppGroupIfNeeded(completion: @escaping (Result<Void, Error>) -> Void)
|
|
|
|
|
{
|
2023-04-04 17:17:46 -05:00
|
|
|
// 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(())) }
|
2020-09-16 12:09:12 -07:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2024-11-04 12:33:53 +05:30
|
|
|
if(previousAppsDirectoryURL.path != appsDirectoryURL.path)
|
|
|
|
|
{
|
|
|
|
|
_ = try FileManager.default.replaceItemAt(appsDirectoryURL, withItemAt: previousAppsDirectoryURL)
|
|
|
|
|
}
|
2020-09-16 12:09:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finish(.success(()))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
print("Failed to migrate database to app group:", error)
|
|
|
|
|
finish(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-22 14:16:01 -05:00
|
|
|
|
2024-08-06 10:43:52 +09:00
|
|
|
func receivedWillMigrateDatabaseNotification()
|
2022-09-22 14:16:01 -05:00
|
|
|
{
|
2024-08-06 10:43:52 +09:00
|
|
|
defer { self.ignoreWillMigrateDatabaseNotification = false }
|
|
|
|
|
|
2022-09-22 14:16:01 -05:00
|
|
|
// Ignore notifications sent by the current process.
|
2024-08-06 10:43:52 +09:00
|
|
|
guard !self.ignoreWillMigrateDatabaseNotification else { return }
|
|
|
|
|
|
2022-09-22 14:16:01 -05:00
|
|
|
exit(104)
|
|
|
|
|
}
|
2019-06-17 16:31:10 -07:00
|
|
|
}
|