Adds BackupAppOperation to backup and restore app data

This commit is contained in:
Riley Testut
2020-05-15 15:11:17 -07:00
parent 7c9d8bd90d
commit c403d7c788
5 changed files with 238 additions and 5 deletions

View File

@@ -34,6 +34,7 @@
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */; };
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; };
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; };
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
@@ -354,6 +355,7 @@
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; };
BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; };
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAppOperation.swift; sourceTree = "<group>"; };
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchProvisioningProfilesOperation.swift; sourceTree = "<group>"; };
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAppOperation.swift; sourceTree = "<group>"; };
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
@@ -1290,6 +1292,7 @@
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -1912,6 +1915,7 @@
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,

View File

@@ -55,7 +55,10 @@ extension AppDelegate
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
}
@UIApplicationMain
@@ -134,13 +137,43 @@ private extension AppDelegate
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host, host.lowercased() == "patreon" else { return false }
guard let host = components.host?.lowercased() else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
switch host
{
case "patreon":
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
}
return true
case "appbackupresponse":
let result: Result<Void, Error>
switch url.path.lowercased()
{
case "/success": result = .success(())
case "/failure":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
guard
let errorDomain = queryItems["errorDomain"],
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
let errorDescription = queryItems["errorDescription"]
else { return false }
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
result = .failure(error)
default: return false
}
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
return true
default: return false
}
return true
}
}
}

View File

@@ -0,0 +1,183 @@
//
// BackupAppOperation.swift
// AltStore
//
// Created by Riley Testut on 5/12/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltKit
import AltSign
extension BackupAppOperation
{
enum Action: String
{
case backup
case restore
}
}
@objc(BackupAppOperation)
class BackupAppOperation: ResultOperation<Void>
{
let action: Action
let context: InstallAppOperationContext
private var appName: String?
private var timeoutTimer: Timer?
init(action: Action, context: InstallAppOperationContext)
{
self.action = action
self.context = context
super.init()
}
override func main()
{
super.main()
do
{
if let error = self.context.error
{
throw error
}
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
context.perform {
do
{
let appName = installedApp.name
self.appName = appName
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
let altstoreOpenURL = altstoreApp.openAppURL
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue
openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)]
guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let currentTime = CFAbsoluteTimeGetCurrent()
UIApplication.shared.open(openURL, options: [:]) { (success) in
let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime
if success
{
self.registerObservers()
}
else if elapsedTime < 0.5
{
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
// Try again in a couple seconds.
print("Failed too quickly, retrying after a few seconds...")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
UIApplication.shared.open(openURL, options: [:]) { (success) in
if success
{
self.registerObservers()
}
else
{
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
}
else
{
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
}
catch
{
self.finish(.failure(error))
}
}
}
catch
{
self.finish(.failure(error))
}
}
override func finish(_ result: Result<Void, Error>)
{
let result = result.mapError { (error) -> Error in
let appName = self.appName ?? self.context.bundleIdentifier
switch (error, self.action)
{
case (let error as NSError, _) where (self.context.error as NSError?) == error: fallthrough
case (OperationError.cancelled, _):
return error
case (let error as NSError, .backup):
let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
case (let error as NSError, .restore):
let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
}
}
switch result
{
case .success: self.progress.completedUnitCount += 1
case .failure: break
}
super.finish(result)
}
}
private extension BackupAppOperation
{
func registerObservers()
{
var applicationWillReturnObserver: NSObjectProtocol!
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
guard let self = self, !self.isFinished else { return }
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
// Final delay to ensure we don't prematurely return failure
// in case timer expired while we were in background, but
// are now returning to app with success response.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
guard let self = self, !self.isFinished else { return }
self.finish(.failure(OperationError.timedOut))
}
}
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
}
var backupResponseObserver: NSObjectProtocol!
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
self?.timeoutTimer?.invalidate()
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
self?.finish(result)
NotificationCenter.default.removeObserver(backupResponseObserver!)
}
}
}

View File

@@ -105,6 +105,12 @@ class InstallAppOperationContext: AppOperationContext
var resignedApp: ALTApplication?
var installationConnection: ServerConnection?
var installedApp: InstalledApp? {
didSet {
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var beginInstallationHandler: ((InstalledApp) -> Void)?
}

View File

@@ -14,6 +14,7 @@ enum OperationError: LocalizedError
case unknown
case unknownResult
case cancelled
case timedOut
case notAuthenticated
case appNotFound
@@ -28,17 +29,23 @@ enum OperationError: LocalizedError
case noSources
case openAppFailed(name: String)
case missingAppGroup
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
case .openAppFailed(let name): return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), name)
case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be found.", comment: "")
case .iOSVersionNotSupported(let app):
let name = app.name