@capgo/capacitor-updater 5.0.0-alpha.0 → 7.0.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 (38) hide show
  1. package/CapgoCapacitorUpdater.podspec +1 -1
  2. package/LICENCE +373 -661
  3. package/README.md +339 -75
  4. package/android/build.gradle +13 -12
  5. package/android/src/main/AndroidManifest.xml +4 -2
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +205 -121
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +32 -24
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +1041 -441
  9. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1217 -536
  10. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +153 -0
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +62 -0
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +14 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +126 -0
  14. package/dist/docs.json +727 -171
  15. package/dist/esm/definitions.d.ts +234 -45
  16. package/dist/esm/definitions.js +5 -0
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/index.d.ts +2 -2
  19. package/dist/esm/index.js +9 -4
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +12 -6
  22. package/dist/esm/web.js +64 -20
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +70 -23
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +70 -23
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Plugin/BundleInfo.swift +38 -19
  29. package/ios/Plugin/BundleStatus.swift +11 -4
  30. package/ios/Plugin/CapacitorUpdater.swift +520 -192
  31. package/ios/Plugin/CapacitorUpdaterPlugin.m +8 -1
  32. package/ios/Plugin/CapacitorUpdaterPlugin.swift +447 -190
  33. package/ios/Plugin/CryptoCipher.swift +240 -0
  34. package/ios/Plugin/DelayCondition.swift +74 -0
  35. package/ios/Plugin/DelayUntilNext.swift +30 -0
  36. package/ios/Plugin/UserDefaultsExtension.swift +48 -0
  37. package/package.json +26 -20
  38. package/ios/Plugin/ObjectPreferences.swift +0 -97
@@ -1,6 +1,13 @@
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
+
1
7
  import Foundation
2
8
  import SSZipArchive
3
9
  import Alamofire
10
+ import zlib
4
11
 
