2019-06-10 15:03:47 -07:00
//
// D o w n l o a d A p p O p e r a t i o n . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 6 / 1 0 / 1 9 .
// C o p y r i g h t © 2 0 1 9 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
import Roxas
2020-09-03 16:39:08 -07:00
import AltStoreCore
2019-06-10 15:03:47 -07:00
import AltSign
@objc ( DownloadAppOperation )
2023-01-04 09:52:12 -05:00
final class DownloadAppOperation : ResultOperation < ALTApplication >
2019-06-10 15:03:47 -07:00
{
2019-07-28 15:08:13 -07:00
let app : AppProtocol
2019-09-10 12:19:46 -07:00
let context : AppOperationContext
2024-08-06 10:43:52 +09:00
private let appName : String
2019-07-28 15:08:13 -07:00
private let bundleIdentifier : String
2024-11-10 02:54:18 +05:30
private var sourceURL : URL ?
2019-06-21 11:20:03 -07:00
private let destinationURL : URL
2024-08-06 10:43:52 +09:00
2019-06-10 15:03:47 -07:00
private let session = URLSession ( configuration : . default )
2021-02-26 16:47:33 -06:00
private let temporaryDirectory = FileManager . default . uniqueTemporaryURL ( )
2024-08-06 10:43:52 +09:00
2020-05-15 14:55:15 -07:00
init ( app : AppProtocol , destinationURL : URL , context : AppOperationContext )
2019-06-10 15:03:47 -07:00
{
self . app = app
2019-09-10 12:19:46 -07:00
self . context = context
2024-08-06 10:43:52 +09:00
self . appName = app . name
2019-07-28 15:08:13 -07:00
self . bundleIdentifier = app . bundleIdentifier
2024-11-10 02:54:18 +05:30
self . sourceURL = app . url
2020-05-15 14:55:15 -07:00
self . destinationURL = destinationURL
2024-08-06 10:43:52 +09:00
2019-06-10 15:03:47 -07:00
super . init ( )
2024-08-06 10:43:52 +09:00
2021-02-26 16:47:33 -06:00
// A p p = 3 , D e p e n d e n c i e s = 1
self . progress . totalUnitCount = 4
2019-06-10 15:03:47 -07:00
}
2024-08-06 10:43:52 +09:00
2019-06-10 15:03:47 -07:00
override func main ( )
{
super . main ( )
2024-08-06 10:43:52 +09:00
2019-09-10 12:19:46 -07:00
if let error = self . context . error
{
self . finish ( . failure ( error ) )
return
}
2024-08-06 10:43:52 +09:00
2019-07-28 15:08:13 -07:00
print ( " Downloading App: " , self . bundleIdentifier )
2024-08-06 10:43:52 +09:00
self . localizedFailure = String ( format : NSLocalizedString ( " %@ could not be downloaded. " , comment : " " ) , self . appName )
guard let storeApp = self . app as ? StoreApp else { return self . download ( self . app ) }
storeApp . managedObjectContext ? . perform {
do {
let latestVersion = try self . verify ( storeApp )
self . download ( latestVersion )
} catch let error as VerificationError where error . code = = . iOSVersionNotSupported {
guard let presentingViewController = self . context . presentingViewController ,
let latestSupportedVersion = storeApp . latestSupportedVersion ,
case let version = latestSupportedVersion . version ,
version != storeApp . installedApp ? . version else {
return self . finish ( . failure ( error ) )
}
let title = NSLocalizedString ( " Unsupported iOS Version " , comment : " " )
let message = error . localizedDescription + " \n \n " + NSLocalizedString ( " Would you like to download the last version compatible with this device instead? " , comment : " " )
DispatchQueue . main . async {
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . alert )
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style ) { _ in
self . finish ( . failure ( OperationError . cancelled ) )
} )
alertController . addAction ( UIAlertAction ( title : String ( format : NSLocalizedString ( " Download %@ %@ " , comment : " " ) , self . appName , version ) , style : . default ) { _ in
self . download ( latestSupportedVersion )
} )
presentingViewController . present ( alertController , animated : true )
}
} catch {
self . finish ( . failure ( error ) )
}
}
}
override func finish ( _ result : Result < ALTApplication , any Error > ) {
do {
try FileManager . default . removeItem ( at : self . temporaryDirectory )
} catch {
print ( " Failed to remove DownloadAppOperation temporary directory: \( self . temporaryDirectory ) . " , error )
}
super . finish ( result )
}
}
private extension DownloadAppOperation {
func verify ( _ storeApp : StoreApp ) throws -> AppVersion {
guard let version = storeApp . latestAvailableVersion else {
let failureReason = String ( format : NSLocalizedString ( " The latest version of %@ could not be determined. " , comment : " " ) , self . appName )
throw OperationError . unknown ( failureReason : failureReason )
}
if let minOSVersion = version . minOSVersion , ! ProcessInfo . processInfo . isOperatingSystemAtLeast ( minOSVersion ) {
throw VerificationError . iOSVersionNotSupported ( app : storeApp , requiredOSVersion : minOSVersion )
} else if let maxOSVersion = version . maxOSVersion , ProcessInfo . processInfo . operatingSystemVersion > maxOSVersion {
throw VerificationError . iOSVersionNotSupported ( app : storeApp , requiredOSVersion : maxOSVersion )
}
return version
}
func download ( @ Managed _ app : AppProtocol ) {
2024-11-10 02:54:18 +05:30
guard let sourceURL = self . sourceURL else { return self . finish ( . failure ( OperationError . appNotFound ( name : self . appName ) ) ) }
2024-08-06 10:43:52 +09:00
self . downloadIPA ( from : sourceURL ) { result in
2021-02-26 16:47:33 -06:00
do
{
let application = try result . get ( )
if self . context . bundleIdentifier = = StoreApp . dolphinAppID , self . context . bundleIdentifier != application . bundleIdentifier
{
if var infoPlist = NSDictionary ( contentsOf : application . bundle . infoPlistURL ) as ? [ String : Any ]
{
// M a n u a l l y u p d a t e t h e a p p ' s b u n d l e i d e n t i f i e r t o m a t c h t h e o n e s p e c i f i e d i n t h e s o u r c e .
// T h i s a l l o w s p e o p l e w h o p r e v i o u s l y i n s t a l l e d t h e a p p t o s t i l l u p d a t e a n d r e f r e s h n o r m a l l y .
infoPlist [ kCFBundleIdentifierKey as String ] = StoreApp . dolphinAppID
( infoPlist as NSDictionary ) . write ( to : application . bundle . infoPlistURL , atomically : true )
}
}
self . downloadDependencies ( for : application ) { result in
do
{
_ = try result . get ( )
try FileManager . default . copyItem ( at : application . fileURL , to : self . destinationURL , shouldReplace : true )
guard let copiedApplication = ALTApplication ( fileURL : self . destinationURL ) else { throw OperationError . invalidApp }
self . finish ( . success ( copiedApplication ) )
self . progress . completedUnitCount += 1
}
catch
{
self . finish ( . failure ( error ) )
}
}
}
catch
{
self . finish ( . failure ( error ) )
}
}
}
2024-08-06 10:43:52 +09:00
func downloadIPA ( from sourceURL : URL , completionHandler : @ escaping ( Result < ALTApplication , Error > ) -> Void )
2021-02-26 16:47:33 -06:00
{
2019-06-21 11:20:03 -07:00
func finishOperation ( _ result : Result < URL , Error > )
2019-06-18 18:31:59 -07:00
{
2019-06-21 11:20:03 -07:00
do
2019-06-18 18:31:59 -07:00
{
2019-06-21 11:20:03 -07:00
let fileURL = try result . get ( )
var isDirectory : ObjCBool = false
2024-08-06 10:43:52 +09:00
guard FileManager . default . fileExists ( atPath : fileURL . path , isDirectory : & isDirectory ) else { throw OperationError . appNotFound ( name : self . appName ) }
2021-02-26 16:47:33 -06:00
try FileManager . default . createDirectory ( at : self . temporaryDirectory , withIntermediateDirectories : true , attributes : nil )
2019-07-25 14:04:26 -07:00
let appBundleURL : URL
2019-06-21 11:20:03 -07:00
if isDirectory . boolValue
{
// D i r e c t o r y , s o a s s u m i n g t h i s i s . a p p b u n d l e .
guard Bundle ( url : fileURL ) != nil else { throw OperationError . invalidApp }
2021-02-26 16:47:33 -06:00
appBundleURL = self . temporaryDirectory . appendingPathComponent ( fileURL . lastPathComponent )
try FileManager . default . copyItem ( at : fileURL , to : appBundleURL )
2019-06-21 11:20:03 -07:00
}
else
{
// F i l e , s o a s s u m i n g t h i s i s a . i p a f i l e .
2021-02-26 16:47:33 -06:00
appBundleURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : self . temporaryDirectory )
2019-06-21 11:20:03 -07:00
}
2019-07-25 14:04:26 -07:00
guard let application = ALTApplication ( fileURL : appBundleURL ) else { throw OperationError . invalidApp }
2021-02-26 16:47:33 -06:00
completionHandler ( . success ( application ) )
2019-06-10 15:03:47 -07:00
}
2019-06-21 11:20:03 -07:00
catch
{
2021-02-26 16:47:33 -06:00
completionHandler ( . failure ( error ) )
2019-06-21 11:20:03 -07:00
}
2019-06-18 18:31:59 -07:00
}
2022-09-08 15:59:24 -05:00
if sourceURL . isFileURL
2019-06-18 18:31:59 -07:00
{
2021-02-26 16:47:33 -06:00
finishOperation ( . success ( sourceURL ) )
2019-06-21 11:20:03 -07:00
2021-02-26 16:47:33 -06:00
self . progress . completedUnitCount += 3
2019-06-18 18:31:59 -07:00
}
2019-06-21 11:20:03 -07:00
else
{
2021-02-26 16:47:33 -06:00
let downloadTask = self . session . downloadTask ( with : sourceURL ) { ( fileURL , response , error ) in
2019-06-21 11:20:03 -07:00
do
{
2024-08-06 10:43:52 +09:00
if let response = response as ? HTTPURLResponse {
guard response . statusCode != 404 else { throw CocoaError ( . fileNoSuchFile , userInfo : [ NSURLErrorKey : sourceURL ] ) }
}
2019-06-21 11:20:03 -07:00
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
finishOperation ( . success ( fileURL ) )
2021-02-26 16:47:33 -06:00
try ? FileManager . default . removeItem ( at : fileURL )
2019-06-21 11:20:03 -07:00
}
catch
{
finishOperation ( . failure ( error ) )
}
2019-06-10 15:03:47 -07:00
}
2021-02-26 16:47:33 -06:00
self . progress . addChild ( downloadTask . progress , withPendingUnitCount : 3 )
2019-06-21 11:20:03 -07:00
downloadTask . resume ( )
2019-06-10 15:03:47 -07:00
}
}
}
2021-02-26 16:47:33 -06:00
private extension DownloadAppOperation
{
struct AltStorePlist : Decodable
{
private enum CodingKeys : String , CodingKey
{
case dependencies = " ALTDependencies "
}
var dependencies : [ Dependency ]
}
struct Dependency : Decodable
{
var downloadURL : URL
var path : String ?
var preferredFilename : String {
let preferredFilename = self . path . map { ( $0 as NSString ) . lastPathComponent } ? ? self . downloadURL . lastPathComponent
return preferredFilename
}
init ( from decoder : Decoder ) throws
{
enum CodingKeys : String , CodingKey
{
case downloadURL
case path
}
let container = try decoder . container ( keyedBy : CodingKeys . self )
let urlString = try container . decode ( String . self , forKey : . downloadURL )
let path = try container . decodeIfPresent ( String . self , forKey : . path )
guard let downloadURL = URL ( string : urlString ) else {
throw DecodingError . dataCorruptedError ( forKey : . downloadURL , in : container , debugDescription : " downloadURL is not a valid URL. " )
}
self . downloadURL = downloadURL
self . path = path
}
}
func downloadDependencies ( for application : ALTApplication , completionHandler : @ escaping ( Result < Set < URL > , Error > ) -> Void )
{
guard FileManager . default . fileExists ( atPath : application . bundle . altstorePlistURL . path ) else {
return completionHandler ( . success ( [ ] ) )
}
do
{
let data = try Data ( contentsOf : application . bundle . altstorePlistURL )
let altstorePlist = try PropertyListDecoder ( ) . decode ( AltStorePlist . self , from : data )
var dependencyURLs = Set < URL > ( )
2024-08-06 10:43:52 +09:00
var dependencyError : Error ?
2021-02-26 16:47:33 -06:00
let dispatchGroup = DispatchGroup ( )
let progress = Progress ( totalUnitCount : Int64 ( altstorePlist . dependencies . count ) , parent : self . progress , pendingUnitCount : 1 )
for dependency in altstorePlist . dependencies
{
dispatchGroup . enter ( )
self . download ( dependency , for : application , progress : progress ) { result in
switch result
{
case . failure ( let error ) : dependencyError = error
case . success ( let fileURL ) : dependencyURLs . insert ( fileURL )
}
dispatchGroup . leave ( )
}
}
dispatchGroup . notify ( qos : . userInitiated , queue : . global ( ) ) {
if let dependencyError = dependencyError
{
completionHandler ( . failure ( dependencyError ) )
}
else
{
completionHandler ( . success ( dependencyURLs ) )
}
}
}
catch let error as DecodingError
{
2024-08-06 10:43:52 +09:00
let nsError = ( error as NSError ) . withLocalizedFailure ( String ( format : NSLocalizedString ( " Could not determine dependencies for %@. " , comment : " " ) , application . name ) )
2021-02-26 16:47:33 -06:00
completionHandler ( . failure ( nsError ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
2024-08-06 10:43:52 +09:00
func download ( _ dependency : Dependency , for application : ALTApplication , progress : Progress , completionHandler : @ escaping ( Result < URL , Error > ) -> Void )
2021-02-26 16:47:33 -06:00
{
let downloadTask = self . session . downloadTask ( with : dependency . downloadURL ) { ( fileURL , response , error ) in
do
{
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
defer { try ? FileManager . default . removeItem ( at : fileURL ) }
let path = dependency . path ? ? dependency . preferredFilename
let destinationURL = application . fileURL . appendingPathComponent ( path )
let directoryURL = destinationURL . deletingLastPathComponent ( )
if ! FileManager . default . fileExists ( atPath : directoryURL . path )
{
try FileManager . default . createDirectory ( at : directoryURL , withIntermediateDirectories : true )
}
try FileManager . default . copyItem ( at : fileURL , to : destinationURL , shouldReplace : true )
completionHandler ( . success ( destinationURL ) )
}
2024-08-06 10:43:52 +09:00
catch let error as NSError
2021-02-26 16:47:33 -06:00
{
2024-08-06 10:43:52 +09:00
let localizedFailure = String ( format : NSLocalizedString ( " The dependency '%@' could not be downloaded. " , comment : " " ) , dependency . preferredFilename )
completionHandler ( . failure ( error . withLocalizedFailure ( localizedFailure ) ) )
2021-02-26 16:47:33 -06:00
}
}
progress . addChild ( downloadTask . progress , withPendingUnitCount : 1 )
downloadTask . resume ( )
}
}