1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // swiftlint:disable closure_body_length 4 5 import Foundation 6 7 /** 8 * Subclass of AppLoader which handles downloading updates from a remote server. 9 */ 10 internal final class RemoteAppLoader: AppLoader { 11 private static let ErrorDomain = "EXUpdatesRemoteAppLoader" 12 13 private let downloader: FileDownloader 14 private var remoteUpdateResponse: UpdateResponse? 15 private let completionQueue: DispatchQueue 16 17 required init( 18 config: UpdatesConfig, 19 database: UpdatesDatabase, 20 directory: URL, 21 launchedUpdate: Update?, 22 completionQueue: DispatchQueue 23 ) { 24 self.downloader = FileDownloader(config: config) 25 self.completionQueue = completionQueue 26 super.init(config: config, database: database, directory: directory, launchedUpdate: launchedUpdate, completionQueue: completionQueue) 27 } 28 29 override func loadUpdate( 30 fromURL url: URL, 31 onUpdateResponse updateResponseBlockArg: @escaping AppLoaderUpdateResponseBlock, 32 asset assetBlockArg: @escaping AppLoaderAssetBlock, 33 success successBlockArg: @escaping AppLoaderSuccessBlock, 34 error errorBlockArg: @escaping AppLoaderErrorBlock 35 ) { 36 self.updateResponseBlock = updateResponseBlockArg 37 self.assetBlock = assetBlockArg 38 self.errorBlock = errorBlockArg 39 40 self.successBlock = { [weak self] (updateResponse: UpdateResponse?) in 41 guard let strongSelf = self else { 42 successBlockArg(updateResponse) 43 return 44 } 45 // even if update is nil (meaning we didn't load a new update), 46 // we want to persist the header data from remoteUpdateResponse 47 if let remoteUpdateResponse = strongSelf.remoteUpdateResponse, 48 let responseHeaderData = remoteUpdateResponse.responseHeaderData { 49 strongSelf.database.databaseQueue.async { 50 do { 51 // swiftlint:disable:next force_unwrapping 52 try strongSelf.database.setMetadata(withResponseHeaderData: responseHeaderData, scopeKey: strongSelf.config.scopeKey!) 53 successBlockArg(updateResponse) 54 } catch { 55 NSLog("Error persisting header data to disk: %@", error.localizedDescription) 56 errorBlockArg(error) 57 } 58 } 59 } else { 60 successBlockArg(updateResponse) 61 } 62 } 63 64 database.databaseQueue.async { 65 let embeddedUpdate = EmbeddedAppLoader.embeddedManifest(withConfig: self.config, database: self.database) 66 let extraHeaders = FileDownloader.extraHeadersForRemoteUpdateRequest( 67 withDatabase: self.database, 68 config: self.config, 69 launchedUpdate: self.launchedUpdate, 70 embeddedUpdate: embeddedUpdate 71 ) 72 self.downloader.downloadRemoteUpdate( 73 fromURL: url, 74 withDatabase: self.database, 75 extraHeaders: extraHeaders 76 ) { updateResponse in 77 self.remoteUpdateResponse = updateResponse 78 self.startLoading(fromUpdateResponse: updateResponse) 79 } errorBlock: { error in 80 self.errorBlock.let { it in 81 it(error) 82 } 83 } 84 } 85 } 86 87 override func downloadAsset(_ asset: UpdateAsset) { 88 let urlOnDisk = self.directory.appendingPathComponent(asset.filename) 89 90 FileDownloader.assetFilesQueue.async { 91 if FileManager.default.fileExists(atPath: urlOnDisk.path) { 92 // file already exists, we don't need to download it again 93 DispatchQueue.global().async { 94 self.handleAssetDownloadAlreadyExists(asset) 95 } 96 } else { 97 guard let assetUrl = asset.url else { 98 self.handleAssetDownload( 99 withError: NSError( 100 domain: RemoteAppLoader.ErrorDomain, 101 code: 1006, 102 userInfo: [ 103 NSLocalizedDescriptionKey: "Failed to download asset with no URL provided" 104 ] 105 ), 106 asset: asset 107 ) 108 return 109 } 110 111 self.downloader.downloadFile( 112 fromURL: assetUrl, 113 verifyingHash: asset.expectedHash, 114 toPath: urlOnDisk.path, 115 extraHeaders: asset.extraRequestHeaders ?? [:] 116 ) { data, response, _ in 117 DispatchQueue.global().async { 118 self.handleAssetDownload(withData: data, response: response, asset: asset) 119 } 120 } errorBlock: { error in 121 DispatchQueue.global().async { 122 self.handleAssetDownload(withError: error, asset: asset) 123 } 124 } 125 } 126 } 127 } 128 } 129