@capgo/capacitor-updater 8.0.0 → 8.0.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.
Files changed (40) hide show
  1. package/CapgoCapacitorUpdater.podspec +2 -2
  2. package/Package.swift +35 -0
  3. package/README.md +667 -206
  4. package/android/build.gradle +16 -11
  5. package/android/proguard-rules.pro +28 -0
  6. package/android/src/main/AndroidManifest.xml +0 -1
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +134 -194
  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/CapacitorUpdater.java +967 -1027
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1283 -1180
  12. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +276 -0
  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 +45 -48
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +440 -113
  17. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +101 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +32 -0
  19. package/dist/docs.json +1316 -473
  20. package/dist/esm/definitions.d.ts +518 -248
  21. package/dist/esm/definitions.js.map +1 -1
  22. package/dist/esm/index.d.ts +2 -2
  23. package/dist/esm/index.js +4 -4
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/web.d.ts +25 -41
  26. package/dist/esm/web.js +67 -35
  27. package/dist/esm/web.js.map +1 -1
  28. package/dist/plugin.cjs.js +67 -35
  29. package/dist/plugin.cjs.js.map +1 -1
  30. package/dist/plugin.js +67 -35
  31. package/dist/plugin.js.map +1 -1
  32. package/ios/Plugin/CapacitorUpdater.swift +736 -361
  33. package/ios/Plugin/CapacitorUpdaterPlugin.swift +436 -136
  34. package/ios/Plugin/CryptoCipherV2.swift +310 -0
  35. package/ios/Plugin/InternalUtils.swift +258 -0
  36. package/package.json +33 -29
  37. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +0 -153
  38. package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
  39. package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
  40. package/ios/Plugin/CryptoCipher.swift +0 -240
@@ -5,243 +5,47 @@
5
5
  */
6
6
 
7
7
  import Foundation
8
+ #if canImport(ZipArchive)
9
+ import ZipArchive
10
+ #else
8
11
  import SSZipArchive
12
+ #endif
9
13
  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
- }
14
+ import Compression
15
+ import UIKit
219
16
 
220
17
  @objc public class CapacitorUpdater: NSObject {
221
18
 
222
19
  private let versionCode: String = Bundle.main.versionCode ?? ""
223
20
  private let versionOs = UIDevice.current.systemVersion
224
- private let documentsDir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
225
21
  private let libraryDir: URL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
226
- private let bundleDirectoryHot: String = "versions"
227
22
  private let DEFAULT_FOLDER: String = ""
228
23
  private let bundleDirectory: String = "NoCloud/ionic_built_snapshots"
229
24
  private let INFO_SUFFIX: String = "_info"
230
25
  private let FALLBACK_VERSION: String = "pastVersion"
231
26
  private let NEXT_VERSION: String = "nextVersion"
27
+ private var unzipPercent = 0
28
+
29
+ // Add this line to declare cacheFolder
30
+ private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
232
31
 
233
- public let TAG: String = "✨ Capacitor-updater:"
32
+ public static let TAG: String = "✨ Capacitor-updater:"
234
33
  public let CAP_SERVER_PATH: String = "serverBasePath"
235
- public var versionName: String = ""
34
+ public var versionBuild: String = ""
236
35
  public var customId: String = ""
237
36
  public var PLUGIN_VERSION: String = ""
238
- public let timeout: Double = 20
37
+ public var timeout: Double = 20
239
38
  public var statsUrl: String = ""
240
39
  public var channelUrl: String = ""
40
+ public var defaultChannel: String = ""
241
41
  public var appId: String = ""
242
- public var deviceID = UIDevice.current.identifierForVendor?.uuidString ?? ""
243
- public var privateKey: String = ""
42
+ public var deviceID = ""
43
+ public var publicKey: String = ""
244
44
 
45
+ public var notifyDownloadRaw: (String, Int, Bool) -> Void = { _, _, _ in }
46
+ public func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false) {
47
+ notifyDownloadRaw(id, percent, ignoreMultipleOfTen)
48
+ }
245
49
  public var notifyDownload: (String, Int) -> Void = { _, _ in }
246
50
 
247
51
  private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
@@ -304,7 +108,7 @@ extension CustomError: LocalizedError {
304
108
  do {
305
109
  try FileManager.default.createDirectory(atPath: source.path, withIntermediateDirectories: true, attributes: nil)
306
110
  } catch {
307
- print("\(self.TAG) Cannot createDirectory \(source.path)")
111
+ print("\(CapacitorUpdater.TAG) Cannot createDirectory \(source.path)")
308
112
  throw CustomError.cannotCreateDirectory
309
113
  }
310
114
  }
@@ -314,7 +118,7 @@ extension CustomError: LocalizedError {
314
118
  do {
315
119
  try FileManager.default.removeItem(atPath: source.path)
316
120
  } catch {
317
- print("\(self.TAG) File not removed. \(source.path)")
121
+ print("\(CapacitorUpdater.TAG) File not removed. \(source.path)")
318
122
  throw CustomError.cannotDeleteDirectory
319
123
  }
320
124
  }
@@ -331,30 +135,19 @@ extension CustomError: LocalizedError {
331
135
  return false
332
136
  }
333
137
  } catch {
334
- print("\(self.TAG) File not moved. source: \(source.path) dest: \(dest.path)")
138
+ print("\(CapacitorUpdater.TAG) File not moved. source: \(source.path) dest: \(dest.path)")
335
139
  throw CustomError.cannotUnflat
336
140
  }
337
141
  }
338
142
 
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")
143
+ private func decryptFileV2(filePath: URL, sessionKey: String, version: String) throws {
144
+ if self.publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
145
+ print("\(CapacitorUpdater.TAG) Cannot find public key or sessionKey")
353
146
  return
354
147
  }
355
148
  do {
356
- guard let rsaPrivateKey: RSAPrivateKey = .load(rsaPrivateKey: self.privateKey) else {
357
- print("cannot decode privateKey", self.privateKey)
149
+ guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: self.publicKey) else {
150
+ print("cannot decode publicKey", self.publicKey)
358
151
  throw CustomError.cannotDecode
359
152
  }
360
153
 
@@ -363,28 +156,109 @@ extension CustomError: LocalizedError {
363
156
  print("cannot decode sessionKey", sessionKey)
364
157
  throw CustomError.cannotDecode
365
158
  }
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)!
159
+
160
+ guard let sessionKeyDataEncrypted = Data(base64Encoded: sessionKeyArray[1]) else {
161
+ throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
162
+ }
163
+
164
+ guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
165
+ throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
166
+ }
167
+
168
+ let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
169
+
170
+ guard let encryptedData = try? Data(contentsOf: filePath) else {
171
+ throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
172
+ }
173
+
174
+ guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
175
+ throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
176
+ }
371
177
 
