@capawesome/capacitor-live-update 6.7.2 → 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 -83
- package/android/build.gradle +12 -12
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +758 -273
- 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 +212 -68
- 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 +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/{results/GetBundleResult.java → events/NextBundleSetEvent.java} +9 -7
- 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/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 +29 -1
- 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 +630 -103
- package/dist/esm/definitions.d.ts +365 -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/DownloadBundleOptions.swift +7 -1
- 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} +16 -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 +370 -151
- package/ios/Plugin/LiveUpdateConfig.swift +4 -4
- package/ios/Plugin/LiveUpdateHttpClient.swift +10 -4
- package/ios/Plugin/LiveUpdatePlugin.swift +138 -35
- package/ios/Plugin/LiveUpdatePreferences.swift +56 -14
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +3 -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)
|
|
@@ -65,10 +68,11 @@ import CommonCrypto
|
|
|
65
68
|
let artifactType = options.getArtifactType()
|
|
66
69
|
let bundleId = options.getBundleId()
|
|
67
70
|
let checksum = options.getChecksum()
|
|
71
|
+
let signature = options.getSignature()
|
|
68
72
|
let url = options.getUrl()
|
|
69
73
|
|
|
70
74
|
// Check if the bundle already exists
|
|
71
|
-
if
|
|
75
|
+
if hasBundleById(bundleId) {
|
|
72
76
|
throw CustomError.bundleAlreadyExists
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -76,41 +80,51 @@ import CommonCrypto
|
|
|
76
80
|
if artifactType == .manifest {
|
|
77
81
|
try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
|
|
78
82
|
} else {
|
|
79
|
-
try await downloadBundleOfTypeZip(bundleId: bundleId,
|
|
83
|
+
try await downloadBundleOfTypeZip(bundleId: bundleId, checksum: checksum, signature: signature, url: url)
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
@objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
|
|
84
88
|
let response: GetLatestBundleResponse? = try await self.fetchLatestBundle(options)
|
|
85
|
-
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, downloadUrl: response?.url)
|
|
89
|
+
return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, checksum: response?.checksum, customProperties: response?.customProperties, downloadUrl: response?.url, signature: response?.signature)
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
@objc public func
|
|
89
|
-
var
|
|
90
|
-
if
|
|
91
|
-
|
|
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)
|
|
92
96
|
}
|
|
93
|
-
let result =
|
|
97
|
+
let result = GetBlockedBundlesResult(bundleIds: bundleIds)
|
|
94
98
|
completion(result, nil)
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
@objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
98
|
-
let bundleIds =
|
|
102
|
+
let bundleIds = getDownloadedBundleIds()
|
|
99
103
|
let result = GetBundlesResult(bundleIds: bundleIds)
|
|
100
104
|
completion(result, nil)
|
|
101
105
|
}
|
|
102
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
|
+
|
|
103
113
|
@objc public func getChannel(completion: @escaping (Result?, Error?) -> Void) {
|
|
104
114
|
let channel = getChannel()
|
|
105
115
|
let result = GetChannelResult(channel: channel)
|
|
106
116
|
completion(result, nil)
|
|
107
117
|
}
|
|
108
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
|
+
|
|
109
126
|
@objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
110
|
-
|
|
111
|
-
if bundleId == defaultWebAssetDir {
|
|
112
|
-
bundleId = nil
|
|
113
|
-
}
|
|
127
|
+
let bundleId = getCurrentBundleId()
|
|
114
128
|
let result = GetCurrentBundleResult(bundleId: bundleId)
|
|
115
129
|
completion(result, nil)
|
|
116
130
|
}
|
|
@@ -129,10 +143,7 @@ import CommonCrypto
|
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
@objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
132
|
-
|
|
133
|
-
if bundleId == defaultWebAssetDir {
|
|
134
|
-
bundleId = nil
|
|
135
|
-
}
|
|
146
|
+
let bundleId: String? = getNextBundleId()
|
|
136
147
|
let result = GetNextBundleResult(bundleId: bundleId)
|
|
137
148
|
completion(result, nil)
|
|
138
149
|
}
|
|
@@ -149,12 +160,48 @@ import CommonCrypto
|
|
|
149
160
|
completion(result, nil)
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
@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) {
|
|
153
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
|
|
154
186
|
stopRollbackTimer()
|
|
187
|
+
// Delete unused bundles
|
|
155
188
|
if config.autoDeleteBundles {
|
|
156
189
|
deleteUnusedBundles()
|
|
157
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
|
|
158
205
|
}
|
|
159
206
|
|
|
160
207
|
@objc public func reload() {
|
|
@@ -164,21 +211,11 @@ import CommonCrypto
|
|
|
164
211
|
}
|
|
165
212
|
|
|
166
213
|
@objc public func reset() {
|
|
167
|
-
self.
|
|
214
|
+
self.setNextBundleById(nil)
|
|
168
215
|
}
|
|
169
216
|
|
|
170
|
-
@objc public func
|
|
171
|
-
|
|
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)
|
|
217
|
+
@objc public func resetConfig() {
|
|
218
|
+
preferences.setAppId(nil)
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
@objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
|
|
@@ -188,6 +225,11 @@ import CommonCrypto
|
|
|
188
225
|
completion(nil)
|
|
189
226
|
}
|
|
190
227
|
|
|
228
|
+
@objc public func setConfig(_ options: SetConfigOptions) {
|
|
229
|
+
let appId = options.getAppId()
|
|
230
|
+
preferences.setAppId(appId)
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
@objc public func setCustomId(_ options: SetCustomIdOptions, completion: @escaping (Error?) -> Void) {
|
|
192
234
|
let customId = options.getCustomId()
|
|
193
235
|
|
|
@@ -199,8 +241,8 @@ import CommonCrypto
|
|
|
199
241
|
let bundleId = options.getBundleId()
|
|
200
242
|
|
|
201
243
|
if let bundleId = bundleId {
|
|
202
|
-
if
|
|
203
|
-
|
|
244
|
+
if hasBundleById(bundleId) {
|
|
245
|
+
setNextBundleById(bundleId)
|
|
204
246
|
} else {
|
|
205
247
|
let error = CustomError.bundleNotFound
|
|
206
248
|
completion(error)
|
|
@@ -214,6 +256,14 @@ import CommonCrypto
|
|
|
214
256
|
}
|
|
215
257
|
|
|
216
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
|
+
|
|
217
267
|
let channel = options.getChannel()
|
|
218
268
|
// Fetch the latest bundle
|
|
219
269
|
let fetchLatestBundleOptions = FetchLatestBundleOptions(channel: channel)
|
|
@@ -223,14 +273,21 @@ import CommonCrypto
|
|
|
223
273
|
}
|
|
224
274
|
let artifactType = response.artifactType
|
|
225
275
|
let latestBundleId = response.bundleId
|
|
276
|
+
let checksum = response.checksum
|
|
277
|
+
let signature = response.signature
|
|
226
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
|
+
}
|
|
227
284
|
// Check if bundle already exists
|
|
228
|
-
if
|
|
285
|
+
if hasBundleById(latestBundleId) {
|
|
229
286
|
var nextBundleId: String?
|
|
230
287
|
let currentBundleId = self.getCurrentBundleId()
|
|
231
288
|
if latestBundleId != currentBundleId {
|
|
232
289
|
// Set the next bundle
|
|
233
|
-
|
|
290
|
+
setNextBundleById(latestBundleId)
|
|
234
291
|
nextBundleId = latestBundleId
|
|
235
292
|
}
|
|
236
293
|
return SyncResult(nextBundleId: nextBundleId)
|
|
@@ -239,10 +296,10 @@ import CommonCrypto
|
|
|
239
296
|
if artifactType == .manifest {
|
|
240
297
|
try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
|
|
241
298
|
} else {
|
|
242
|
-
try await downloadBundleOfTypeZip(bundleId: latestBundleId,
|
|
299
|
+
try await downloadBundleOfTypeZip(bundleId: latestBundleId, checksum: checksum, signature: signature, url: downloadUrl)
|
|
243
300
|
}
|
|
244
301
|
// Set the next bundle
|
|
245
|
-
|
|
302
|
+
setNextBundleById(latestBundleId)
|
|
246
303
|
return SyncResult(nextBundleId: latestBundleId)
|
|
247
304
|
}
|
|
248
305
|
|
|
@@ -271,12 +328,12 @@ import CommonCrypto
|
|
|
271
328
|
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
272
329
|
}
|
|
273
330
|
|
|
274
|
-
private func buildCapacitorServerPathFor(bundleId: String) -> String {
|
|
331
|
+
private func buildCapacitorServerPathFor(bundleId: String?) -> String {
|
|
275
332
|
let path: String
|
|
276
|
-
if bundleId
|
|
277
|
-
path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
|
|
278
|
-
} else {
|
|
333
|
+
if let bundleId = bundleId {
|
|
279
334
|
path = buildBundlePathFor(bundleId: bundleId)
|
|
335
|
+
} else {
|
|
336
|
+
path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
|
|
280
337
|
}
|
|
281
338
|
return path
|
|
282
339
|
}
|
|
@@ -290,32 +347,41 @@ import CommonCrypto
|
|
|
290
347
|
return url
|
|
291
348
|
}
|
|
292
349
|
|
|
293
|
-
private func copyCurrentBundleFile(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
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)
|
|
299
353
|
let parentDirectory = destination.deletingLastPathComponent()
|
|
300
354
|
try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
301
355
|
|
|
302
356
|
let sourceURL: URL
|
|
303
|
-
if currentBundleId
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
306
364
|
}
|
|
307
365
|
sourceURL = file
|
|
308
|
-
} else {
|
|
309
|
-
sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
|
|
310
366
|
}
|
|
311
367
|
|
|
312
368
|
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
|
313
369
|
}
|
|
314
370
|
|
|
315
|
-
private func
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|
|
318
383
|
}
|
|
384
|
+
return missingItems
|
|
319
385
|
}
|
|
320
386
|
|
|
321
387
|
private func createBundlesDirectory() {
|
|
@@ -336,26 +402,26 @@ import CommonCrypto
|
|
|
336
402
|
return temporaryDirectory
|
|
337
403
|
}
|
|
338
404
|
|
|
339
|
-
private func
|
|
405
|
+
private func deleteBundleById(_ bundleId: String) throws {
|
|
340
406
|
// Delete the bundle directory
|
|
341
407
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
342
408
|
try FileManager.default.removeItem(atPath: path)
|
|
343
409
|
// Reset the next bundle if it is the deleted bundle
|
|
344
410
|
let nextBundleId = getNextBundleId()
|
|
345
411
|
if bundleId == nextBundleId {
|
|
346
|
-
|
|
412
|
+
setNextBundleById(nil)
|
|
347
413
|
}
|
|
348
414
|
}
|
|
349
415
|
|
|
350
416
|
private func deleteUnusedBundles() {
|
|
351
|
-
let bundleIds =
|
|
417
|
+
let bundleIds = getDownloadedBundleIds()
|
|
352
418
|
let currentBundleId = getCurrentBundleId()
|
|
353
419
|
let nextBundleId = getNextBundleId()
|
|
354
420
|
|
|
355
421
|
for bundleId in bundleIds {
|
|
356
422
|
if bundleId != currentBundleId && bundleId != nextBundleId {
|
|
357
423
|
do {
|
|
358
|
-
try
|
|
424
|
+
try deleteBundleById(bundleId)
|
|
359
425
|
} catch {
|
|
360
426
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
|
|
361
427
|
}
|
|
@@ -363,12 +429,12 @@ import CommonCrypto
|
|
|
363
429
|
}
|
|
364
430
|
}
|
|
365
431
|
|
|
366
|
-
private func downloadAndVerifyFile(url: String, file: URL, checksum: String?) async throws {
|
|
432
|
+
private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
|
|
367
433
|
let destination: DownloadRequest.Destination = { _, _ in
|
|
368
434
|
return (file, [.createIntermediateDirectories])
|
|
369
435
|
}
|
|
370
436
|
let urlComponents = URLComponents(string: url)!
|
|
371
|
-
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)
|
|
372
438
|
if let error = result.error {
|
|
373
439
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
|
|
374
440
|
if let urlError = error.underlyingError as? URLError {
|
|
@@ -381,31 +447,59 @@ import CommonCrypto
|
|
|
381
447
|
guard let response = result.response else {
|
|
382
448
|
throw CustomError.unknown
|
|
383
449
|
}
|
|
384
|
-
let checksum = checksum
|
|
385
|
-
let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
|
|
450
|
+
let checksum = checksum ?? LiveUpdateHttpClient.getChecksumFromResponse(response: response)
|
|
451
|
+
let signature = signature ?? LiveUpdateHttpClient.getSignatureFromResponse(response: response)
|
|
386
452
|
try verifyFile(url: file, checksum: checksum, signature: signature)
|
|
387
453
|
}
|
|
388
454
|
|
|
389
|
-
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 {
|
|
390
456
|
let fileURL = directory.appendingPathComponent(href)
|
|
391
457
|
var parameters = [String: String]()
|
|
392
458
|
parameters["href"] = href
|
|
393
459
|
var urlComponents = URLComponents(string: baseUrl)!
|
|
394
460
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
395
461
|
let url = urlComponents.string!
|
|
396
|
-
try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil)
|
|
462
|
+
try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil, signature: nil, callback: callback)
|
|
397
463
|
return fileURL
|
|
398
464
|
}
|
|
399
465
|
|
|
400
|
-
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()
|
|
401
475
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
402
|
-
for
|
|
476
|
+
for fileToDownload in filesToDownload {
|
|
403
477
|
group.addTask {
|
|
404
|
-
_ = 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
|
+
}
|
|
405
495
|
}
|
|
406
496
|
}
|
|
407
|
-
|
|
408
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
|
+
}
|
|
409
503
|
}
|
|
410
504
|
}
|
|
411
505
|
|
|
@@ -413,7 +507,7 @@ import CommonCrypto
|
|
|
413
507
|
// Create a temporary directory
|
|
414
508
|
let temporaryDirectory = try createTemporaryDirectory()
|
|
415
509
|
// Download the latest manifest
|
|
416
|
-
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)
|
|
417
511
|
let latestManifest = try loadManifest(file: latestManifestFile)
|
|
418
512
|
// Load the current manifest
|
|
419
513
|
let currentManifest = try loadCurrentManifest()
|
|
@@ -427,18 +521,28 @@ import CommonCrypto
|
|
|
427
521
|
itemsToDownload.append(contentsOf: latestManifest.items)
|
|
428
522
|
}
|
|
429
523
|
// Copy the files
|
|
430
|
-
|
|
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
|
+
}
|
|
431
529
|
// Download the files
|
|
432
|
-
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
|
+
})
|
|
433
534
|
// Add the bundle
|
|
434
535
|
try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
|
|
435
536
|
}
|
|
436
537
|
|
|
437
|
-
private func downloadBundleOfTypeZip(bundleId: String,
|
|
538
|
+
private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
|
|
438
539
|
let timestamp = String(Int(Date().timeIntervalSince1970))
|
|
439
540
|
let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
|
|
440
541
|
// Download the bundle
|
|
441
|
-
try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum
|
|
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
|
+
})
|
|
442
546
|
// Add the bundle
|
|
443
547
|
try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
|
|
444
548
|
}
|
|
@@ -455,7 +559,7 @@ import CommonCrypto
|
|
|
455
559
|
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
456
560
|
parameters["platform"] = "1"
|
|
457
561
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
458
|
-
var urlComponents = URLComponents(string: "https://\(
|
|
562
|
+
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
|
|
459
563
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
460
564
|
let url = try urlComponents.asURL()
|
|
461
565
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
|
|
@@ -478,7 +582,7 @@ import CommonCrypto
|
|
|
478
582
|
}
|
|
479
583
|
}
|
|
480
584
|
|
|
481
|
-
private func
|
|
585
|
+
private func getDownloadedBundleIds() -> [String] {
|
|
482
586
|
let url = libraryDirectoryUrl.appendingPathComponent(bundlesDirectory)
|
|
483
587
|
do {
|
|
484
588
|
let pathExists = FileManager.default.fileExists(atPath: url.path)
|
|
@@ -492,6 +596,13 @@ import CommonCrypto
|
|
|
492
596
|
}
|
|
493
597
|
}
|
|
494
598
|
|
|
599
|
+
private func getAppId() -> String? {
|
|
600
|
+
if let appId = preferences.getAppId() {
|
|
601
|
+
return appId
|
|
602
|
+
}
|
|
603
|
+
return config.appId
|
|
604
|
+
}
|
|
605
|
+
|
|
495
606
|
private func getChannel() -> String? {
|
|
496
607
|
var channel: String?
|
|
497
608
|
if let _ = config.defaultChannel {
|
|
@@ -517,12 +628,16 @@ import CommonCrypto
|
|
|
517
628
|
return digest.map { String(format: "%02hhx", $0) }.joined()
|
|
518
629
|
}
|
|
519
630
|
|
|
520
|
-
/// - 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.
|
|
521
632
|
private func getCurrentBundleId() -> String? {
|
|
522
633
|
guard let path = getCurrentCapacitorServerPath() else {
|
|
523
634
|
return nil
|
|
524
635
|
}
|
|
525
|
-
|
|
636
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
637
|
+
if bundleId == defaultWebAssetDir {
|
|
638
|
+
return nil
|
|
639
|
+
}
|
|
640
|
+
return bundleId
|
|
526
641
|
}
|
|
527
642
|
|
|
528
643
|
/// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
|
|
@@ -538,21 +653,30 @@ import CommonCrypto
|
|
|
538
653
|
return deviceId.lowercased()
|
|
539
654
|
}
|
|
540
655
|
|
|
541
|
-
/// - Returns: The next bundle ID
|
|
542
|
-
private func getNextBundleId() -> String {
|
|
656
|
+
/// - Returns: The next bundle ID or `nil` if the default bundle will be used.
|
|
657
|
+
private func getNextBundleId() -> String? {
|
|
543
658
|
let path = getNextCapacitorServerPath()
|
|
544
|
-
|
|
659
|
+
let bundleId = URL(fileURLWithPath: path).lastPathComponent
|
|
660
|
+
if bundleId == defaultWebAssetDir {
|
|
661
|
+
return nil
|
|
662
|
+
}
|
|
663
|
+
return bundleId
|
|
545
664
|
}
|
|
546
665
|
|
|
547
666
|
/// - Returns: The absolute path to the next bundle directory.
|
|
548
667
|
private func getNextCapacitorServerPath() -> String {
|
|
549
|
-
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId:
|
|
668
|
+
let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
|
|
550
669
|
if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
|
|
551
670
|
return path.isEmpty ? defaultCapacitorServerPath : path
|
|
552
671
|
}
|
|
553
672
|
return defaultCapacitorServerPath
|
|
554
673
|
}
|
|
555
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
|
+
|
|
556
680
|
private func getVersionCode() -> String {
|
|
557
681
|
guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
558
682
|
return ""
|
|
@@ -567,27 +691,24 @@ import CommonCrypto
|
|
|
567
691
|
return appVersionName
|
|
568
692
|
}
|
|
569
693
|
|
|
570
|
-
private func
|
|
694
|
+
private func hasBundleById(_ bundleId: String) -> Bool {
|
|
571
695
|
let path = buildBundlePathFor(bundleId: bundleId)
|
|
572
696
|
return FileManager.default.fileExists(atPath: path)
|
|
573
697
|
}
|
|
574
698
|
|
|
575
699
|
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 {
|
|
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 {
|
|
583
704
|
return try loadManifest(file: manifestFileUrl)
|
|
584
705
|
} else {
|
|
585
706
|
return nil
|
|
586
707
|
}
|
|
587
708
|
} else {
|
|
588
|
-
let
|
|
589
|
-
let
|
|
590
|
-
if
|
|
709
|
+
let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
|
|
710
|
+
let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
|
|
711
|
+
if let manifestFileUrl = manifestFileUrl {
|
|
591
712
|
return try loadManifest(file: manifestFileUrl)
|
|
592
713
|
} else {
|
|
593
714
|
return nil
|
|
@@ -602,21 +723,48 @@ import CommonCrypto
|
|
|
602
723
|
return Manifest(items: items)
|
|
603
724
|
}
|
|
604
725
|
|
|
605
|
-
private func
|
|
606
|
-
|
|
607
|
-
|
|
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.")
|
|
608
735
|
return
|
|
609
736
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
}
|
|
613
752
|
}
|
|
614
753
|
|
|
615
|
-
private func
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
}
|
|
620
768
|
}
|
|
621
769
|
|
|
622
770
|
private func searchIndexHtmlFile(url: URL) -> URL? {
|
|
@@ -646,21 +794,74 @@ import CommonCrypto
|
|
|
646
794
|
return nil
|
|
647
795
|
}
|
|
648
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
|
+
|
|
649
803
|
private func setCurrentCapacitorServerPath(path: String) {
|
|
650
804
|
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
651
805
|
return
|
|
652
806
|
}
|
|
653
807
|
viewController.setServerBasePath(path: path)
|
|
808
|
+
// Notify listeners
|
|
809
|
+
notifyReloadedListeners()
|
|
654
810
|
}
|
|
655
811
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
setCurrentCapacitorServerPath(path: path)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
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?) {
|
|
662
814
|
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
663
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)
|
|
664
865
|
}
|
|
665
866
|
|
|
666
867
|
private func setNextCapacitorServerPath(path: String) {
|
|
@@ -673,12 +874,32 @@ import CommonCrypto
|
|
|
673
874
|
}
|
|
674
875
|
}
|
|
675
876
|
|
|
676
|
-
private func
|
|
677
|
-
let
|
|
678
|
-
|
|
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)
|
|
679
897
|
}
|
|
680
898
|
|
|
681
899
|
private func startRollbackTimer() {
|
|
900
|
+
guard config.readyTimeout > 0 else {
|
|
901
|
+
return
|
|
902
|
+
}
|
|
682
903
|
stopRollbackTimer()
|
|
683
904
|
rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
|
|
684
905
|
self?.rollback()
|
|
@@ -690,6 +911,18 @@ import CommonCrypto
|
|
|
690
911
|
rollbackDispatchWorkItem?.cancel()
|
|
691
912
|
}
|
|
692
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
|
+
|
|
693
926
|
private func unzipFile(zipFile: URL) -> URL {
|
|
694
927
|
let destinationDirectory = zipFile.deletingPathExtension()
|
|
695
928
|
SSZipArchive.unzipFile(atPath: zipFile.path, toDestination: destinationDirectory.path)
|
|
@@ -785,27 +1018,6 @@ import CommonCrypto
|
|
|
785
1018
|
}
|
|
786
1019
|
return verificationResult
|
|
787
1020
|
}
|
|
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
1021
|
}
|
|
810
1022
|
|
|
811
1023
|
extension DispatchQueue {
|
|
@@ -820,3 +1032,10 @@ extension DispatchQueue {
|
|
|
820
1032
|
}
|
|
821
1033
|
}
|
|
822
1034
|
}
|
|
1035
|
+
|
|
1036
|
+
extension Progress {
|
|
1037
|
+
convenience init(totalUnitCount: Int64, completedUnitCount: Int64) {
|
|
1038
|
+
self.init(totalUnitCount: totalUnitCount)
|
|
1039
|
+
self.completedUnitCount = completedUnitCount
|
|
1040
|
+
}
|
|
1041
|
+
}
|