@capgo/capacitor-updater 8.0.0 → 8.1.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.
Files changed (64) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +37 -0
  3. package/README.md +1461 -231
  4. package/android/build.gradle +29 -12
  5. package/android/proguard-rules.pro +45 -0
  6. package/android/src/main/AndroidManifest.xml +0 -1
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +223 -195
  8. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
  9. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +13 -0
  10. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +2159 -1234
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1507 -0
  12. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +330 -121
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +43 -49
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  17. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +808 -117
  19. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +156 -0
  20. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +32 -0
  21. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  22. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  23. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  24. package/dist/docs.json +2187 -625
  25. package/dist/esm/definitions.d.ts +1286 -249
  26. package/dist/esm/definitions.js.map +1 -1
  27. package/dist/esm/history.d.ts +1 -0
  28. package/dist/esm/history.js +283 -0
  29. package/dist/esm/history.js.map +1 -0
  30. package/dist/esm/index.d.ts +3 -2
  31. package/dist/esm/index.js +5 -4
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/web.d.ts +36 -41
  34. package/dist/esm/web.js +94 -35
  35. package/dist/esm/web.js.map +1 -1
  36. package/dist/plugin.cjs.js +376 -35
  37. package/dist/plugin.cjs.js.map +1 -1
  38. package/dist/plugin.js +376 -35
  39. package/dist/plugin.js.map +1 -1
  40. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
  41. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  42. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +37 -10
  43. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +1 -1
  44. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1605 -0
  45. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1526 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +267 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  48. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  49. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +311 -0
  50. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  51. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  52. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  53. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  54. package/package.json +41 -35
  55. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +0 -1130
  56. package/ios/Plugin/CapacitorUpdater.swift +0 -858
  57. package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
  58. package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
  59. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -675
  60. package/ios/Plugin/CryptoCipher.swift +0 -240
  61. /package/{LICENCE → LICENSE} +0 -0
  62. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  63. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  64. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -1,858 +0,0 @@
