mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-13 16:53:29 +01:00
[AltStore] Revises database model to support both store apps and sideloaded apps
This commit is contained in:
@@ -10,11 +10,27 @@ import Foundation
|
||||
import CoreData
|
||||
import Network
|
||||
|
||||
import AltSign
|
||||
|
||||
class AppOperationContext
|
||||
{
|
||||
var appIdentifier: String
|
||||
var group: OperationGroup
|
||||
lazy var temporaryDirectory: URL = {
|
||||
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { self.error = error }
|
||||
|
||||
return temporaryDirectory
|
||||
}()
|
||||
|
||||
var bundleIdentifier: String
|
||||
var group: OperationGroup
|
||||
|
||||
var app: ALTApplication?
|
||||
var resignedApp: ALTApplication?
|
||||
|
||||
var connection: NWConnection?
|
||||
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||
@@ -22,9 +38,6 @@ class AppOperationContext
|
||||
}
|
||||
private var installedAppContext: NSManagedObjectContext?
|
||||
|
||||
var resignedFileURL: URL?
|
||||
var connection: NWConnection?
|
||||
|
||||
var isFinished = false
|
||||
|
||||
var error: Error? {
|
||||
@@ -37,9 +50,9 @@ class AppOperationContext
|
||||
}
|
||||
private var _error: Error?
|
||||
|
||||
init(appIdentifier: String, group: OperationGroup)
|
||||
init(bundleIdentifier: String, group: OperationGroup)
|
||||
{
|
||||
self.appIdentifier = appIdentifier
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.group = group
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,21 @@ import Roxas
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let app: App
|
||||
let app: AppProtocol
|
||||
|
||||
var useCachedAppIfAvailable = false
|
||||
lazy var context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
private let appIdentifier: String
|
||||
private let bundleIdentifier: String
|
||||
private let sourceURL: URL
|
||||
private let destinationURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
|
||||
init(app: App)
|
||||
init(app: AppProtocol)
|
||||
{
|
||||
self.app = app
|
||||
self.appIdentifier = app.identifier
|
||||
self.sourceURL = app.downloadURL
|
||||
self.bundleIdentifier = app.bundleIdentifier
|
||||
self.sourceURL = app.url
|
||||
self.destinationURL = InstalledApp.fileURL(for: app)
|
||||
|
||||
super.init()
|
||||
@@ -41,7 +38,7 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
super.main()
|
||||
|
||||
print("Downloading App:", self.appIdentifier)
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
func finishOperation(_ result: Result<URL, Error>)
|
||||
{
|
||||
@@ -77,24 +74,8 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
try FileManager.default.copyItem(at: appBundleURL, to: self.destinationURL, shouldReplace: true)
|
||||
|
||||
self.context.perform {
|
||||
let app = self.context.object(with: self.app.objectID) as! App
|
||||
|
||||
let installedApp: InstalledApp
|
||||
|
||||
if let app = app.installedApp
|
||||
{
|
||||
installedApp = app
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp(app: app, bundleIdentifier: app.identifier, context: self.context)
|
||||
}
|
||||
|
||||
installedApp.version = app.version
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(copiedApplication))
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -10,10 +10,11 @@ import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
class InstallAppOperation: ResultOperation<Void>
|
||||
class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
@@ -37,35 +38,53 @@ class InstallAppOperation: ResultOperation<Void>
|
||||
}
|
||||
|
||||
guard
|
||||
let installedApp = self.context.installedApp,
|
||||
let resignedApp = self.context.resignedApp,
|
||||
let connection = self.context.connection,
|
||||
let server = self.context.group.server
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
print("Installing app:", installedApp.app.identifier)
|
||||
self.context.group.beginInstallationHandler?(installedApp)
|
||||
}
|
||||
|
||||
let request = BeginInstallationRequest()
|
||||
server.send(request, via: connection) { (result) in
|
||||
switch result
|
||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
backgroundContext.perform {
|
||||
let installedApp: InstalledApp
|
||||
|
||||
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts.
|
||||
if let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), self.context.bundleIdentifier), in: backgroundContext)
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
|
||||
self.receive(from: connection, server: server) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
installedApp.managedObjectContext?.performAndWait {
|
||||
installedApp.refreshedDate = Date()
|
||||
}
|
||||
|
||||
case .failure: break
|
||||
}
|
||||
installedApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext)
|
||||
}
|
||||
|
||||
if let profile = resignedApp.provisioningProfile
|
||||
{
|
||||
installedApp.refreshedDate = profile.creationDate
|
||||
installedApp.expirationDate = profile.expirationDate
|
||||
}
|
||||
|
||||
self.context.group.beginInstallationHandler?(installedApp)
|
||||
|
||||
let request = BeginInstallationRequest()
|
||||
server.send(request, via: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
|
||||
self.finish(result)
|
||||
self.receive(from: connection, server: server) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
backgroundContext.perform {
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,12 +100,12 @@ class InstallAppOperation: ResultOperation<Void>
|
||||
|
||||
if let error = response.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else if response.progress == 1.0
|
||||
{
|
||||
self.progress.completedUnitCount = self.progress.totalUnitCount
|
||||
self.finish(.success(()))
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ class OperationGroup
|
||||
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
private var progressByApp = [App: Progress]()
|
||||
private var progressByBundleIdentifier = [String: Progress]()
|
||||
|
||||
private let operationQueue = OperationQueue()
|
||||
private let installOperationQueue = OperationQueue()
|
||||
@@ -63,17 +63,17 @@ class OperationGroup
|
||||
}
|
||||
}
|
||||
|
||||
func set(_ progress: Progress, for app: App)
|
||||
func set(_ progress: Progress, for app: AppProtocol)
|
||||
{
|
||||
self.progressByApp[app] = progress
|
||||
self.progressByBundleIdentifier[app.bundleIdentifier] = progress
|
||||
|
||||
self.progress.totalUnitCount += 1
|
||||
self.progress.addChild(progress, withPendingUnitCount: 1)
|
||||
}
|
||||
|
||||
func progress(for app: App) -> Progress?
|
||||
func progress(for app: AppProtocol) -> Progress?
|
||||
{
|
||||
let progress = self.progressByApp[app]
|
||||
let progress = self.progressByBundleIdentifier[app.bundleIdentifier]
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@ import Roxas
|
||||
import AltSign
|
||||
|
||||
@objc(ResignAppOperation)
|
||||
class ResignAppOperation: ResultOperation<URL>
|
||||
class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
private let temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
@@ -31,16 +29,6 @@ class ResignAppOperation: ResultOperation<URL>
|
||||
{
|
||||
super.main()
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
@@ -48,68 +36,49 @@ class ResignAppOperation: ResultOperation<URL>
|
||||
}
|
||||
|
||||
guard
|
||||
let installedApp = self.context.installedApp,
|
||||
let appContext = installedApp.managedObjectContext,
|
||||
let app = self.context.app,
|
||||
let signer = self.context.group.signer
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
appContext.perform {
|
||||
let appIdentifier = installedApp.app.identifier
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: signer.team) { (result) in
|
||||
guard let _ = self.process(result) else { return }
|
||||
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: signer.team) { (result) in
|
||||
guard let _ = self.process(result) else { return }
|
||||
// Prepare Provisioning Profiles
|
||||
self.prepareProvisioningProfiles(app.fileURL, team: signer.team) { (result) in
|
||||
guard let profiles = self.process(result) else { return }
|
||||
|
||||
// Prepare Provisioning Profiles
|
||||
appContext.perform {
|
||||
self.prepareProvisioningProfiles(installedApp.fileURL, team: signer.team) { (result) in
|
||||
guard let profiles = self.process(result) else { return }
|
||||
// Prepare app bundle
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
print("Resigning App:", self.context.bundleIdentifier)
|
||||
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
|
||||
// Prepare app bundle
|
||||
appContext.perform {
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
// Finish
|
||||
do
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
print("Resigning App:", appIdentifier)
|
||||
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
|
||||
// Finish
|
||||
appContext.perform {
|
||||
do
|
||||
{
|
||||
installedApp.refreshedDate = Date()
|
||||
|
||||
if let profile = profiles[installedApp.app.identifier]
|
||||
{
|
||||
installedApp.expirationDate = profile.expirationDate
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp.expirationDate = installedApp.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7)
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true)
|
||||
|
||||
self.finish(.success(installedApp.refreshedIPAURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(resignedApplication))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,17 +100,6 @@ class ResignAppOperation: ResultOperation<URL>
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<URL, Error>)
|
||||
{
|
||||
super.finish(result)
|
||||
|
||||
if FileManager.default.fileExists(atPath: self.temporaryDirectory.path, isDirectory: nil)
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: self.temporaryDirectory) }
|
||||
catch { print("Failed to remove app bundle.", error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ResignAppOperation
|
||||
@@ -386,15 +344,14 @@ private extension ResignAppOperation
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAppBundle(for installedApp: InstalledApp, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
|
||||
let bundleIdentifier = installedApp.bundleIdentifier
|
||||
let openURL = installedApp.openAppURL
|
||||
let appIdentifier = installedApp.app.identifier
|
||||
let bundleIdentifier = app.bundleIdentifier
|
||||
let openURL = InstalledApp.openAppURL(for: app)
|
||||
|
||||
let fileURL = installedApp.fileURL
|
||||
let fileURL = app.fileURL
|
||||
|
||||
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
|
||||
{
|
||||
@@ -420,7 +377,7 @@ private extension ResignAppOperation
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
let appBundleURL = self.temporaryDirectory.appendingPathComponent("App.app")
|
||||
let appBundleURL = self.context.temporaryDirectory.appendingPathComponent("App.app")
|
||||
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
||||
|
||||
// Become current so we can observe progress from unzipAppBundle().
|
||||
@@ -438,7 +395,7 @@ private extension ResignAppOperation
|
||||
|
||||
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
|
||||
|
||||
if appIdentifier == App.altstoreAppID
|
||||
if self.context.bundleIdentifier == App.altstoreAppID
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
||||
additionalValues[Bundle.Info.deviceID] = udid
|
||||
|
||||
@@ -39,7 +39,10 @@ class SendAppOperation: ResultOperation<NWConnection>
|
||||
return
|
||||
}
|
||||
|
||||
guard let fileURL = self.context.resignedFileURL, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
guard let app = self.context.app, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
// Connect to server.
|
||||
self.connect(to: server) { (result) in
|
||||
|
||||
Reference in New Issue
Block a user