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