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 return 43 } 44 // even if update is nil (meaning we didn't load a new update), 45 // we want to persist the header data from remoteUpdateResponse 46 if let remoteUpdateResponse = strongSelf.remoteUpdateResponse, 47 let responseHeaderData = remoteUpdateResponse.responseHeaderData { 48 strongSelf.database.databaseQueue.async { 49 do { 50 // swiftlint:disable:next force_unwrapping 51 try strongSelf.database.setMetadata(withResponseHeaderData: responseHeaderData, scopeKey: strongSelf.config.scopeKey!) 52 successBlockArg(updateResponse) 53 } catch { 54 NSLog("Error persisting header data to disk: %@", error.localizedDescription) 55 errorBlockArg(error) 56 } 57 } 58 } else { 59 successBlockArg(updateResponse) 60 } 61 } 62 63 database.databaseQueue.async { 64 let embeddedUpdate = EmbeddedAppLoader.embeddedManifest(withConfig: self.config, database: self.database) 65 let extraHeaders = FileDownloader.extraHeadersForRemoteUpdateRequest( 66 withDatabase: self.database, 67 config: self.config, 68 launchedUpdate: self.launchedUpdate, 69 embeddedUpdate: embeddedUpdate 70 ) 71 self.downloader.downloadRemoteUpdate( 72 fromURL: url, 73 withDatabase: self.database, 74 extraHeaders: extraHeaders 75 ) { updateResponse in 76 self.remoteUpdateResponse = updateResponse 77 self.startLoading(fromUpdateResponse: updateResponse) 78 } errorBlock: { error in 79 self.errorBlock.let { it in 80 it(error) 81 } 82 } 83 } 84 } 85 86 override func downloadAsset(_ asset: UpdateAsset) { 87 let urlOnDisk = self.directory.appendingPathComponent(asset.filename) 88 89 FileDownloader.assetFilesQueue.async { 90 if FileManager.default.fileExists(atPath: urlOnDisk.path) { 91 // file already exists, we don't need to download it again 92 DispatchQueue.global().async { 93 self.handleAssetDownloadAlreadyExists(asset) 94 } 95 } else { 96 guard let assetUrl = asset.url else { 97 self.handleAssetDownload( 98 withError: NSError( 99 domain: RemoteAppLoader.ErrorDomain, 100 code: 1006, 101 userInfo: [ 102 NSLocalizedDescriptionKey: "Failed to download asset with no URL provided" 103 ] 104 ), 105 asset: asset 106 ) 107 return 108 } 109 110 self.downloader.downloadFile( 111 fromURL: assetUrl, 112 verifyingHash: asset.expectedHash, 113 toPath: urlOnDisk.path, 114 extraHeaders: asset.extraRequestHeaders ?? [:] 115 ) { data, response, _ in 116 DispatchQueue.global().async { 117 self.handleAssetDownload(withData: data, response: response, asset: asset) 118 } 119 } errorBlock: { error in 120 DispatchQueue.global().async { 121 self.handleAssetDownload(withError: error, asset: asset) 122 } 123 } 124 } 125 } 126 } 127 } 128