mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[Feature]: Import external backup, Restore n-1 backup if current backup is corrupted by importing wrong directory
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
551A15E55999499418AC1022 /* Pods_AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 707E746318F0B6F1A44935D3 /* Pods_AltStoreCore.framework */; };
|
||||
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; };
|
||||
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; };
|
||||
A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8087E742D2D2958002DB21B /* ImportExport.swift */; };
|
||||
A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; };
|
||||
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
|
||||
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
|
||||
@@ -616,6 +617,7 @@
|
||||
7935E4499B2FC11DA8BAB2CC /* Pods-AltStoreCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStoreCore.release.xcconfig"; path = "Target Support Files/Pods-AltStoreCore/Pods-AltStoreCore.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = "<group>"; };
|
||||
A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A8087E742D2D2958002DB21B /* ImportExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExport.swift; sourceTree = "<group>"; };
|
||||
A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = "<group>"; };
|
||||
A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = "<group>"; };
|
||||
A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
@@ -1134,6 +1136,14 @@
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8087E712D2D291B002DB21B /* importexport */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8087E742D2D2958002DB21B /* ImportExport.swift */,
|
||||
);
|
||||
path = importexport;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A809F68A2D04D71200F0F0F3 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1196,6 +1206,7 @@
|
||||
A8C38C1C2D2068D100E83DBD /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8087E712D2D291B002DB21B /* importexport */,
|
||||
A8B516DE2D2666900047047C /* dignostics */,
|
||||
A8C38C272D206AA500E83DBD /* common */,
|
||||
A8C38C202D206A3A00E83DBD /* iostreams */,
|
||||
@@ -2903,6 +2914,7 @@
|
||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */,
|
||||
A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */,
|
||||
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
|
||||
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- <key>com.apple.security.files.user-selected.read-write</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array> -->
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
||||
|
||||
@@ -1357,6 +1357,50 @@ private extension MyAppsViewController
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func importBackup(for installedApp: InstalledApp){
|
||||
ImportExport.importBackup(presentingViewController: self, for: installedApp) { result in
|
||||
var toast: ToastView
|
||||
switch(result){
|
||||
case .failure(let error):
|
||||
toast = ToastView(error: error, opensLog: false)
|
||||
break
|
||||
case .success:
|
||||
toast = ToastView(text: "Import Backup successful for \(installedApp.name)",
|
||||
detailText: "Use 'Restore Backup' option to restore data from this imported backup")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
toast.show(in: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getPreviousBackupURL(_ installedApp: InstalledApp) -> URL
|
||||
{
|
||||
let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
|
||||
let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
|
||||
return backupBakURL
|
||||
}
|
||||
|
||||
func restorePreviousBackup(for installedApp: InstalledApp){
|
||||
let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
|
||||
let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
|
||||
|
||||
// backupBakURL is expected to exist at this point, this needs to be ensured by caller logic
|
||||
// or invoke this action only when backupBakURL exists
|
||||
|
||||
// delete the current backup
|
||||
if(FileManager.default.fileExists(atPath: backupURL.path)){
|
||||
try! FileManager.default.removeItem(at: backupURL)
|
||||
}
|
||||
|
||||
// restore the previously saved backup as current backup
|
||||
// (don't delete the N-1 backup yet so copy instead of move)
|
||||
try! FileManager.default.copyItem(at: backupBakURL, to: backupURL)
|
||||
|
||||
//perform restore of data from the backup
|
||||
restore(installedApp)
|
||||
}
|
||||
|
||||
func restore(_ installedApp: InstalledApp)
|
||||
{
|
||||
guard minimuxerStatus else { return }
|
||||
@@ -1810,10 +1854,18 @@ extension MyAppsViewController
|
||||
self.exportBackup(for: installedApp)
|
||||
}
|
||||
|
||||
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||
let importBackupAction = UIAction(title: NSLocalizedString("Import Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||
self.importBackup(for: installedApp)
|
||||
}
|
||||
|
||||
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: "Restores the last or current backup of this app"), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||
self.restore(installedApp)
|
||||
}
|
||||
|
||||
let restorePreviousBackupAction = UIAction(title: NSLocalizedString("Restore Previous Backup", comment: "Restores the backup saved before the current backup was created."), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||
self.restorePreviousBackup(for: installedApp)
|
||||
}
|
||||
|
||||
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
|
||||
self.chooseIcon(for: installedApp)
|
||||
}
|
||||
@@ -1878,11 +1930,11 @@ extension MyAppsViewController
|
||||
var outError: NSError? = nil
|
||||
|
||||
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
||||
#if DEBUG
|
||||
backupExists = true
|
||||
#else
|
||||
// #if DEBUG
|
||||
// backupExists = true
|
||||
// #else
|
||||
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
||||
#endif
|
||||
// #endif
|
||||
}
|
||||
|
||||
if backupExists
|
||||
@@ -1903,16 +1955,23 @@ extension MyAppsViewController
|
||||
if installedApp.isActive
|
||||
{
|
||||
actions.append(deactivateAction)
|
||||
// import backup into shared backups dir is allowed
|
||||
actions.append(importBackupAction)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||
{
|
||||
actions.append(removeAction)
|
||||
// have an option to restore the n-1 backup
|
||||
if FileManager.default.fileExists(atPath: getPreviousBackupURL(installedApp).path){
|
||||
actions.append(restorePreviousBackupAction)
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// #if DEBUG
|
||||
// if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||
// {
|
||||
// actions.append(removeAction)
|
||||
// }
|
||||
//
|
||||
// #else
|
||||
|
||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||
{
|
||||
@@ -1926,9 +1985,29 @@ extension MyAppsViewController
|
||||
actions.append(removeAction)
|
||||
}
|
||||
|
||||
#endif
|
||||
// #endif
|
||||
}
|
||||
|
||||
// Change the order of entries to make changes to how the context menu is displayed
|
||||
let orderedActions = [
|
||||
openMenu,
|
||||
refreshAction,
|
||||
activateAction,
|
||||
jitAction,
|
||||
changeIconMenu,
|
||||
backupAction,
|
||||
exportBackupAction,
|
||||
importBackupAction,
|
||||
restoreBackupAction,
|
||||
restorePreviousBackupAction,
|
||||
deactivateAction,
|
||||
removeAction,
|
||||
]
|
||||
|
||||
// remove non-selected actions from the all-actions ordered list
|
||||
// this way the declaration of the action in the above code doesn't determine the context menu order
|
||||
actions = orderedActions.filter{ action in actions.contains(action)}
|
||||
|
||||
var title: String?
|
||||
|
||||
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
|
||||
|
||||
135
SideStore/Utils/importexport/ImportExport.swift
Normal file
135
SideStore/Utils/importexport/ImportExport.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// ImportExport.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 07/01/25.
|
||||
// Copyright © 2025 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import UIKit
|
||||
import AltStoreCore
|
||||
|
||||
class ImportExport {
|
||||
|
||||
private static var documentPickerHandler: DocumentPickerHandler?
|
||||
|
||||
public static func getPreviousBackupURL(_ backupURL: URL) -> URL {
|
||||
let backupParentDirectory = backupURL.deletingLastPathComponent()
|
||||
let backupName = backupURL.lastPathComponent
|
||||
let backupBakURL = backupParentDirectory.appendingPathComponent("\(backupName).bak")
|
||||
return backupBakURL
|
||||
}
|
||||
|
||||
/// Renames the existing backup contents at `backupURL` to `<foldername>.bak`.
|
||||
private static func renameBackupContents(at backupURL: URL) throws {
|
||||
|
||||
// rename backup to backup.bak dir only if backup dir exists
|
||||
guard FileManager.default.fileExists(atPath: backupURL.path) else { return }
|
||||
|
||||
let backupBakURL = getPreviousBackupURL(backupURL)
|
||||
|
||||
let fileManager = FileManager.default
|
||||
if fileManager.fileExists(atPath: backupBakURL.path) {
|
||||
try fileManager.removeItem(at: backupBakURL) // Remove any existing .bak directory
|
||||
}
|
||||
|
||||
try fileManager.moveItem(at: backupURL, to: backupBakURL)
|
||||
}
|
||||
|
||||
/// Handles importing new backup data into the designated backup directory.
|
||||
private static func importBackupContents(from documentPickerURL: URL, to backupURL: URL) throws {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Ensure the backup directory exists.
|
||||
if !fileManager.fileExists(atPath: backupURL.path) {
|
||||
try fileManager.createDirectory(at: backupURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
print("Backup URL: \(backupURL)")
|
||||
print("Document Picker URL: \(documentPickerURL)")
|
||||
|
||||
// Enumerate the contents of the selected directory and copy them to the backup directory.
|
||||
let selectedContents = try fileManager.contentsOfDirectory(
|
||||
at: documentPickerURL,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .skipsHiddenFiles
|
||||
)
|
||||
for itemURL in selectedContents {
|
||||
let destinationURL = backupURL.appendingPathComponent(itemURL.lastPathComponent)
|
||||
|
||||
// Remove the existing file if it exists at the destination.
|
||||
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||
try fileManager.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
// Copy the item.
|
||||
try fileManager.copyItem(at: itemURL, to: destinationURL)
|
||||
}
|
||||
}
|
||||
|
||||
public static func importBackup(presentingViewController: UIViewController,
|
||||
for installedApp: InstalledApp,
|
||||
completionHandler: @escaping (Result<Void, Error>) -> Void){
|
||||
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else {
|
||||
return completionHandler(.failure(OperationError.invalidParameters("Error: Backup directory URL not found.")))
|
||||
}
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder], asCopy: false)
|
||||
documentPicker.allowsMultipleSelection = false
|
||||
|
||||
// Create a handler and set it as the delegate
|
||||
Self.documentPickerHandler = DocumentPickerHandler { selectedURL in
|
||||
guard let selectedURL = selectedURL else {
|
||||
return completionHandler(.failure( OperationError.cancelled))
|
||||
}
|
||||
|
||||
// resolve symlinks if any, so that prefix match works
|
||||
let appUserDataDir = FileManager.default.documentsDirectory.resolvingSymlinksInPath()
|
||||
guard selectedURL.resolvingSymlinksInPath().path.hasPrefix(appUserDataDir.path) else {
|
||||
return completionHandler(.failure(
|
||||
OperationError.forbidden(failureReason: "Selected backup data directory is not within the app's user data directory"))
|
||||
)
|
||||
}
|
||||
|
||||
do {
|
||||
// Rename existing backup contents to `<foldername>.bak`.
|
||||
try Self.renameBackupContents(at: backupURL)
|
||||
|
||||
// Import the contents of the selected folder into the backup directory.
|
||||
try Self.importBackupContents(from: selectedURL, to: backupURL)
|
||||
|
||||
print("Backup imported successfully to:", backupURL.path)
|
||||
return completionHandler(.success(()))
|
||||
} catch {
|
||||
print("Backup Error:", error)
|
||||
return completionHandler(.failure( OperationError.invalidParameters(error.localizedDescription)))
|
||||
}
|
||||
}
|
||||
|
||||
documentPicker.delegate = Self.documentPickerHandler
|
||||
// Present the picker
|
||||
presentingViewController.present(documentPicker, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private struct AssociatedKeys {
|
||||
static var documentPickerHandler = "documentPickerHandler"
|
||||
}
|
||||
|
||||
|
||||
class DocumentPickerHandler: NSObject, UIDocumentPickerDelegate {
|
||||
private let completion: (URL?) -> Void
|
||||
|
||||
init(completion: @escaping (URL?) -> Void) {
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
completion(urls.first)
|
||||
}
|
||||
|
||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user