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
2021-02-26 16:47:33 -06:00
private extension DownloadAppOperation
{
struct DependencyError : ALTLocalizedError
{
let dependency : Dependency
let error : Error
var failure : String ? {
return String ( format : NSLocalizedString ( " Could not download “%@”. " , comment : " " ) , self . dependency . preferredFilename )
}
var underlyingError : Error ? {
return self . error
}
}
}
2019-06-10 15:03:47 -07:00
@objc ( DownloadAppOperation )
2019-07-28 15:08:13 -07:00
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
2019-06-18 18:31:59 -07:00
2019-07-28 15:08:13 -07:00
private let bundleIdentifier : String
2019-06-21 11:20:03 -07:00
private let sourceURL : URL
private let destinationURL : URL
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 ( )
2019-06-10 15:03:47 -07: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
2019-07-28 15:08:13 -07:00
self . bundleIdentifier = app . bundleIdentifier
self . sourceURL = app . url
2020-05-15 14:55:15 -07:00
self . destinationURL = destinationURL
2019-06-10 15:03:47 -07:00
super . init ( )
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
}
override func main ( )
{
super . main ( )
2019-09-10 12:19:46 -07:00
if let error = self . context . error
{
self . finish ( . failure ( error ) )
return
}
2019-07-28 15:08:13 -07:00
print ( " Downloading App: " , self . bundleIdentifier )
2019-06-21 11:20:03 -07:00
2021-02-26 16:47:33 -06:00
self . downloadApp ( from : self . sourceURL ) { result in
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 ) )
}
}
}
override func finish ( _ result : Result < ALTApplication , 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 downloadApp ( from sourceURL : URL , completionHandler : @ escaping ( Result < ALTApplication , Error > ) -> Void )
{
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
guard FileManager . default . fileExists ( atPath : fileURL . path , isDirectory : & isDirectory ) else { throw OperationError . appNotFound }
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
}
2019-06-21 11:20:03 -07:00
if self . 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
{
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 > ( )
var dependencyError : DependencyError ?
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
{
let nsError = ( error as NSError ) . withLocalizedFailure ( String ( format : NSLocalizedString ( " Could not download dependencies for %@. " , comment : " " ) , application . name ) )
completionHandler ( . failure ( nsError ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
func download ( _ dependency : Dependency , for application : ALTApplication , progress : Progress , completionHandler : @ escaping ( Result < URL , DependencyError > ) -> Void )
{
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 ) )
}
catch
{
completionHandler ( . failure ( DependencyError ( dependency : dependency , error : error ) ) )
}
}
progress . addChild ( downloadTask . progress , withPendingUnitCount : 1 )
downloadTask . resume ( )
}
}