@capawesome/capacitor-live-update 6.8.0 → 7.0.0-dev.1765885034
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 +3 -3
- package/Package.swift +32 -0
- package/README.md +419 -86
- package/android/build.gradle +12 -12
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +754 -279
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +29 -27
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +40 -7
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +210 -67
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +52 -3
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +11 -2
- 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/{results/GetBundleResult.java → events/NextBundleSetEvent.java} +9 -7
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SyncOptions.java +4 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +8 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
- 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 +597 -102
- package/dist/esm/definitions.d.ts +338 -39
- 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 +9 -2
- package/dist/esm/web.js +21 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +22 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +22 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
- package/ios/Plugin/Classes/{Results/GetBundleResult.swift → Events/NextBundleSetEvent.swift} +3 -3
- package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
- package/ios/Plugin/Classes/Options/SyncOptions.swift +4 -0
- package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -1
- package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
- package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
- package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
- package/ios/Plugin/LiveUpdate.swift +365 -149
- package/ios/Plugin/LiveUpdateConfig.swift +4 -4
- package/ios/Plugin/LiveUpdateHttpClient.swift +10 -4
- package/ios/Plugin/LiveUpdatePlugin.swift +136 -34
- package/ios/Plugin/LiveUpdatePreferences.swift +54 -12
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -0
- package/package.json +19 -20
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetBundleOptions.java +0 -18
- package/ios/Plugin/Classes/Options/SetBundleOptions.swift +0 -14
- package/ios/Plugin/LiveUpdatePlugin.h +0 -10
- package/ios/Plugin/LiveUpdatePlugin.m +0 -27
|
@@ -1,18 +1,22 @@
|
|
|
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
|
|
7
11
|
|
|
8
12
|
// swiftlint:disable type_body_length
|
|
9
13
|
@objc public class LiveUpdate: NSObject {
|
|
14
|
+
private let autoUpdateIntervalMs: Int64 = 15 * 60 * 1000 // 15 minutes
|
|
10
15
|
private let bundlesDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE! (See https://dub.sh/BLluidt)
|
|
11
16
|
private let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
12
17
|
private let config: LiveUpdateConfig
|
|
13
18
|
private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
|
|
14
19
|
private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
|
|
15
|
-
private let host: String
|
|
16
20
|
private let httpClient: LiveUpdateHttpClient
|
|
17
21
|
private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
|
18
22
|
private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
|
|
@@ -20,41 +24,40 @@ import CommonCrypto
|
|
|
20
24
|
private let preferences: LiveUpdatePreferences
|
|
21
25
|
|
|
22
26
|
private var rollbackDispatchWorkItem: DispatchWorkItem?
|
|
27
|
+
private var rollbackPerformed = false
|
|
28
|
+
private var lastAutoUpdateCheckTimestamp: Int64 = 0
|
|
29
|
+
private var syncInProgress = false
|
|
23
30
|
|
|
24
31
|
init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
|
|
25
32
|
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
33
|
self.httpClient = LiveUpdateHttpClient(config: config)
|
|
32
34
|
self.plugin = plugin
|
|
33
35
|
self.preferences = LiveUpdatePreferences()
|
|
34
36
|
super.init()
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
// Check version and reset config if version changed
|
|
39
|
+
checkAndResetConfigIfVersionChanged()
|
|
40
|
+
|
|
41
|
+
// Start the rollback timer to rollback to the default bundle
|
|
42
|
+
// if the app is not ready after a certain time
|
|
43
|
+
startRollbackTimer()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@objc public func clearBlockedBundles() {
|
|
47
|
+
preferences.setBlockedBundleIds(nil)
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
@objc public func deleteBundle(_ options: DeleteBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
48
51
|
let bundleId = options.getBundleId()
|
|
49
52
|
|
|
50
|
-
if !
|
|
53
|
+
if !hasBundleById(bundleId) {
|
|
51
54
|
let error = CustomError.bundleNotFound
|
|
52
55
|
completion(error)
|
|
53
56
|
return
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
do {
|
|
57
|
-
try
|
|
60
|
+
try deleteBundleById(bundleId)
|
|
58
61
|
completion(nil)
|
|
59
62
|
} catch {
|
|
60
63
|
completion(error)
|
|
@@ -69,7 +72,7 @@ import CommonCrypto
|
|
|
69
72
|
let url = options.getUrl()
|
|
70
73
|
|
|
71
74
|
// Check if the bundle already exists
|
|
72
|
-
if
|
|
75
|
+
if hasBundleById(bundleId) {
|
|
73
76
|
throw CustomError.bundleAlreadyExists
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -77,41 +80,51 @@ import CommonCrypto
|
|
|
77
80
|
if artifactType == .manifest {
|
|
78
81
|
try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
|
|
79
82
|
} else {
|
|
80
|
-
try await downloadBundleOfTypeZip(bundleId: bundleId,
|
|
83
|
+
try await downloadBundleOfTypeZip(bundleId: bundleId, checksum: checksum, signature: signature, url: url)
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
@objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
|
|
85
88
|
let response: GetLatestBundleResponse? = try await self.fetchLatestBundle(options)
|
|
86
|
-
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, checksum: response?.checksum, downloadUrl: response?.url, signature: response?.signature)
|
|
89
|
+
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, checksum: response?.checksum, customProperties: response?.customProperties, downloadUrl: response?.url, signature: response?.signature)
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
@objc public func
|
|
90
|
-
var
|
|
91
|
-
if
|
|
92
|
-
|
|
92
|
+
@objc public func getBlockedBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
93
|
+
var bundleIds: [String] = []
|
|
94
|
+
if let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty {
|
|
95
|
+
bundleIds = blockedIds.split(separator: ",").map(String.init)
|
|
93
96
|
}
|
|
94
|
-
let result =
|
|
97
|
+
let result = GetBlockedBundlesResult(bundleIds: bundleIds)
|
|
95
98
|
completion(result, nil)
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
@objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
99
|
-
let bundleIds =
|
|
102
|
+
let bundleIds = getDownloadedBundleIds()
|
|
100
103
|
let result = GetBundlesResult(bundleIds: bundleIds)
|
|
101
104
|
completion(result, nil)
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
@objc public func getDownloadedBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
108
|
+
let bundleIds = getDownloadedBundleIds()
|
|
109
|
+
let result = GetDownloadedBundlesResult(bundleIds: bundleIds)
|
|
110
|
+
completion(result, nil)
|
|
111
|
+
}
|
|
112
|
+
|
|
104
113
|
@objc public func getChannel(completion: @escaping (Result?, Error?) -> Void) {
|
|
105
114
|
let channel = getChannel()
|
|
106
115
|
let result = GetChannelResult(channel: channel)
|
|
107
116
|
completion(result, nil)
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
@objc public func getConfig(completion: @escaping (Result?, Error?) -> Void) {
|
|
120
|
+
let appId = getAppId()
|
|
121
|
+
let autoUpdateStrategy = config.autoUpdateStrategy
|
|
122
|
+
let result = GetConfigResult(appId: appId, autoUpdateStrategy: autoUpdateStrategy)
|
|
123
|
+
completion(result, nil)
|
|
124
|
+
}
|
|
125
|
+
|
|
110
126
|
@objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
111
|
-
|
|
112
|
-
if bundleId == defaultWebAssetDir {
|
|
113
|
-
bundleId = nil
|
|
114
|
-
}
|
|
127
|
+
let bundleId = getCurrentBundleId()
|
|
115
128
|
let result = GetCurrentBundleResult(bundleId: bundleId)
|
|
116
129
|
completion(result, nil)
|
|
117
130
|
}
|
|
@@ -130,10 +143,7 @@ import CommonCrypto
|
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
@objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
133
|
-
|
|
134
|
-
if bundleId == defaultWebAssetDir {
|
|
135
|
-
bundleId = nil
|
|
136
|
-
}
|
|
146
|
+
let bundleId: String? = getNextBundleId()
|
|
137
147
|
let result = GetNextBundleResult(bundleId: bundleId)
|
|
138
148
|
completion(result, nil)
|
|
139
149
|
}
|
|
@@ -150,12 +160,48 @@ import CommonCrypto
|
|
|
150
160
|
completion(result, nil)
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
@objc public func
|
|
163
|
+
@objc public func isSyncing(completion: @escaping (Result?, Error?) -> Void) {
|
|
164
|
+
let result = IsSyncingResult(syncing: syncInProgress)
|
|
165
|
+
completion(result, nil)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@objc public func handleLoad() {
|
|
169
|
+
if config.autoUpdateStrategy == "background" {
|
|
170
|
+
performAutoUpdate()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@objc public func handleAppWillEnterForeground() {
|
|
175
|
+
if config.autoUpdateStrategy == "background" {
|
|
176
|
+
performAutoUpdate()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
|
|
154
181
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
|
|
182
|
+
if config.readyTimeout <= 0 {
|
|
183
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
|
|
184
|
+
}
|
|
185
|
+
// Stop the rollback timer
|
|
155
186
|
stopRollbackTimer()
|
|
187
|
+
// Delete unused bundles
|
|
156
188
|
if config.autoDeleteBundles {
|
|
157
189
|
deleteUnusedBundles()
|
|
158
190
|
}
|
|
191
|
+
// Get the current and previous bundle IDs
|
|
192
|
+
let currentBundleId = getCurrentBundleId()
|
|
193
|
+
let previousBundleId = getPreviousBundleId()
|
|
194
|
+
// Block the rolled back bundle if enabled
|
|
195
|
+
if config.autoBlockRolledBackBundles && rollbackPerformed, let previousBundleId = previousBundleId {
|
|
196
|
+
addBlockedBundleId(previousBundleId)
|
|
197
|
+
}
|
|
198
|
+
// Return the result
|
|
199
|
+
let result = ReadyResult(currentBundleId: currentBundleId, previousBundleId: previousBundleId, rollback: rollbackPerformed)
|
|
200
|
+
completion(result, nil)
|
|
201
|
+
// Set the new previous bundle ID
|
|
202
|
+
setPreviousBundleId(bundleId: currentBundleId)
|
|
203
|
+
// Reset the rollback flag
|
|
204
|
+
rollbackPerformed = false
|
|
159
205
|
}
|
|
160
206
|
|
|
161
207
|
@objc public func reload() {
|
|
@@ -165,21 +211,11 @@ import CommonCrypto
|
|
|
165
211
|
}
|
|
166
212
|
|
|
167
213
|
@objc public func reset() {
|
|
168
|
-
self.
|
|
214
|
+
self.setNextBundleById(nil)
|
|
169
215
|
}
|
|
170
216
|
|
|
171
|
-
@objc public func
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Check if the bundle already exists
|
|
175
|
-
if !hasBundle(bundleId: bundleId) {
|
|
176
|
-
let error = CustomError.bundleNotFound
|
|
177
|
-
completion(error)
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
setNextBundle(bundleId: bundleId)
|
|
182
|
-
completion(nil)
|
|
217
|
+
@objc public func resetConfig() {
|
|
218
|
+
preferences.setAppId(nil)
|
|
183
219
|
}
|
|
184
220
|
|
|
185
221
|
@objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
|
|
@@ -189,6 +225,11 @@ import CommonCrypto
|
|
|
189
225
|
completion(nil)
|
|
190
226
|
}
|
|
191
227
|
|
|
228
|
+
@objc public func setConfig(_ options: SetConfigOptions) {
|
|
229
|
+
let appId = options.getAppId()
|
|
230
|
+
preferences.setAppId(appId)
|
|
231
|
+
}
|
|
232
|
+
|
|
192
233
|
@objc public func setCustomId(_ options: SetCustomIdOptions, completion: @escaping (Error?) -> Void) {
|
|
193
234
|
let customId = options.getCustomId()
|
|
194
235
|
|
|
@@ -200,8 +241,8 @@ import CommonCrypto
|
|
|
200
241
|
let bundleId = options.getBundleId()
|
|
201
242
|
|
|
202
243
|
if let bundleId = bundleId {
|
|
203
|
-
if
|
|
204
|
-
|
|
244
|
+
if hasBundleById(bundleId) {
|
|
245
|
+
setNextBundleById(bundleId)
|
|
205
246
|
} else {
|
|
206
247
|
let error = CustomError.bundleNotFound
|
|
207
248
|
completion(error)
|
|
@@ -215,6 +256,14 @@ import CommonCrypto
|
|
|
215
256
|
}
|
|
216
257
|
|
|
217
258
|
@objc public func sync(_ options: SyncOptions) async throws -> SyncResult {
|
|
259
|
+
if syncInProgress {
|
|
260
|
+
throw CustomError.syncInProgress
|
|
261
|
+
}
|
|
262
|
+
syncInProgress = true
|
|
263
|
+
defer {
|
|
264
|
+
syncInProgress = false
|
|
265
|
+
}
|
|
266
|
+
|
|
218
267
|
let channel = options.getChannel()
|
|
219
268
|
// Fetch the latest bundle
|
|
220
269
|
let fetchLatestBundleOptions = FetchLatestBundleOptions(channel: channel)
|
|
@@ -227,13 +276,18 @@ import CommonCrypto
|
|
|
227
276
|
let checksum = response.checksum
|
|
228
277
|
let signature = response.signature
|
|
229
278
|
let downloadUrl = response.url
|
|
279
|
+
// Check if the bundle is blocked
|
|
280
|
+
if isBlockedBundleId(latestBundleId) {
|
|
281
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle is blocked and will not be downloaded.")
|
|
282
|
+
return SyncResult(nextBundleId: nil)
|
|
283
|
+
}
|
|
230
284
|
// Check if bundle already exists
|
|
231
|
-
if
|
|
285
|
+
if hasBundleById(latestBundleId) {
|
|
232
286
|
var nextBundleId: String?
|
|
233
287
|
let currentBundleId = self.getCurrentBundleId()
|
|
234
288
|
if latestBundleId != currentBundleId {
|
|
235
289
|
// Set the next bundle
|
|
236
|
-
|
|
290
|
+
setNextBundleById(latestBundleId)
|
|
237
291
|
nextBundleId = latestBundleId
|
|
238
292
|
}
|
|
239
293
|
return SyncResult(nextBundleId: nextBundleId)
|
|
@@ -242,10 +296,10 @@ import CommonCrypto
|
|
|
242
296
|
if artifactType == .manifest {
|
|
243
297
|
try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
|
|
244
298
|
} else {
|
|
245
|
-
try await downloadBundleOfTypeZip(bundleId: latestBundleId,
|
|
299
|
+
try await downloadBundleOfTypeZip(bundleId: latestBundleId, checksum: checksum, signature: signature, url: downloadUrl)
|
|
246
300
|
}
|
|
247
301
|
// Set the next bundle
|
|
248
|
-
|
|
302
|
+
setNextBundleById(latestBundleId)
|
|
249
303
|
return SyncResult(nextBundleId: latestBundleId)
|
|
250
304
|
}
|
|
251
305
|
|
|
@@ -274,12 +328,12 @@ import CommonCrypto
|
|
|
274
328
|
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
275
329
|
}
|
|
276
330
|
|
|
277
|
-
private func buildCapacitorServerPathFor(bundleId: String) -> String {
|
|
331
|
+
private func buildCapacitorServerPathFor(bundleId: String?) -> String {
|
|
278
332
|
let path: String
|
|
279
|
-
if bundleId
|
|
280
|
-
path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
|
|
281
|
-
} else {
|
|
333
|
+
if let bundleId = bundleId {
|
|
282
334
|
path = buildBundlePathFor(bundleId: bundleId)
|
|
335
|
+
} else {
|
|
336
|
+
path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
|
|
283
337
|
}
|
|
284
338
|
return path
|
|
285
339
|
}
|
|
@@ -293,32 +347,41 @@ import CommonCrypto
|
|
|
293
347
|
return url
|
|
294
348
|
}
|
|
295
349
|
|
|
296
|
-
private func copyCurrentBundleFile(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let destination = toDirectory.appendingPathComponent(href)
|
|
350
|
+
private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
|
|
351
|
+
let currentBundleId = getCurrentBundleId()
|
|
352
|
+
let destination = toDirectory.appendingPathComponent(fileToCopy.href)
|
|
302
353
|
let parentDirectory = destination.deletingLastPathComponent()
|
|
303
354
|
try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
304
355
|
|
|
305
356
|
let sourceURL: URL
|
|
306
|
-
if currentBundleId
|
|
307
|
-
|
|
308
|
-
|
|
357
|
+
if let currentBundleId = currentBundleId {
|
|
358
|
+
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(fileToCopy.href)
|
|
359
|
+
} else {
|
|
360
|
+
guard let file = Bundle.main.url(forResource: fileToCopy.href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
|
|
361
|
+
// If the file does not exist in the current bundle, throw an error
|
|
362
|
+
// We can use CustomError.unknown here since this error will not be handled by the user
|
|
363
|
+
throw CustomError.unknown
|
|
309
364
|
}
|
|
310
365
|
sourceURL = file
|
|
311
|
-
} else {
|
|
312
|
-
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
|
|
313
366
|
}
|
|
314
367
|
|
|
315
368
|
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
|
316
369
|
}
|
|
317
370
|
|
|
318
|
-
private func
|
|
319
|
-
|
|
320
|
-
|
|
371
|
+
private func copyCurrentBundleFilesAndReturnFailures(
|
|
372
|
+
filesToCopy: [ManifestItem],
|
|
373
|
+
toDirectory: URL
|
|
374
|
+
) -> [ManifestItem] {
|
|
375
|
+
var missingItems = [ManifestItem]()
|
|
376
|
+
for fileToCopy in filesToCopy {
|
|
377
|
+
let success = tryCopyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
|
|
378
|
+
if !success {
|
|
379
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to copy file: \(fileToCopy.href)")
|
|
380
|
+
// If the file could not be copied, add it to the list of missing items
|
|
381
|
+
missingItems.append(fileToCopy)
|
|
382
|
+
}
|
|
321
383
|
}
|
|
384
|
+
return missingItems
|
|
322
385
|
}
|
|
323
386
|
|
|
324
387
|
private func createBundlesDirectory() {
|
|
@@ -339,26 +402,26 @@ import CommonCrypto
|
|
|
339
402
|
return temporaryDirectory
|
|
340
403
|
}
|
|
341
404
|
|
|
342
|
-
private func
|
|
405
|
+
private func deleteBundleById(_ bundleId: String) throws {
|
|
343
406
|
// Delete the bundle directory
|
|
344
407
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
345
408
|
try FileManager.default.removeItem(atPath: path)
|
|
346
409
|
// Reset the next bundle if it is the deleted bundle
|
|
347
410
|
let nextBundleId = getNextBundleId()
|
|
348
411
|
if bundleId == nextBundleId {
|
|
349
|
-
|
|
412
|
+
setNextBundleById(nil)
|
|
350
413
|
}
|
|
351
414
|
}
|
|
352
415
|
|
|
353
416
|
private func deleteUnusedBundles() {
|
|
354
|
-
let bundleIds =
|
|
417
|
+
let bundleIds = getDownloadedBundleIds()
|
|
355
418
|
let currentBundleId = getCurrentBundleId()
|
|
356
419
|
let nextBundleId = getNextBundleId()
|
|
357
420
|
|
|
358
421
|
for bundleId in bundleIds {
|
|
359
422
|
if bundleId != currentBundleId && bundleId != nextBundleId {
|
|
360
423
|
do {
|
|
361
|
-
try
|
|
424
|
+
try deleteBundleById(bundleId)
|
|
362
425
|
} catch {
|
|
363
426
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
|
|
364
427
|
}
|
|
@@ -366,12 +429,12 @@ import CommonCrypto
|
|
|
366
429
|
}
|
|
367
430
|
}
|
|
368
431
|
|
|
369
|
-
private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?) async throws {
|
|
432
|
+
private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
|
|
370
433
|
let destination: DownloadRequest.Destination = { _, _ in
|
|
371
434
|
return (file, [.createIntermediateDirectories])
|
|
372
435
|
}
|
|
373
436
|
let urlComponents = URLComponents(string: url)!
|
|
374
|
-
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination)
|
|
437
|
+
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
|
|
375
438
|
if let error = result.error {
|
|
376
439
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
|
|
377
440
|
if let urlError = error.underlyingError as? URLError {
|
|
@@ -389,26 +452,54 @@ import CommonCrypto
|
|
|
389
452
|
try verifyFile(url: file, checksum: checksum, signature: signature)
|
|
390
453
|
}
|
|
391
454
|
|
|
392
|
-
private func downloadBundleFile(baseUrl: String, href: String, directory: URL) async throws -> URL {
|
|
455
|
+
private func downloadBundleFile(baseUrl: String, href: String, directory: URL, callback: ((Progress) -> Void)?) async throws -> URL {
|
|
393
456
|
let fileURL = directory.appendingPathComponent(href)
|
|
394
457
|
var parameters = [String: String]()
|
|
395
458
|
parameters["href"] = href
|
|
396
459
|
var urlComponents = URLComponents(string: baseUrl)!
|
|
397
460
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
398
461
|
let url = urlComponents.string!
|
|
399
|
-
try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil, signature: nil)
|
|
462
|
+
try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil, signature: nil, callback: callback)
|
|
400
463
|
return fileURL
|
|
401
464
|
}
|
|
402
465
|
|
|
403
|
-
private func downloadBundleFiles(url: String,
|
|
466
|
+
private func downloadBundleFiles(url: String, filesToDownload: [ManifestItem], directory: URL, callback: ((Progress) -> Void)?) async throws {
|
|
467
|
+
let totalBytesToDownload = Int64(filesToDownload.map { $0.sizeInBytes }.reduce(0, +))
|
|
468
|
+
actor TotalBytesDownloaded {
|
|
469
|
+
var value: Int64 = 0
|
|
470
|
+
func add(_ amount: Int64) {
|
|
471
|
+
value += amount
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
let totalBytesDownloaded = TotalBytesDownloaded()
|
|
404
475
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
405
|
-
for
|
|
476
|
+
for fileToDownload in filesToDownload {
|
|
406
477
|
group.addTask {
|
|
407
|
-
_ = try await self.downloadBundleFile(baseUrl: url, href: href, directory: directory
|
|
478
|
+
_ = try await self.downloadBundleFile(baseUrl: url, href: fileToDownload.href, directory: directory, callback: { progress in
|
|
479
|
+
Task {
|
|
480
|
+
// Progress is only reported if the file is downloaded in chunks
|
|
481
|
+
if let callback = callback {
|
|
482
|
+
let totalBytesDownloaded = await totalBytesDownloaded.value
|
|
483
|
+
let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: progress.completedUnitCount + totalBytesDownloaded)
|
|
484
|
+
callback(totalProgress)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
// Emit the current progress in case the file was not downloaded in chunks
|
|
489
|
+
if let callback = callback {
|
|
490
|
+
await totalBytesDownloaded.add(Int64(fileToDownload.sizeInBytes))
|
|
491
|
+
let totalBytesDownloaded = await totalBytesDownloaded.value
|
|
492
|
+
let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesDownloaded)
|
|
493
|
+
callback(totalProgress)
|
|
494
|
+
}
|
|
408
495
|
}
|
|
409
496
|
}
|
|
410
|
-
|
|
411
497
|
try await group.waitForAll()
|
|
498
|
+
// Call the callback one last time to make sure the progress is at 100%
|
|
499
|
+
if let callback = callback {
|
|
500
|
+
let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesToDownload)
|
|
501
|
+
callback(totalProgress)
|
|
502
|
+
}
|
|
412
503
|
}
|
|
413
504
|
}
|
|
414
505
|
|
|
@@ -416,7 +507,7 @@ import CommonCrypto
|
|
|
416
507
|
// Create a temporary directory
|
|
417
508
|
let temporaryDirectory = try createTemporaryDirectory()
|
|
418
509
|
// Download the latest manifest
|
|
419
|
-
let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory)
|
|
510
|
+
let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
|
|
420
511
|
let latestManifest = try loadManifest(file: latestManifestFile)
|
|
421
512
|
// Load the current manifest
|
|
422
513
|
let currentManifest = try loadCurrentManifest()
|
|
@@ -430,18 +521,28 @@ import CommonCrypto
|
|
|
430
521
|
itemsToDownload.append(contentsOf: latestManifest.items)
|
|
431
522
|
}
|
|
432
523
|
// Copy the files
|
|
433
|
-
|
|
524
|
+
let missingItems = copyCurrentBundleFilesAndReturnFailures(filesToCopy: itemsToCopy, toDirectory: temporaryDirectory)
|
|
525
|
+
// If items could not be copied, add them to the list of items to download
|
|
526
|
+
if !missingItems.isEmpty {
|
|
527
|
+
itemsToDownload.append(contentsOf: missingItems)
|
|
528
|
+
}
|
|
434
529
|
// Download the files
|
|
435
|
-
try await self.downloadBundleFiles(url: url,
|
|
530
|
+
try await self.downloadBundleFiles(url: url, filesToDownload: itemsToDownload, directory: temporaryDirectory, callback: { progress in
|
|
531
|
+
let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
|
|
532
|
+
self.notifyDownloadBundleProgressListeners(event)
|
|
533
|
+
})
|
|
436
534
|
// Add the bundle
|
|
437
535
|
try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
|
|
438
536
|
}
|
|
439
537
|
|
|
440
|
-
private func downloadBundleOfTypeZip(bundleId: String,
|
|
538
|
+
private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
|
|
441
539
|
let timestamp = String(Int(Date().timeIntervalSince1970))
|
|
442
540
|
let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
|
|
443
541
|
// Download the bundle
|
|
444
|
-
try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum, signature: signature
|
|
542
|
+
try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum, signature: signature, callback: { progress in
|
|
543
|
+
let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
|
|
544
|
+
self.notifyDownloadBundleProgressListeners(event)
|
|
545
|
+
})
|
|
445
546
|
// Add the bundle
|
|
446
547
|
try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
|
|
447
548
|
}
|
|
@@ -458,7 +559,7 @@ import CommonCrypto
|
|
|
458
559
|
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
459
560
|
parameters["platform"] = "1"
|
|
460
561
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
461
|
-
var urlComponents = URLComponents(string: "https://\(
|
|
562
|
+
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
|
|
462
563
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
463
564
|
let url = try urlComponents.asURL()
|
|
464
565
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
|
|
@@ -481,7 +582,7 @@ import CommonCrypto
|
|
|
481
582
|
}
|
|
482
583
|
}
|
|
483
584
|
|
|
484
|
-
private func
|
|
585
|
+
private func getDownloadedBundleIds() -> [String] {
|
|
485
586
|
let url = libraryDirectoryUrl.appendingPathComponent(bundlesDirectory)
|
|
486
587
|
do {
|
|
487
588
|
let pathExists = FileManager.default.fileExists(atPath: url.path)
|
|
@@ -495,6 +596,13 @@ import CommonCrypto
|
|
|
495
596
|
}
|
|
496
597
|
}
|
|
497
598
|
|
|
599
|
+
private func getAppId() -> String? {
|
|
600
|
+
if let appId = preferences.getAppId() {
|
|
601
|
+
return appId
|
|
602
|
+
}
|
|
603
|
+
return config.appId
|
|
604
|
+
}
|
|
605
|
+
|
|
498
606
|
private func getChannel() -> String? {
|
|
499
607
|
var channel: String?
|
|
500
608
|
if let _ = config.defaultChannel {
|
|
@@ -520,12 +628,16 @@ import CommonCrypto
|
|
|
520
628
|
return digest.map { String(format: "%02hhx", $0) }.joined()
|
|
521
629
|
}
|
|
522
630
|
|
|
523
|
-
/// - Returns: The current bundle ID
|
|
631
|
+
/// - Returns: The current bundle ID or `nil` if no view controller was found (should never happen) or the default bundle is in use.
|
|
524
632
|
private func getCurrentBundleId() -> String? {
|
|
525
633
|
guard let path = getCurrentCapacitorServerPath() else {
|
|
526
634
|
return nil
|
|
527
635
|
}
|
|
528
|
-
|
|
636
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
637
|
+
if bundleId == defaultWebAssetDir {
|
|
638
|
+
return nil
|
|
639
|
+
}
|
|
640
|
+
return bundleId
|
|
529
641
|
}
|
|
530
642
|
|
|
531
643
|
/// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
|
|
@@ -541,21 +653,30 @@ import CommonCrypto
|
|
|
541
653
|
return deviceId.lowercased()
|
|
542
654
|
}
|
|
543
655
|
|
|
544
|
-
/// - Returns: The next bundle ID
|
|
545
|
-
private func getNextBundleId() -> String {
|
|
656
|
+
/// - Returns: The next bundle ID or `nil` if the default bundle will be used.
|
|
657
|
+
private func getNextBundleId() -> String? {
|
|
546
658
|
let path = getNextCapacitorServerPath()
|
|
547
|
-
|
|
659
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
660
|
+
if bundleId == defaultWebAssetDir {
|
|
661
|
+
return nil
|
|
662
|
+
}
|
|
663
|
+
return bundleId
|
|
548
664
|
}
|
|
549
665
|
|
|
550
666
|
/// - Returns: The absolute path to the next bundle directory.
|
|
551
667
|
private func getNextCapacitorServerPath() -> String {
|
|
552
|
-
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId:
|
|
668
|
+
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
|
|
553
669
|
if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
|
|
554
670
|
return path.isEmpty ? defaultCapacitorServerPath : path
|
|
555
671
|
}
|
|
556
672
|
return defaultCapacitorServerPath
|
|
557
673
|
}
|
|
558
674
|
|
|
675
|
+
/// - Returns: The previous bundle ID or `nil` if the default bundle was used.
|
|
676
|
+
private func getPreviousBundleId() -> String? {
|
|
677
|
+
return preferences.getPreviousBundleId()
|
|
678
|
+
}
|
|
679
|
+
|
|
559
680
|
private func getVersionCode() -> String {
|
|
560
681
|
guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
561
682
|
return ""
|
|
@@ -570,27 +691,24 @@ import CommonCrypto
|
|
|
570
691
|
return appVersionName
|
|
571
692
|
}
|
|
572
693
|
|
|
573
|
-
private func
|
|
694
|
+
private func hasBundleById(_ bundleId: String) -> Bool {
|
|
574
695
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
575
696
|
return FileManager.default.fileExists(atPath: path)
|
|
576
697
|
}
|
|
577
698
|
|
|
578
699
|
private func loadCurrentManifest() throws -> Manifest? {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
584
|
-
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
585
|
-
if let manifestFileUrl = manifestFileUrl {
|
|
700
|
+
if let currentBundleId = getCurrentBundleId() {
|
|
701
|
+
let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
|
|
702
|
+
let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
|
|
703
|
+
if manifestFileExists {
|
|
586
704
|
return try loadManifest(file: manifestFileUrl)
|
|
587
705
|
} else {
|
|
588
706
|
return nil
|
|
589
707
|
}
|
|
590
708
|
} else {
|
|
591
|
-
let
|
|
592
|
-
let
|
|
593
|
-
if
|
|
709
|
+
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
710
|
+
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
711
|
+
if let manifestFileUrl = manifestFileUrl {
|
|
594
712
|
return try loadManifest(file: manifestFileUrl)
|
|
595
713
|
} else {
|
|
596
714
|
return nil
|
|
@@ -605,21 +723,48 @@ import CommonCrypto
|
|
|
605
723
|
return Manifest(items: items)
|
|
606
724
|
}
|
|
607
725
|
|
|
608
|
-
private func
|
|
609
|
-
|
|
610
|
-
|
|
726
|
+
private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
|
|
727
|
+
plugin.notifyDownloadBundleProgressListeners(event)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private func performAutoUpdate() {
|
|
731
|
+
// Check if enough time has passed since the last check
|
|
732
|
+
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
|
733
|
+
if lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs {
|
|
734
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update skipped. Last check was less than 15 minutes ago.")
|
|
611
735
|
return
|
|
612
736
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
737
|
+
|
|
738
|
+
// Update the timestamp
|
|
739
|
+
lastAutoUpdateCheckTimestamp = now
|
|
740
|
+
|
|
741
|
+
// Run sync in background task
|
|
742
|
+
Task {
|
|
743
|
+
do {
|
|
744
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update started.")
|
|
745
|
+
let options = SyncOptions(channel: nil)
|
|
746
|
+
_ = try await sync(options)
|
|
747
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update completed successfully.")
|
|
748
|
+
} catch {
|
|
749
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update failed: ", error.localizedDescription)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
616
752
|
}
|
|
617
753
|
|
|
618
|
-
private func
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
754
|
+
private func rollback() {
|
|
755
|
+
// Set the rollback flag
|
|
756
|
+
rollbackPerformed = true
|
|
757
|
+
// Set the new previous bundle ID
|
|
758
|
+
let currentBundleId = getCurrentBundleId()
|
|
759
|
+
setPreviousBundleId(bundleId: currentBundleId)
|
|
760
|
+
// Perform the rollback
|
|
761
|
+
if let _ = currentBundleId {
|
|
762
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
|
|
763
|
+
setNextBundleById(nil)
|
|
764
|
+
setCurrentBundleById(nil)
|
|
765
|
+
} else {
|
|
766
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
767
|
+
}
|
|
623
768
|
}
|
|
624
769
|
|
|
625
770
|
private func searchIndexHtmlFile(url: URL) -> URL? {
|
|
@@ -649,21 +794,74 @@ import CommonCrypto
|
|
|
649
794
|
return nil
|
|
650
795
|
}
|
|
651
796
|
|
|
797
|
+
/// - Parameter bundleId: The bundle ID to set as the current bundle. If `nil`, the default bundle will be used.
|
|
798
|
+
private func setCurrentBundleById(_ bundleId: String?) {
|
|
799
|
+
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
800
|
+
setCurrentCapacitorServerPath(path: path)
|
|
801
|
+
}
|
|
802
|
+
|
|
652
803
|
private func setCurrentCapacitorServerPath(path: String) {
|
|
653
804
|
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
654
805
|
return
|
|
655
806
|
}
|
|
656
807
|
viewController.setServerBasePath(path: path)
|
|
808
|
+
// Notify listeners
|
|
809
|
+
notifyReloadedListeners()
|
|
657
810
|
}
|
|
658
811
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
setCurrentCapacitorServerPath(path: path)
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
private func setNextBundle(bundleId: String) {
|
|
812
|
+
/// - Parameter bundleId: The bundle ID to set as the next bundle. If `nil`, the default bundle will be used.
|
|
813
|
+
private func setNextBundleById(_ bundleId: String?) {
|
|
665
814
|
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
666
815
|
setNextCapacitorServerPath(path: path)
|
|
816
|
+
|
|
817
|
+
// Notify listeners
|
|
818
|
+
notifyNextBundleSetListeners(bundleId)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private func notifyNextBundleSetListeners(_ bundleId: String?) {
|
|
822
|
+
let event = NextBundleSetEvent(bundleId: bundleId)
|
|
823
|
+
plugin.notifyNextBundleSetListeners(event)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private func notifyReloadedListeners() {
|
|
827
|
+
plugin.notifyReloadedListeners()
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private func addBlockedBundleId(_ bundleId: String) {
|
|
831
|
+
var blockedList: [String] = []
|
|
832
|
+
|
|
833
|
+
// Parse existing blocked IDs
|
|
834
|
+
if let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty {
|
|
835
|
+
blockedList = blockedIds.split(separator: ",").map(String.init)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Skip if already blocked
|
|
839
|
+
if blockedList.contains(bundleId) {
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Remove oldest if limit reached
|
|
844
|
+
if blockedList.count >= 100 {
|
|
845
|
+
blockedList.removeFirst()
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Add new bundle
|
|
849
|
+
blockedList.append(bundleId)
|
|
850
|
+
|
|
851
|
+
// Save back to preferences
|
|
852
|
+
let newBlockedIds = blockedList.joined(separator: ",")
|
|
853
|
+
preferences.setBlockedBundleIds(newBlockedIds)
|
|
854
|
+
|
|
855
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle blocked: ", bundleId)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private func isBlockedBundleId(_ bundleId: String) -> Bool {
|
|
859
|
+
guard let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty else {
|
|
860
|
+
return false
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
let blockedList = blockedIds.split(separator: ",").map(String.init)
|
|
864
|
+
return blockedList.contains(bundleId)
|
|
667
865
|
}
|
|
668
866
|
|
|
669
867
|
private func setNextCapacitorServerPath(path: String) {
|
|
@@ -676,12 +874,32 @@ import CommonCrypto
|
|
|
676
874
|
}
|
|
677
875
|
}
|
|
678
876
|
|
|
679
|
-
private func
|
|
680
|
-
let
|
|
681
|
-
|
|
877
|
+
private func checkAndResetConfigIfVersionChanged() {
|
|
878
|
+
let currentVersionCode = getVersionCode()
|
|
879
|
+
let currentVersionName = getVersionName()
|
|
880
|
+
let lastVersionCode = preferences.getLastVersionCode()
|
|
881
|
+
let lastVersionName = preferences.getLastVersionName()
|
|
882
|
+
|
|
883
|
+
if lastVersionCode == nil || lastVersionName == nil || lastVersionCode != currentVersionCode || lastVersionName != currentVersionName {
|
|
884
|
+
CAPLog.print(
|
|
885
|
+
"[", LiveUpdatePlugin.tag, "] ",
|
|
886
|
+
"App version changed (last: \(lastVersionName ?? "nil")/\(lastVersionCode ?? "nil"), current: \(currentVersionName)/\(currentVersionCode)), resetting config."
|
|
887
|
+
)
|
|
888
|
+
resetConfig()
|
|
889
|
+
preferences.setLastVersionCode(currentVersionCode)
|
|
890
|
+
preferences.setLastVersionName(currentVersionName)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/// - Parameter bundleId: The bundle ID to save as the previous bundle ID. If `nil`, the value will be removed.
|
|
895
|
+
private func setPreviousBundleId(bundleId: String?) {
|
|
896
|
+
preferences.setPreviousBundleId(bundleId)
|
|
682
897
|
}
|
|
683
898
|
|
|
684
899
|
private func startRollbackTimer() {
|
|
900
|
+
guard config.readyTimeout > 0 else {
|
|
901
|
+
return
|
|
902
|
+
}
|
|
685
903
|
stopRollbackTimer()
|
|
686
904
|
rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
|
|
687
905
|
self?.rollback()
|
|
@@ -693,6 +911,18 @@ import CommonCrypto
|
|
|
693
911
|
rollbackDispatchWorkItem?.cancel()
|
|
694
912
|
}
|
|
695
913
|
|
|
914
|
+
private func tryCopyCurrentBundleFile(
|
|
915
|
+
fileToCopy: ManifestItem,
|
|
916
|
+
toDirectory: URL
|
|
917
|
+
) -> Bool {
|
|
918
|
+
do {
|
|
919
|
+
try copyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
|
|
920
|
+
return true
|
|
921
|
+
} catch {
|
|
922
|
+
return false
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
696
926
|
private func unzipFile(zipFile: URL) -> URL {
|
|
697
927
|
let destinationDirectory = zipFile.deletingPathExtension()
|
|
698
928
|
SSZipArchive.unzipFile(atPath: zipFile.path, toDestination: destinationDirectory.path)
|
|
@@ -788,27 +1018,6 @@ import CommonCrypto
|
|
|
788
1018
|
}
|
|
789
1019
|
return verificationResult
|
|
790
1020
|
}
|
|
791
|
-
|
|
792
|
-
private func wasUpdated() -> Bool {
|
|
793
|
-
guard let lastBundleShortVersionString = preferences.getLastBundleShortVersionString() else {
|
|
794
|
-
// If the last bundle short version is not set, the app may have been updated
|
|
795
|
-
return true
|
|
796
|
-
}
|
|
797
|
-
guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
|
|
798
|
-
// If the last bundle version is not set, the app may have been updated
|
|
799
|
-
return true
|
|
800
|
-
}
|
|
801
|
-
guard let currentBundleShortVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
|
|
802
|
-
// If the current bundle short version is not set, return true for safety reasons
|
|
803
|
-
return true
|
|
804
|
-
}
|
|
805
|
-
guard let currentBundleVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
806
|
-
// If the current bundle version is not set, return true for safety reasons
|
|
807
|
-
return true
|
|
808
|
-
}
|
|
809
|
-
// `CFBundleVersion` is not unique across different versions of the app
|
|
810
|
-
return lastBundleShortVersionString + "_" + lastBundleVersionString != currentBundleShortVersionString + "_" + currentBundleVersionString
|
|
811
|
-
}
|
|
812
1021
|
}
|
|
813
1022
|
|
|
814
1023
|
extension DispatchQueue {
|
|
@@ -823,3 +1032,10 @@ extension DispatchQueue {
|
|
|
823
1032
|
}
|
|
824
1033
|
}
|
|
825
1034
|
}
|
|
1035
|
+
|
|
1036
|
+
extension Progress {
|
|
1037
|
+
convenience init(totalUnitCount: Int64, completedUnitCount: Int64) {
|
|
1038
|
+
self.init(totalUnitCount: totalUnitCount)
|
|
1039
|
+
self.completedUnitCount = completedUnitCount
|
|
1040
|
+
}
|
|
1041
|
+
}
|