372
178
  try decryptedData.write(to: filePath)
179
+
373
180
  } catch {
374
- print("\(self.TAG) Cannot decode: \(filePath.path)", error)
181
+ print("\(CapacitorUpdater.TAG) Cannot decode: \(filePath.path)", error)
375
182
  self.sendStats(action: "decrypt_fail", versionName: version)
376
183
  throw CustomError.cannotDecode
377
184
  }
378
185
  }
379
186
 
380
- private func saveDownloaded(sourceZip: URL, id: String, base: URL) throws {
187
+ private func unzipProgressHandler(entry: String, zipInfo: unz_file_info, entryNumber: Int, total: Int, destUnZip: URL, id: String, unzipError: inout NSError?) {
188
+ if entry.contains("\\") {
189
+ print("\(CapacitorUpdater.TAG) unzip: Windows path is not supported, please use unix path as required by zip RFC: \(entry)")
190
+ self.sendStats(action: "windows_path_fail")
191
+ }
192
+
193
+ let fileURL = destUnZip.appendingPathComponent(entry)
194
+ let canonicalPath = fileURL.path
195
+ let canonicalDir = destUnZip.path
196
+
197
+ if !canonicalPath.hasPrefix(canonicalDir) {
198
+ self.sendStats(action: "canonical_path_fail")
199
+ unzipError = NSError(domain: "CanonicalPathError", code: 0, userInfo: nil)
200
+ }
201
+
202
+ let isDirectory = entry.hasSuffix("/")
203
+ if !isDirectory {
204
+ let folderURL = fileURL.deletingLastPathComponent()
205
+ if !FileManager.default.fileExists(atPath: folderURL.path) {
206
+ do {
207
+ try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)
208
+ } catch {
209
+ self.sendStats(action: "directory_path_fail")
210
+ unzipError = error as NSError
211
+ }
212
+ }
213
+ }
214
+
215
+ let newPercent = self.calcTotalPercent(percent: Int(Double(entryNumber) / Double(total) * 100), min: 75, max: 81)
216
+ if newPercent != self.unzipPercent {
217
+ self.unzipPercent = newPercent
218
+ self.notifyDownload(id: id, percent: newPercent)
219
+ }
220
+ }
221
+
222
+ private func saveDownloaded(sourceZip: URL, id: String, base: URL, notify: Bool) throws {
381
223
  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
224
+ let destPersist: URL = base.appendingPathComponent(id)
225
+ let destUnZip: URL = libraryDir.appendingPathComponent(randomString(length: 10))
226
+
227
+ self.unzipPercent = 0
228
+ self.notifyDownload(id: id, percent: 75)
229
+
230
+ let semaphore = DispatchSemaphore(value: 0)
231
+ var unzipError: NSError?
232
+
233
+ let success = SSZipArchive.unzipFile(atPath: sourceZip.path,
234
+ toDestination: destUnZip.path,
235
+ preserveAttributes: true,
236
+ overwrite: true,
237
+ nestedZipLevel: 1,
238
+ password: nil,
239
+ error: &unzipError,
240
+ delegate: nil,
241
+ progressHandler: { [weak self] (entry, zipInfo, entryNumber, total) in
242
+ DispatchQueue.global(qos: .background).async {
243
+ guard let self = self else { return }
244
+ if !notify {
245
+ return
246
+ }
247
+ self.unzipProgressHandler(entry: entry, zipInfo: zipInfo, entryNumber: entryNumber, total: total, destUnZip: destUnZip, id: id, unzipError: &unzipError)
248
+ }
249
+ },
250
+ completionHandler: { _, _, _ in
251
+ semaphore.signal()
252
+ })
253
+
254
+ semaphore.wait()
255
+
256
+ if !success || unzipError != nil {
257
+ self.sendStats(action: "unzip_fail")
258
+ throw unzipError ?? CustomError.cannotUnzip
386
259
  }
387
- if try unflatFolder(source: destUnZip, dest: destHot) {
260
+
261
+ if try unflatFolder(source: destUnZip, dest: destPersist) {
388
262
  try deleteFolder(source: destUnZip)
389
263
  }
390
264
  }
@@ -395,7 +269,7 @@ extension CustomError: LocalizedError {
395
269
  device_id: self.deviceID,
396
270
  app_id: self.appId,
397
271
  custom_id: self.customId,
398
- version_build: self.versionName,
272
+ version_build: self.versionBuild,
399
273
  version_code: self.versionCode,
400
274
  version_os: self.versionOs,
401
275
  version_name: self.getCurrentBundle().getVersionName(),
@@ -403,15 +277,19 @@ extension CustomError: LocalizedError {
403
277
  is_emulator: self.isEmulator(),
404
278
  is_prod: self.isProd(),
405
279
  action: nil,
406
- channel: nil
280
+ channel: nil,
281
+ defaultChannel: self.defaultChannel
407
282
  )
408
283
  }
409
284
 
410
- public func getLatest(url: URL) -> AppVersion {
285
+ public func getLatest(url: URL, channel: String?) -> AppVersion {
411
286
  let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
412
287
  let latest: AppVersion = AppVersion()
413
- let parameters: InfoObject = self.createInfoObject()
414
- print("\(self.TAG) Auto-update parameters: \(parameters)")
288
+ var parameters: InfoObject = self.createInfoObject()
289
+ if let channel = channel {
290
+ parameters.defaultChannel = channel
291
+ }
292
+ print("\(CapacitorUpdater.TAG) Auto-update parameters: \(parameters)")
415
293
  let request = AF.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
416
294
 
417
295
  request.validate().responseDecodable(of: AppVersionDec.self) { response in
@@ -438,8 +316,14 @@ extension CustomError: LocalizedError {
438
316
  if let sessionKey = response.value?.session_key {
439
317
  latest.sessionKey = sessionKey
440
318
  }
319
+ if let data = response.value?.data {
320
+ latest.data = data
321
+ }
322
+ if let manifest = response.value?.manifest {
323
+ latest.manifest = manifest
324
+ }
441
325
  case let .failure(error):
442
- print("\(self.TAG) Error getting Latest", response.value ?? "", error )
326
+ print("\(CapacitorUpdater.TAG) Error getting Latest", response.value ?? "", error )
443
327
  latest.message = "Error getting Latest \(String(describing: response.value))"
444
328
  latest.error = "response_error"
445
329
  }
@@ -452,97 +336,526 @@ extension CustomError: LocalizedError {
452
336
  private func setCurrentBundle(bundle: String) {
453
337
  UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
454
338
  UserDefaults.standard.synchronize()
455
- print("\(self.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
339
+ print("\(CapacitorUpdater.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
456
340
  }
457
341
 
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 = ""
342
+ private var tempDataPath: URL {
343
+ return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("package.tmp")
344
+ }
462
345
 
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))
346
+ private var updateInfo: URL {
347
+ return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("update.dat")
348
+ }
349
+ private var tempData = Data()
350
+
351
+ private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
352
+ let actualHash = CryptoCipherV2.calcChecksum(filePath: file)
353
+ return actualHash == expectedHash
354
+ }
355
+
356
+ public func decryptChecksum(checksum: String, version: String) throws -> String {
357
+ if self.publicKey.isEmpty {
358
+ return checksum
359
+ }
360
+ do {
361
+ let checksumBytes: Data = Data(base64Encoded: checksum)!
362
+ guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: self.publicKey) else {
363
+ print("cannot decode publicKey", self.publicKey)
364
+ throw CustomError.cannotDecode
365
+ }
366
+ guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
367
+ throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
368
+ }
369
+ return decryptedChecksum.base64EncodedString()
370
+ } catch {
371
+ print("\(CapacitorUpdater.TAG) Cannot decrypt checksum: \(checksum)", error)
372
+ self.sendStats(action: "decrypt_fail", versionName: version)
373
+ throw CustomError.cannotDecode
374
+ }
375
+ }
376
+
377
+ public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
378
+ let id = self.randomString(length: 10)
379
+ print("\(CapacitorUpdater.TAG) downloadManifest start \(id)")
380
+ let destFolder = self.getBundleDirectory(id: id)
381
+ let builtinFolder = Bundle.main.bundleURL.appendingPathComponent("public")
382
+
383
+ try FileManager.default.createDirectory(at: cacheFolder, withIntermediateDirectories: true, attributes: nil)
384
+ try FileManager.default.createDirectory(at: destFolder, withIntermediateDirectories: true, attributes: nil)
385
+
386
+ // Create and save BundleInfo before starting the download process
387
+ let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "")
388
+ self.saveBundleInfo(id: id, bundle: bundleInfo)
389
+
390
+ // Notify the start of the download process
391
+ self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
392
+
393
+ let dispatchGroup = DispatchGroup()
394
+ var downloadError: Error?
395
+
396
+ let totalFiles = manifest.count
397
+ var completedFiles = 0
398
+
399
+ for entry in manifest {
400
+ guard let fileName = entry.file_name,
401
+ var fileHash = entry.file_hash,
402
+ let downloadUrl = entry.download_url else {
403
+ continue
404
+ }
467
405
 
468
- return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
406
+ if !self.publicKey.isEmpty && !sessionKey.isEmpty {
407
+ do {
408
+ fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey, version: version)
409
+ } catch {
410
+ downloadError = error
411
+ print("\(CapacitorUpdater.TAG) CryptoCipherV2.decryptChecksum error \(id) \(fileName) error: \(error)")
412
+ }
413
+ }
414
+
415
+ let fileNameWithoutPath = (fileName as NSString).lastPathComponent
416
+ let cacheFileName = "\(fileHash)_\(fileNameWithoutPath)"
417
+ let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
418
+ let destFilePath = destFolder.appendingPathComponent(fileName)
419
+ let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
420
+
421
+ // Create necessary subdirectories in the destination folder
422
+ try FileManager.default.createDirectory(at: destFilePath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
423
+
424
+ dispatchGroup.enter()
425
+
426
+ if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
427
+ try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
428
+ print("\(CapacitorUpdater.TAG) downloadManifest \(fileName) using builtin file \(id)")
429
+ completedFiles += 1
430
+ self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
431
+ dispatchGroup.leave()
432
+ } else if FileManager.default.fileExists(atPath: cacheFilePath.path) && verifyChecksum(file: cacheFilePath, expectedHash: fileHash) {
433
+ try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
434
+ print("\(CapacitorUpdater.TAG) downloadManifest \(fileName) copy from cache \(id)")
435
+ completedFiles += 1
436
+ self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
437
+ dispatchGroup.leave()
438
+ } else {
439
+ // File not in cache, download, decompress, and save to both cache and destination
440
+ AF.download(downloadUrl).responseData { response in
441
+ defer { dispatchGroup.leave() }
442
+
443
+ switch response.result {
444
+ case .success(let data):
445
+ do {
446
+ let statusCode = response.response?.statusCode ?? 200
447
+ if statusCode < 200 || statusCode >= 300 {
448
+ if let stringData = String(data: data, encoding: .utf8) {
449
+ throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData)"])
450
+ } else {
451
+ throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid"])
452
+ }
453
+ }
454
+
455
+ // Add decryption step if public key is set and sessionKey is provided
456
+ var finalData = data
457
+ if !self.publicKey.isEmpty && !sessionKey.isEmpty {
458
+ let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
459
+ try finalData.write(to: tempFile)
460
+ do {
461
+ try CryptoCipherV2.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
462
+ } catch {
463
+ self.sendStats(action: "decrypt_fail", versionName: version)
464
+ throw error
465
+ }
466
+ // TODO: try and do self.sendStats(action: "decrypt_fail", versionName: version) if fail
467
+ finalData = try Data(contentsOf: tempFile)
468
+ try FileManager.default.removeItem(at: tempFile)
469
+ }
470
+
471
+ // Decompress the Brotli data
472
+ guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
473
+ throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data"])
474
+ }
475
+ finalData = decompressedData
476
+
477
+ try finalData.write(to: destFilePath)
478
+ if !self.publicKey.isEmpty && !sessionKey.isEmpty {
479
+ // assume that calcChecksum != null
480
+ let calculatedChecksum = CryptoCipherV2.calcChecksum(filePath: destFilePath)
481
+ if calculatedChecksum != fileHash {
482
+ throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash))"])
483
+ }
484
+ }
485
+
486
+ // Save decrypted data to cache and destination
487
+ try finalData.write(to: cacheFilePath)
488
+
489
+ completedFiles += 1
490
+ self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
491
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
492
+ } catch {
493
+ downloadError = error
494
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error)")
495
+ }
496
+ case .failure(let error):
497
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) download error: \(error). Debug response: \(response.debugDescription).")
498
+ }
499
+ }
500
+ }
469
501
  }