1
- /*
2
- * This Source Code Form is subject to the terms of the Mozilla Public
3
- * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
- */
6
-
7
- import Foundation
8
- import SSZipArchive
9
- import Alamofire
10
- import zlib
11
-
12
- extension URL {
13
- var isDirectory: Bool {
14
- (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
15
- }
16
- var exist: Bool {
17
- return FileManager().fileExists(atPath: self.path)
18
- }
19
- }
20
- extension Date {
21
- func adding(minutes: Int) -> Date {
22
- return Calendar.current.date(byAdding: .minute, value: minutes, to: self)!
23
- }
24
- }
25
- struct SetChannelDec: Decodable {
26
- let status: String?
27
- let error: String?
28
- let message: String?
29
- }
30
- public class SetChannel: NSObject {
31
- var status: String = ""
32
- var error: String = ""
33
- var message: String = ""
34
- }
35
- extension SetChannel {
36
- func toDict() -> [String: Any] {
37
- var dict: [String: Any] = [String: Any]()
38
- let otherSelf: Mirror = Mirror(reflecting: self)
39
- for child: Mirror.Child in otherSelf.children {
40
- if let key: String = child.label {
41
- dict[key] = child.value
42
- }
43
- }
44
- return dict
45
- }
46
- }
47
- struct GetChannelDec: Decodable {
48
- let channel: String?
49
- let status: String?
50
- let error: String?
51
- let message: String?
52
- let allowSet: Bool?
53
- }
54
- public class GetChannel: NSObject {
55
- var channel: String = ""
56
- var status: String = ""
57
- var error: String = ""
58
- var message: String = ""
59
- var allowSet: Bool = true
60
- }
61
- extension GetChannel {
62
- func toDict() -> [String: Any] {
63
- var dict: [String: Any] = [String: Any]()
64
- let otherSelf: Mirror = Mirror(reflecting: self)
65
- for child: Mirror.Child in otherSelf.children {
66
- if let key: String = child.label {
67
- dict[key] = child.value
68
- }
69
- }
70
- return dict
71
- }
72
- }
73
- struct InfoObject: Codable {
74
- let platform: String?
75
- let device_id: String?
76
- let app_id: String?
77
- let custom_id: String?
78
- let version_build: String?
79
- let version_code: String?
80
- let version_os: String?
81
- let version_name: String?
82
- let plugin_version: String?
83
- let is_emulator: Bool?
84
- let is_prod: Bool?
85
- var action: String?
86
- var channel: String?
87
- }
88
- struct AppVersionDec: Decodable {
89
- let version: String?
90
- let checksum: String?
91
- let url: String?
92
- let message: String?
93
- let error: String?
94
- let session_key: String?
95
- let major: Bool?
96
- }
97
- public class AppVersion: NSObject {
98
- var version: String = ""
99
- var checksum: String = ""
100
- var url: String = ""
101
- var message: String?
102
- var error: String?
103
- var sessionKey: String?
104
- var major: Bool?
105
- }
106
-
107
- extension AppVersion {
108
- func toDict() -> [String: Any] {
109
- var dict: [String: Any] = [String: Any]()
110
- let otherSelf: Mirror = Mirror(reflecting: self)
111
- for child: Mirror.Child in otherSelf.children {
112
- if let key: String = child.label {
113
- dict[key] = child.value
114
- }
115
- }
116
- return dict
117
- }
118
- }
119
-
120
- extension OperatingSystemVersion {
121
- func getFullVersion(separator: String = ".") -> String {
122
- return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
123
- }
124
- }
125
- extension Bundle {
126
- var versionName: String? {
127
- return infoDictionary?["CFBundleShortVersionString"] as? String
128
- }
129
- var versionCode: String? {
130
- return infoDictionary?["CFBundleVersion"] as? String
131
- }
132
- }
133
-
134
- extension ISO8601DateFormatter {
135
- convenience init(_ formatOptions: Options) {
136
- self.init()
137
- self.formatOptions = formatOptions
138
- }
139
- }
140
- extension Formatter {
141
- static let iso8601withFractionalSeconds: ISO8601DateFormatter = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
142
- }
143
- extension Date {
144
- var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
145
- }
146
- extension String {
147
-
148
- var fileURL: URL {
149
- return URL(fileURLWithPath: self)
150
- }
151
-
152
- var lastPathComponent: String {
153
- get {
154
- return fileURL.lastPathComponent
155
- }
156
- }
157
- var iso8601withFractionalSeconds: Date? {
158
- return Formatter.iso8601withFractionalSeconds.date(from: self)
159
- }
160
- func trim(using characterSet: CharacterSet = .whitespacesAndNewlines) -> String {
161
- return trimmingCharacters(in: characterSet)
162
- }
163
- }
164
-
165
- enum CustomError: Error {
166
- // Throw when an unzip fail
167
- case cannotUnzip
168
- case cannotWrite
169
- case cannotDecode
170
- case cannotUnflat
171
- case cannotCreateDirectory
172
- case cannotDeleteDirectory
173
-
174
- // Throw in all other cases
175
- case unexpected(code: Int)
176
- }
177
-
178
- extension CustomError: LocalizedError {
179
- public var errorDescription: String? {
180
- switch self {
181
- case .cannotUnzip:
182
- return NSLocalizedString(
183
- "The file cannot be unzip",
184
- comment: "Invalid zip"
185
- )
186
- case .cannotCreateDirectory:
187
- return NSLocalizedString(
188
- "The folder cannot be created",
189
- comment: "Invalid folder"
190
- )
191
- case .cannotDeleteDirectory:
192
- return NSLocalizedString(
193
- "The folder cannot be deleted",
194
- comment: "Invalid folder"
195
- )
196
- case .cannotUnflat:
197
- return NSLocalizedString(
198
- "The file cannot be unflat",
199
- comment: "Invalid folder"
200
- )
201
- case .unexpected:
202
- return NSLocalizedString(
203
- "An unexpected error occurred.",
204
- comment: "Unexpected Error"
205
- )
206
- case .cannotDecode:
207
- return NSLocalizedString(
208
- "Decoding the zip failed with this key",
209
- comment: "Invalid private key"
210
- )
211
- case .cannotWrite:
212
- return NSLocalizedString(
213
- "Cannot write to the destination",
214
- comment: "Invalid destination"
215
- )
216
- }
217
- }
218
- }
219
-
220
- @objc public class CapacitorUpdater: NSObject {
221
-
222
- private let versionCode: String = Bundle.main.versionCode ?? ""
223
- private let versionOs = UIDevice.current.systemVersion
224
- private let documentsDir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
225
- private let libraryDir: URL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
226
- private let bundleDirectoryHot: String = "versions"
227
- private let DEFAULT_FOLDER: String = ""
228
- private let bundleDirectory: String = "NoCloud/ionic_built_snapshots"
229
- private let INFO_SUFFIX: String = "_info"
230
- private let FALLBACK_VERSION: String = "pastVersion"
231
- private let NEXT_VERSION: String = "nextVersion"
232
-
233
- public let TAG: String = "✨ Capacitor-updater:"
234
- public let CAP_SERVER_PATH: String = "serverBasePath"
235
- public var versionName: String = ""
236
- public var customId: String = ""
237
- public var PLUGIN_VERSION: String = ""
238
- public let timeout: Double = 20
239
- public var statsUrl: String = ""
240
- public var channelUrl: String = ""
241
- public var appId: String = ""
242
- public var deviceID = UIDevice.current.identifierForVendor?.uuidString ?? ""
243
- public var privateKey: String = ""
244
-
245
- public var notifyDownload: (String, Int) -> Void = { _, _ in }
246
-
247
- private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
248
- return (percent * (max - min)) / 100 + min
249
- }
250
-
251
- private func randomString(length: Int) -> String {
252
- let letters: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
253
- return String((0..<length).map { _ in letters.randomElement()! })
254
- }
255
-
256
- private var isDevEnvironment: Bool {
257
- #if DEBUG
258
- return true
259
- #else
260
- return false
261
- #endif
262
- }
263
-
264
- private func isProd() -> Bool {
265
- return !self.isDevEnvironment && !self.isAppStoreReceiptSandbox() && !self.hasEmbeddedMobileProvision()
266
- }
267
-
268
- // MARK: Private
269
- private func hasEmbeddedMobileProvision() -> Bool {
270
- guard Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") == nil else {
271
- return true
272
- }
273
- return false
274
- }
275
-
276
- private func isAppStoreReceiptSandbox() -> Bool {
277
-
278
- if isEmulator() {
279
- return false
280
- } else {
281
- guard let url: URL = Bundle.main.appStoreReceiptURL else {
282
- return false
283
- }
284
- guard url.lastPathComponent == "sandboxReceipt" else {
285
- return false
286
- }
287
- return true
288
- }
289
- }
290
-
291
- private func isEmulator() -> Bool {
292
- #if targetEnvironment(simulator)
293
- return true
294
- #else
295
- return false
296
- #endif
297
- }
298
- // Persistent path /var/mobile/Containers/Data/Application/8C0C07BE-0FD3-4FD4-B7DF-90A88E12B8C3/Library/NoCloud/ionic_built_snapshots/FOLDER
299
- // Hot Reload path /var/mobile/Containers/Data/Application/8C0C07BE-0FD3-4FD4-B7DF-90A88E12B8C3/Documents/FOLDER
300
- // Normal /private/var/containers/Bundle/Application/8C0C07BE-0FD3-4FD4-B7DF-90A88E12B8C3/App.app/public
301
-
302
- private func prepareFolder(source: URL) throws {
303
- if !FileManager.default.fileExists(atPath: source.path) {
304
- do {
305
- try FileManager.default.createDirectory(atPath: source.path, withIntermediateDirectories: true, attributes: nil)
306
- } catch {
307
- print("\(self.TAG) Cannot createDirectory \(source.path)")
308
- throw CustomError.cannotCreateDirectory
309
- }
310
- }
311
- }
312
-
313
- private func deleteFolder(source: URL) throws {
314
- do {
315
- try FileManager.default.removeItem(atPath: source.path)
316
- } catch {
317
- print("\(self.TAG) File not removed. \(source.path)")
318
- throw CustomError.cannotDeleteDirectory
319
- }
320
- }
321
-
322
- private func unflatFolder(source: URL, dest: URL) throws -> Bool {
323
- let index: URL = source.appendingPathComponent("index.html")
324
- do {
325
- let files: [String] = try FileManager.default.contentsOfDirectory(atPath: source.path)
326
- if files.count == 1 && source.appendingPathComponent(files[0]).isDirectory && !FileManager.default.fileExists(atPath: index.path) {
327
- try FileManager.default.moveItem(at: source.appendingPathComponent(files[0]), to: dest)
328
- return true
329
- } else {
330
- try FileManager.default.moveItem(at: source, to: dest)
331
- return false
332
- }
333
- } catch {
334
- print("\(self.TAG) File not moved. source: \(source.path) dest: \(dest.path)")
335
- throw CustomError.cannotUnflat
336
- }
337
- }
338
-
339
- private func getChecksum(filePath: URL) -> String {
340
- do {
341
- let fileData: Data = try Data.init(contentsOf: filePath)
342
- let checksum: uLong = fileData.withUnsafeBytes { crc32(0, $0.bindMemory(to: Bytef.self).baseAddress, uInt(fileData.count)) }
343
- return String(format: "%08X", checksum).lowercased()
344
- } catch {
345
- print("\(self.TAG) Cannot get checksum: \(filePath.path)", error)
346
- return ""
347
- }
348
- }
349
-
350
- private func decryptFile(filePath: URL, sessionKey: String, version: String) throws {
351
- if self.privateKey.isEmpty || sessionKey.isEmpty {
352
- print("\(self.TAG) Cannot found privateKey or sessionKey")
353
- return
354
- }
355
- do {
356
- guard let rsaPrivateKey: RSAPrivateKey = .load(rsaPrivateKey: self.privateKey) else {
357
- print("cannot decode privateKey", self.privateKey)
358
- throw CustomError.cannotDecode
359
- }
360
-
361
- let sessionKeyArray: [String] = sessionKey.components(separatedBy: ":")
362
- guard let ivData: Data = Data(base64Encoded: sessionKeyArray[0]) else {
363
- print("cannot decode sessionKey", sessionKey)
364
- throw CustomError.cannotDecode
365
- }
366
- let sessionKeyDataEncrypted: Data = Data(base64Encoded: sessionKeyArray[1])!
367
- let sessionKeyDataDecrypted: Data = rsaPrivateKey.decrypt(data: sessionKeyDataEncrypted)!
368
- let aesPrivateKey: AES128Key = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
369
- let encryptedData: Data = try Data(contentsOf: filePath)
370
- let decryptedData: Data = aesPrivateKey.decrypt(data: encryptedData)!
371
-
372
- try decryptedData.write(to: filePath)
373
- } catch {
374
- print("\(self.TAG) Cannot decode: \(filePath.path)", error)
375
- self.sendStats(action: "decrypt_fail", versionName: version)
376
- throw CustomError.cannotDecode
377
- }
378
- }
379
-
380
- private func saveDownloaded(sourceZip: URL, id: String, base: URL) throws {
381
- try prepareFolder(source: base)
382
- let destHot: URL = base.appendingPathComponent(id)
383
- let destUnZip: URL = documentsDir.appendingPathComponent(randomString(length: 10))
384
- if !SSZipArchive.unzipFile(atPath: sourceZip.path, toDestination: destUnZip.path) {
385
- throw CustomError.cannotUnzip
386
- }
387
- if try unflatFolder(source: destUnZip, dest: destHot) {
388
- try deleteFolder(source: destUnZip)
389
- }
390
- }
391
-
392
- private func createInfoObject() -> InfoObject {
393
- return InfoObject(
394
- platform: "ios",
395
- device_id: self.deviceID,
396
- app_id: self.appId,
397
- custom_id: self.customId,
398
- version_build: self.versionName,
399
- version_code: self.versionCode,
400
- version_os: self.versionOs,
401
- version_name: self.getCurrentBundle().getVersionName(),
402
- plugin_version: self.PLUGIN_VERSION,
403
- is_emulator: self.isEmulator(),
404
- is_prod: self.isProd(),
405
- action: nil,
406
- channel: nil
407
- )
408
- }
409
-
410
- public func getLatest(url: URL) -> AppVersion {
411
- let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
412
- let latest: AppVersion = AppVersion()
413
- let parameters: InfoObject = self.createInfoObject()
414
- print("\(self.TAG) Auto-update parameters: \(parameters)")
415
- let request = AF.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
416
-
417
- request.validate().responseDecodable(of: AppVersionDec.self) { response in
418
- switch response.result {
419
- case .success:
420
- if let url = response.value?.url {
421
- latest.url = url
422
- }
423
- if let checksum = response.value?.checksum {
424
- latest.checksum = checksum
425
- }
426
- if let version = response.value?.version {
427
- latest.version = version
428
- }
429
- if let major = response.value?.major {
430
- latest.major = major
431
- }
432
- if let error = response.value?.error {
433
- latest.error = error
434
- }
435
- if let message = response.value?.message {
436
- latest.message = message
437
- }
438
- if let sessionKey = response.value?.session_key {
439
- latest.sessionKey = sessionKey
440
- }
441
- case let .failure(error):
442
- print("\(self.TAG) Error getting Latest", response.value ?? "", error )
443
- latest.message = "Error getting Latest \(String(describing: response.value))"
444
- latest.error = "response_error"
445
- }
446
- semaphore.signal()
447
- }
448
- semaphore.wait()
449
- return latest
450
- }
451
-
452
- private func setCurrentBundle(bundle: String) {
453
- UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
454
- UserDefaults.standard.synchronize()
455
- print("\(self.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
456
- }
457
-
458
- public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
459
- let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
460
- let id: String = self.randomString(length: 10)
461
- var checksum: String = ""
462
-
463
- var mainError: NSError?
464
- let destination: DownloadRequest.Destination = { _, _ in
465
- let documentsURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
466
- let fileURL: URL = documentsURL.appendingPathComponent(self.randomString(length: 10))
467
-
468
- return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
469
- }
470
- let request = AF.download(url, to: destination)
471
-
472
- request.downloadProgress { progress in
473
- let percent = self.calcTotalPercent(percent: Int(progress.fractionCompleted * 100), min: 10, max: 70)
474
- self.notifyDownload(id, percent)
475
- }
476
- request.responseURL { (response) in
477
- if let fileURL = response.fileURL {
478
- switch response.result {
479
- case .success:
480
- self.notifyDownload(id, 71)
481
- do {
482
- try self.decryptFile(filePath: fileURL, sessionKey: sessionKey, version: version)
483
- checksum = self.getChecksum(filePath: fileURL)
484
- try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.documentsDir.appendingPathComponent(self.bundleDirectoryHot))
485
- self.notifyDownload(id, 85)
486
- try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory))
487
- self.notifyDownload(id, 100)
488
- try self.deleteFolder(source: fileURL)
489
- } catch {
490
- print("\(self.TAG) download unzip error", error)
491
- mainError = error as NSError
492
- }
493
- case let .failure(error):
494
- print("\(self.TAG) download error", response.value ?? "", error)
495
- mainError = error as NSError
496
- }
497
- }
498
- semaphore.signal()
499
- }
500
- self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
501
- self.notifyDownload(id, 0)
502
- semaphore.wait()
503
- if mainError != nil {
504
- throw mainError!
505
- }
506
- let info: BundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
507
- self.saveBundleInfo(id: id, bundle: info)
508
- return info
509
- }
510
-
511
- public func list() -> [BundleInfo] {
512
- let dest: URL = documentsDir.appendingPathComponent(bundleDirectoryHot)
513
- do {
514
- let files: [String] = try FileManager.default.contentsOfDirectory(atPath: dest.path)
515
- var res: [BundleInfo] = []
516
- print("\(self.TAG) list File : \(dest.path)")
517
- if dest.exist {
518
- for id: String in files {
519
- res.append(self.getBundleInfo(id: id))
520
- }
521
- }
522
- return res
523
- } catch {
524
- print("\(self.TAG) No version available \(dest.path)")
525
- return []
526
- }
527
- }
528
-
529
- public func delete(id: String, removeInfo: Bool) -> Bool {
530
- let deleted: BundleInfo = self.getBundleInfo(id: id)
531
- if deleted.isBuiltin() || self.getCurrentBundleId() == id {
532
- print("\(self.TAG) Cannot delete \(id)")
533
- return false
534
- }
535
- let destHot: URL = documentsDir.appendingPathComponent(bundleDirectoryHot).appendingPathComponent(id)
536
- let destPersist: URL = libraryDir.appendingPathComponent(bundleDirectory).appendingPathComponent(id)
537
- do {
538
- try FileManager.default.removeItem(atPath: destHot.path)
539
- } catch {
540
- print("\(self.TAG) Hot Folder \(destHot.path), not removed.")
541
- }
542
- do {
543
- try FileManager.default.removeItem(atPath: destPersist.path)
544
- } catch {
545
- print("\(self.TAG) Folder \(destPersist.path), not removed.")
546
- return false
547
- }
548
- if removeInfo {
549
- self.removeBundleInfo(id: id)
550
- } else {
551
- self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.localizedString))
552
- }
553
- print("\(self.TAG) bundle delete \(deleted.getVersionName())")
554
- self.sendStats(action: "delete", versionName: deleted.getVersionName())
555
- return true
556
- }
557
-
558
- public func delete(id: String) -> Bool {
559
- return self.delete(id: id, removeInfo: true)
560
- }
561
-
562
- public func getBundleDirectory(id: String) -> URL {
563
- return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
564
- }
565
-
566
- public func set(bundle: BundleInfo) -> Bool {
567
- return self.set(id: bundle.getId())
568
- }
569
-
570
- private func bundleExists(id: String) -> Bool {
571
- let destHot: URL = self.getPathHot(id: id)
572
- let destHotPersist: URL = self.getPathPersist(id: id)
573
- let indexHot: URL = destHot.appendingPathComponent("index.html")
574
- let indexPersist: URL = destHotPersist.appendingPathComponent("index.html")
575
- let url: URL = self.getBundleDirectory(id: id)
576
- let bundleIndo: BundleInfo = self.getBundleInfo(id: id)
577
- if url.isDirectory && destHotPersist.isDirectory && indexHot.exist && indexPersist.exist && !bundleIndo.isDeleted() {
578
- return true
579
- }
580
- return false
581
- }
582
-
583
- public func set(id: String) -> Bool {
584
- let newBundle: BundleInfo = self.getBundleInfo(id: id)
585
- if newBundle.isBuiltin() {
586
- self.reset()
587
- return true
588
- }
589
- if bundleExists(id: id) {
590
- self.setCurrentBundle(bundle: self.getBundleDirectory(id: id).path)
591
- self.setBundleStatus(id: id, status: BundleStatus.PENDING)
592
- self.sendStats(action: "set", versionName: newBundle.getVersionName())
593
- return true
594
- }
595
- self.setBundleStatus(id: id, status: BundleStatus.ERROR)
596
- self.sendStats(action: "set_fail", versionName: newBundle.getVersionName())
597
- return false
598
- }
599
-
600
- public func getPathHot(id: String) -> URL {
601
- return documentsDir.appendingPathComponent(self.bundleDirectoryHot).appendingPathComponent(id)
602
- }
603
-
604
- public func getPathPersist(id: String) -> URL {
605
- return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
606
- }
607
-
608
- public func reset() {
609
- self.reset(isInternal: false)
610
- }
611
-
612
- public func reset(isInternal: Bool) {
613
- print("\(self.TAG) reset: \(isInternal)")
614
- self.setCurrentBundle(bundle: "")
615
- self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
616
- _ = self.setNextBundle(next: Optional<String>.none)
617
- if !isInternal {
618
- self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName())
619
- }
620
- }
621
-
622
- public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
623
- self.setBundleStatus(id: bundle.getId(), status: BundleStatus.SUCCESS)
624
- let fallback: BundleInfo = self.getFallbackBundle()
625
- print("\(self.TAG) Fallback bundle is: \(fallback.toString())")
626
- print("\(self.TAG) Version successfully loaded: \(bundle.toString())")
627
- if autoDeletePrevious && !fallback.isBuiltin() {
628
- let res = self.delete(id: fallback.getId())
629
- if res {
630
- print("\(self.TAG) Deleted previous bundle: \(fallback.toString())")
631
- } else {
632
- print("\(self.TAG) Failed to delete previous bundle: \(fallback.toString())")
633
- }
634
- }
635
- self.setFallbackBundle(fallback: bundle)
636
- }
637
-
638
- public func setError(bundle: BundleInfo) {
639
- self.setBundleStatus(id: bundle.getId(), status: BundleStatus.ERROR)
640
- }
641
-
642
- func setChannel(channel: String) -> SetChannel {
643
- let setChannel: SetChannel = SetChannel()
644
- if (self.channelUrl ).isEmpty {
645
- print("\(self.TAG) Channel URL is not set")
646
- setChannel.message = "Channel URL is not set"
647
- setChannel.error = "missing_config"
648
- return setChannel
649
- }
650
- let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
651
- var parameters: InfoObject = self.createInfoObject()
652
- parameters.channel = channel
653
-
654
- let request = AF.request(self.channelUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
655
-
656
- request.validate().responseDecodable(of: SetChannelDec.self) { response in
657
- switch response.result {
658
- case .success:
659
- if let status = response.value?.status {
660
- setChannel.status = status
661
- }
662
- if let error = response.value?.error {
663
- setChannel.error = error
664
- }
665
- if let message = response.value?.message {
666
- setChannel.message = message
667
- }
668
- case let .failure(error):
669
- print("\(self.TAG) Error set Channel", response.value ?? "", error)
670
- setChannel.message = "Error set Channel \(String(describing: response.value))"
671
- setChannel.error = "response_error"
672
- }
673
- semaphore.signal()
674
- }
675
- semaphore.wait()
676
- return setChannel
677
- }
678
-
679
- func getChannel() -> GetChannel {
680
- let getChannel: GetChannel = GetChannel()
681
- if (self.channelUrl ).isEmpty {
682
- print("\(self.TAG) Channel URL is not set")
683
- getChannel.message = "Channel URL is not set"
684
- getChannel.error = "missing_config"
685
- return getChannel
686
- }
687
- let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
688
- let parameters: InfoObject = self.createInfoObject()
689
- let request = AF.request(self.channelUrl, method: .put, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
690
-
691
- request.validate().responseDecodable(of: GetChannelDec.self) { response in
692
- switch response.result {
693
- case .success:
694
- if let status = response.value?.status {
695
- getChannel.status = status
696
- }
697
- if let error = response.value?.error {
698
- getChannel.error = error
699
- }
700
- if let message = response.value?.message {
701
- getChannel.message = message
702
- }
703
- if let channel = response.value?.channel {
704
- getChannel.channel = channel
705
- }
706
- if let allowSet = response.value?.allowSet {
707
- getChannel.allowSet = allowSet
708
- }
709
- case let .failure(error):
710
- print("\(self.TAG) Error get Channel", response.value ?? "", error)
711
- getChannel.message = "Error get Channel \(String(describing: response.value)))"
712
- getChannel.error = "response_error"
713
- }
714
- semaphore.signal()
715
- }
716
- semaphore.wait()
717
- return getChannel
718
- }
719
-
720
- func sendStats(action: String, versionName: String) {
721
- if (self.statsUrl ).isEmpty {
722
- return
723
- }
724
- var parameters: InfoObject = self.createInfoObject()
725
- parameters.action = action
726
- DispatchQueue.global(qos: .background).async {
727
- let request = AF.request(self.statsUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
728
- request.responseData { response in
729
- switch response.result {
730
- case .success:
731
- print("\(self.TAG) Stats send for \(action), version \(versionName)")
732
- case let .failure(error):
733
- print("\(self.TAG) Error sending stats: ", response.value ?? "", error)
734
- }
735
- }
736
- }
737
- }
738
-
739
- public func getBundleInfo(id: String?) -> BundleInfo {
740
- var trueId = BundleInfo.VERSION_UNKNOWN
741
- if id != nil {
742
- trueId = id!
743
- }
744
- print("\(self.TAG) Getting info for bundle [\(trueId)]")
745
- let result: BundleInfo
746
- if BundleInfo.ID_BUILTIN == trueId {
747
- result = BundleInfo(id: trueId, version: "", status: BundleStatus.SUCCESS, checksum: "")
748
- } else if BundleInfo.VERSION_UNKNOWN == trueId {
749
- result = BundleInfo(id: trueId, version: "", status: BundleStatus.ERROR, checksum: "")
750
- } else {
751
- do {
752
- result = try UserDefaults.standard.getObj(forKey: "\(trueId)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
753
- } catch {
754
- print("\(self.TAG) Failed to parse info for bundle [\(trueId)]", error.localizedDescription)
755
- result = BundleInfo(id: trueId, version: "", status: BundleStatus.PENDING, checksum: "")
756
- }
757
- }
758
- // print("\(self.TAG) Returning info bundle [\(result.toString())]")
759
- return result
760
- }
761
-
762
- public func getBundleInfoByVersionName(version: String) -> BundleInfo? {
763
- let installed: [BundleInfo] = self.list()
764
- for i in installed {
765
- if i.getVersionName() == version {
766
- return i
767
- }
768
- }
769
- return nil
770
- }
771
-
772
- private func removeBundleInfo(id: String) {
773
- self.saveBundleInfo(id: id, bundle: nil)
774
- }
775
-
776
- private func saveBundleInfo(id: String, bundle: BundleInfo?) {
777
- if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
778
- print("\(self.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
779
- return
780
- }
781
- if bundle == nil {
782
- print("\(self.TAG) Removing info for bundle [\(id)]")
783
- UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
784
- } else {
785
- let update = bundle!.setId(id: id)
786
- print("\(self.TAG) Storing info for bundle [\(id)]", update.toString())
787
- do {
788
- try UserDefaults.standard.setObj(update, forKey: "\(id)\(self.INFO_SUFFIX)")
789
- } catch {
790
- print("\(self.TAG) Failed to save info for bundle [\(id)]", error.localizedDescription)
791
- }
792
- }
793
- UserDefaults.standard.synchronize()
794
- }
795
-
796
- public func setVersionName(id: String, version: String) {
797
- print("\(self.TAG) Setting version for folder [\(id)] to \(version)")
798
- let info = self.getBundleInfo(id: id)
799
- self.saveBundleInfo(id: id, bundle: info.setVersionName(version: version))
800
- }
801
-
802
- private func setBundleStatus(id: String, status: BundleStatus) {
803
- print("\(self.TAG) Setting status for bundle [\(id)] to \(status)")
804
- let info = self.getBundleInfo(id: id)
805
- self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
806
- }
807
-
808
- public func getCurrentBundle() -> BundleInfo {
809
- return self.getBundleInfo(id: self.getCurrentBundleId())
810
- }
811
-
812
- public func getCurrentBundleId() -> String {
813
- guard let bundlePath: String = UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) else {
814
- return BundleInfo.ID_BUILTIN
815
- }
816
- if (bundlePath ).isEmpty {
817
- return BundleInfo.ID_BUILTIN
818
- }
819
- let bundleID: String = bundlePath.components(separatedBy: "/").last ?? bundlePath
820
- return bundleID
821
- }
822
-
823
- public func isUsingBuiltin() -> Bool {
824
- return (UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) ?? "") == self.DEFAULT_FOLDER
825
- }
826
-
827
- public func getFallbackBundle() -> BundleInfo {
828
- let id: String = UserDefaults.standard.string(forKey: self.FALLBACK_VERSION) ?? BundleInfo.ID_BUILTIN
829
- return self.getBundleInfo(id: id)
830
- }
831
-
832
- private func setFallbackBundle(fallback: BundleInfo?) {
833
- UserDefaults.standard.set(fallback == nil ? BundleInfo.ID_BUILTIN : fallback!.getId(), forKey: self.FALLBACK_VERSION)
834
- UserDefaults.standard.synchronize()
835
- }
836
-
837
- public func getNextBundle() -> BundleInfo? {
838
- let id: String? = UserDefaults.standard.string(forKey: self.NEXT_VERSION)
839
- return self.getBundleInfo(id: id)
840
- }
841
-
842
- public func setNextBundle(next: String?) -> Bool {
843
- guard let nextId: String = next else {
844
- UserDefaults.standard.removeObject(forKey: self.NEXT_VERSION)
845
- UserDefaults.standard.synchronize()
846
- return false
847
- }
848
- let newBundle: BundleInfo = self.getBundleInfo(id: nextId)
849
- let bundle: URL = self.getBundleDirectory(id: nextId)
850
- if !newBundle.isBuiltin() && !bundle.exist {
851
- return false
852
- }
853
- UserDefaults.standard.set(nextId, forKey: self.NEXT_VERSION)
854
- UserDefaults.standard.synchronize()
855
- self.setBundleStatus(id: nextId, status: BundleStatus.PENDING)
856
- return true
857
- }
858
- }