@capawesome/capacitor-live-update 6.7.1 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapawesomeCapacitorLiveUpdate.podspec +2 -2
- package/Package.swift +32 -0
- package/README.md +91 -81
- package/android/build.gradle +9 -9
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +165 -132
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +6 -27
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +7 -2
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +50 -78
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +12 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +9 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +32 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +1 -10
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +13 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
- package/dist/docs.json +191 -158
- package/dist/esm/definitions.d.ts +107 -52
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +1 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +1 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +1 -7
- package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -1
- package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
- package/ios/Plugin/LiveUpdate.swift +167 -148
- package/ios/Plugin/LiveUpdateConfig.swift +2 -4
- package/ios/Plugin/LiveUpdateHttpClient.swift +6 -2
- package/ios/Plugin/LiveUpdatePlugin.swift +44 -38
- package/ios/Plugin/LiveUpdatePreferences.swift +9 -5
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -0
- package/package.json +17 -18
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetBundleOptions.java +0 -18
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBundleResult.java +0 -24
- package/ios/Plugin/Classes/Options/SetBundleOptions.swift +0 -14
- package/ios/Plugin/Classes/Results/GetBundleResult.swift +0 -16
- package/ios/Plugin/LiveUpdatePlugin.h +0 -10
- package/ios/Plugin/LiveUpdatePlugin.m +0 -27
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CryptoKit
|
|
3
|
+
#if canImport(ZipArchive)
|
|
4
|
+
import ZipArchive
|
|
5
|
+
#else
|
|
3
6
|
import SSZipArchive
|
|
7
|
+
#endif
|
|
4
8
|
import Capacitor
|
|
5
9
|
import Alamofire
|
|
6
10
|
import CommonCrypto
|
|
@@ -12,7 +16,6 @@ import CommonCrypto
|
|
|
12
16
|
private let config: LiveUpdateConfig
|
|
13
17
|
private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
|
|
14
18
|
private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
|
|
15
|
-
private let host: String
|
|
16
19
|
private let httpClient: LiveUpdateHttpClient
|
|
17
20
|
private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
|
18
21
|
private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
|
|
@@ -20,40 +23,31 @@ import CommonCrypto
|
|
|
20
23
|
private let preferences: LiveUpdatePreferences
|
|
21
24
|
|
|
22
25
|
private var rollbackDispatchWorkItem: DispatchWorkItem?
|
|
26
|
+
private var rollbackPerformed = false
|
|
23
27
|
|
|
24
28
|
init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
|
|
25
29
|
self.config = config
|
|
26
|
-
if (config.location != nil) && config.location == "eu" {
|
|
27
|
-
self.host = "api.cloud.capawesome.eu"
|
|
28
|
-
} else {
|
|
29
|
-
self.host = "api.cloud.capawesome.io"
|
|
30
|
-
}
|
|
31
30
|
self.httpClient = LiveUpdateHttpClient(config: config)
|
|
32
31
|
self.plugin = plugin
|
|
33
32
|
self.preferences = LiveUpdatePreferences()
|
|
34
33
|
super.init()
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} else {
|
|
40
|
-
startRollbackTimer()
|
|
41
|
-
}
|
|
42
|
-
saveCurrentBundleVersion()
|
|
43
|
-
}
|
|
35
|
+
// Start the rollback timer to rollback to the default bundle
|
|
36
|
+
// if the app is not ready after a certain time
|
|
37
|
+
startRollbackTimer()
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
@objc public func deleteBundle(_ options: DeleteBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
47
41
|
let bundleId = options.getBundleId()
|
|
48
42
|
|
|
49
|
-
if !
|
|
43
|
+
if !hasBundleById(bundleId) {
|
|
50
44
|
let error = CustomError.bundleNotFound
|
|
51
45
|
completion(error)
|
|
52
46
|
return
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
do {
|
|
56
|
-
try
|
|
50
|
+
try deleteBundleById(bundleId)
|
|
57
51
|
completion(nil)
|
|
58
52
|
} catch {
|
|
59
53
|
completion(error)
|
|
@@ -63,11 +57,10 @@ import CommonCrypto
|
|
|
63
57
|
@objc public func downloadBundle(_ options: DownloadBundleOptions) async throws {
|
|
64
58
|
let artifactType = options.getArtifactType()
|
|
65
59
|
let bundleId = options.getBundleId()
|
|
66
|
-
let checksum = options.getChecksum()
|
|
67
60
|
let url = options.getUrl()
|
|
68
61
|
|
|
69
62
|
// Check if the bundle already exists
|
|
70
|
-
if
|
|
63
|
+
if hasBundleById(bundleId) {
|
|
71
64
|
throw CustomError.bundleAlreadyExists
|
|
72
65
|
}
|
|
73
66
|
|
|
@@ -75,22 +68,13 @@ import CommonCrypto
|
|
|
75
68
|
if artifactType == .manifest {
|
|
76
69
|
try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
|
|
77
70
|
} else {
|
|
78
|
-
try await downloadBundleOfTypeZip(bundleId: bundleId, url: url
|
|
71
|
+
try await downloadBundleOfTypeZip(bundleId: bundleId, url: url)
|
|
79
72
|
}
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
@objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
|
|
83
76
|
let response: GetLatestBundleResponse? = try await self.fetchLatestBundle(options)
|
|
84
|
-
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, downloadUrl: response?.url)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@objc public func getBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
88
|
-
var bundleId = getCurrentBundleId()
|
|
89
|
-
if bundleId == defaultWebAssetDir {
|
|
90
|
-
bundleId = nil
|
|
91
|
-
}
|
|
92
|
-
let result = GetBundleResult(bundleId: bundleId)
|
|
93
|
-
completion(result, nil)
|
|
77
|
+
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, customProperties: response?.customProperties, downloadUrl: response?.url)
|
|
94
78
|
}
|
|
95
79
|
|
|
96
80
|
@objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
@@ -106,10 +90,7 @@ import CommonCrypto
|
|
|
106
90
|
}
|
|
107
91
|
|
|
108
92
|
@objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
109
|
-
|
|
110
|
-
if bundleId == defaultWebAssetDir {
|
|
111
|
-
bundleId = nil
|
|
112
|
-
}
|
|
93
|
+
let bundleId = getCurrentBundleId()
|
|
113
94
|
let result = GetCurrentBundleResult(bundleId: bundleId)
|
|
114
95
|
completion(result, nil)
|
|
115
96
|
}
|
|
@@ -128,10 +109,7 @@ import CommonCrypto
|
|
|
128
109
|
}
|
|
129
110
|
|
|
130
111
|
@objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
131
|
-
|
|
132
|
-
if bundleId == defaultWebAssetDir {
|
|
133
|
-
bundleId = nil
|
|
134
|
-
}
|
|
112
|
+
let bundleId: String? = getNextBundleId()
|
|
135
113
|
let result = GetNextBundleResult(bundleId: bundleId)
|
|
136
114
|
completion(result, nil)
|
|
137
115
|
}
|
|
@@ -148,12 +126,27 @@ import CommonCrypto
|
|
|
148
126
|
completion(result, nil)
|
|
149
127
|
}
|
|
150
128
|
|
|
151
|
-
@objc public func ready() {
|
|
129
|
+
@objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
|
|
152
130
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
|
|
131
|
+
if config.readyTimeout <= 0 {
|
|
132
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
|
|
133
|
+
}
|
|
134
|
+
// Stop the rollback timer
|
|
153
135
|
stopRollbackTimer()
|
|
136
|
+
// Delete unused bundles
|
|
154
137
|
if config.autoDeleteBundles {
|
|
155
138
|
deleteUnusedBundles()
|
|
156
139
|
}
|
|
140
|
+
// Get the current and previous bundle IDs
|
|
141
|
+
let currentBundleId = getCurrentBundleId()
|
|
142
|
+
let previousBundleId = getPreviousBundleId()
|
|
143
|
+
// Return the result
|
|
144
|
+
let result = ReadyResult(currentBundleId: currentBundleId, previousBundleId: previousBundleId, rollback: rollbackPerformed)
|
|
145
|
+
completion(result, nil)
|
|
146
|
+
// Set the new previous bundle ID
|
|
147
|
+
setPreviousBundleId(bundleId: currentBundleId)
|
|
148
|
+
// Reset the rollback flag
|
|
149
|
+
rollbackPerformed = false
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
@objc public func reload() {
|
|
@@ -163,21 +156,7 @@ import CommonCrypto
|
|
|
163
156
|
}
|
|
164
157
|
|
|
165
158
|
@objc public func reset() {
|
|
166
|
-
self.
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@objc public func setBundle(_ options: SetBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
170
|
-
let bundleId = options.getBundleId()
|
|
171
|
-
|
|
172
|
-
// Check if the bundle already exists
|
|
173
|
-
if !hasBundle(bundleId: bundleId) {
|
|
174
|
-
let error = CustomError.bundleNotFound
|
|
175
|
-
completion(error)
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
setNextBundle(bundleId: bundleId)
|
|
180
|
-
completion(nil)
|
|
159
|
+
self.setNextBundleById(nil)
|
|
181
160
|
}
|
|
182
161
|
|
|
183
162
|
@objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
|
|
@@ -198,8 +177,8 @@ import CommonCrypto
|
|
|
198
177
|
let bundleId = options.getBundleId()
|
|
199
178
|
|
|
200
179
|
if let bundleId = bundleId {
|
|
201
|
-
if
|
|
202
|
-
|
|
180
|
+
if hasBundleById(bundleId) {
|
|
181
|
+
setNextBundleById(bundleId)
|
|
203
182
|
} else {
|
|
204
183
|
let error = CustomError.bundleNotFound
|
|
205
184
|
completion(error)
|
|
@@ -224,12 +203,12 @@ import CommonCrypto
|
|
|
224
203
|
let latestBundleId = response.bundleId
|
|
225
204
|
let downloadUrl = response.url
|
|
226
205
|
// Check if bundle already exists
|
|
227
|
-
if
|
|
206
|
+
if hasBundleById(latestBundleId) {
|
|
228
207
|
var nextBundleId: String?
|
|
229
208
|
let currentBundleId = self.getCurrentBundleId()
|
|
230
209
|
if latestBundleId != currentBundleId {
|
|
231
210
|
// Set the next bundle
|
|
232
|
-
|
|
211
|
+
setNextBundleById(latestBundleId)
|
|
233
212
|
nextBundleId = latestBundleId
|
|
234
213
|
}
|
|
235
214
|
return SyncResult(nextBundleId: nextBundleId)
|
|
@@ -238,10 +217,10 @@ import CommonCrypto
|
|
|
238
217
|
if artifactType == .manifest {
|
|
239
218
|
try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
|
|
240
219
|
} else {
|
|
241
|
-
try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl
|
|
220
|
+
try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl)
|
|
242
221
|
}
|
|
243
222
|
// Set the next bundle
|
|
244
|
-
|
|
223
|
+
setNextBundleById(latestBundleId)
|
|
245
224
|
return SyncResult(nextBundleId: latestBundleId)
|
|
246
225
|
}
|
|
247
226
|
|
|
@@ -270,12 +249,12 @@ import CommonCrypto
|
|
|
270
249
|
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
271
250
|
}
|
|
272
251
|
|
|
273
|
-
private func buildCapacitorServerPathFor(bundleId: String) -> String {
|
|
252
|
+
private func buildCapacitorServerPathFor(bundleId: String?) -> String {
|
|
274
253
|
let path: String
|
|
275
|
-
if bundleId
|
|
276
|
-
path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
|
|
277
|
-
} else {
|
|
254
|
+
if let bundleId = bundleId {
|
|
278
255
|
path = buildBundlePathFor(bundleId: bundleId)
|
|
256
|
+
} else {
|
|
257
|
+
path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
|
|
279
258
|
}
|
|
280
259
|
return path
|
|
281
260
|
}
|
|
@@ -289,31 +268,28 @@ import CommonCrypto
|
|
|
289
268
|
return url
|
|
290
269
|
}
|
|
291
270
|
|
|
292
|
-
private func copyCurrentBundleFile(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
let destination = toDirectory.appendingPathComponent(href)
|
|
271
|
+
private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
|
|
272
|
+
let currentBundleId = getCurrentBundleId()
|
|
273
|
+
let destination = toDirectory.appendingPathComponent(fileToCopy.href)
|
|
298
274
|
let parentDirectory = destination.deletingLastPathComponent()
|
|
299
275
|
try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
300
276
|
|
|
301
277
|
let sourceURL: URL
|
|
302
|
-
if currentBundleId
|
|
303
|
-
|
|
278
|
+
if let currentBundleId = currentBundleId {
|
|
279
|
+
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(fileToCopy.href)
|
|
280
|
+
} else {
|
|
281
|
+
guard let file = Bundle.main.url(forResource: fileToCopy.href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
|
|
304
282
|
return
|
|
305
283
|
}
|
|
306
284
|
sourceURL = file
|
|
307
|
-
} else {
|
|
308
|
-
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
|
|
309
285
|
}
|
|
310
286
|
|
|
311
287
|
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
|
312
288
|
}
|
|
313
289
|
|
|
314
|
-
private func copyCurrentBundleFiles(
|
|
315
|
-
for
|
|
316
|
-
try copyCurrentBundleFile(
|
|
290
|
+
private func copyCurrentBundleFiles(filesToCopy: [ManifestItem], toDirectory: URL) throws {
|
|
291
|
+
for fileToCopy in filesToCopy {
|
|
292
|
+
try copyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
|
|
317
293
|
}
|
|
318
294
|
}
|
|
319
295
|
|
|
@@ -335,14 +311,14 @@ import CommonCrypto
|
|
|
335
311
|
return temporaryDirectory
|
|
336
312
|
}
|
|
337
313
|
|
|
338
|
-
private func
|
|
314
|
+
private func deleteBundleById(_ bundleId: String) throws {
|
|
339
315
|
// Delete the bundle directory
|
|
340
316
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
341
317
|
try FileManager.default.removeItem(atPath: path)
|
|
342
318
|
// Reset the next bundle if it is the deleted bundle
|
|
343
319
|
let nextBundleId = getNextBundleId()
|
|
344
320
|
if bundleId == nextBundleId {
|
|
345
|
-
|
|
321
|
+
setNextBundleById(nil)
|
|
346
322
|
}
|
|
347
323
|
}
|
|
348
324
|
|
|
@@ -354,7 +330,7 @@ import CommonCrypto
|
|
|
354
330
|
for bundleId in bundleIds {
|
|
355
331
|
if bundleId != currentBundleId && bundleId != nextBundleId {
|
|
356
332
|
do {
|
|
357
|
-
try
|
|
333
|
+
try deleteBundleById(bundleId)
|
|
358
334
|
} catch {
|
|
359
335
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
|
|
360
336
|
}
|
|
@@ -362,12 +338,12 @@ import CommonCrypto
|
|
|
362
338
|
}
|
|
363
339
|
}
|
|
364
340
|
|
|
365
|
-
private func downloadAndVerifyFile(url: String, file: URL,
|
|
341
|
+
private func downloadAndVerifyFile(url: String, file: URL, callback: ((Progress) -> Void)?) async throws {
|
|
366
342
|
let destination: DownloadRequest.Destination = { _, _ in
|
|
367
343
|
return (file, [.createIntermediateDirectories])
|
|
368
344
|
}
|
|
369
345
|
let urlComponents = URLComponents(string: url)!
|
|
370
|
-
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination)
|
|
346
|
+
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
|
|
371
347
|
if let error = result.error {
|
|
372
348
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
|
|
373
349
|
if let urlError = error.underlyingError as? URLError {
|
|
@@ -380,31 +356,59 @@ import CommonCrypto
|
|
|
380
356
|
guard let response = result.response else {
|
|
381
357
|
throw CustomError.unknown
|
|
382
358
|
}
|
|
383
|
-
let checksum =
|
|
359
|
+
let checksum = LiveUpdateHttpClient.getChecksumFromResponse(response: response)
|
|
384
360
|
let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
|
|
385
361
|
try verifyFile(url: file, checksum: checksum, signature: signature)
|
|
386
362
|
}
|
|
387
363
|
|
|
388
|
-
private func downloadBundleFile(baseUrl: String, href: String, directory: URL) async throws -> URL {
|
|
364
|
+
private func downloadBundleFile(baseUrl: String, href: String, directory: URL, callback: ((Progress) -> Void)?) async throws -> URL {
|
|
389
365
|
let fileURL = directory.appendingPathComponent(href)
|
|
390
366
|
var parameters = [String: String]()
|
|
391
367
|
parameters["href"] = href
|
|
392
368
|
var urlComponents = URLComponents(string: baseUrl)!
|
|
393
369
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
394
370
|
let url = urlComponents.string!
|
|
395
|
-
try await self.downloadAndVerifyFile(url: url, file: fileURL,
|
|
371
|
+
try await self.downloadAndVerifyFile(url: url, file: fileURL, callback: callback)
|
|
396
372
|
return fileURL
|
|
397
373
|
}
|
|
398
374
|
|
|
399
|
-
private func downloadBundleFiles(url: String,
|
|
375
|
+
private func downloadBundleFiles(url: String, filesToDownload: [ManifestItem], directory: URL, callback: ((Progress) -> Void)?) async throws {
|
|
376
|
+
let totalBytesToDownload = Int64(filesToDownload.map { $0.sizeInBytes }.reduce(0, +))
|
|
377
|
+
actor TotalBytesDownloaded {
|
|
378
|
+
var value: Int64 = 0
|
|
379
|
+
func add(_ amount: Int64) {
|
|
380
|
+
value += amount
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
let totalBytesDownloaded = TotalBytesDownloaded()
|
|
400
384
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
401
|
-
for
|
|
385
|
+
for fileToDownload in filesToDownload {
|
|
402
386
|
group.addTask {
|
|
403
|
-
_ = try await self.downloadBundleFile(baseUrl: url, href: href, directory: directory
|
|
387
|
+
_ = try await self.downloadBundleFile(baseUrl: url, href: fileToDownload.href, directory: directory, callback: { progress in
|
|
388
|
+
Task {
|
|
389
|
+
// Progress is only reported if the file is downloaded in chunks
|
|
390
|
+
if let callback = callback {
|
|
391
|
+
let totalBytesDownloaded = await totalBytesDownloaded.value
|
|
392
|
+
let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: progress.completedUnitCount + totalBytesDownloaded)
|
|
393
|
+
callback(totalProgress)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
// Emit the current progress in case the file was not downloaded in chunks
|
|
398
|
+
if let callback = callback {
|
|
399
|
+
await totalBytesDownloaded.add(Int64(fileToDownload.sizeInBytes))
|
|
400
|
+
let totalBytesDownloaded = await totalBytesDownloaded.value
|
|
401
|
+
let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesDownloaded)
|
|
402
|
+
callback(totalProgress)
|
|
403
|
+
}
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
|
-
|
|
407
406
|
try await group.waitForAll()
|
|
407
|
+
// Call the callback one last time to make sure the progress is at 100%
|
|
408
|
+
if let callback = callback {
|
|
409
|
+
let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesToDownload)
|
|
410
|
+
callback(totalProgress)
|
|
411
|
+
}
|
|
408
412
|
}
|
|
409
413
|
}
|
|
410
414
|
|
|
@@ -412,7 +416,7 @@ import CommonCrypto
|
|
|
412
416
|
// Create a temporary directory
|
|
413
417
|
let temporaryDirectory = try createTemporaryDirectory()
|
|
414
418
|
// Download the latest manifest
|
|
415
|
-
let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory)
|
|
419
|
+
let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
|
|
416
420
|
let latestManifest = try loadManifest(file: latestManifestFile)
|
|
417
421
|
// Load the current manifest
|
|
418
422
|
let currentManifest = try loadCurrentManifest()
|
|
@@ -426,18 +430,24 @@ import CommonCrypto
|
|
|
426
430
|
itemsToDownload.append(contentsOf: latestManifest.items)
|
|
427
431
|
}
|
|
428
432
|
// Copy the files
|
|
429
|
-
try self.copyCurrentBundleFiles(
|
|
433
|
+
try self.copyCurrentBundleFiles(filesToCopy: itemsToCopy, toDirectory: temporaryDirectory)
|
|
430
434
|
// Download the files
|
|
431
|
-
try await self.downloadBundleFiles(url: url,
|
|
435
|
+
try await self.downloadBundleFiles(url: url, filesToDownload: itemsToDownload, directory: temporaryDirectory, callback: { progress in
|
|
436
|
+
let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
|
|
437
|
+
self.notifyDownloadBundleProgressListeners(event)
|
|
438
|
+
})
|
|
432
439
|
// Add the bundle
|
|
433
440
|
try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
|
|
434
441
|
}
|
|
435
442
|
|
|
436
|
-
private func downloadBundleOfTypeZip(bundleId: String, url: String
|
|
443
|
+
private func downloadBundleOfTypeZip(bundleId: String, url: String) async throws {
|
|
437
444
|
let timestamp = String(Int(Date().timeIntervalSince1970))
|
|
438
445
|
let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
|
|
439
446
|
// Download the bundle
|
|
440
|
-
try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl,
|
|
447
|
+
try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, callback: { progress in
|
|
448
|
+
let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
|
|
449
|
+
self.notifyDownloadBundleProgressListeners(event)
|
|
450
|
+
})
|
|
441
451
|
// Add the bundle
|
|
442
452
|
try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
|
|
443
453
|
}
|
|
@@ -454,7 +464,7 @@ import CommonCrypto
|
|
|
454
464
|
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
455
465
|
parameters["platform"] = "1"
|
|
456
466
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
457
|
-
var urlComponents = URLComponents(string: "https://\(
|
|
467
|
+
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(config.appId ?? "")/bundles/latest")!
|
|
458
468
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
459
469
|
let url = try urlComponents.asURL()
|
|
460
470
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
|
|
@@ -516,12 +526,16 @@ import CommonCrypto
|
|
|
516
526
|
return digest.map { String(format: "%02hhx", $0) }.joined()
|
|
517
527
|
}
|
|
518
528
|
|
|
519
|
-
/// - Returns: The current bundle ID
|
|
529
|
+
/// - Returns: The current bundle ID or `nil` if no view controller was found (should never happen) or the default bundle is in use.
|
|
520
530
|
private func getCurrentBundleId() -> String? {
|
|
521
531
|
guard let path = getCurrentCapacitorServerPath() else {
|
|
522
532
|
return nil
|
|
523
533
|
}
|
|
524
|
-
|
|
534
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
535
|
+
if bundleId == defaultWebAssetDir {
|
|
536
|
+
return nil
|
|
537
|
+
}
|
|
538
|
+
return bundleId
|
|
525
539
|
}
|
|
526
540
|
|
|
527
541
|
/// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
|
|
@@ -537,21 +551,30 @@ import CommonCrypto
|
|
|
537
551
|
return deviceId.lowercased()
|
|
538
552
|
}
|
|
539
553
|
|
|
540
|
-
/// - Returns: The next bundle ID
|
|
541
|
-
private func getNextBundleId() -> String {
|
|
554
|
+
/// - Returns: The next bundle ID or `nil` if the default bundle will be used.
|
|
555
|
+
private func getNextBundleId() -> String? {
|
|
542
556
|
let path = getNextCapacitorServerPath()
|
|
543
|
-
|
|
557
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
558
|
+
if bundleId == defaultWebAssetDir {
|
|
559
|
+
return nil
|
|
560
|
+
}
|
|
561
|
+
return bundleId
|
|
544
562
|
}
|
|
545
563
|
|
|
546
564
|
/// - Returns: The absolute path to the next bundle directory.
|
|
547
565
|
private func getNextCapacitorServerPath() -> String {
|
|
548
|
-
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId:
|
|
566
|
+
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
|
|
549
567
|
if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
|
|
550
568
|
return path.isEmpty ? defaultCapacitorServerPath : path
|
|
551
569
|
}
|
|
552
570
|
return defaultCapacitorServerPath
|
|
553
571
|
}
|
|
554
572
|
|
|
573
|
+
/// - Returns: The previous bundle ID or `nil` if the default bundle was used.
|
|
574
|
+
private func getPreviousBundleId() -> String? {
|
|
575
|
+
return preferences.getPreviousBundleId()
|
|
576
|
+
}
|
|
577
|
+
|
|
555
578
|
private func getVersionCode() -> String {
|
|
556
579
|
guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
557
580
|
return ""
|
|
@@ -566,27 +589,24 @@ import CommonCrypto
|
|
|
566
589
|
return appVersionName
|
|
567
590
|
}
|
|
568
591
|
|
|
569
|
-
private func
|
|
592
|
+
private func hasBundleById(_ bundleId: String) -> Bool {
|
|
570
593
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
571
594
|
return FileManager.default.fileExists(atPath: path)
|
|
572
595
|
}
|
|
573
596
|
|
|
574
597
|
private func loadCurrentManifest() throws -> Manifest? {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
580
|
-
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
581
|
-
if let manifestFileUrl = manifestFileUrl {
|
|
598
|
+
if let currentBundleId = getCurrentBundleId() {
|
|
599
|
+
let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
|
|
600
|
+
let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
|
|
601
|
+
if manifestFileExists {
|
|
582
602
|
return try loadManifest(file: manifestFileUrl)
|
|
583
603
|
} else {
|
|
584
604
|
return nil
|
|
585
605
|
}
|
|
586
606
|
} else {
|
|
587
|
-
let
|
|
588
|
-
let
|
|
589
|
-
if
|
|
607
|
+
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
608
|
+
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
609
|
+
if let manifestFileUrl = manifestFileUrl {
|
|
590
610
|
return try loadManifest(file: manifestFileUrl)
|
|
591
611
|
} else {
|
|
592
612
|
return nil
|
|
@@ -601,21 +621,24 @@ import CommonCrypto
|
|
|
601
621
|
return Manifest(items: items)
|
|
602
622
|
}
|
|
603
623
|
|
|
604
|
-
private func
|
|
605
|
-
|
|
606
|
-
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
607
|
-
return
|
|
608
|
-
}
|
|
609
|
-
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
|
|
610
|
-
setNextCapacitorServerPathToDefaultWebAssetDir()
|
|
611
|
-
setCurrentCapacitorServerPathToDefaultWebAssetDir()
|
|
624
|
+
private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
|
|
625
|
+
plugin.notifyDownloadBundleProgressListeners(event)
|
|
612
626
|
}
|
|
613
627
|
|
|
614
|
-
private func
|
|
615
|
-
|
|
616
|
-
|
|
628
|
+
private func rollback() {
|
|
629
|
+
// Set the rollback flag
|
|
630
|
+
rollbackPerformed = true
|
|
631
|
+
// Set the new previous bundle ID
|
|
632
|
+
let currentBundleId = getCurrentBundleId()
|
|
633
|
+
setPreviousBundleId(bundleId: currentBundleId)
|
|
634
|
+
// Perform the rollback
|
|
635
|
+
if let _ = currentBundleId {
|
|
636
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
|
|
637
|
+
setNextBundleById(nil)
|
|
638
|
+
setCurrentBundleById(nil)
|
|
639
|
+
} else {
|
|
640
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
617
641
|
}
|
|
618
|
-
preferences.setLastBundleVersion(currentBundleVersion)
|
|
619
642
|
}
|
|
620
643
|
|
|
621
644
|
private func searchIndexHtmlFile(url: URL) -> URL? {
|
|
@@ -645,6 +668,12 @@ import CommonCrypto
|
|
|
645
668
|
return nil
|
|
646
669
|
}
|
|
647
670
|
|
|
671
|
+
/// - Parameter bundleId: The bundle ID to set as the current bundle. If `nil`, the default bundle will be used.
|
|
672
|
+
private func setCurrentBundleById(_ bundleId: String?) {
|
|
673
|
+
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
674
|
+
setCurrentCapacitorServerPath(path: path)
|
|
675
|
+
}
|
|
676
|
+
|
|
648
677
|
private func setCurrentCapacitorServerPath(path: String) {
|
|
649
678
|
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
650
679
|
return
|
|
@@ -652,12 +681,8 @@ import CommonCrypto
|
|
|
652
681
|
viewController.setServerBasePath(path: path)
|
|
653
682
|
}
|
|
654
683
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
setCurrentCapacitorServerPath(path: path)
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
private func setNextBundle(bundleId: String) {
|
|
684
|
+
/// - Parameter bundleId: The bundle ID to set as the next bundle. If `nil`, the default bundle will be used.
|
|
685
|
+
private func setNextBundleById(_ bundleId: String?) {
|
|
661
686
|
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
662
687
|
setNextCapacitorServerPath(path: path)
|
|
663
688
|
}
|
|
@@ -672,12 +697,15 @@ import CommonCrypto
|
|
|
672
697
|
}
|
|
673
698
|
}
|
|
674
699
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
700
|
+
/// - Parameter bundleId: The bundle ID to save as the previous bundle ID. If `nil`, the value will be removed.
|
|
701
|
+
private func setPreviousBundleId(bundleId: String?) {
|
|
702
|
+
preferences.setPreviousBundleId(bundleId)
|
|
678
703
|
}
|
|
679
704
|
|
|
680
705
|
private func startRollbackTimer() {
|
|
706
|
+
guard config.readyTimeout > 0 else {
|
|
707
|
+
return
|
|
708
|
+
}
|
|
681
709
|
stopRollbackTimer()
|
|
682
710
|
rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
|
|
683
711
|
self?.rollback()
|
|
@@ -784,22 +812,6 @@ import CommonCrypto
|
|
|
784
812
|
}
|
|
785
813
|
return verificationResult
|
|
786
814
|
}
|
|
787
|
-
|
|
788
|
-
private func wasUpdated() -> Bool {
|
|
789
|
-
guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
|
|
790
|
-
return false
|
|
791
|
-
}
|
|
792
|
-
guard let lastBundleVersion = Int(lastBundleVersionString) else {
|
|
793
|
-
return false
|
|
794
|
-
}
|
|
795
|
-
guard let currentBundleVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
796
|
-
return false
|
|
797
|
-
}
|
|
798
|
-
guard let currentBundleVersion = Int(currentBundleVersionString) else {
|
|
799
|
-
return false
|
|
800
|
-
}
|
|
801
|
-
return currentBundleVersion > lastBundleVersion
|
|
802
|
-
}
|
|
803
815
|
}
|
|
804
816
|
|
|
805
817
|
extension DispatchQueue {
|
|
@@ -814,3 +826,10 @@ extension DispatchQueue {
|
|
|
814
826
|
}
|
|
815
827
|
}
|
|
816
828
|
}
|
|
829
|
+
|
|
830
|
+
extension Progress {
|
|
831
|
+
convenience init(totalUnitCount: Int64, completedUnitCount: Int64) {
|
|
832
|
+
self.init(totalUnitCount: totalUnitCount)
|
|
833
|
+
self.completedUnitCount = completedUnitCount
|
|
834
|
+
}
|
|
835
|
+
}
|
|
@@ -2,10 +2,8 @@ public struct LiveUpdateConfig {
|
|
|
2
2
|
var appId: String?
|
|
3
3
|
var autoDeleteBundles = false
|
|
4
4
|
var defaultChannel: String?
|
|
5
|
-
var enabled = true
|
|
6
5
|
var httpTimeout = 60000
|
|
7
|
-
var location: String?
|
|
8
6
|
var publicKey: String?
|
|
9
|
-
var readyTimeout =
|
|
10
|
-
var
|
|
7
|
+
var readyTimeout = 0
|
|
8
|
+
var serverDomain = "api.cloud.capawesome.io"
|
|
11
9
|
}
|
|
@@ -18,12 +18,16 @@ public class LiveUpdateHttpClient: NSObject {
|
|
|
18
18
|
self.config = config
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public func download(url: URL, destination: @escaping DownloadRequest.Destination) async throws -> AFDownloadResponse<Data> {
|
|
21
|
+
public func download(url: URL, destination: @escaping DownloadRequest.Destination, callback: ((Progress) -> Void)?) async throws -> AFDownloadResponse<Data> {
|
|
22
22
|
var request = URLRequest(url: url)
|
|
23
23
|
request.httpMethod = HTTPMethod.get.rawValue
|
|
24
24
|
request.timeoutInterval = Double(config.httpTimeout) / 1000.0
|
|
25
25
|
return try await withCheckedThrowingContinuation { continuation in
|
|
26
|
-
AF.download(request, to: destination).
|
|
26
|
+
AF.download(request, to: destination).downloadProgress { progress in
|
|
27
|
+
if let callback = callback {
|
|
28
|
+
callback(progress)
|
|
29
|
+
}
|
|
30
|
+
}.responseData { response in
|
|
27
31
|
continuation.resume(returning: response)
|
|
28
32
|
}
|
|
29
33
|
}
|