470
- let request = AF.download(url, to: destination)
471
502
 
472
- request.downloadProgress { progress in
473
- let percent = self.calcTotalPercent(percent: Int(progress.fractionCompleted * 100), min: 10, max: 70)
474
- self.notifyDownload(id, percent)
503
+ dispatchGroup.wait()
504
+
505
+ if let error = downloadError {
506
+ // Update bundle status to ERROR if download failed
507
+ let errorBundle = bundleInfo.setStatus(status: BundleStatus.ERROR.localizedString)
508
+ self.saveBundleInfo(id: id, bundle: errorBundle)
509
+ throw error
475
510
  }
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
511
+
512
+ // Update bundle status to PENDING after successful download
513
+ let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.localizedString)
514
+ self.saveBundleInfo(id: id, bundle: updatedBundle)
515
+
516
+ print("\(CapacitorUpdater.TAG) downloadManifest done \(id)")
517
+ return updatedBundle
518
+ }
519
+
520
+ private func decompressBrotli(data: Data, fileName: String) -> Data? {
521
+ let outputBufferSize = 65536
522
+ var outputBuffer = [UInt8](repeating: 0, count: outputBufferSize)
523
+ var decompressedData = Data()
524
+
525
+ let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
526
+ var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
527
+ guard status != COMPRESSION_STATUS_ERROR else {
528
+ print("\(CapacitorUpdater.TAG) Unable to initialize the decompression stream. \(fileName)")
529
+ return nil
530
+ }
531
+
532
+ defer {
533
+ compression_stream_destroy(streamPointer)
534
+ streamPointer.deallocate()
535
+ }
536
+
537
+ streamPointer.pointee.src_size = 0
538
+ streamPointer.pointee.dst_ptr = UnsafeMutablePointer<UInt8>(&outputBuffer)
539
+ streamPointer.pointee.dst_size = outputBufferSize
540
+
541
+ let input = data
542
+
543
+ while true {
544
+ if streamPointer.pointee.src_size == 0 {
545
+ streamPointer.pointee.src_size = input.count
546
+ input.withUnsafeBytes { rawBufferPointer in
547
+ if let baseAddress = rawBufferPointer.baseAddress {
548
+ streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
549
+ } else {
550
+ print("\(CapacitorUpdater.TAG) Error: Unable to get base address of input data. \(fileName)")
551
+ status = COMPRESSION_STATUS_ERROR
552
+ return
492
553
  }
493
- case let .failure(error):
494
- print("\(self.TAG) download error", response.value ?? "", error)
495
- mainError = error as NSError
496
554
  }
497
555
  }
498
- semaphore.signal()
556
+
557
+ if status == COMPRESSION_STATUS_ERROR {
558
+ break
559
+ }
560
+
561
+ status = compression_stream_process(streamPointer, 0)
562
+
563
+ let have = outputBufferSize - streamPointer.pointee.dst_size
564
+ if have > 0 {
565
+ decompressedData.append(outputBuffer, count: have)
566
+ }
567
+
568
+ if status == COMPRESSION_STATUS_END {
569
+ break
570
+ } else if status == COMPRESSION_STATUS_ERROR {
571
+ print("\(CapacitorUpdater.TAG) Error during Brotli decompression. \(fileName)")
572
+ // Try to decode as text if mostly ASCII
573
+ if let text = String(data: data, encoding: .utf8) {
574
+ let asciiCount = text.unicodeScalars.filter { $0.isASCII }.count
575
+ let totalCount = text.unicodeScalars.count
576
+ if totalCount > 0 && Double(asciiCount) / Double(totalCount) >= 0.8 {
577
+ print("\(CapacitorUpdater.TAG) Compressed data as text: \(text)")
578
+ }
579
+ }
580
+ return nil
581
+ }
582
+
583
+ if streamPointer.pointee.dst_size == 0 {
584
+ streamPointer.pointee.dst_ptr = UnsafeMutablePointer<UInt8>(&outputBuffer)
585
+ streamPointer.pointee.dst_size = outputBufferSize
586
+ }
587
+
588
+ if input.count == 0 {
589
+ break
590
+ }
591
+ }
592
+
593
+ return status == COMPRESSION_STATUS_END ? decompressedData : nil
594
+ }
595
+
596
+ public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
597
+ let id: String = self.randomString(length: 10)
598
+ let semaphore = DispatchSemaphore(value: 0)
599
+ if version != getLocalUpdateVersion() {
600
+ cleanDownloadData()
601
+ }
602
+ ensureResumableFilesExist()
603
+ saveDownloadInfo(version)
604
+ var checksum = ""
605
+ var targetSize = -1
606
+ var lastSentProgress = 0
607
+ var totalReceivedBytes: Int64 = loadDownloadProgress() // Retrieving the amount of already downloaded data if exist, defined at 0 otherwise
608
+ let requestHeaders: HTTPHeaders = ["Range": "bytes=\(totalReceivedBytes)-"]
609
+ // Opening connection for streaming the bytes
610
+ if totalReceivedBytes == 0 {
611
+ self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
612
+ }
613
+ var mainError: NSError?
614
+ let monitor = ClosureEventMonitor()
615
+ monitor.requestDidCompleteTaskWithError = { (_, _, error) in
616
+ if error != nil {
617
+ print("\(CapacitorUpdater.TAG) Downloading failed - ClosureEventMonitor activated")
618
+ mainError = error as NSError?
619
+ }
620
+ }
621
+ let session = Session(eventMonitors: [monitor])
622
+
623
+ let request = session.streamRequest(url, headers: requestHeaders).validate().onHTTPResponse(perform: { response in
624
+ if let contentLength = response.headers.value(for: "Content-Length") {
625
+ targetSize = (Int(contentLength) ?? -1) + Int(totalReceivedBytes)
626
+ }
627
+ }).responseStream { [weak self] streamResponse in
628
+ guard let self = self else { return }
629
+ switch streamResponse.event {
630
+ case .stream(let result):
631
+ if case .success(let data) = result {
632
+ self.tempData.append(data)
633
+
634
+ self.savePartialData(startingAt: UInt64(totalReceivedBytes)) // Saving the received data in the package.tmp file
635
+ totalReceivedBytes += Int64(data.count)
636
+
637
+ let percent = max(10, Int((Double(totalReceivedBytes) / Double(targetSize)) * 70.0))
638
+
639
+ let currentMilestone = (percent / 10) * 10
640
+ if currentMilestone > lastSentProgress && currentMilestone <= 70 {
641
+ for milestone in stride(from: lastSentProgress + 10, through: currentMilestone, by: 10) {
642
+ self.notifyDownload(id: id, percent: milestone, ignoreMultipleOfTen: false)
643
+ }
644
+ lastSentProgress = currentMilestone
645
+ }
646
+
647
+ } else {
648
+ print("\(CapacitorUpdater.TAG) Download failed")
649
+ }
650
+
651
+ case .complete:
652
+ print("\(CapacitorUpdater.TAG) Download complete, total received bytes: \(totalReceivedBytes)")
653
+ self.notifyDownload(id: id, percent: 70, ignoreMultipleOfTen: true)
654
+ semaphore.signal()
655
+ }
499
656
  }
