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-11-30 14:28:57 -06:00
import WebKit
import UniformTypeIdentifiers
2019-06-10 15:03:47 -07:00
2020-09-03 16:39:08 -07:00
import AltStoreCore
2019-06-10 15:03:47 -07:00
import AltSign
2023-11-30 14:28:57 -06:00
import Roxas
2019-06-10 15:03:47 -07:00
@objc ( DownloadAppOperation )
2023-01-04 09:52:12 -05:00
final class DownloadAppOperation : ResultOperation < ALTApplication >
2019-06-10 15:03:47 -07:00
{
2023-12-01 16:03:06 -06:00
@ Managed
private ( set ) var app : AppProtocol
2024-12-07 17:45:09 +05:30
2023-05-11 17:02:20 -05:00
let context : InstallAppOperationContext
2024-12-07 17:45:09 +05:30
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-12-07 17:45:09 +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-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
private var downloadPatreonAppContinuation : CheckedContinuation < URL , Error > ?
2024-12-07 17:45:09 +05:30
2023-05-11 17:02:20 -05:00
init ( app : AppProtocol , destinationURL : URL , context : InstallAppOperationContext )
2019-06-10 15:03:47 -07:00
{
self . app = app
2019-09-10 12:19:46 -07:00
self . context = context
2024-12-07 17:45:09 +05:30
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-12-07 17:45:09 +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
2023-10-18 14:06:10 -05:00
// S e t _ a f t e r _ c h e c k i n g s e l f . c o n t e x t . e r r o r t o p r e v e n t o v e r w r i t i n g l o c a l i z e d f a i l u r e f o r p r e v i o u s e r r o r s .
2024-08-06 10:43:52 +09:00
self . localizedFailure = String ( format : NSLocalizedString ( " %@ could not be downloaded. " , comment : " " ) , self . appName )
2024-12-07 17:45:09 +05:30
2023-12-01 16:03:06 -06:00
self . $ app . perform { app in
do
{
var appVersion : AppVersion ?
2024-12-07 17:45:09 +05:30
2023-12-01 16:03:06 -06:00
if let version = app as ? AppVersion
{
appVersion = version
}
else if let storeApp = app as ? StoreApp
{
guard let latestVersion = storeApp . latestAvailableVersion else {
let failureReason = String ( format : NSLocalizedString ( " The latest version of %@ could not be determined. " , comment : " " ) , self . appName )
throw OperationError . unknown ( failureReason : failureReason )
2024-12-07 17:45:09 +05:30
}
2023-12-01 16:03:06 -06:00
// A t t e m p t t o d o w n l o a d l a t e s t _ a v a i l a b l e _ v e r s i o n , a n d f a l l b a c k t o o l d e r v e r s i o n s i f n e c e s s a r y .
appVersion = latestVersion
}
2024-12-07 17:45:09 +05:30
2023-12-01 16:03:06 -06:00
if let appVersion
{
try self . verify ( appVersion )
}
2024-12-07 17:45:09 +05:30
2023-12-01 16:03:06 -06:00
self . download ( appVersion ? ? app )
2023-05-18 14:51:26 -05:00
}
catch let error as VerificationError where error . code = = . iOSVersionNotSupported
{
2024-12-07 17:45:09 +05:30
guard let presentingViewController = self . context . presentingViewController , let storeApp = app . storeApp , let latestSupportedVersion = storeApp . latestSupportedVersion ,
case let version = latestSupportedVersion . version ,
version != storeApp . installedApp ? . version
2023-05-18 14:51:26 -05:00
else { return self . finish ( . failure ( error ) ) }
2024-12-07 17:45:09 +05:30
2023-05-18 14:51:26 -05:00
if let installedApp = storeApp . installedApp
{
2025-02-08 04:45:22 +05:30
// g u a r d ! i n s t a l l e d A p p . m a t c h e s ( l a t e s t S u p p o r t e d V e r s i o n ) e l s e { r e t u r n s e l f . f i n i s h ( . f a i l u r e ( e r r o r ) ) }
guard installedApp . hasUpdate else { return self . finish ( . failure ( error ) ) }
2024-08-06 10:43:52 +09:00
}
2024-12-07 17:45:09 +05:30
2024-08-06 10:43:52 +09:00
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 : " " )
2023-05-18 14:51:26 -05:00
let localizedVersion = latestSupportedVersion . localizedVersion
2024-12-07 17:45:09 +05:30
2024-08-06 10:43:52 +09:00
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 ) )
} )
2023-05-18 14:51:26 -05:00
alertController . addAction ( UIAlertAction ( title : String ( format : NSLocalizedString ( " Download %@ %@ " , comment : " " ) , self . appName , localizedVersion ) , style : . default ) { _ in
2024-08-06 10:43:52 +09:00
self . download ( latestSupportedVersion )
} )
presentingViewController . present ( alertController , animated : true )
}
2024-12-07 17:45:09 +05:30
}
catch
{
2024-08-06 10:43:52 +09:00
self . finish ( . failure ( error ) )
}
}
}
2024-12-07 17:45:09 +05:30
override func finish ( _ result : Result < ALTApplication , Error > )
{
2024-12-13 16:30:24 +05:30
if ( FileManager . default . fileExists ( atPath : self . temporaryDirectory . path ) ) {
do
{
try FileManager . default . removeItem ( at : self . temporaryDirectory )
}
catch
{
print ( " Failed to remove DownloadAppOperation temporary directory: \( self . temporaryDirectory ) . " , error )
}
2024-08-06 10:43:52 +09:00
}
2024-12-07 17:45:09 +05:30
2024-08-06 10:43:52 +09:00
super . finish ( result )
}
}
2023-12-01 16:03:06 -06:00
private extension DownloadAppOperation
{
func verify ( _ version : AppVersion ) throws
{
if let minOSVersion = version . minOSVersion , ! ProcessInfo . processInfo . isOperatingSystemAtLeast ( minOSVersion )
{
throw VerificationError . iOSVersionNotSupported ( app : version , requiredOSVersion : minOSVersion )
2024-08-06 10:43:52 +09:00
}
2023-12-01 16:03:06 -06:00
else if let maxOSVersion = version . maxOSVersion , ProcessInfo . processInfo . operatingSystemVersion > maxOSVersion
{
throw VerificationError . iOSVersionNotSupported ( app : version , requiredOSVersion : maxOSVersion )
2024-08-06 10:43:52 +09:00
}
}
2023-05-11 17:02:20 -05:00
2024-12-13 20:13:07 +05:30
func printWithTid ( _ msg : String ) {
print ( " DownloadAppOperation: Thread: \( Thread . current . name ? ? Thread . current . description ) - " + msg )
}
2023-05-11 17:02:20 -05:00
func download ( @ Managed _ app : AppProtocol )
{
2024-12-13 16:30:24 +05:30
guard let sourceURL = self . sourceURL else {
return self . finish ( . failure ( OperationError . appNotFound ( name : self . appName ) ) )
}
if let appVersion = app as ? AppVersion
{
// A l l d o w n l o a d s g o t h r o u g h t h i s p a t h , a n d ` a p p ` i s
// a l w a y s a n A p p V e r s i o n i f d o w n l o a d i n g f r o m a s o u r c e ,
// s o c o n t e x t . a p p V e r s i o n ! = n i l m e a n s d o w n l o a d i n g f r o m s o u r c e .
self . context . appVersion = appVersion
}
downloadIPA ( from : sourceURL ) { result in
do
2021-02-26 16:47:33 -06:00
{
2024-12-13 16:30:24 +05:30
let application = try result . get ( )
if self . context . bundleIdentifier = = StoreApp . dolphinAppID , self . context . bundleIdentifier != application . bundleIdentifier
2021-02-26 16:47:33 -06:00
{
2024-12-13 16:30:24 +05:30
if var infoPlist = NSDictionary ( contentsOf : application . bundle . infoPlistURL ) as ? [ String : Any ]
2021-02-26 16:47:33 -06:00
{
2024-12-13 16:30:24 +05:30
// 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 )
2021-02-26 16:47:33 -06:00
}
}
2024-12-13 16:30:24 +05:30
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 ) )
}
2023-11-30 14:28:57 -06:00
}
2024-12-07 17:45:09 +05:30
}
2024-12-13 16:30:24 +05:30
catch
{
self . finish ( . failure ( error ) )
}
2024-12-07 17:45:09 +05:30
}
func downloadIPA ( from sourceURL : URL , completionHandler : @ escaping ( Result < ALTApplication , Error > ) -> Void )
{
Task < Void , Never > . detached ( priority : . userInitiated ) {
do
2023-11-30 14:28:57 -06:00
{
2024-12-07 17:45:09 +05:30
let fileURL : URL
if sourceURL . isFileURL
2023-11-30 14:28:57 -06:00
{
2024-12-07 17:45:09 +05:30
fileURL = sourceURL
self . progress . completedUnitCount += 3
}
else if let host = sourceURL . host , host . lowercased ( ) . hasSuffix ( " patreon.com " ) && sourceURL . path . lowercased ( ) = = " /file "
{
// P a t r e o n a p p
fileURL = try await downloadPatreonApp ( from : sourceURL )
2024-12-13 20:13:07 +05:30
self . printWithTid ( " downloadPatreonApp: completed at \( fileURL . path ) " )
2024-12-07 17:45:09 +05:30
}
else
{
// R e g u l a r a p p
fileURL = try await downloadFile ( from : sourceURL )
2024-12-13 20:13:07 +05:30
self . printWithTid ( " downloadFile: completed at \( fileURL . path ) " )
2023-11-30 14:28:57 -06:00
}
2019-06-21 11:20:03 -07:00
2024-12-07 17:45:09 +05:30
defer {
2024-12-13 16:30:24 +05:30
if ! sourceURL . isFileURL && FileManager . default . fileExists ( atPath : fileURL . path )
2024-12-07 17:45:09 +05:30
{
try ? FileManager . default . removeItem ( at : fileURL )
}
}
2023-05-11 17:02:20 -05:00
2024-12-07 17:45:09 +05:30
var isDirectory : ObjCBool = false
2024-12-13 20:13:07 +05:30
guard FileManager . default . fileExists ( atPath : fileURL . path , isDirectory : & isDirectory ) else {
throw OperationError . appNotFound ( name : self . appName )
}
2023-05-11 17:02:20 -05:00
2024-12-07 17:45:09 +05:30
try FileManager . default . createDirectory ( at : self . temporaryDirectory , withIntermediateDirectories : true , attributes : nil )
let appBundleURL : URL
if isDirectory . boolValue
2022-11-15 17:48:45 -06:00
{
2024-12-07 17:45:09 +05:30
// 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 }
appBundleURL = self . temporaryDirectory . appendingPathComponent ( fileURL . lastPathComponent )
try FileManager . default . copyItem ( at : fileURL , to : appBundleURL )
}
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 .
appBundleURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : self . temporaryDirectory )
// U s e c o n t e x t ' s t e m p o r a r y D i r e c t o r y t o e n s u r e . i p a i s n ' t d e l e t e d b e f o r e w e ' r e d o n e i n s t a l l i n g .
let ipaURL = self . context . temporaryDirectory . appendingPathComponent ( " App.ipa " )
try FileManager . default . copyItem ( at : fileURL , to : ipaURL )
self . context . ipaURL = ipaURL
2024-08-06 10:43:52 +09:00
}
2022-11-15 17:48:45 -06:00
2024-12-07 17:45:09 +05:30
guard let application = ALTApplication ( fileURL : appBundleURL ) else { throw OperationError . invalidApp }
2024-12-13 20:13:07 +05:30
// p e r f o r m c l e a n u p o f t h e t e m p f i l e s
if ( FileManager . default . fileExists ( atPath : fileURL . path ) ) {
self . printWithTid ( " Removing downloaded temp file at: \( fileURL . path ) " )
do {
try FileManager . default . removeItem ( at : fileURL )
} catch {
self . printWithTid ( " Removing downloaded temp error: \( error ) " )
}
}
2024-12-07 17:45:09 +05:30
completionHandler ( . success ( application ) )
2019-06-21 11:20:03 -07:00
}
catch
{
2024-12-07 17:45:09 +05:30
completionHandler ( . failure ( error ) )
2019-06-21 11:20:03 -07:00
}
2019-06-10 15:03:47 -07:00
}
2023-11-30 14:33:08 -06:00
}
2024-12-07 17:45:09 +05:30
func downloadFile ( from downloadURL : URL ) async throws -> URL
2023-11-30 14:28:57 -06:00
{
try await withCheckedThrowingContinuation { continuation in
2024-12-07 17:45:09 +05:30
let downloadTask = self . session . downloadTask ( with : downloadURL ) { ( fileURL , response , error ) in
2023-11-30 14:28:57 -06:00
do
{
2024-12-07 17:45:09 +05:30
if let response = response as ? HTTPURLResponse
{
guard response . statusCode != 403 else { throw URLError ( . noPermissionsToReadFile ) }
guard response . statusCode != 404 else { throw CocoaError ( . fileNoSuchFile , userInfo : [ NSURLErrorKey : downloadURL ] ) }
}
2023-11-30 14:28:57 -06:00
2024-12-07 17:45:09 +05:30
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
continuation . resume ( returning : fileURL )
2024-12-13 20:13:07 +05:30
// s e l f . p r i n t W i t h T i d ( " d o w n l o a d t a s k c o m p l e t e d : f i l e U R L : \ ( f i l e U R L ) U R L : \ ( d o w n l o a d U R L ) " )
2023-11-30 14:28:57 -06:00
}
catch
{
2024-12-13 20:13:07 +05:30
// s e l f . p r i n t W i t h T i d ( " d o w n l o a d t a s k E r r o r : \ ( e r r o r ) U R L : \ ( d o w n l o a d U R L ) " )
2023-11-30 14:28:57 -06:00
continuation . resume ( throwing : error )
}
}
2024-12-07 17:45:09 +05:30
self . progress . addChild ( downloadTask . progress , withPendingUnitCount : 3 )
downloadTask . resume ( )
2024-12-13 20:13:07 +05:30
self . printWithTid ( " download started: \( downloadURL ) " )
2024-12-07 17:45:09 +05:30
}
}
func downloadPatreonApp ( from patreonURL : URL ) async throws -> URL
{
guard ! UserDefaults . shared . skipPatreonDownloads else {
// S k i p a l l h a c k s , t a k e u s e r s t r a i g h t t o P a t r e o n p o s t .
return try await downloadFromPatreonPost ( )
2023-11-30 14:28:57 -06:00
}
do
{
2024-12-07 17:45:09 +05:30
// U s e r i s p l e d g e d t o t h i s a p p , a t t e m p t t o d o w n l o a d .
2023-11-30 14:28:57 -06:00
2024-12-07 17:45:09 +05:30
let fileURL = try await downloadFile ( from : patreonURL )
2023-11-30 14:28:57 -06:00
return fileURL
}
catch URLError . noPermissionsToReadFile
{
2024-12-07 17:45:09 +05:30
guard let presentingViewController = self . context . presentingViewController else { throw OperationError . pledgeRequired ( appName : self . appName ) }
2023-11-30 14:28:57 -06:00
2024-12-07 17:45:09 +05:30
// A t t e m p t t o s i g n - i n a g a i n i n c a s e o u r P a t r e o n s e s s i o n h a s e x p i r e d .
try await withCheckedThrowingContinuation { continuation in
PatreonAPI . shared . authenticate ( presentingViewController : presentingViewController ) { result in
do
{
let account = try result . get ( )
try account . managedObjectContext ? . save ( )
continuation . resume ( )
}
catch
{
continuation . resume ( throwing : error )
}
}
}
do
{
// S u c c e s s , s o t r y t o d o w n l o a d o n c e m o r e n o w t h a t w e ' r e d e f i n i t e l y a u t h e n t i c a t e d .
let fileURL = try await downloadFile ( from : patreonURL )
return fileURL
}
catch URLError . noPermissionsToReadFile
{
// W e k n o w a u t h e n t i c a t i o n s u c c e e d e d , s o f a i l u r e m u s t m e a n u s e r i s n ' t p a t r o n / o n t h e c o r r e c t t i e r ,
// o r t h a t o u r h a c k y w o r k a r o u n d f o r d o w n l o a d i n g P a t r e o n a t t a c h m e n t s h a s f a i l e d .
// E i t h e r w a y , t a k i n g t h e m d i r e c t l y t o t h e p o s t s e r v e s a s a d e c e n t f a l l b a c k .
return try await downloadFromPatreonPost ( )
}
}
func downloadFromPatreonPost ( ) async throws -> URL
{
guard let presentingViewController = self . context . presentingViewController else { throw OperationError . pledgeRequired ( appName : self . appName ) }
let downloadURL : URL
if let components = URLComponents ( url : patreonURL , resolvingAgainstBaseURL : false ) ,
let postItem = components . queryItems ? . first ( where : { $0 . name = = " h " } ) ,
let postID = postItem . value ,
let patreonPostURL = URL ( string : " https://www.patreon.com/posts/ " + postID )
{
downloadURL = patreonPostURL
}
else
{
downloadURL = patreonURL
}
return try await downloadFromPatreon ( downloadURL , presentingViewController : presentingViewController )
2023-11-30 14:28:57 -06:00
}
}
2024-12-07 17:45:09 +05:30
@ MainActor
func downloadFromPatreon ( _ patreonURL : URL , presentingViewController : UIViewController ) async throws -> URL
2023-11-30 14:28:57 -06:00
{
2024-12-07 17:45:09 +05:30
let webViewController = WebViewController ( url : patreonURL )
webViewController . delegate = self
webViewController . webView . navigationDelegate = self
let navigationController = UINavigationController ( rootViewController : webViewController )
presentingViewController . present ( navigationController , animated : true )
2023-11-30 14:28:57 -06:00
let downloadURL : URL
2024-12-07 17:45:09 +05:30
do
2023-11-30 14:28:57 -06:00
{
2024-12-07 17:45:09 +05:30
defer {
navigationController . dismiss ( animated : true )
}
downloadURL = try await withCheckedThrowingContinuation { continuation in
self . downloadPatreonAppContinuation = continuation
}
2023-11-30 14:28:57 -06:00
}
2024-12-07 17:45:09 +05:30
let fileURL = try await downloadFile ( from : downloadURL )
return fileURL
2023-11-30 14:28:57 -06:00
}
}
}
extension DownloadAppOperation : WebViewControllerDelegate
{
2024-12-07 17:45:09 +05:30
func webViewControllerDidFinish ( _ webViewController : WebViewController )
2023-11-30 14:28:57 -06:00
{
guard let continuation = self . downloadPatreonAppContinuation else { return }
self . downloadPatreonAppContinuation = nil
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
continuation . resume ( throwing : CancellationError ( ) )
}
}
extension DownloadAppOperation : WKNavigationDelegate
{
2024-12-07 17:45:09 +05:30
func webView ( _ webView : WKWebView , decidePolicyFor navigationAction : WKNavigationAction ) async -> WKNavigationActionPolicy
2023-11-30 14:28:57 -06:00
{
guard #available ( iOS 14.5 , * ) , navigationAction . shouldPerformDownload else { return . allow }
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
guard let continuation = self . downloadPatreonAppContinuation else { return . allow }
self . downloadPatreonAppContinuation = nil
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
if let downloadURL = navigationAction . request . url
{
continuation . resume ( returning : downloadURL )
}
else
{
continuation . resume ( throwing : URLError ( . badURL ) )
}
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
return . cancel
}
2024-12-07 17:45:09 +05:30
func webView ( _ webView : WKWebView , decidePolicyFor navigationResponse : WKNavigationResponse ) async -> WKNavigationResponsePolicy
2023-11-30 14:28:57 -06:00
{
// C a l l e d f o r P a t r e o n a t t a c h m e n t s
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
guard ! navigationResponse . canShowMIMEType else { return . allow }
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
guard let continuation = self . downloadPatreonAppContinuation else { return . allow }
self . downloadPatreonAppContinuation = nil
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
guard let response = navigationResponse . response as ? HTTPURLResponse , let responseURL = response . url ,
let mimeType = response . mimeType , let type = UTType ( mimeType : mimeType ) ,
type . conforms ( to : . ipa ) || type . conforms ( to : . zip ) || type . conforms ( to : . application )
else {
continuation . resume ( throwing : OperationError . invalidApp )
return . cancel
}
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
continuation . resume ( returning : responseURL )
2024-12-07 17:45:09 +05:30
2023-11-30 14:28:57 -06:00
return . cancel
}
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-12-07 17:45:09 +05:30
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-12-07 17:45:09 +05:30
let localizedFailure = String ( format : NSLocalizedString ( " The dependency '%@' could not be downloaded. " , comment : " " ) , dependency . preferredFilename )
2024-08-06 10:43:52 +09:00
completionHandler ( . failure ( error . withLocalizedFailure ( localizedFailure ) ) )
2021-02-26 16:47:33 -06:00
}
}
progress . addChild ( downloadTask . progress , withPendingUnitCount : 1 )
downloadTask . resume ( )
}
}