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
2023-03-01 14:36:52 -05:00
import RoxasUIKit
2019-06-10 15:03:47 -07:00
import AltSign
2023-03-01 14:36:52 -05:00
import SideKit
2023-03-01 00:48:36 -05:00
import SideStoreCore
2023-03-01 14:36:52 -05:00
import Shared
2023-03-02 00:40:11 -05:00
import os . log
2019-06-10 15:03:47 -07:00
2023-03-01 00:48:36 -05:00
private extension DownloadAppOperation {
struct DependencyError : ALTLocalizedError {
2021-02-26 16:47:33 -06:00
let dependency : Dependency
let error : Error
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
var failure : String ? {
2023-03-01 00:48:36 -05:00
String ( format : NSLocalizedString ( " Could not download “%@”. " , comment : " " ) , dependency . preferredFilename )
2021-02-26 16:47:33 -06:00
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
var underlyingError : Error ? {
2023-03-01 00:48:36 -05:00
error
2021-02-26 16:47:33 -06:00
}
}
}
2019-06-10 15:03:47 -07:00
@objc ( DownloadAppOperation )
2023-03-01 00:48:36 -05:00
final class DownloadAppOperation : ResultOperation < ALTApplication > {
2019-07-28 15:08:13 -07:00
let app : AppProtocol
2019-09-10 12:19:46 -07:00
let context : AppOperationContext
2023-03-01 00:48:36 -05:00
2019-07-28 15:08:13 -07:00
private let bundleIdentifier : String
2022-09-08 15:59:24 -05:00
private var sourceURL : URL ?
2019-06-21 11:20:03 -07:00
private let destinationURL : URL
2023-03-01 00:48:36 -05: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 ( )
2023-03-01 00:48:36 -05: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
2023-03-01 00:48:36 -05:00
bundleIdentifier = app . bundleIdentifier
sourceURL = app . url
2020-05-15 14:55:15 -07:00
self . destinationURL = destinationURL
2023-03-01 00:48:36 -05:00
2019-06-10 15:03:47 -07:00
super . init ( )
2023-03-01 00:48:36 -05: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
2023-03-01 00:48:36 -05:00
progress . totalUnitCount = 4
2019-06-10 15:03:47 -07:00
}
2023-03-01 00:48:36 -05:00
override func main ( ) {
2019-06-10 15:03:47 -07:00
super . main ( )
2023-03-01 00:48:36 -05:00
if let error = context . error {
finish ( . failure ( error ) )
2019-09-10 12:19:46 -07:00
return
}
2023-03-01 00:48:36 -05:00
2023-03-02 00:40:11 -05:00
os_log ( " Downloading App: %@ " , type : . info , bundleIdentifier )
2023-03-01 00:48:36 -05:00
guard let sourceURL = sourceURL else { return finish ( . failure ( OperationError . appNotFound ) ) }
downloadApp ( from : sourceURL ) { result in
do {
2021-02-26 16:47:33 -06:00
let application = try result . get ( )
2023-03-01 00:48:36 -05:00
if self . context . bundleIdentifier = = StoreApp . dolphinAppID , self . context . bundleIdentifier != application . bundleIdentifier {
if var infoPlist = NSDictionary ( contentsOf : application . bundle . infoPlistURL ) as ? [ String : Any ] {
2021-02-26 16:47:33 -06:00
// 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 )
}
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
self . downloadDependencies ( for : application ) { result in
2023-03-01 00:48:36 -05:00
do {
2021-02-26 16:47:33 -06:00
_ = try result . get ( )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
try FileManager . default . copyItem ( at : application . fileURL , to : self . destinationURL , shouldReplace : true )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
guard let copiedApplication = ALTApplication ( fileURL : self . destinationURL ) else { throw OperationError . invalidApp }
self . finish ( . success ( copiedApplication ) )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
self . progress . completedUnitCount += 1
2023-03-01 00:48:36 -05:00
} catch {
2021-02-26 16:47:33 -06:00
self . finish ( . failure ( error ) )
}
}
2023-03-01 00:48:36 -05:00
} catch {
2021-02-26 16:47:33 -06:00
self . finish ( . failure ( error ) )
}
}
}
2023-03-01 00:48:36 -05:00
override func finish ( _ result : Result < ALTApplication , Error > ) {
do {
try FileManager . default . removeItem ( at : temporaryDirectory )
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to remove DownloadAppOperation temporary directory: %@. %@ " , type : . error , temporaryDirectory . absoluteString , error . localizedDescription )
2021-02-26 16:47:33 -06:00
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
super . finish ( result )
}
}
2023-03-01 00:48:36 -05:00
private extension DownloadAppOperation {
func downloadApp ( from sourceURL : URL , completionHandler : @ escaping ( Result < ALTApplication , Error > ) -> Void ) {
func finishOperation ( _ result : Result < URL , Error > ) {
do {
2019-06-21 11:20:03 -07:00
let fileURL = try result . get ( )
2023-03-01 00:48:36 -05:00
2019-06-21 11:20:03 -07:00
var isDirectory : ObjCBool = false
guard FileManager . default . fileExists ( atPath : fileURL . path , isDirectory : & isDirectory ) else { throw OperationError . appNotFound }
2023-03-01 00:48:36 -05:00
try FileManager . default . createDirectory ( at : temporaryDirectory , withIntermediateDirectories : true , attributes : nil )
2019-07-25 14:04:26 -07:00
let appBundleURL : URL
2023-03-01 00:48:36 -05:00
if isDirectory . boolValue {
2019-06-21 11:20:03 -07:00
// 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 }
2023-03-01 00:48:36 -05:00
appBundleURL = temporaryDirectory . appendingPathComponent ( fileURL . lastPathComponent )
2021-02-26 16:47:33 -06:00
try FileManager . default . copyItem ( at : fileURL , to : appBundleURL )
2023-03-01 00:48:36 -05:00
} else {
2019-06-21 11:20:03 -07:00
// 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 .
2023-03-01 00:48:36 -05:00
appBundleURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : temporaryDirectory )
2019-06-21 11:20:03 -07:00
}
2023-03-01 00:48:36 -05: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 ) )
2023-03-01 00:48:36 -05: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
}
2023-03-01 00:48:36 -05:00
if sourceURL . isFileURL {
2021-02-26 16:47:33 -06:00
finishOperation ( . success ( sourceURL ) )
2023-03-01 00:48:36 -05:00
progress . completedUnitCount += 3
} else {
let downloadTask = session . downloadTask ( with : sourceURL ) { fileURL , response , error in
do {
2019-06-21 11:20:03 -07:00
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
finishOperation ( . success ( fileURL ) )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
try ? FileManager . default . removeItem ( at : fileURL )
2023-03-01 00:48:36 -05:00
} catch {
2019-06-21 11:20:03 -07:00
finishOperation ( . failure ( error ) )
}
2019-06-10 15:03:47 -07:00
}
2023-03-01 00:48:36 -05:00
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
2023-03-01 00:48:36 -05:00
private extension DownloadAppOperation {
struct AltStorePlist : Decodable {
private enum CodingKeys : String , CodingKey {
2021-02-26 16:47:33 -06:00
case dependencies = " ALTDependencies "
}
var dependencies : [ Dependency ]
}
2023-03-01 00:48:36 -05:00
struct Dependency : Decodable {
2021-02-26 16:47:33 -06:00
var downloadURL : URL
var path : String ?
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
var preferredFilename : String {
2023-03-01 00:48:36 -05:00
let preferredFilename = path . map { ( $0 as NSString ) . lastPathComponent } ? ? downloadURL . lastPathComponent
2021-02-26 16:47:33 -06:00
return preferredFilename
}
2023-03-01 00:48:36 -05:00
init ( from decoder : Decoder ) throws {
enum CodingKeys : String , CodingKey {
2021-02-26 16:47:33 -06:00
case downloadURL
case path
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
let container = try decoder . container ( keyedBy : CodingKeys . self )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
let urlString = try container . decode ( String . self , forKey : . downloadURL )
let path = try container . decodeIfPresent ( String . self , forKey : . path )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
guard let downloadURL = URL ( string : urlString ) else {
throw DecodingError . dataCorruptedError ( forKey : . downloadURL , in : container , debugDescription : " downloadURL is not a valid URL. " )
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
self . downloadURL = downloadURL
self . path = path
}
}
2023-03-01 00:48:36 -05:00
func downloadDependencies ( for application : ALTApplication , completionHandler : @ escaping ( Result < Set < URL > , Error > ) -> Void ) {
2021-02-26 16:47:33 -06:00
guard FileManager . default . fileExists ( atPath : application . bundle . altstorePlistURL . path ) else {
return completionHandler ( . success ( [ ] ) )
}
2023-03-01 00:48:36 -05:00
do {
2021-02-26 16:47:33 -06:00
let data = try Data ( contentsOf : application . bundle . altstorePlistURL )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
let altstorePlist = try PropertyListDecoder ( ) . decode ( AltStorePlist . self , from : data )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
var dependencyURLs = Set < URL > ( )
var dependencyError : DependencyError ?
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
let dispatchGroup = DispatchGroup ( )
let progress = Progress ( totalUnitCount : Int64 ( altstorePlist . dependencies . count ) , parent : self . progress , pendingUnitCount : 1 )
2023-03-01 00:48:36 -05:00
for dependency in altstorePlist . dependencies {
2021-02-26 16:47:33 -06:00
dispatchGroup . enter ( )
2023-03-01 00:48:36 -05:00
download ( dependency , for : application , progress : progress ) { result in
switch result {
case let . failure ( error ) : dependencyError = error
case let . success ( fileURL ) : dependencyURLs . insert ( fileURL )
2021-02-26 16:47:33 -06:00
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
dispatchGroup . leave ( )
}
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
dispatchGroup . notify ( qos : . userInitiated , queue : . global ( ) ) {
2023-03-01 00:48:36 -05:00
if let dependencyError = dependencyError {
2021-02-26 16:47:33 -06:00
completionHandler ( . failure ( dependencyError ) )
2023-03-01 00:48:36 -05:00
} else {
2021-02-26 16:47:33 -06:00
completionHandler ( . success ( dependencyURLs ) )
}
}
2023-03-01 00:48:36 -05:00
} catch let error as DecodingError {
2021-02-26 16:47:33 -06:00
let nsError = ( error as NSError ) . withLocalizedFailure ( String ( format : NSLocalizedString ( " Could not download dependencies for %@. " , comment : " " ) , application . name ) )
completionHandler ( . failure ( nsError ) )
2023-03-01 00:48:36 -05:00
} catch {
2021-02-26 16:47:33 -06:00
completionHandler ( . failure ( error ) )
}
}
2023-03-01 00:48:36 -05:00
func download ( _ dependency : Dependency , for application : ALTApplication , progress : Progress , completionHandler : @ escaping ( Result < URL , DependencyError > ) -> Void ) {
let downloadTask = session . downloadTask ( with : dependency . downloadURL ) { fileURL , response , error in
do {
2021-02-26 16:47:33 -06:00
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
defer { try ? FileManager . default . removeItem ( at : fileURL ) }
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
let path = dependency . path ? ? dependency . preferredFilename
let destinationURL = application . fileURL . appendingPathComponent ( path )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
let directoryURL = destinationURL . deletingLastPathComponent ( )
2023-03-01 00:48:36 -05:00
if ! FileManager . default . fileExists ( atPath : directoryURL . path ) {
2021-02-26 16:47:33 -06:00
try FileManager . default . createDirectory ( at : directoryURL , withIntermediateDirectories : true )
}
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
try FileManager . default . copyItem ( at : fileURL , to : destinationURL , shouldReplace : true )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
completionHandler ( . success ( destinationURL ) )
2023-03-01 00:48:36 -05:00
} catch {
2021-02-26 16:47:33 -06:00
completionHandler ( . failure ( DependencyError ( dependency : dependency , error : error ) ) )
}
}
progress . addChild ( downloadTask . progress , withPendingUnitCount : 1 )
2023-03-01 00:48:36 -05:00
2021-02-26 16:47:33 -06:00
downloadTask . resume ( )
}
}