mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 19:53:25 +01:00
Adds BackupAppOperation to backup and restore app data
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
|
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
|
||||||
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
|
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
|
||||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
|
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 */; };
|
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; };
|
||||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; };
|
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; };
|
||||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
|
||||||
@@ -1290,6 +1292,7 @@
|
|||||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
||||||
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
|
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
|
||||||
BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
|
BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
|
||||||
|
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */,
|
||||||
);
|
);
|
||||||
path = Operations;
|
path = Operations;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1912,6 +1915,7 @@
|
|||||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
||||||
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
||||||
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
||||||
|
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
|
||||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ extension AppDelegate
|
|||||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
||||||
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
||||||
|
|
||||||
|
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
|
||||||
|
|
||||||
static let importAppDeepLinkURLKey = "fileURL"
|
static let importAppDeepLinkURLKey = "fileURL"
|
||||||
|
static let appBackupResultKey = "result"
|
||||||
}
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@@ -134,13 +137,43 @@ private extension AppDelegate
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
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 {
|
switch host
|
||||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
{
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
183
AltStore/Operations/BackupAppOperation.swift
Normal file
183
AltStore/Operations/BackupAppOperation.swift
Normal 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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,12 @@ class InstallAppOperationContext: AppOperationContext
|
|||||||
|
|
||||||
var resignedApp: ALTApplication?
|
var resignedApp: ALTApplication?
|
||||||
var installationConnection: ServerConnection?
|
var installationConnection: ServerConnection?
|
||||||
|
var installedApp: InstalledApp? {
|
||||||
|
didSet {
|
||||||
|
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var installedAppContext: NSManagedObjectContext?
|
||||||
|
|
||||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ enum OperationError: LocalizedError
|
|||||||
case unknown
|
case unknown
|
||||||
case unknownResult
|
case unknownResult
|
||||||
case cancelled
|
case cancelled
|
||||||
|
case timedOut
|
||||||
|
|
||||||
case notAuthenticated
|
case notAuthenticated
|
||||||
case appNotFound
|
case appNotFound
|
||||||
@@ -28,17 +29,23 @@ enum OperationError: LocalizedError
|
|||||||
|
|
||||||
case noSources
|
case noSources
|
||||||
|
|
||||||
|
case openAppFailed(name: String)
|
||||||
|
case missingAppGroup
|
||||||
|
|
||||||
var failureReason: String? {
|
var failureReason: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
||||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", 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 .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||||
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
||||||
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
||||||
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
||||||
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
||||||
case .noSources: return NSLocalizedString("There are no AltStore sources.", 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):
|
case .iOSVersionNotSupported(let app):
|
||||||
let name = app.name
|
let name = app.name
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user