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