500
657
  self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
501
- self.notifyDownload(id, 0)
658
+ let reachabilityManager = NetworkReachabilityManager()
659
+ reachabilityManager?.startListening { status in
660
+ switch status {
661
+ case .notReachable:
662
+ // Stop the download request if the network is not reachable
663
+ request.cancel()
664
+ mainError = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet, userInfo: nil)
665
+ semaphore.signal()
666
+ default:
667
+ break
668
+ }
669
+ }
502
670
  semaphore.wait()
671
+ reachabilityManager?.stopListening()
672
+
503
673
  if mainError != nil {
674
+ print("\(CapacitorUpdater.TAG) Failed to download: \(String(describing: mainError))")
675
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
504
676
  throw mainError!
505
677
  }
506
- let info: BundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
678
+
679
+ let finalPath = tempDataPath.deletingLastPathComponent().appendingPathComponent("\(id)")
680
+ do {
681
+ try self.decryptFileV2(filePath: tempDataPath, sessionKey: sessionKey, version: version)
682
+ try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
683
+ } catch {
684
+ print("\(CapacitorUpdater.TAG) Failed decrypt file : \(error)")
685
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
686
+ cleanDownloadData()
687
+ throw error
688
+ }
689
+
690
+ do {
691
+ checksum = CryptoCipherV2.calcChecksum(filePath: finalPath)
692
+ print("\(CapacitorUpdater.TAG) Downloading: 80% (unzipping)")
693
+ try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
694
+
695
+ } catch {
696
+ print("\(CapacitorUpdater.TAG) Failed to unzip file: \(error)")
697
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
698
+ cleanDownloadData()
699
+ // todo: cleanup zip attempts
700
+ throw error
701
+ }
702
+
703
+ self.notifyDownload(id: id, percent: 90)
704
+ print("\(CapacitorUpdater.TAG) Downloading: 90% (wrapping up)")
705
+ let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
507
706
  self.saveBundleInfo(id: id, bundle: info)
