1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // swiftlint:disable closure_body_length 4 // swiftlint:disable function_parameter_count 5 6 import Foundation 7 8 /** 9 * Subclass of AppLoader which handles downloading updates from a remote server. 10 */ 11 public final class RemoteAppLoader: AppLoader { 12 private static let ErrorDomain = "EXUpdatesRemoteAppLoader" 13 14 private let downloader: FileDownloader 15 private var remoteUpdateResponse: UpdateResponse? 16 private let completionQueue: DispatchQueue 17 18 public required override init( 19 config: UpdatesConfig, 20 database: UpdatesDatabase, 21 directory: URL, 22 launchedUpdate: Update?, 23 completionQueue: DispatchQueue 24 ) { 25 self.downloader = FileDownloader(config: config) 26 self.completionQueue = completionQueue 27 super.init(config: config, database: database, directory: directory, launchedUpdate: launchedUpdate, completionQueue: completionQueue) 28 } 29 30 override public func loadUpdate( 31 fromURL url: URL, 32 onUpdateResponse updateResponseBlockArg: @escaping AppLoaderUpdateResponseBlock, 33 asset assetBlockArg: @escaping AppLoaderAssetBlock, 34 success successBlockArg: @escaping AppLoaderSuccessBlock, 35 error errorBlockArg: @escaping AppLoaderErrorBlock 36 ) { 37 self.updateResponseBlock = updateResponseBlockArg 38 self.assetBlock = assetBlockArg 39 self.errorBlock = errorBlockArg 40 41 self.successBlock = { [weak self] (updateResponse: UpdateResponse?) in 42 guard let strongSelf = self else { 43 successBlockArg(updateResponse) 44 return 45 } 46 // even if update is nil (meaning we didn't load a new update), 47 // we want to persist the header data from remoteUpdateResponse 48 if let remoteUpdateResponse = strongSelf.remoteUpdateResponse, 49 let responseHeaderData = remoteUpdateResponse.responseHeaderData { 50 strongSelf.database.databaseQueue.async { 51 do { 52 // swiftlint:disable:next force_unwrapping 53 try strongSelf.database.setMetadata(withResponseHeaderData: responseHeaderData, scopeKey: strongSelf.config.scopeKey!) 54 successBlockArg(updateResponse) 55 } catch { 56 NSLog("Error persisting header data to disk: %@", error.localizedDescription) 57 errorBlockArg(error) 58 } 59 } 60 } else { 61 successBlockArg(updateResponse) 62 } 63 } 64 65 database.databaseQueue.async { 66 let embeddedUpdate = EmbeddedAppLoader.embeddedManifest(withConfig: self.config, database: self.database) 67 let extraHeaders = FileDownloader.extraHeadersForRemoteUpdateRequest( 68 withDatabase: self.database, 69 config: self.config, 70 launchedUpdate: self.launchedUpdate, 71 embeddedUpdate: embeddedUpdate 72 ) 73 self.downloader.downloadRemoteUpdate( 74 fromURL: url, 75 withDatabase: self.database, 76 extraHeaders: extraHeaders 77 ) { updateResponse in 78 self.remoteUpdateResponse = updateResponse 79 self.startLoading(fromUpdateResponse: updateResponse) 80 } errorBlock: { error in 81 self.errorBlock.let { it in 82 it(error) 83 } 84 } 85 } 86 } 87 88 override public func downloadAsset(_ asset: UpdateAsset) { 89 let urlOnDisk = self.directory.appendingPathComponent(asset.filename) 90 91 FileDownloader.assetFilesQueue.async { 92 if FileManager.default.fileExists(atPath: urlOnDisk.path) { 93 // file already exists, we don't need to download it again 94 DispatchQueue.global().async { 95 self.handleAssetDownloadAlreadyExists(asset) 96 } 97 } else { 98 guard let assetUrl = asset.url else { 99 self.handleAssetDownload( 100 withError: NSError( 101 domain: RemoteAppLoader.ErrorDomain, 102 code: 1006, 103 userInfo: [ 104 NSLocalizedDescriptionKey: "Failed to download asset with no URL provided" 105 ] 106 ), 107 asset: asset 108 ) 109 return 110 } 111 112 self.downloader.downloadFile( 113 fromURL: assetUrl, 114 verifyingHash: asset.expectedHash, 115 toPath: urlOnDisk.path, 116 extraHeaders: asset.extraRequestHeaders ?? [:] 117 ) { data, response, _ in 118 DispatchQueue.global().async { 119 self.handleAssetDownload(withData: data, response: response, asset: asset) 120 } 121 } errorBlock: { error in 122 DispatchQueue.global().async { 123 self.handleAssetDownload(withError: error, asset: asset) 124 } 125 } 126 } 127 } 128 } 129 130 static func processSuccessLoaderResult( 131 config: UpdatesConfig, 132 database: UpdatesDatabase, 133 selectionPolicy: SelectionPolicy, 134 launchedUpdate: Update?, 135 directory: URL, 136 loaderTaskQueue: DispatchQueue, 137 updateResponse: UpdateResponse?, 138 priorError: Error?, 139 onComplete: @escaping (_ updateToLaunch: Update?, _ error: Error?, _ didRollBackToEmbedded: Bool) -> Void 140 ) { 141 let updateBeingLaunched = updateResponse?.manifestUpdateResponsePart?.updateManifest 142 143 if let rollBackDirective = updateResponse?.directiveUpdateResponsePart?.updateDirective as? RollBackToEmbeddedUpdateDirective { 144 self.processRollBackToEmbeddedDirective( 145 config: config, 146 database: database, 147 selectionPolicy: selectionPolicy, 148 launchedUpdate: launchedUpdate, 149 directory: directory, 150 loaderTaskQueue: loaderTaskQueue, 151 rollBackDirective: rollBackDirective, 152 manifestFilters: updateResponse?.responseHeaderData?.manifestFilters, 153 priorError: priorError, 154 onComplete: onComplete 155 ) 156 } else { 157 onComplete(updateBeingLaunched, priorError, false) 158 } 159 } 160 161 /** 162 * If directive is to roll-back to the embedded update and there is an embedded update, 163 * we need to update embedded update in the DB with the newer commitTime from the message so that 164 * the selection policy will choose it. That way future updates can continue to be applied 165 * over this roll back, but older ones won't. 166 * The embedded update is guaranteed to be in the DB from the earlier [EmbeddedAppLoader] call in this task. 167 */ 168 private static func processRollBackToEmbeddedDirective( 169 config: UpdatesConfig, 170 database: UpdatesDatabase, 171 selectionPolicy: SelectionPolicy, 172 launchedUpdate: Update?, 173 directory: URL, 174 loaderTaskQueue: DispatchQueue, 175 rollBackDirective: RollBackToEmbeddedUpdateDirective, 176 manifestFilters: [String: Any]?, 177 priorError: Error?, 178 onComplete: @escaping (_ updateToLaunch: Update?, _ error: Error?, _ didRollBackToEmbedded: Bool) -> Void 179 ) { 180 if !config.hasEmbeddedUpdate { 181 onComplete(nil, priorError, false) 182 return 183 } 184 185 guard let embeddedManifest = EmbeddedAppLoader.embeddedManifest(withConfig: config, database: database) else { 186 onComplete(nil, priorError, false) 187 return 188 } 189 190 if !selectionPolicy.shouldLoadRollBackToEmbeddedDirective( 191 rollBackDirective, 192 withEmbeddedUpdate: embeddedManifest, 193 launchedUpdate: launchedUpdate, 194 filters: manifestFilters 195 ) { 196 onComplete(nil, priorError, false) 197 return 198 } 199 200 // update the embedded update commit time in the in-memory embedded update since it is a singleton 201 embeddedManifest.commitTime = rollBackDirective.commitTime 202 203 EmbeddedAppLoader( 204 config: config, 205 database: database, 206 directory: directory, 207 launchedUpdate: nil, 208 completionQueue: loaderTaskQueue 209 ).loadUpdateResponseFromEmbeddedManifest( 210 withCallback: { _ in 211 return true 212 }, asset: { _, _, _, _ in 213 }, success: { updateResponse in 214 do { 215 let update = updateResponse?.manifestUpdateResponsePart?.updateManifest 216 // do this synchronously as it is needed to launch, and we're already on a background dispatch queue so no UI will be blocked 217 try database.databaseQueue.sync { 218 // swiftlint:disable force_unwrapping 219 try database.setUpdateCommitTime(rollBackDirective.commitTime, onUpdate: update!) 220 // swiftlint:enable force_unwrapping 221 } 222 onComplete(update, priorError, true) 223 } catch { 224 onComplete(nil, error, false) 225 } 226 }, error: { embeddedLoaderError in 227 onComplete(nil, embeddedLoaderError, false) 228 } 229 ) 230 } 231 } 232 233 // swiftlint:enable function_parameter_count 234