5
12
  extension URL {
6
13
  var isDirectory: Bool {
@@ -15,28 +22,111 @@ extension Date {
15
22
  return Calendar.current.date(byAdding: .minute, value: minutes, to: self)!
16
23
  }
17
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
+ }
18
88
  struct AppVersionDec: Decodable {
19
89
  let version: String?
90
+ let checksum: String?
20
91
  let url: String?
21
92
  let message: String?
93
+ let error: String?
94
+ let session_key: String?
22
95
  let major: Bool?
23
96
  }
24
97
  public class AppVersion: NSObject {
25
98
  var version: String = ""
99
+ var checksum: String = ""
26
100
  var url: String = ""
27
101
  var message: String?
102
+ var error: String?
103
+ var sessionKey: String?
28
104
  var major: Bool?
29
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
+
30
120
  extension OperatingSystemVersion {
31
121
  func getFullVersion(separator: String = ".") -> String {
32
122
  return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
33
123
  }
34
124
  }
35
125
  extension Bundle {
36
- var releaseVersionNumber: String? {
126
+ var versionName: String? {
37
127
  return infoDictionary?["CFBundleShortVersionString"] as? String
38
128
  }
39
- var buildVersionNumber: String? {
129
+ var versionCode: String? {
40
130
  return infoDictionary?["CFBundleVersion"] as? String
41
131
  }
42
132
  }
@@ -48,18 +138,18 @@ extension ISO8601DateFormatter {
48
138
  }
49
139
  }
50
140
  extension Formatter {
51
- static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
141
+ static let iso8601withFractionalSeconds: ISO8601DateFormatter = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
52
142
  }
53
143
  extension Date {
54
144
  var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
55
145
  }
56
146
  extension String {
57
-
147
+
58
148
  var fileURL: URL {
59
149
  return URL(fileURLWithPath: self)
60
150
  }
61
-
62
- var lastPathComponent:String {
151
+
152
+ var lastPathComponent: String {
63
153
  get {
64
154
  return fileURL.lastPathComponent
65
155
  }
@@ -75,6 +165,8 @@ extension String {
75
165
  enum CustomError: Error {
76
166
  // Throw when an unzip fail
77
167
  case cannotUnzip
168
+ case cannotWrite
169
+ case cannotDecode
78
170
  case cannotUnflat
79
171
  case cannotCreateDirectory
80
172
  case cannotDeleteDirectory
@@ -106,56 +198,109 @@ extension CustomError: LocalizedError {
106
198
  "The file cannot be unflat",
107
199
  comment: "Invalid folder"
108
200
  )
109
- case .unexpected(_):
201
+ case .unexpected:
110
202
  return NSLocalizedString(
111
203
  "An unexpected error occurred.",
112
204
  comment: "Unexpected Error"
113
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
+ )
114
216
  }
115
217
  }
116
218
  }
117
219
 
118
220
  @objc public class CapacitorUpdater: NSObject {
119
-
120
- private let versionBuild = Bundle.main.releaseVersionNumber ?? ""
121
- private let versionCode = Bundle.main.buildVersionNumber ?? ""
122
- private let versionOs = ProcessInfo().operatingSystemVersion.getFullVersion()
123
- private let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
124
- private let libraryDir = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
125
- private let bundleDirectoryHot = "versions"
126
- private let DEFAULT_FOLDER = ""
127
- private let bundleDirectory = "NoCloud/ionic_built_snapshots"
128
- private let INFO_SUFFIX = "_info"
129
- private let FALLBACK_VERSION = "pastVersion"
130
- private let NEXT_VERSION = "nextVersion"
131
-
132
- private var lastPathHot = ""
133
- private var lastPathPersist = ""
134
-
135
- public let TAG = "✨ Capacitor-updater:";
136
- public let CAP_SERVER_PATH = "serverBasePath"
137
- public let pluginVersion = "5.0.0-alpha.0"
138
- public var statsUrl = ""
139
- public var appId = ""
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 = ""
140
242
  public var deviceID = UIDevice.current.identifierForVendor?.uuidString ?? ""
141
-
142
- public var notifyDownload: (String, Int) -> Void = { _,_ in }
243
+ public var privateKey: String = ""
244
+
245
+ public var notifyDownload: (String, Int) -> Void = { _, _ in }
143
246
 
144
247
  private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
145
- return (percent * (max - min)) / 100 + min;
248
+ return (percent * (max - min)) / 100 + min
146
249
  }
147
-
250
+
148
251
  private func randomString(length: Int) -> String {
149
- let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
150
- return String((0..<length).map{ _ in letters.randomElement()! })
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
151
297
  }
152
-
153
298
  // Persistent path /var/mobile/Containers/Data/Application/8C0C07BE-0FD3-4FD4-B7DF-90A88E12B8C3/Library/NoCloud/ionic_built_snapshots/FOLDER
154
299
  // Hot Reload path /var/mobile/Containers/Data/Application/8C0C07BE-0FD3-4FD4-B7DF-90A88E12B8C3/Documents/FOLDER
155
300
  // Normal /private/var/containers/Bundle/Application/8C0C07BE-0FD3-4FD4-B7DF-90A88E12B8C3/App.app/public
156
-
301
+
157
302
  private func prepareFolder(source: URL) throws {
158
- if (!FileManager.default.fileExists(atPath: source.path)) {
303
+ if !FileManager.default.fileExists(atPath: source.path) {
159
304
  do {
160
305
  try FileManager.default.createDirectory(atPath: source.path, withIntermediateDirectories: true, attributes: nil)
161
306
  } catch {
@@ -164,7 +309,7 @@ extension CustomError: LocalizedError {
164
309
  }
165
310
  }
166
311
  }
167
-
312
+
168
313
  private func deleteFolder(source: URL) throws {
169
314
  do {
170
315
  try FileManager.default.removeItem(atPath: source.path)
@@ -173,12 +318,12 @@ extension CustomError: LocalizedError {
173
318
  throw CustomError.cannotDeleteDirectory
174
319
  }
175
320
  }
176
-
321
+
177
322
  private func unflatFolder(source: URL, dest: URL) throws -> Bool {
178
- let index = source.appendingPathComponent("index.html")
323
+ let index: URL = source.appendingPathComponent("index.html")
179
324
  do {
180
- let files = try FileManager.default.contentsOfDirectory(atPath: source.path)
181
- if (files.count == 1 && source.appendingPathComponent(files[0]).isDirectory && !FileManager.default.fileExists(atPath: index.path)) {
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) {
182
327
  try FileManager.default.moveItem(at: source.appendingPathComponent(files[0]), to: dest)
183
328
  return true
184
329
  } else {
@@ -190,77 +335,140 @@ extension CustomError: LocalizedError {
190
335
  throw CustomError.cannotUnflat
191
336
  }
192
337
  }
193
-
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
+
194
380
  private func saveDownloaded(sourceZip: URL, id: String, base: URL) throws {
195
381
  try prepareFolder(source: base)
196
- let destHot = base.appendingPathComponent(id)
197
- let destUnZip = documentsDir.appendingPathComponent(randomString(length: 10))
198
- if (!SSZipArchive.unzipFile(atPath: sourceZip.path, toDestination: destUnZip.path)) {
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) {
199
385
  throw CustomError.cannotUnzip
200
386
  }
201
- if (try unflatFolder(source: destUnZip, dest: destHot)) {
387
+ if try unflatFolder(source: destUnZip, dest: destHot) {
202
388
  try deleteFolder(source: destUnZip)
203
389
  }
204
390
  }
205
391
 
206
- public func getLatest(url: URL) -> AppVersion? {
207
- let semaphore = DispatchSemaphore(value: 0)
208
- let latest = AppVersion()
209
- let parameters: [String: String] = [
210
- "platform": "ios",
211
- "device_id": self.deviceID,
212
- "app_id": self.appId,
213
- "version_build": self.versionBuild,
214
- "version_code": self.versionCode,
215
- "version_os": self.versionOs,
216
- "plugin_version": self.pluginVersion,
217
- "version_name": self.getCurrentBundle().getVersionName()
218
- ]
219
- let request = AF.request(url, method: .post,parameters: parameters, encoder: JSONParameterEncoder.default)
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 })
220
416
 
221
417
  request.validate().responseDecodable(of: AppVersionDec.self) { response in
222
418
  switch response.result {
223
- case .success:
224
- if let url = response.value?.url {
225
- latest.url = url
226
- }
227
- if let version = response.value?.version {
228
- latest.version = version
229
- }
230
- if let major = response.value?.major {
231
- latest.major = major
232
- }
233
- if let message = response.value?.message {
234
- latest.message = message
235
- print("\(self.TAG) Auto-update message: \(message)")
236
- }
237
- case let .failure(error):
238
- print("\(self.TAG) Error getting Latest", error )
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"
239
445
  }
240
446
  semaphore.signal()
241
447
  }
242
448
  semaphore.wait()
243
- return latest.url != "" ? latest : nil
449
+ return latest
244
450
  }
245
-
451
+
246
452
  private func setCurrentBundle(bundle: String) {
247
453
  UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
248
- print("\(self.TAG) Current bundle set to: \(bundle)")
249
454
  UserDefaults.standard.synchronize()
455
+ print("\(self.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
250
456
  }
251
457
 
252
- public func download(url: URL, version: String) throws -> BundleInfo {
253
- let semaphore = DispatchSemaphore(value: 0)
458
+ public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
459
+ let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
254
460
  let id: String = self.randomString(length: 10)
255
- var mainError: NSError? = nil
461
+ var checksum: String = ""
462
+
463
+ var mainError: NSError?
256
464
  let destination: DownloadRequest.Destination = { _, _ in
257
- let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
258
- let fileURL = documentsURL.appendingPathComponent(self.randomString(length: 10))
465
+ let documentsURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
466
+ let fileURL: URL = documentsURL.appendingPathComponent(self.randomString(length: 10))
259
467
 
260
468
  return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
261
469
  }
262
470
  let request = AF.download(url, to: destination)
263
-
471
+
264
472
  request.downloadProgress { progress in
265
473
  let percent = self.calcTotalPercent(percent: Int(progress.fractionCompleted * 100), min: 10, max: 70)
266
474
  self.notifyDownload(id, percent)
@@ -271,6 +479,8 @@ extension CustomError: LocalizedError {
271
479
  case .success:
272
480
  self.notifyDownload(id, 71)
273
481
  do {
482
+ try self.decryptFile(filePath: fileURL, sessionKey: sessionKey, version: version)
483
+ checksum = self.getChecksum(filePath: fileURL)
274
484
  try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.documentsDir.appendingPathComponent(self.bundleDirectoryHot))
275
485
  self.notifyDownload(id, 85)
276
486
  try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory))
@@ -281,32 +491,32 @@ extension CustomError: LocalizedError {
281
491
  mainError = error as NSError
282
492
  }
283
493
  case let .failure(error):
284
- print("\(self.TAG) download error", error)
494
+ print("\(self.TAG) download error", response.value ?? "", error)
285
495
  mainError = error as NSError
286
496
  }
287
497
  }
288
498
  semaphore.signal()
289
499
  }
290
- self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date()))
500
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
291
501
  self.notifyDownload(id, 0)
292
502
  semaphore.wait()
293
- if (mainError != nil) {
503
+ if mainError != nil {
294
504
  throw mainError!
295
505
  }
296
- let info: BundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date())
506
+ let info: BundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
297
507
  self.saveBundleInfo(id: id, bundle: info)
298
508
  return info
299
509
  }
300
510
 
301
511
  public func list() -> [BundleInfo] {
302
- let dest = documentsDir.appendingPathComponent(bundleDirectoryHot)
512
+ let dest: URL = documentsDir.appendingPathComponent(bundleDirectoryHot)
303
513
  do {
304
- let files = try FileManager.default.contentsOfDirectory(atPath: dest.path)
514
+ let files: [String] = try FileManager.default.contentsOfDirectory(atPath: dest.path)
305
515
  var res: [BundleInfo] = []
306
516
  print("\(self.TAG) list File : \(dest.path)")
307
- if (dest.exist) {
308
- for id in files {
309
- res.append(self.getBundleInfo(id: id));
517
+ if dest.exist {
518
+ for id: String in files {
519
+ res.append(self.getBundleInfo(id: id))
310
520
  }
311
521
  }
312
522
  return res
@@ -315,11 +525,15 @@ extension CustomError: LocalizedError {
315
525
  return []
316
526
  }
317
527
  }
318
-
319
- public func delete(id: String) -> Bool {
528
+
529
+ public func delete(id: String, removeInfo: Bool) -> Bool {
320
530
  let deleted: BundleInfo = self.getBundleInfo(id: id)
321
- let destHot = documentsDir.appendingPathComponent(bundleDirectoryHot).appendingPathComponent(id)
322
- let destPersist = libraryDir.appendingPathComponent(bundleDirectory).appendingPathComponent(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)
323
537
  do {
324
538
  try FileManager.default.removeItem(atPath: destHot.path)
325
539
  } catch {
@@ -331,107 +545,224 @@ extension CustomError: LocalizedError {
331
545
  print("\(self.TAG) Folder \(destPersist.path), not removed.")
332
546
  return false
333
547
  }
334
- self.removeBundleInfo(id: id)
335
- self.sendStats(action: "delete", bundle: deleted)
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())
336
555
  return true
337
556
  }
338
557
 
558
+ public func delete(id: String) -> Bool {
559
+ return self.delete(id: id, removeInfo: true)
560
+ }
561
+
339
562
  public func getBundleDirectory(id: String) -> URL {
340
563
  return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
341
564
  }
342
565
 
343
566
  public func set(bundle: BundleInfo) -> Bool {
344
- return self.set(id: bundle.getId());
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
345
581
  }
346
582
 
347
583
  public func set(id: String) -> Bool {
348
- let destHot = self.getPathHot(id: id)
349
- let destHotPersist = self.getPathPersist(id: id)
350
- let indexHot = destHot.appendingPathComponent("index.html")
351
- let indexPersist = destHotPersist.appendingPathComponent("index.html")
352
- let existing: BundleInfo = self.getBundleInfo(id: id)
353
- let bundle: URL = self.getBundleDirectory(id: id)
354
- print("bundle", bundle.path)
355
- if (bundle.isDirectory && destHotPersist.isDirectory && indexHot.exist && indexPersist.exist) {
356
- self.setCurrentBundle(bundle: String(bundle.path.suffix(10)))
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)
357
591
  self.setBundleStatus(id: id, status: BundleStatus.PENDING)
358
- sendStats(action: "set", bundle: existing)
592
+ self.sendStats(action: "set", versionName: newBundle.getVersionName())
359
593
  return true
360
594
  }
361
- sendStats(action: "set_fail", bundle: existing)
595
+ self.setBundleStatus(id: id, status: BundleStatus.ERROR)
596
+ self.sendStats(action: "set_fail", versionName: newBundle.getVersionName())
362
597
  return false
363
598
  }
364
-
599
+
365
600
  public func getPathHot(id: String) -> URL {
366
601
  return documentsDir.appendingPathComponent(self.bundleDirectoryHot).appendingPathComponent(id)
367
602
  }
368
-
603
+
369
604
  public func getPathPersist(id: String) -> URL {
370
605
  return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
371
606
  }
372
-
607
+
373
608
  public func reset() {
374
609
  self.reset(isInternal: false)
375
610
  }
376
-
611
+
377
612
  public func reset(isInternal: Bool) {
613
+ print("\(self.TAG) reset: \(isInternal)")
378
614
  self.setCurrentBundle(bundle: "")
379
- self.setFallbackVersion(fallback: Optional<BundleInfo>.none)
380
- let _ = self.setNextVersion(next: Optional<String>.none)
381
- UserDefaults.standard.synchronize()
382
- if(!isInternal) {
383
- sendStats(action: "reset", bundle: self.getCurrentBundle())
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())
384
619
  }
385
620
  }
386
-
387
- public func commit(bundle: BundleInfo) {
621
+
622
+ public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
388
623
  self.setBundleStatus(id: bundle.getId(), status: BundleStatus.SUCCESS)
389
- self.setFallbackVersion(fallback: bundle)
390
- }
391
-
392
- public func rollback(bundle: BundleInfo) {
393
- self.setBundleStatus(id: bundle.getId(), status: BundleStatus.ERROR);
394
- }
395
-
396
- func sendStats(action: String, bundle: BundleInfo) {
397
- if (statsUrl == "") { return }
398
- let parameters: [String: String] = [
399
- "platform": "ios",
400
- "action": action,
401
- "device_id": self.deviceID,
402
- "version_name": bundle.getVersionName(),
403
- "version_build": self.versionBuild,
404
- "version_code": self.versionCode,
405
- "version_os": self.versionOs,
406
- "plugin_version": self.pluginVersion,
407
- "app_id": self.appId
408
- ]
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 })
409
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
410
726
  DispatchQueue.global(qos: .background).async {
411
- let _ = AF.request(self.statsUrl, method: .post,parameters: parameters, encoder: JSONParameterEncoder.default)
412
- print("\(self.TAG) Stats send for \(action), version \(bundle.getVersionName())")
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
+ }
413
736
  }
414
737
  }
415
738
 
416
- public func getBundleInfo(id: String = BundleInfo.ID_BUILTIN) -> BundleInfo {
417
- print("\(self.TAG) Getting info for bundle [\(id)]")
418
- if(BundleInfo.ID_BUILTIN == id) {
419
- return BundleInfo(id: id, version: "", status: BundleStatus.SUCCESS)
739
+ public func getBundleInfo(id: String?) -> BundleInfo {
740
+ var trueId = BundleInfo.VERSION_UNKNOWN
741
+ if id != nil {
742
+ trueId = id!
420
743
  }
421
- do {
422
- let result: BundleInfo = try UserDefaults.standard.getObj(forKey: "\(id)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
423
- print("\(self.TAG) Returning info bundle [\(id)]", result.toString())
424
- return result
425
- } catch {
426
- print("\(self.TAG) Failed to parse info for bundle [\(id)]", error.localizedDescription)
427
- return BundleInfo(id: id, version: "", status: BundleStatus.PENDING)
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
+ }
428
757
  }
758
+ // print("\(self.TAG) Returning info bundle [\(result.toString())]")
759
+ return result
429
760
  }
430
761
 
431
762
  public func getBundleInfoByVersionName(version: String) -> BundleInfo? {
432
- let installed : Array<BundleInfo> = self.list()
763
+ let installed: [BundleInfo] = self.list()
433
764
  for i in installed {
434
- if(i.getVersionName() == version) {
765
+ if i.getVersionName() == version {
435
766
  return i
436
767
  }
437
768
  }
@@ -443,11 +774,11 @@ extension CustomError: LocalizedError {
443
774
  }
444
775
 
445
776
  private func saveBundleInfo(id: String, bundle: BundleInfo?) {
446
- if (bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown())) {
447
- print("\(self.TAG) Not saving info for bundle [\(id)]", bundle!.toString())
777
+ if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
778
+ print("\(self.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
448
779
  return
449
780
  }
450
- if(bundle == nil) {
781
+ if bundle == nil {
451
782
  print("\(self.TAG) Removing info for bundle [\(id)]")
452
783
  UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
453
784
  } else {
@@ -474,57 +805,54 @@ extension CustomError: LocalizedError {
474
805
  self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
475
806
  }
476
807
 
477
- private func getCurrentBundleVersion() -> String {
478
- if(self.isUsingBuiltin()) {
479
- return BundleInfo.ID_BUILTIN
480
- } else {
481
- let path: String = self.getCurrentBundleId()
482
- return path.lastPathComponent
483
- }
484
- }
485
-
486
808
  public func getCurrentBundle() -> BundleInfo {
487
- return self.getBundleInfo(id: self.getCurrentBundleId());
809
+ return self.getBundleInfo(id: self.getCurrentBundleId())
488
810
  }
489
811
 
490
812
  public func getCurrentBundleId() -> String {
491
- return UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) ?? self.DEFAULT_FOLDER
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
492
821
  }
493
822
 
494
823
  public func isUsingBuiltin() -> Bool {
495
- return self.getCurrentBundleId() == self.DEFAULT_FOLDER
824
+ return (UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) ?? "") == self.DEFAULT_FOLDER
496
825
  }
497
826
 
498
- public func getFallbackVersion() -> BundleInfo {
827
+ public func getFallbackBundle() -> BundleInfo {
499
828
  let id: String = UserDefaults.standard.string(forKey: self.FALLBACK_VERSION) ?? BundleInfo.ID_BUILTIN
500
829
  return self.getBundleInfo(id: id)
501
830
  }
502
831
 
503
- private func setFallbackVersion(fallback: BundleInfo?) {
832
+ private func setFallbackBundle(fallback: BundleInfo?) {
504
833
  UserDefaults.standard.set(fallback == nil ? BundleInfo.ID_BUILTIN : fallback!.getId(), forKey: self.FALLBACK_VERSION)
834
+ UserDefaults.standard.synchronize()
505
835
  }
506
836
 
507
- public func getNextVersion() -> BundleInfo? {
508
- let id: String = UserDefaults.standard.string(forKey: self.NEXT_VERSION) ?? ""
509
- if(id != "") {
510
- return self.getBundleInfo(id: id)
511
- } else {
512
- return nil
513
- }
837
+ public func getNextBundle() -> BundleInfo? {
838
+ let id: String? = UserDefaults.standard.string(forKey: self.NEXT_VERSION)
839
+ return self.getBundleInfo(id: id)
514
840
  }
515
841
 
516
- public func setNextVersion(next: String?) -> Bool {
517
- if (next == nil) {
842
+ public func setNextBundle(next: String?) -> Bool {
843
+ guard let nextId: String = next else {
518
844
  UserDefaults.standard.removeObject(forKey: self.NEXT_VERSION)
519
- } else {
520
- let bundle: URL = self.getBundleDirectory(id: next!)
521
- if (!bundle.exist) {
522
- return false
523
- }
524
- UserDefaults.standard.set(next, forKey: self.NEXT_VERSION)
525
- self.setBundleStatus(id: next!, status: BundleStatus.PENDING);
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
526
852
  }
853
+ UserDefaults.standard.set(nextId, forKey: self.NEXT_VERSION)
527
854
  UserDefaults.standard.synchronize()
855
+ self.setBundleStatus(id: nextId, status: BundleStatus.PENDING)
528
856
  return true
529
857
  }
530
858
  }