707
+ self.cleanDownloadData()
708
+ self.notifyDownload(id: id, percent: 100)
709
+ print("\(CapacitorUpdater.TAG) Downloading: 100% (complete)")
508
710
  return info
509
711
  }
712
+ private func ensureResumableFilesExist() {
713
+ let fileManager = FileManager.default
714
+ if !fileManager.fileExists(atPath: tempDataPath.path) {
715
+ if !fileManager.createFile(atPath: tempDataPath.path, contents: Data()) {
716
+ print("\(CapacitorUpdater.TAG) Cannot ensure that a file at \(tempDataPath.path) exists")
717
+ }
718
+ }
719
+
720
+ if !fileManager.fileExists(atPath: updateInfo.path) {
721
+ if !fileManager.createFile(atPath: updateInfo.path, contents: Data()) {
722
+ print("\(CapacitorUpdater.TAG) Cannot ensure that a file at \(updateInfo.path) exists")
723
+ }
724
+ }
725
+ }
510
726
 
511
- public func list() -> [BundleInfo] {
512
- let dest: URL = documentsDir.appendingPathComponent(bundleDirectoryHot)
727
+ private func cleanDownloadData() {
728
+ // Deleting package.tmp
729
+ let fileManager = FileManager.default
730
+ if fileManager.fileExists(atPath: tempDataPath.path) {
731
+ do {
732
+ try fileManager.removeItem(at: tempDataPath)
733
+ } catch {
734
+ print("\(CapacitorUpdater.TAG) Could not delete file at \(tempDataPath): \(error)")
735
+ }
736
+ }
737
+ // Deleting update.dat
738
+ if fileManager.fileExists(atPath: updateInfo.path) {
739
+ do {
740
+ try fileManager.removeItem(at: updateInfo)
741
+ } catch {
742
+ print("\(CapacitorUpdater.TAG) Could not delete file at \(updateInfo): \(error)")
743
+ }
744
+ }
745
+ }
746
+
747
+ private func savePartialData(startingAt byteOffset: UInt64) {
748
+ let fileManager = FileManager.default
513
749
  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
- }
750
+ // Check if package.tmp exist
751
+ if !fileManager.fileExists(atPath: tempDataPath.path) {
752
+ try self.tempData.write(to: tempDataPath, options: .atomicWrite)
753
+ } else {
754
+ // If yes, it start writing on it
755
+ let fileHandle = try FileHandle(forWritingTo: tempDataPath)
756
+ fileHandle.seek(toFileOffset: byteOffset) // Moving at the specified position to start writing
757
+ fileHandle.write(self.tempData)
758
+ fileHandle.closeFile()
759
+ }
760
+ } catch {
761
+ print("Failed to write data starting at byte \(byteOffset): \(error)")
762
+ }
763
+ self.tempData.removeAll() // Clearing tempData to avoid writing the same data multiple times
764
+ }
765
+
766
+ private func saveDownloadInfo(_ version: String) {
767
+ do {
768
+ try "\(version)".write(to: updateInfo, atomically: true, encoding: .utf8)
769
+ } catch {
770
+ print("\(CapacitorUpdater.TAG) Failed to save progress: \(error)")
771
+ }
772
+ }
773
+ private func getLocalUpdateVersion() -> String { // Return the version that was tried to be downloaded on last download attempt
774
+ if !FileManager.default.fileExists(atPath: updateInfo.path) {
775
+ return "nil"
776
+ }
777
+ guard let versionString = try? String(contentsOf: updateInfo),
778
+ let version = Optional(versionString) else {
779
+ return "nil"
780
+ }
781
+ return version
782
+ }
783
+ private func loadDownloadProgress() -> Int64 {
784
+
785
+ let fileManager = FileManager.default
786
+ do {
787
+ let attributes = try fileManager.attributesOfItem(atPath: tempDataPath.path)
788
+ if let fileSize = attributes[.size] as? NSNumber {
789
+ return fileSize.int64Value
521
790
  }
522
- return res
523
791
  } catch {
524
- print("\(self.TAG) No version available \(dest.path)")
525
- return []
792
+ print("\(CapacitorUpdater.TAG) Could not retrieve already downloaded data size : \(error)")
526
793
  }
794
+ return 0
795
+ }
796
+
797
+ public func list(raw: Bool = false) -> [BundleInfo] {
798
+ if !raw {
799
+ // UserDefaults.standard.dictionaryRepresentation().values
800
+ let dest: URL = libraryDir.appendingPathComponent(bundleDirectory)
801
+ do {
802
+ let files: [String] = try FileManager.default.contentsOfDirectory(atPath: dest.path)
803
+ var res: [BundleInfo] = []
804
+ print("\(CapacitorUpdater.TAG) list File : \(dest.path)")
805
+ if dest.exist {
806
+ for id: String in files {
807
+ res.append(self.getBundleInfo(id: id))
808
+ }
809
+ }
810
+ return res
811
+ } catch {
812
+ print("\(CapacitorUpdater.TAG) No version available \(dest.path)")
813
+ return []
814
+ }
815
+ } else {
816
+ guard let regex = try? NSRegularExpression(pattern: "^[0-9A-Za-z]{10}_info$") else {
817
+ print("\(CapacitorUpdater.TAG) Invald regex ?????")
818
+ return []
819
+ }
820
+ return UserDefaults.standard.dictionaryRepresentation().keys.filter {
821
+ let range = NSRange($0.startIndex..., in: $0)
822
+ let matches = regex.matches(in: $0, range: range)
823
+ return !matches.isEmpty
824
+ }.map {
825
+ $0.components(separatedBy: "_")[0]
826
+ }.map {
827
+ self.getBundleInfo(id: $0)
828
+ }
829
+ }
830
+
527
831
  }
