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