@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.
- package/CapgoCapacitorUpdater.podspec +2 -2
- package/Package.swift +35 -0
- package/README.md +667 -206
- package/android/build.gradle +16 -11
- package/android/proguard-rules.pro +28 -0
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +134 -194
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +13 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +967 -1027
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1283 -1180
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +276 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +45 -48
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +440 -113
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +101 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +32 -0
- package/dist/docs.json +1316 -473
- package/dist/esm/definitions.d.ts +518 -248
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +25 -41
- package/dist/esm/web.js +67 -35
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +67 -35
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +67 -35
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/CapacitorUpdater.swift +736 -361
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +436 -136
- package/ios/Plugin/CryptoCipherV2.swift +310 -0
- package/ios/Plugin/InternalUtils.swift +258 -0
- package/package.json +33 -29
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +0 -153
- package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
- package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
- 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
|
|
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
|
|
34
|
+
public var versionBuild: String = ""
|
|
236
35
|
public var customId: String = ""
|
|
237
36
|
public var PLUGIN_VERSION: String = ""
|
|
238
|
-
public
|
|
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 =
|
|
243
|
-
public var
|
|
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("\(
|
|
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("\(
|
|
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("\(
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
357
|
-
print("cannot decode
|
|
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
|
-
|
|
367
|
-
let
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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("\(
|
|
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
|
|
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
|
|
383
|
-
let destUnZip: URL =
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
414
|
-
|
|
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("\(
|
|
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("\(
|
|
339
|
+
print("\(CapacitorUpdater.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
|
|
456
340
|
}
|
|
457
341
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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("\(
|
|
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("\(
|
|
836
|
+
print("\(CapacitorUpdater.TAG) Cannot delete \(id)")
|
|
533
837
|
return false
|
|
534
838
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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("\(
|
|
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("\(
|
|
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
|
|
572
|
-
let
|
|
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
|
|
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
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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("\(
|
|
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("\(
|
|
626
|
-
print("\(
|
|
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("\(
|
|
947
|
+
print("\(CapacitorUpdater.TAG) Deleted previous bundle: \(fallback.toString())")
|
|
631
948
|
} else {
|
|
632
|
-
print("\(
|
|
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("\(
|
|
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("\(
|
|
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("\(
|
|
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
|
-
|
|
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
|
-
|
|
721
|
-
|
|
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
|
-
|
|
1089
|
+
operationQueue.maxConcurrentOperationCount = 1
|
|
1090
|
+
|
|
1091
|
+
let versionName = versionName ?? getCurrentBundle().getVersionName()
|
|
1092
|
+
|
|
1093
|
+
var parameters = createInfoObject()
|
|
725
1094
|
parameters.action = action
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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("\(
|
|
1109
|
+
print("\(CapacitorUpdater.TAG) Stats sent for \(action), version \(versionName)")
|
|
732
1110
|
case let .failure(error):
|
|
733
|
-
print("\(
|
|
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("\(
|
|
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("\(
|
|
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("\(
|
|
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
|
-
|
|
1158
|
+
public func saveBundleInfo(id: String, bundle: BundleInfo?) {
|
|
777
1159
|
if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
|
|
778
|
-
print("\(
|
|
1160
|
+
print("\(CapacitorUpdater.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
|
|
779
1161
|
return
|
|
780
1162
|
}
|
|
781
1163
|
if bundle == nil {
|
|
782
|
-
print("\(
|
|
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("\(
|
|
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("\(
|
|
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("\(
|
|
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
|
|
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
|
-
|
|
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)
|