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