528
832
 
529
833
  public func delete(id: String, removeInfo: Bool) -> Bool {
530
834
  let deleted: BundleInfo = self.getBundleInfo(id: id)
531
835
  if deleted.isBuiltin() || self.getCurrentBundleId() == id {
532
- print("\(self.TAG) Cannot delete \(id)")
836
+ print("\(CapacitorUpdater.TAG) Cannot delete \(id)")
533
837
  return false
534
838
  }
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.")
839
+
840
+ // Check if this is the next bundle and prevent deletion if it is
841
+ if let next = self.getNextBundle(),
842
+ !next.isDeleted() &&
843
+ !next.isErrorStatus() &&
844
+ next.getId() == id {
845
+ print("\(CapacitorUpdater.TAG) Cannot delete the next bundle \(id)")
846
+ return false
541
847
  }
848
+
849
+ let destPersist: URL = libraryDir.appendingPathComponent(bundleDirectory).appendingPathComponent(id)
542
850
  do {
543
851
  try FileManager.default.removeItem(atPath: destPersist.path)
544
852
  } catch {
545
- print("\(self.TAG) Folder \(destPersist.path), not removed.")
853
+ print("\(CapacitorUpdater.TAG) Folder \(destPersist.path), not removed.")
854
+ // even if, we don;t care. Android doesn't care
855
+ if removeInfo {
856
+ self.removeBundleInfo(id: id)
857
+ }
858
+ self.sendStats(action: "delete", versionName: deleted.getVersionName())
546
859
  return false
547
860
  }
548
861
  if removeInfo {
@@ -550,7 +863,7 @@ extension CustomError: LocalizedError {
550
863
  } else {
551
864
  self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.localizedString))
552
865
  }
553
- print("\(self.TAG) bundle delete \(deleted.getVersionName())")
866
+ print("\(CapacitorUpdater.TAG) bundle delete \(deleted.getVersionName())")
554
867
  self.sendStats(action: "delete", versionName: deleted.getVersionName())
555
868
  return true
556
869
  }
@@ -568,13 +881,15 @@ extension CustomError: LocalizedError {
568
881
  }
569
882
 
570
883
  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)
884
+ let destPersist: URL = self.getBundleDirectory(id: id)
885
+ let indexPersist: URL = destPersist.appendingPathComponent("index.html")
576
886
  let bundleIndo: BundleInfo = self.getBundleInfo(id: id)
577
- if url.isDirectory && destHotPersist.isDirectory && indexHot.exist && indexPersist.exist && !bundleIndo.isDeleted() {
887
+ if
888
+ destPersist.exist &&
889
+ destPersist.isDirectory &&
890
+ !indexPersist.isDirectory &&
891
+ indexPersist.exist &&
892
+ !bundleIndo.isDeleted() {
578
893
  return true
579
894
  }
580
895
  return false
@@ -587,9 +902,10 @@ extension CustomError: LocalizedError {
587
902
  return true
588
903
  }
589
904
  if bundleExists(id: id) {
905
+ let currentBundleName = self.getCurrentBundle().getVersionName()
590
906
  self.setCurrentBundle(bundle: self.getBundleDirectory(id: id).path)
591
907
  self.setBundleStatus(id: id, status: BundleStatus.PENDING)
592
- self.sendStats(action: "set", versionName: newBundle.getVersionName())
908
+ self.sendStats(action: "set", versionName: newBundle.getVersionName(), oldVersionName: currentBundleName)
593
909
  return true
594
910
  }
595
911
  self.setBundleStatus(id: id, status: BundleStatus.ERROR)
@@ -597,12 +913,12 @@ extension CustomError: LocalizedError {
597
913
  return false
598
914
  }
599
915
 
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)
916
+ public func autoReset() {
917
+ let currentBundle: BundleInfo = self.getCurrentBundle()
918
+ if !currentBundle.isBuiltin() && !self.bundleExists(id: currentBundle.getId()) {
919
+ print("\(CapacitorUpdater.TAG) Folder at bundle path does not exist. Triggering reset.")
920
+ self.reset()
921
+ }
606
922
  }
