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