mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-18 19:23:43 +01:00
Adds option to explicitly back up installed apps
This commit is contained in:
@@ -389,6 +389,31 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backup(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let group = RefreshGroup()
|
||||||
|
group.completionHandler = { (results) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
|
|
||||||
|
let installedApp = try result.get()
|
||||||
|
assert(installedApp.managedObjectContext != nil)
|
||||||
|
|
||||||
|
installedApp.managedObjectContext?.perform {
|
||||||
|
completionHandler(.success(installedApp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let operation = AppOperation.backup(installedApp)
|
||||||
|
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||||
|
}
|
||||||
|
|
||||||
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||||
{
|
{
|
||||||
let group = RefreshGroup()
|
let group = RefreshGroup()
|
||||||
@@ -479,14 +504,15 @@ private extension AppManager
|
|||||||
case refresh(InstalledApp)
|
case refresh(InstalledApp)
|
||||||
case activate(InstalledApp)
|
case activate(InstalledApp)
|
||||||
case deactivate(InstalledApp)
|
case deactivate(InstalledApp)
|
||||||
|
case backup(InstalledApp)
|
||||||
case restore(InstalledApp)
|
case restore(InstalledApp)
|
||||||
|
|
||||||
var app: AppProtocol {
|
var app: AppProtocol {
|
||||||
switch self
|
switch self
|
||||||
{
|
{
|
||||||
case .install(let app), .update(let app),
|
case .install(let app), .update(let app), .refresh(let app as AppProtocol),
|
||||||
.refresh(let app as AppProtocol), .activate(let app as AppProtocol),
|
.activate(let app as AppProtocol), .deactivate(let app as AppProtocol),
|
||||||
.deactivate(let app as AppProtocol), .restore(let app as AppProtocol):
|
.backup(let app as AppProtocol), .restore(let app as AppProtocol):
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,6 +632,12 @@ private extension AppManager
|
|||||||
}
|
}
|
||||||
progress?.addChild(deactivateProgress, withPendingUnitCount: 80)
|
progress?.addChild(deactivateProgress, withPendingUnitCount: 80)
|
||||||
|
|
||||||
|
case .backup(let app):
|
||||||
|
let backupProgress = self._backup(app, operation: operation, group: group) { (result) in
|
||||||
|
self.finish(operation, result: result, group: group, progress: progress)
|
||||||
|
}
|
||||||
|
progress?.addChild(backupProgress, withPendingUnitCount: 80)
|
||||||
|
|
||||||
case .restore(let app):
|
case .restore(let app):
|
||||||
// Restoring, which is effectively just activating an app.
|
// Restoring, which is effectively just activating an app.
|
||||||
|
|
||||||
@@ -1017,6 +1049,68 @@ private extension AppManager
|
|||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func _backup(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||||
|
{
|
||||||
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
|
let restoreContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
|
let appContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||||
|
|
||||||
|
let installBackupAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
let installBackupAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
|
app.managedObjectContext?.perform {
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let progress = self._installBackupApp(for: app, operation: appOperation, group: group, context: restoreContext) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success(let installedApp): restoreContext.installedApp = installedApp
|
||||||
|
case .failure(let error):
|
||||||
|
restoreContext.error = error
|
||||||
|
appContext.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
installBackupAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress.addChild(installBackupAppProgress, withPendingUnitCount: 30)
|
||||||
|
|
||||||
|
let backupAppOperation = BackupAppOperation(action: .backup, context: restoreContext)
|
||||||
|
backupAppOperation.resultHandler = { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success: break
|
||||||
|
case .failure(let error):
|
||||||
|
restoreContext.error = error
|
||||||
|
appContext.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backupAppOperation.addDependency(installBackupAppOperation)
|
||||||
|
progress.addChild(backupAppOperation.progress, withPendingUnitCount: 15)
|
||||||
|
|
||||||
|
let installAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
let installAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
|
app.managedObjectContext?.perform {
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let progress = self._install(app, operation: appOperation, group: group, context: appContext) { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
installAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installAppOperation.addDependency(backupAppOperation)
|
||||||
|
progress.addChild(installAppProgress, withPendingUnitCount: 55)
|
||||||
|
|
||||||
|
group.add([installBackupAppOperation, backupAppOperation, installAppOperation])
|
||||||
|
self.run([installBackupAppOperation, installAppOperation, backupAppOperation], context: group.context)
|
||||||
|
|
||||||
|
return progress
|
||||||
|
}
|
||||||
|
|
||||||
private func _installBackupApp(for app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
private func _installBackupApp(for app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||||
{
|
{
|
||||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
@@ -1176,7 +1270,7 @@ private extension AppManager
|
|||||||
event = nil
|
event = nil
|
||||||
|
|
||||||
case .update: event = .updatedApp(installedApp)
|
case .update: event = .updatedApp(installedApp)
|
||||||
case .activate, .deactivate, .restore: event = nil
|
case .activate, .deactivate, .backup, .restore: event = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let event = event
|
if let event = event
|
||||||
@@ -1234,7 +1328,7 @@ private extension AppManager
|
|||||||
switch operation
|
switch operation
|
||||||
{
|
{
|
||||||
case .install, .update: return self.installationProgress[operation.bundleIdentifier]
|
case .install, .update: return self.installationProgress[operation.bundleIdentifier]
|
||||||
case .refresh, .activate, .deactivate, .restore: return self.refreshProgress[operation.bundleIdentifier]
|
case .refresh, .activate, .deactivate, .backup, .restore: return self.refreshProgress[operation.bundleIdentifier]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1243,7 +1337,7 @@ private extension AppManager
|
|||||||
switch operation
|
switch operation
|
||||||
{
|
{
|
||||||
case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress
|
case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress
|
||||||
case .refresh, .activate, .deactivate, .restore: self.refreshProgress[operation.bundleIdentifier] = progress
|
case .refresh, .activate, .deactivate, .backup, .restore: self.refreshProgress[operation.bundleIdentifier] = progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1174,6 +1174,45 @@ private extension MyAppsViewController
|
|||||||
self.present(alertController, animated: true, completion: nil)
|
self.present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backup(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
let title = NSLocalizedString("Start Backup?", comment: "")
|
||||||
|
let message = NSLocalizedString("This will replace any previous backups. Please leave AltStore open until the backup is complete.", comment: "")
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||||
|
alertController.addAction(.cancel)
|
||||||
|
|
||||||
|
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
|
||||||
|
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (action) in
|
||||||
|
AppManager.shared.backup(installedApp, presentingViewController: self) { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let app = try result.get()
|
||||||
|
try? app.managedObjectContext?.save()
|
||||||
|
|
||||||
|
print("Finished backing up app:", app.bundleIdentifier)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to back up app:", error)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
|
||||||
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
func restore(_ installedApp: InstalledApp)
|
func restore(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
|
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
|
||||||
@@ -1404,6 +1443,10 @@ extension MyAppsViewController
|
|||||||
self.remove(installedApp)
|
self.remove(installedApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc")) { (action) in
|
||||||
|
self.backup(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in
|
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in
|
||||||
self.exportBackup(for: installedApp)
|
self.exportBackup(for: installedApp)
|
||||||
}
|
}
|
||||||
@@ -1419,14 +1462,26 @@ extension MyAppsViewController
|
|||||||
if installedApp.isActive
|
if installedApp.isActive
|
||||||
{
|
{
|
||||||
actions.append(refreshAction)
|
actions.append(refreshAction)
|
||||||
actions.append(deactivateAction)
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
actions.append(activateAction)
|
actions.append(activateAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp), !UserDefaults.standard.isLegacyDeactivationSupported
|
if installedApp.isActive
|
||||||
|
{
|
||||||
|
actions.append(backupAction)
|
||||||
|
}
|
||||||
|
else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported
|
||||||
|
{
|
||||||
|
// Allow backing up inactive apps if they are still installed,
|
||||||
|
// but on an iOS version that no longer supports legacy deactivation.
|
||||||
|
// This handles edge case where you can't install more apps until you
|
||||||
|
// delete some, but can't activate inactive apps again to back them up first.
|
||||||
|
actions.append(backupAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
||||||
{
|
{
|
||||||
var backupExists = false
|
var backupExists = false
|
||||||
var outError: NSError? = nil
|
var outError: NSError? = nil
|
||||||
@@ -1454,6 +1509,11 @@ extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if installedApp.isActive
|
||||||
|
{
|
||||||
|
actions.append(deactivateAction)
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
||||||
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||||
|
|||||||
Reference in New Issue
Block a user