607
923
 
608
924
  public func reset() {
@@ -610,26 +926,27 @@ extension CustomError: LocalizedError {
610
926
  }
611
927
 
612
928
  public func reset(isInternal: Bool) {
613
- print("\(self.TAG) reset: \(isInternal)")
929
+ print("\(CapacitorUpdater.TAG) reset: \(isInternal)")
930
+ let currentBundleName = self.getCurrentBundle().getVersionName()
614
931
  self.setCurrentBundle(bundle: "")
615
932
  self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
616
933
  _ = self.setNextBundle(next: Optional<String>.none)
617
934
  if !isInternal {
618
- self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName())
935
+ self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName(), oldVersionName: currentBundleName)
619
936
  }
620
937
  }
621
938
 
622
939
  public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
623
940
  self.setBundleStatus(id: bundle.getId(), status: BundleStatus.SUCCESS)
624
941
  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() {
942
+ print("\(CapacitorUpdater.TAG) Fallback bundle is: \(fallback.toString())")
943
+ print("\(CapacitorUpdater.TAG) Version successfully loaded: \(bundle.toString())")
944
+ if autoDeletePrevious && !fallback.isBuiltin() && fallback.getId() != bundle.getId() {
628
945
  let res = self.delete(id: fallback.getId())
629
946
  if res {
630
- print("\(self.TAG) Deleted previous bundle: \(fallback.toString())")
947
+ print("\(CapacitorUpdater.TAG) Deleted previous bundle: \(fallback.toString())")
631
948
  } else {
632
- print("\(self.TAG) Failed to delete previous bundle: \(fallback.toString())")
949
+ print("\(CapacitorUpdater.TAG) Failed to delete previous bundle: \(fallback.toString())")
633
950
  }
634
951
  }
635
952
  self.setFallbackBundle(fallback: bundle)
@@ -639,10 +956,46 @@ extension CustomError: LocalizedError {
639
956
  self.setBundleStatus(id: bundle.getId(), status: BundleStatus.ERROR)
640
957
  }
641
958
 
959
+ func unsetChannel() -> SetChannel {
960
+ let setChannel: SetChannel = SetChannel()
961
+ if (self.channelUrl ).isEmpty {
962
+ print("\(CapacitorUpdater.TAG) Channel URL is not set")
963
+ setChannel.message = "Channel URL is not set"
964
+ setChannel.error = "missing_config"
965
+ return setChannel
966
+ }
967
+ let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
968
+ let parameters: InfoObject = self.createInfoObject()
969
+
970
+ let request = AF.request(self.channelUrl, method: .delete, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
971
+
972
+ request.validate().responseDecodable(of: SetChannelDec.self) { response in
973
+ switch response.result {
974
+ case .success:
975
+ if let status = response.value?.status {
976
+ setChannel.status = status
977
+ }
978
+ if let error = response.value?.error {
979
+ setChannel.error = error
980
+ }
981
+ if let message = response.value?.message {
982
+ setChannel.message = message
983
+ }
984
+ case let .failure(error):
985
+ print("\(CapacitorUpdater.TAG) Error unset Channel", response.value ?? "", error)
986
+ setChannel.message = "Error unset Channel \(String(describing: response.value))"
987
+ setChannel.error = "response_error"
988
+ }
989
+ semaphore.signal()
990
+ }
991
+ semaphore.wait()
992
+ return setChannel
993
+ }
994
+
642
995
  func setChannel(channel: String) -> SetChannel {
643
996
  let setChannel: SetChannel = SetChannel()
644
997
  if (self.channelUrl ).isEmpty {
645
- print("\(self.TAG) Channel URL is not set")
998
+ print("\(CapacitorUpdater.TAG) Channel URL is not set")
646
999
  setChannel.message = "Channel URL is not set"
647
1000
  setChannel.error = "missing_config"
648
1001
  return setChannel
@@ -666,7 +1019,7 @@ extension CustomError: LocalizedError {
666
1019
  setChannel.message = message
667
1020
  }
668
1021
  case let .failure(error):
669
- print("\(self.TAG) Error set Channel", response.value ?? "", error)
1022
+ print("\(CapacitorUpdater.TAG) Error set Channel", response.value ?? "", error)
670
1023
  setChannel.message = "Error set Channel \(String(describing: response.value))"
671
1024
  setChannel.error = "response_error"
672
1025
  }
@@ -679,7 +1032,7 @@ extension CustomError: LocalizedError {
679
1032
  func getChannel() -> GetChannel {
680
1033
  let getChannel: GetChannel = GetChannel()
681
1034
  if (self.channelUrl ).isEmpty {
682
- print("\(self.TAG) Channel URL is not set")
1035
+ print("\(CapacitorUpdater.TAG) Channel URL is not set")
683
1036
  getChannel.message = "Channel URL is not set"
684
1037
  getChannel.error = "missing_config"
685
1038
  return getChannel
@@ -689,6 +1042,9 @@ extension CustomError: LocalizedError {
689
1042
  let request = AF.request(self.channelUrl, method: .put, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
690
1043
 
691
1044
  request.validate().responseDecodable(of: GetChannelDec.self) { response in
1045
+ defer {
1046
+ semaphore.signal()
1047
+ }
692
1048
  switch response.result {
693
1049
  case .success:
694
1050
  if let status = response.value?.status {
@@ -707,33 +1063,59 @@ extension CustomError: LocalizedError {
707
1063
  getChannel.allowSet = allowSet
708
1064
  }
709
1065
  case let .failure(error):
710
- print("\(self.TAG) Error get Channel", response.value ?? "", error)
1066
+ if let data = response.data, let bodyString = String(data: data, encoding: .utf8) {
1067
+ if bodyString.contains("channel_not_found") && response.response?.statusCode == 400 && !self.defaultChannel.isEmpty {
1068
+ getChannel.channel = self.defaultChannel
1069
+ getChannel.status = "default"
1070
+ return
1071
+ }
1072
+ }
1073
+
1074
+ print("\(CapacitorUpdater.TAG) Error get Channel", response.value ?? "", error)
711
1075
  getChannel.message = "Error get Channel \(String(describing: response.value)))"
712
1076
  getChannel.error = "response_error"
713
1077
  }
714
- semaphore.signal()
715
1078
  }
716
1079
  semaphore.wait()
717
1080
  return getChannel
718
1081
  }
719
1082
 
720
- func sendStats(action: String, versionName: String) {
721
- if (self.statsUrl ).isEmpty {
1083
+ private let operationQueue = OperationQueue()
1084
+
1085
+ func sendStats(action: String, versionName: String? = nil, oldVersionName: String? = "") {
1086
+ guard !statsUrl.isEmpty else {
722
1087
  return
723
1088
  }
724
- var parameters: InfoObject = self.createInfoObject()
1089
+ operationQueue.maxConcurrentOperationCount = 1
1090
+
1091
+ let versionName = versionName ?? getCurrentBundle().getVersionName()
1092
+
1093
+ var parameters = createInfoObject()
725
1094
  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
1095
+ parameters.version_name = versionName
1096
+ parameters.old_version_name = oldVersionName ?? ""
1097
+
1098
+ let operation = BlockOperation {
1099
+ let semaphore = DispatchSemaphore(value: 0)
1100
+ AF.request(
1101
+ self.statsUrl,
1102
+ method: .post,
1103
+ parameters: parameters,
1104
+ encoder: JSONParameterEncoder.default,
1105
+ requestModifier: { $0.timeoutInterval = self.timeout }
1106
+ ).responseData { response in
729
1107
  switch response.result {
730
1108
  case .success:
731
- print("\(self.TAG) Stats send for \(action), version \(versionName)")
1109
+ print("\(CapacitorUpdater.TAG) Stats sent for \(action), version \(versionName)")
732
1110
  case let .failure(error):
733
- print("\(self.TAG) Error sending stats: ", response.value ?? "", error)
1111
+ print("\(CapacitorUpdater.TAG) Error sending stats: ", response.value ?? "", error.localizedDescription)
734
1112
  }
1113
+ semaphore.signal()
735
1114
  }
1115
+ semaphore.signal()
736
1116
  }
1117
+ operationQueue.addOperation(operation)
1118
+
737
1119
  }
738
1120
 
739
1121
  public func getBundleInfo(id: String?) -> BundleInfo {
@@ -741,7 +1123,7 @@ extension CustomError: LocalizedError {
741
1123
  if id != nil {
742
1124
  trueId = id!
743
1125
  }
744
- print("\(self.TAG) Getting info for bundle [\(trueId)]")
1126
+ // print("\(CapacitorUpdater.TAG) Getting info for bundle [\(trueId)]")
745
1127
  let result: BundleInfo
746
1128
  if BundleInfo.ID_BUILTIN == trueId {
747
1129
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.SUCCESS, checksum: "")
@@ -751,11 +1133,11 @@ extension CustomError: LocalizedError {
751
1133
  do {
752
1134
  result = try UserDefaults.standard.getObj(forKey: "\(trueId)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
753
1135
  } catch {
754
- print("\(self.TAG) Failed to parse info for bundle [\(trueId)]", error.localizedDescription)
1136
+ print("\(CapacitorUpdater.TAG) Failed to parse info for bundle [\(trueId)]", error.localizedDescription)
755
1137
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.PENDING, checksum: "")
756
1138
  }
757
1139
  }
758
- // print("\(self.TAG) Returning info bundle [\(result.toString())]")
1140
+ // print("\(CapacitorUpdater.TAG) Returning info bundle [\(result.toString())]")
759
1141
  return result
760
1142
  }
761
1143
 
@@ -773,34 +1155,28 @@ extension CustomError: LocalizedError {
773
1155
  self.saveBundleInfo(id: id, bundle: nil)
774
1156
  }
775
1157
 
776
- private func saveBundleInfo(id: String, bundle: BundleInfo?) {
1158
+ public func saveBundleInfo(id: String, bundle: BundleInfo?) {
777
1159
  if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
778
- print("\(self.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
1160
+ print("\(CapacitorUpdater.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
779
1161
  return
780
1162
  }
781
1163
  if bundle == nil {
782
- print("\(self.TAG) Removing info for bundle [\(id)]")
1164
+ print("\(CapacitorUpdater.TAG) Removing info for bundle [\(id)]")
783
1165
  UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
784
1166
  } else {
785
1167
  let update = bundle!.setId(id: id)
786
- print("\(self.TAG) Storing info for bundle [\(id)]", update.toString())
1168
+ print("\(CapacitorUpdater.TAG) Storing info for bundle [\(id)]", update.toString())
787
1169
  do {
788
1170
  try UserDefaults.standard.setObj(update, forKey: "\(id)\(self.INFO_SUFFIX)")
789
1171
  } catch {
790
- print("\(self.TAG) Failed to save info for bundle [\(id)]", error.localizedDescription)
1172
+ print("\(CapacitorUpdater.TAG) Failed to save info for bundle [\(id)]", error.localizedDescription)
791
1173
  }
792
1174
  }
793
1175
  UserDefaults.standard.synchronize()
794
1176
  }
795
1177
 
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
1178
  private func setBundleStatus(id: String, status: BundleStatus) {
803
- print("\(self.TAG) Setting status for bundle [\(id)] to \(status)")
1179
+ print("\(CapacitorUpdater.TAG) Setting status for bundle [\(id)] to \(status)")
804
1180
  let info = self.getBundleInfo(id: id)
805
1181
  self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
806
1182
  }
@@ -813,7 +1189,7 @@ extension CustomError: LocalizedError {
813
1189
  guard let bundlePath: String = UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) else {
814
1190
  return BundleInfo.ID_BUILTIN
815
1191
  }
816
- if (bundlePath ).isEmpty {
1192
+ if (bundlePath).isEmpty {
817
1193
  return BundleInfo.ID_BUILTIN
818
1194
  }
819
1195
  let bundleID: String = bundlePath.components(separatedBy: "/").last ?? bundlePath
@@ -846,8 +1222,7 @@ extension CustomError: LocalizedError {
846
1222
  return false
847
1223
  }
848
1224
  let newBundle: BundleInfo = self.getBundleInfo(id: nextId)
849
- let bundle: URL = self.getBundleDirectory(id: nextId)
850
- if !newBundle.isBuiltin() && !bundle.exist {
1225
+ if !newBundle.isBuiltin() && !self.bundleExists(id: nextId) {
851
1226
  return false
852
1227
  }
853
1228
  UserDefaults.standard.set(nextId, forKey: self.NEXT_VERSION)