@capawesome/capacitor-live-update 6.7.2 → 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 -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 +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 -154
- 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 +6 -20
- 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,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)
|
|
@@ -64,11 +57,10 @@ import CommonCrypto
|
|
|
64
57
|
@objc public func downloadBundle(_ options: DownloadBundleOptions) async throws {
|
|
65
58
|
let artifactType = options.getArtifactType()
|
|
66
59
|
let bundleId = options.getBundleId()
|
|
67
|
-
let checksum = options.getChecksum()
|
|
68
60
|
let url = options.getUrl()
|
|
69
61
|
|
|
70
62
|
// Check if the bundle already exists
|
|
71
|
-
if
|
|
63
|
+
if hasBundleById(bundleId) {
|
|
72
64
|
throw CustomError.bundleAlreadyExists
|
|
73
65
|
}
|
|
74
66
|
|
|
@@ -76,22 +68,13 @@ import CommonCrypto
|
|
|
76
68
|
if artifactType == .manifest {
|
|
77
69
|
try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
|
|
78
70
|
} else {
|
|
79
|
-
try await downloadBundleOfTypeZip(bundleId: bundleId, url: url
|
|
71
|
+
try await downloadBundleOfTypeZip(bundleId: bundleId, url: url)
|
|
80
72
|
}
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
@objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
|
|
84
76
|
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)
|
|
77
|
+
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, customProperties: response?.customProperties, downloadUrl: response?.url)
|
|
95
78
|
}
|
|
96
79
|
|
|
97
80
|
@objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
@@ -107,10 +90,7 @@ import CommonCrypto
|
|
|
107
90
|
}
|
|
108
91
|
|
|
109
92
|
@objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
110
|
-
|
|
111
|
-
if bundleId == defaultWebAssetDir {
|
|
112
|
-
bundleId = nil
|
|
113
|
-
}
|
|
93
|
+
let bundleId = getCurrentBundleId()
|
|
114
94
|
let result = GetCurrentBundleResult(bundleId: bundleId)
|
|
115
95
|
completion(result, nil)
|
|
116
96
|
}
|
|
@@ -129,10 +109,7 @@ import CommonCrypto
|
|
|
129
109
|
}
|
|
130
110
|
|
|
131
111
|
@objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
132
|
-
|
|
133
|
-
if bundleId == defaultWebAssetDir {
|
|
134
|
-
bundleId = nil
|
|
135
|
-
}
|
|
112
|
+
let bundleId: String? = getNextBundleId()
|
|
136
113
|
let result = GetNextBundleResult(bundleId: bundleId)
|
|
137
114
|
completion(result, nil)
|
|
138
115
|
}
|
|
@@ -149,12 +126,27 @@ import CommonCrypto
|
|
|
149
126
|
completion(result, nil)
|
|
150
127
|
}
|
|
151
128
|
|
|
152
|
-
@objc public func ready() {
|
|
129
|
+
@objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
|
|
153
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
|
|
154
135
|
stopRollbackTimer()
|
|
136
|
+
// Delete unused bundles
|
|
155
137
|
if config.autoDeleteBundles {
|
|
156
138
|
deleteUnusedBundles()
|
|
157
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
|
|
158
150
|
}
|
|
159
151
|
|
|
160
152
|
@objc public func reload() {
|
|
@@ -164,21 +156,7 @@ import CommonCrypto
|
|
|
164
156
|
}
|
|
165
157
|
|
|
166
158
|
@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)
|
|
159
|
+
self.setNextBundleById(nil)
|
|
182
160
|
}
|
|
183
161
|
|
|
184
162
|
@objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
|
|
@@ -199,8 +177,8 @@ import CommonCrypto
|
|
|
199
177
|
let bundleId = options.getBundleId()
|
|
200
178
|
|
|
201
179
|
if let bundleId = bundleId {
|
|
202
|
-
if
|
|
203
|
-
|
|
180
|
+
if hasBundleById(bundleId) {
|
|
181
|
+
setNextBundleById(bundleId)
|
|
204
182
|
} else {
|
|
205
183
|
let error = CustomError.bundleNotFound
|
|
206
184
|
completion(error)
|
|
@@ -225,12 +203,12 @@ import CommonCrypto
|
|
|
225
203
|
let latestBundleId = response.bundleId
|
|
226
204
|
let downloadUrl = response.url
|
|
227
205
|
// Check if bundle already exists
|
|
228
|
-
if
|
|
206
|
+
if hasBundleById(latestBundleId) {
|
|
229
207
|
var nextBundleId: String?
|
|
230
208
|
let currentBundleId = self.getCurrentBundleId()
|
|
231
209
|
if latestBundleId != currentBundleId {
|
|
232
210
|
// Set the next bundle
|
|
233
|
-
|
|
211
|
+
setNextBundleById(latestBundleId)
|
|
234
212
|
nextBundleId = latestBundleId
|
|
235
213
|
}
|
|
236
214
|
return SyncResult(nextBundleId: nextBundleId)
|
|
@@ -239,10 +217,10 @@ import CommonCrypto
|
|
|
239
217
|
if artifactType == .manifest {
|
|
240
218
|
try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
|
|
241
219
|
} else {
|
|
242
|
-
try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl
|
|
220
|
+
try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl)
|
|
243
221
|
}
|
|
244
222
|
// Set the next bundle
|
|
245
|
-
|
|
223
|
+
setNextBundleById(latestBundleId)
|
|
246
224
|
return SyncResult(nextBundleId: latestBundleId)
|
|
247
225
|
}
|
|
248
226
|
|
|
@@ -271,12 +249,12 @@ import CommonCrypto
|
|
|
271
249
|
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
272
250
|
}
|
|
273
251
|
|
|
274
|
-
private func buildCapacitorServerPathFor(bundleId: String) -> String {
|
|
252
|
+
private func buildCapacitorServerPathFor(bundleId: String?) -> String {
|
|
275
253
|
let path: String
|
|
276
|
-
if bundleId
|
|
277
|
-
path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
|
|
278
|
-
} else {
|
|
254
|
+
if let bundleId = bundleId {
|
|
279
255
|
path = buildBundlePathFor(bundleId: bundleId)
|
|
256
|
+
} else {
|
|
257
|
+
path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
|
|
280
258
|
}
|
|
281
259
|
return path
|
|
282
260
|
}
|
|
@@ -290,31 +268,28 @@ import CommonCrypto
|
|
|
290
268
|
return url
|
|
291
269
|
}
|
|
292
270
|
|
|
293
|
-
private func copyCurrentBundleFile(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
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)
|
|
299
274
|
let parentDirectory = destination.deletingLastPathComponent()
|
|
300
275
|
try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
301
276
|
|
|
302
277
|
let sourceURL: URL
|
|
303
|
-
if currentBundleId
|
|
304
|
-
|
|
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 {
|
|
305
282
|
return
|
|
306
283
|
}
|
|
307
284
|
sourceURL = file
|
|
308
|
-
} else {
|
|
309
|
-
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
|
|
310
285
|
}
|
|
311
286
|
|
|
312
287
|
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
|
313
288
|
}
|
|
314
289
|
|
|
315
|
-
private func copyCurrentBundleFiles(
|
|
316
|
-
for
|
|
317
|
-
try copyCurrentBundleFile(
|
|
290
|
+
private func copyCurrentBundleFiles(filesToCopy: [ManifestItem], toDirectory: URL) throws {
|
|
291
|
+
for fileToCopy in filesToCopy {
|
|
292
|
+
try copyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
|
|
318
293
|
}
|
|
319
294
|
}
|
|
320
295
|
|
|
@@ -336,14 +311,14 @@ import CommonCrypto
|
|
|
336
311
|
return temporaryDirectory
|
|
337
312
|
}
|
|
338
313
|
|
|
339
|
-
private func
|
|
314
|
+
private func deleteBundleById(_ bundleId: String) throws {
|
|
340
315
|
// Delete the bundle directory
|
|
341
316
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
342
317
|
try FileManager.default.removeItem(atPath: path)
|
|
343
318
|
// Reset the next bundle if it is the deleted bundle
|
|
344
319
|
let nextBundleId = getNextBundleId()
|
|
345
320
|
if bundleId == nextBundleId {
|
|
346
|
-
|
|
321
|
+
setNextBundleById(nil)
|
|
347
322
|
}
|
|
348
323
|
}
|
|
349
324
|
|
|
@@ -355,7 +330,7 @@ import CommonCrypto
|
|
|
355
330
|
for bundleId in bundleIds {
|
|
356
331
|
if bundleId != currentBundleId && bundleId != nextBundleId {
|
|
357
332
|
do {
|
|
358
|
-
try
|
|
333
|
+
try deleteBundleById(bundleId)
|
|
359
334
|
} catch {
|
|
360
335
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
|
|
361
336
|
}
|
|
@@ -363,12 +338,12 @@ import CommonCrypto
|
|
|
363
338
|
}
|
|
364
339
|
}
|
|
365
340
|
|
|
366
|
-
private func downloadAndVerifyFile(url: String, file: URL,
|
|
341
|
+
private func downloadAndVerifyFile(url: String, file: URL, callback: ((Progress) -> Void)?) async throws {
|
|
367
342
|
let destination: DownloadRequest.Destination = { _, _ in
|
|
368
343
|
return (file, [.createIntermediateDirectories])
|
|
369
344
|
}
|
|
370
345
|
let urlComponents = URLComponents(string: url)!
|
|
371
|
-
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)
|
|
372
347
|
if let error = result.error {
|
|
373
348
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
|
|
374
349
|
if let urlError = error.underlyingError as? URLError {
|
|
@@ -381,31 +356,59 @@ import CommonCrypto
|
|
|
381
356
|
guard let response = result.response else {
|
|
382
357
|
throw CustomError.unknown
|
|
383
358
|
}
|
|
384
|
-
let checksum =
|
|
359
|
+
let checksum = LiveUpdateHttpClient.getChecksumFromResponse(response: response)
|
|
385
360
|
let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
|
|
386
361
|
try verifyFile(url: file, checksum: checksum, signature: signature)
|
|
387
362
|
}
|
|
388
363
|
|
|
389
|
-
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 {
|
|
390
365
|
let fileURL = directory.appendingPathComponent(href)
|
|
391
366
|
var parameters = [String: String]()
|
|
392
367
|
parameters["href"] = href
|
|
393
368
|
var urlComponents = URLComponents(string: baseUrl)!
|
|
394
369
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
395
370
|
let url = urlComponents.string!
|
|
396
|
-
try await self.downloadAndVerifyFile(url: url, file: fileURL,
|
|
371
|
+
try await self.downloadAndVerifyFile(url: url, file: fileURL, callback: callback)
|
|
397
372
|
return fileURL
|
|
398
373
|
}
|
|
399
374
|
|
|
400
|
-
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()
|
|
401
384
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
402
|
-
for
|
|
385
|
+
for fileToDownload in filesToDownload {
|
|
403
386
|
group.addTask {
|
|
404
|
-
_ = 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
|
+
}
|
|
405
404
|
}
|
|
406
405
|
}
|
|
407
|
-
|
|
408
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
|
+
}
|
|
409
412
|
}
|
|
410
413
|
}
|
|
411
414
|
|
|
@@ -413,7 +416,7 @@ import CommonCrypto
|
|
|
413
416
|
// Create a temporary directory
|
|
414
417
|
let temporaryDirectory = try createTemporaryDirectory()
|
|
415
418
|
// Download the latest manifest
|
|
416
|
-
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)
|
|
417
420
|
let latestManifest = try loadManifest(file: latestManifestFile)
|
|
418
421
|
// Load the current manifest
|
|
419
422
|
let currentManifest = try loadCurrentManifest()
|
|
@@ -427,18 +430,24 @@ import CommonCrypto
|
|
|
427
430
|
itemsToDownload.append(contentsOf: latestManifest.items)
|
|
428
431
|
}
|
|
429
432
|
// Copy the files
|
|
430
|
-
try self.copyCurrentBundleFiles(
|
|
433
|
+
try self.copyCurrentBundleFiles(filesToCopy: itemsToCopy, toDirectory: temporaryDirectory)
|
|
431
434
|
// Download the files
|
|
432
|
-
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
|
+
})
|
|
433
439
|
// Add the bundle
|
|
434
440
|
try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
|
|
435
441
|
}
|
|
436
442
|
|
|
437
|
-
private func downloadBundleOfTypeZip(bundleId: String, url: String
|
|
443
|
+
private func downloadBundleOfTypeZip(bundleId: String, url: String) async throws {
|
|
438
444
|
let timestamp = String(Int(Date().timeIntervalSince1970))
|
|
439
445
|
let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
|
|
440
446
|
// Download the bundle
|
|
441
|
-
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
|
+
})
|
|
442
451
|
// Add the bundle
|
|
443
452
|
try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
|
|
444
453
|
}
|
|
@@ -455,7 +464,7 @@ import CommonCrypto
|
|
|
455
464
|
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
456
465
|
parameters["platform"] = "1"
|
|
457
466
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
458
|
-
var urlComponents = URLComponents(string: "https://\(
|
|
467
|
+
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(config.appId ?? "")/bundles/latest")!
|
|
459
468
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
460
469
|
let url = try urlComponents.asURL()
|
|
461
470
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
|
|
@@ -517,12 +526,16 @@ import CommonCrypto
|
|
|
517
526
|
return digest.map { String(format: "%02hhx", $0) }.joined()
|
|
518
527
|
}
|
|
519
528
|
|
|
520
|
-
/// - 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.
|
|
521
530
|
private func getCurrentBundleId() -> String? {
|
|
522
531
|
guard let path = getCurrentCapacitorServerPath() else {
|
|
523
532
|
return nil
|
|
524
533
|
}
|
|
525
|
-
|
|
534
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
535
|
+
if bundleId == defaultWebAssetDir {
|
|
536
|
+
return nil
|
|
537
|
+
}
|
|
538
|
+
return bundleId
|
|
526
539
|
}
|
|
527
540
|
|
|
528
541
|
/// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
|
|
@@ -538,21 +551,30 @@ import CommonCrypto
|
|
|
538
551
|
return deviceId.lowercased()
|
|
539
552
|
}
|
|
540
553
|
|
|
541
|
-
/// - Returns: The next bundle ID
|
|
542
|
-
private func getNextBundleId() -> String {
|
|
554
|
+
/// - Returns: The next bundle ID or `nil` if the default bundle will be used.
|
|
555
|
+
private func getNextBundleId() -> String? {
|
|
543
556
|
let path = getNextCapacitorServerPath()
|
|
544
|
-
|
|
557
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
558
|
+
if bundleId == defaultWebAssetDir {
|
|
559
|
+
return nil
|
|
560
|
+
}
|
|
561
|
+
return bundleId
|
|
545
562
|
}
|
|
546
563
|
|
|
547
564
|
/// - Returns: The absolute path to the next bundle directory.
|
|
548
565
|
private func getNextCapacitorServerPath() -> String {
|
|
549
|
-
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId:
|
|
566
|
+
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
|
|
550
567
|
if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
|
|
551
568
|
return path.isEmpty ? defaultCapacitorServerPath : path
|
|
552
569
|
}
|
|
553
570
|
return defaultCapacitorServerPath
|
|
554
571
|
}
|
|
555
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
|
+
|
|
556
578
|
private func getVersionCode() -> String {
|
|
557
579
|
guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
558
580
|
return ""
|
|
@@ -567,27 +589,24 @@ import CommonCrypto
|
|
|
567
589
|
return appVersionName
|
|
568
590
|
}
|
|
569
591
|
|
|
570
|
-
private func
|
|
592
|
+
private func hasBundleById(_ bundleId: String) -> Bool {
|
|
571
593
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
572
594
|
return FileManager.default.fileExists(atPath: path)
|
|
573
595
|
}
|
|
574
596
|
|
|
575
597
|
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 {
|
|
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 {
|
|
583
602
|
return try loadManifest(file: manifestFileUrl)
|
|
584
603
|
} else {
|
|
585
604
|
return nil
|
|
586
605
|
}
|
|
587
606
|
} else {
|
|
588
|
-
let
|
|
589
|
-
let
|
|
590
|
-
if
|
|
607
|
+
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
608
|
+
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
609
|
+
if let manifestFileUrl = manifestFileUrl {
|
|
591
610
|
return try loadManifest(file: manifestFileUrl)
|
|
592
611
|
} else {
|
|
593
612
|
return nil
|
|
@@ -602,21 +621,24 @@ import CommonCrypto
|
|
|
602
621
|
return Manifest(items: items)
|
|
603
622
|
}
|
|
604
623
|
|
|
624
|
+
private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
|
|
625
|
+
plugin.notifyDownloadBundleProgressListeners(event)
|
|
626
|
+
}
|
|
627
|
+
|
|
605
628
|
private func rollback() {
|
|
606
|
-
|
|
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 {
|
|
607
640
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
608
|
-
return
|
|
609
641
|
}
|
|
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
642
|
}
|
|
621
643
|
|
|
622
644
|
private func searchIndexHtmlFile(url: URL) -> URL? {
|
|
@@ -646,6 +668,12 @@ import CommonCrypto
|
|
|
646
668
|
return nil
|
|
647
669
|
}
|
|
648
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
|
+
|
|
649
677
|
private func setCurrentCapacitorServerPath(path: String) {
|
|
650
678
|
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
651
679
|
return
|
|
@@ -653,12 +681,8 @@ import CommonCrypto
|
|
|
653
681
|
viewController.setServerBasePath(path: path)
|
|
654
682
|
}
|
|
655
683
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
setCurrentCapacitorServerPath(path: path)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
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?) {
|
|
662
686
|
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
663
687
|
setNextCapacitorServerPath(path: path)
|
|
664
688
|
}
|
|
@@ -673,12 +697,15 @@ import CommonCrypto
|
|
|
673
697
|
}
|
|
674
698
|
}
|
|
675
699
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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)
|
|
679
703
|
}
|
|
680
704
|
|
|
681
705
|
private func startRollbackTimer() {
|
|
706
|
+
guard config.readyTimeout > 0 else {
|
|
707
|
+
return
|
|
708
|
+
}
|
|
682
709
|
stopRollbackTimer()
|
|
683
710
|
rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
|
|
684
711
|
self?.rollback()
|
|
@@ -785,27 +812,6 @@ import CommonCrypto
|
|
|
785
812
|
}
|
|
786
813
|
return verificationResult
|
|
787
814
|
}
|
|
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
815
|
}
|
|
810
816
|
|
|
811
817
|
extension DispatchQueue {
|
|
@@ -820,3 +826,10 @@ extension DispatchQueue {
|
|
|
820
826
|
}
|
|
821
827
|
}
|
|
822
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
|
}
|