@capgo/capacitor-updater 8.45.9 → 8.45.11
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/README.md +74 -28
- package/android/build.gradle +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +185 -86
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +40 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +27 -3
- package/dist/docs.json +163 -4
- package/dist/esm/definitions.d.ts +82 -19
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +289 -94
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +672 -300
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +31 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +20 -3
- package/package.json +3 -2
|
@@ -81,15 +81,51 @@ import UIKit
|
|
|
81
81
|
CapgoUpdater.buildUserAgent(appId: appId, pluginVersion: pluginVersion, versionOs: versionOs)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
struct RequestResult {
|
|
85
|
+
let data: Data?
|
|
86
|
+
let response: HTTPURLResponse?
|
|
87
|
+
let error: Error?
|
|
88
|
+
let timedOut: Bool
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private struct DownloadRequestResult {
|
|
92
|
+
let fileURL: URL?
|
|
93
|
+
let response: HTTPURLResponse?
|
|
94
|
+
let error: Error?
|
|
95
|
+
let timedOut: Bool
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func isTimedOutError(_ error: Error?) -> Bool {
|
|
99
|
+
guard let nsError = error as NSError? else {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
private lazy var alamofireSession: Session = {
|
|
85
|
-
let configuration = URLSessionConfiguration.
|
|
107
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
86
108
|
configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
|
|
109
|
+
configuration.httpCookieStorage = nil
|
|
110
|
+
configuration.httpShouldSetCookies = false
|
|
111
|
+
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
112
|
+
configuration.urlCache = nil
|
|
87
113
|
return Session(configuration: configuration)
|
|
88
114
|
}()
|
|
115
|
+
private let networkResponseQueue = DispatchQueue(label: "ee.forgr.capacitor-updater.network-response", qos: .utility)
|
|
89
116
|
|
|
90
117
|
public var notifyDownloadRaw: (String, Int, Bool, BundleInfo?) -> Void = { _, _, _, _ in }
|
|
91
118
|
public func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
|
|
92
|
-
|
|
119
|
+
let emit = {
|
|
120
|
+
self.notifyDownloadRaw(id, percent, ignoreMultipleOfTen, bundle)
|
|
121
|
+
}
|
|
122
|
+
if Thread.isMainThread {
|
|
123
|
+
emit()
|
|
124
|
+
} else {
|
|
125
|
+
DispatchQueue.main.async {
|
|
126
|
+
emit()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
93
129
|
}
|
|
94
130
|
public var notifyDownload: (String, Int) -> Void = { _, _ in }
|
|
95
131
|
public var notifyListeners: (String, [String: Any]) -> Void = { _, _ in }
|
|
@@ -98,6 +134,145 @@ import UIKit
|
|
|
98
134
|
self.logger = logger
|
|
99
135
|
}
|
|
100
136
|
|
|
137
|
+
private func createRequest(url: URL, method: String, parameters: [String: Any]? = nil, expectsJSONResponse: Bool = false) -> URLRequest? {
|
|
138
|
+
var request = URLRequest(url: url)
|
|
139
|
+
request.httpMethod = method
|
|
140
|
+
request.timeoutInterval = self.timeout
|
|
141
|
+
request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
|
142
|
+
if expectsJSONResponse || parameters != nil {
|
|
143
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
guard let parameters else {
|
|
147
|
+
return request
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
guard JSONSerialization.isValidJSONObject(parameters) else {
|
|
151
|
+
logger.error("Invalid JSON body for \(method) \(url.absoluteString)")
|
|
152
|
+
return nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
do {
|
|
156
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
|
157
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
158
|
+
return request
|
|
159
|
+
} catch {
|
|
160
|
+
logger.error("Error encoding request body for \(method) \(url.absoluteString)")
|
|
161
|
+
logger.debug("Error: \(error.localizedDescription)")
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func performRequest(_ request: URLRequest, label: String) -> RequestResult {
|
|
167
|
+
let waitTimeout = max(self.timeout + 5, 10)
|
|
168
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
169
|
+
var responseData: Data?
|
|
170
|
+
var httpResponse: HTTPURLResponse?
|
|
171
|
+
var requestError: Error?
|
|
172
|
+
let dataRequest = self.alamofireSession.request(request).responseData(queue: self.networkResponseQueue) { response in
|
|
173
|
+
responseData = response.data
|
|
174
|
+
httpResponse = response.response
|
|
175
|
+
requestError = response.error
|
|
176
|
+
semaphore.signal()
|
|
177
|
+
}
|
|
178
|
+
dataRequest.resume()
|
|
179
|
+
|
|
180
|
+
if semaphore.wait(timeout: .now() + waitTimeout) == .timedOut {
|
|
181
|
+
dataRequest.cancel()
|
|
182
|
+
logger.error("\(label) timed out after \(Int(waitTimeout))s")
|
|
183
|
+
return RequestResult(data: responseData, response: httpResponse, error: requestError, timedOut: true)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return RequestResult(data: responseData, response: httpResponse, error: requestError, timedOut: false)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func performDownloadRequest(_ request: URLRequest, label: String) -> DownloadRequestResult {
|
|
190
|
+
let waitTimeout = max(self.timeout + 5, 10)
|
|
191
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
192
|
+
var tempFileURL: URL?
|
|
193
|
+
var httpResponse: HTTPURLResponse?
|
|
194
|
+
var requestError: Error?
|
|
195
|
+
let temporaryDownloadURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
196
|
+
let destination: DownloadRequest.Destination = { _, _ in
|
|
197
|
+
(temporaryDownloadURL, [.removePreviousFile, .createIntermediateDirectories])
|
|
198
|
+
}
|
|
199
|
+
let downloadRequest = self.alamofireSession.download(request, to: destination).response(queue: self.networkResponseQueue) { response in
|
|
200
|
+
tempFileURL = response.fileURL
|
|
201
|
+
httpResponse = response.response
|
|
202
|
+
requestError = response.error
|
|
203
|
+
semaphore.signal()
|
|
204
|
+
}
|
|
205
|
+
downloadRequest.resume()
|
|
206
|
+
|
|
207
|
+
if semaphore.wait(timeout: .now() + waitTimeout) == .timedOut {
|
|
208
|
+
downloadRequest.cancel()
|
|
209
|
+
logger.error("\(label) timed out after \(Int(waitTimeout))s")
|
|
210
|
+
return DownloadRequestResult(
|
|
211
|
+
fileURL: existingDownloadFileURL(tempFileURL, fallback: temporaryDownloadURL),
|
|
212
|
+
response: httpResponse,
|
|
213
|
+
error: requestError,
|
|
214
|
+
timedOut: true
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if isTimedOutError(requestError) {
|
|
219
|
+
logger.error("\(label) timed out after \(Int(waitTimeout))s")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return DownloadRequestResult(
|
|
223
|
+
fileURL: existingDownloadFileURL(tempFileURL, fallback: temporaryDownloadURL),
|
|
224
|
+
response: httpResponse,
|
|
225
|
+
error: requestError,
|
|
226
|
+
timedOut: isTimedOutError(requestError)
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func existingDownloadFileURL(_ fileURL: URL?, fallback: URL) -> URL? {
|
|
231
|
+
let fileManager = FileManager.default
|
|
232
|
+
if let fileURL, fileManager.fileExists(atPath: fileURL.path) {
|
|
233
|
+
return fileURL
|
|
234
|
+
}
|
|
235
|
+
return fileManager.fileExists(atPath: fallback.path) ? fallback : nil
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func storeDownloadedFile(_ downloadedFileURL: URL, at tempPath: URL, existingBytes: Int64, response: HTTPURLResponse?) throws {
|
|
239
|
+
let fileManager = FileManager.default
|
|
240
|
+
if existingBytes > 0 && (response?.statusCode == 206 || response == nil) {
|
|
241
|
+
let resumedData = try Data(contentsOf: downloadedFileURL)
|
|
242
|
+
let fileHandle = try FileHandle(forWritingTo: tempPath)
|
|
243
|
+
fileHandle.seek(toFileOffset: UInt64(existingBytes))
|
|
244
|
+
fileHandle.write(resumedData)
|
|
245
|
+
try fileHandle.close()
|
|
246
|
+
try? fileManager.removeItem(at: downloadedFileURL)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if fileManager.fileExists(atPath: tempPath.path) {
|
|
251
|
+
try fileManager.removeItem(at: tempPath)
|
|
252
|
+
}
|
|
253
|
+
try fileManager.moveItem(at: downloadedFileURL, to: tempPath)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private func persistPartialDownload(_ downloadResult: DownloadRequestResult, id: String, tempPath: URL, existingBytes: Int64) {
|
|
257
|
+
guard let downloadedFileURL = downloadResult.fileURL else {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
guard FileManager.default.fileExists(atPath: downloadedFileURL.path) else {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if let statusCode = downloadResult.response?.statusCode, statusCode < 200 || statusCode >= 300 {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
do {
|
|
268
|
+
try storeDownloadedFile(downloadedFileURL, at: tempPath, existingBytes: existingBytes, response: downloadResult.response)
|
|
269
|
+
logger.info("Stored partial download for retry")
|
|
270
|
+
} catch {
|
|
271
|
+
logger.error("Failed to store partial download")
|
|
272
|
+
logger.debug("Path: \(downloadedFileURL.path), Error: \(error)")
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
101
276
|
deinit {
|
|
102
277
|
// Invalidate the stats timer to prevent memory leaks
|
|
103
278
|
statsFlushTimer?.invalidate()
|
|
@@ -224,8 +399,8 @@ import UIKit
|
|
|
224
399
|
self.alamofireSession.request(
|
|
225
400
|
self.statsUrl,
|
|
226
401
|
method: .post,
|
|
227
|
-
parameters: parameters,
|
|
228
|
-
|
|
402
|
+
parameters: parameters.toParameters(),
|
|
403
|
+
encoding: JSONEncoding.default,
|
|
229
404
|
requestModifier: { $0.timeoutInterval = self.timeout }
|
|
230
405
|
).responseData { response in
|
|
231
406
|
switch response.result {
|
|
@@ -334,6 +509,65 @@ import UIKit
|
|
|
334
509
|
}
|
|
335
510
|
}
|
|
336
511
|
|
|
512
|
+
private func extractZipEntry(_ archive: Archive, entry: Entry, to destPath: URL) throws {
|
|
513
|
+
let fileManager = FileManager.default
|
|
514
|
+
|
|
515
|
+
switch entry.type {
|
|
516
|
+
case .directory:
|
|
517
|
+
try fileManager.createDirectory(at: destPath, withIntermediateDirectories: true, attributes: nil)
|
|
518
|
+
case .file:
|
|
519
|
+
let parentDir = destPath.deletingLastPathComponent()
|
|
520
|
+
try fileManager.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil)
|
|
521
|
+
|
|
522
|
+
if fileManager.fileExists(atPath: destPath.path) {
|
|
523
|
+
try fileManager.removeItem(at: destPath)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
guard fileManager.createFile(atPath: destPath.path, contents: nil) else {
|
|
527
|
+
throw CustomError.cannotUnzip
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let fileHandle = try FileHandle(forWritingTo: destPath)
|
|
531
|
+
defer {
|
|
532
|
+
fileHandle.closeFile()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
_ = try archive.extract(entry, bufferSize: 16 * 1024, skipCRC32: true) { data in
|
|
536
|
+
if !data.isEmpty {
|
|
537
|
+
fileHandle.write(data)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
case .symlink:
|
|
541
|
+
var linkData = Data()
|
|
542
|
+
_ = try archive.extract(entry, bufferSize: 16 * 1024, skipCRC32: true) { data in
|
|
543
|
+
linkData.append(data)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
guard let linkPath = String(data: linkData, encoding: .utf8) else {
|
|
547
|
+
throw CustomError.cannotUnzip
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let parentDir = destPath.deletingLastPathComponent()
|
|
551
|
+
try fileManager.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil)
|
|
552
|
+
|
|
553
|
+
let isAbsolutePath = (linkPath as NSString).isAbsolutePath
|
|
554
|
+
let linkURL = URL(fileURLWithPath: linkPath, relativeTo: isAbsolutePath ? nil : parentDir)
|
|
555
|
+
let canonicalPath = linkURL.standardizedFileURL.path
|
|
556
|
+
let canonicalDir = parentDir.standardizedFileURL.path
|
|
557
|
+
let normalizedDir = canonicalDir.hasSuffix("/") ? canonicalDir : "\(canonicalDir)/"
|
|
558
|
+
|
|
559
|
+
if canonicalPath != canonicalDir && !canonicalPath.hasPrefix(normalizedDir) {
|
|
560
|
+
throw CustomError.cannotUnzip
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if fileManager.fileExists(atPath: destPath.path) {
|
|
564
|
+
try fileManager.removeItem(at: destPath)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try fileManager.createSymbolicLink(atPath: destPath.path, withDestinationPath: linkPath)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
337
571
|
private func saveDownloaded(sourceZip: URL, id: String, base: URL, notify: Bool) throws {
|
|
338
572
|
try prepareFolder(source: base)
|
|
339
573
|
let destPersist: URL = base.appendingPathComponent(id)
|
|
@@ -365,14 +599,26 @@ import UIKit
|
|
|
365
599
|
|
|
366
600
|
let destPath = destUnZip.appendingPathComponent(entry.path)
|
|
367
601
|
|
|
602
|
+
if entry.type == .directory {
|
|
603
|
+
try FileManager.default.createDirectory(at: destPath, withIntermediateDirectories: true, attributes: nil)
|
|
604
|
+
processedEntries += 1
|
|
605
|
+
if notify && totalEntries > 0 {
|
|
606
|
+
let newPercent = self.calcTotalPercent(percent: Int(Double(processedEntries) / Double(totalEntries) * 100), min: 75, max: 81)
|
|
607
|
+
if newPercent != self.unzipPercent {
|
|
608
|
+
self.unzipPercent = newPercent
|
|
609
|
+
self.notifyDownload(id: id, percent: newPercent)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
|
|
368
615
|
// Create parent directories if needed
|
|
369
616
|
let parentDir = destPath.deletingLastPathComponent()
|
|
370
617
|
if !FileManager.default.fileExists(atPath: parentDir.path) {
|
|
371
618
|
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil)
|
|
372
619
|
}
|
|
373
620
|
|
|
374
|
-
|
|
375
|
-
_ = try archive.extract(entry, to: destPath, skipCRC32: true)
|
|
621
|
+
try self.extractZipEntry(archive, entry: entry, to: destPath)
|
|
376
622
|
|
|
377
623
|
// Update progress
|
|
378
624
|
processedEntries += 1
|
|
@@ -476,65 +722,117 @@ import UIKit
|
|
|
476
722
|
}
|
|
477
723
|
|
|
478
724
|
public func getLatest(url: URL, channel: String?) -> AppVersion {
|
|
479
|
-
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
480
725
|
let latest: AppVersion = AppVersion()
|
|
726
|
+
func applyLatestResponse(_ value: AppVersionDec?) {
|
|
727
|
+
if let url = value?.url {
|
|
728
|
+
latest.url = url
|
|
729
|
+
}
|
|
730
|
+
if let checksum = value?.checksum {
|
|
731
|
+
latest.checksum = checksum
|
|
732
|
+
}
|
|
733
|
+
if let version = value?.version {
|
|
734
|
+
latest.version = version
|
|
735
|
+
}
|
|
736
|
+
if let major = value?.major {
|
|
737
|
+
latest.major = major
|
|
738
|
+
}
|
|
739
|
+
if let breaking = value?.breaking {
|
|
740
|
+
latest.breaking = breaking
|
|
741
|
+
}
|
|
742
|
+
if let error = value?.error {
|
|
743
|
+
latest.error = error
|
|
744
|
+
}
|
|
745
|
+
if let kind = value?.kind {
|
|
746
|
+
latest.kind = kind
|
|
747
|
+
}
|
|
748
|
+
if let message = value?.message {
|
|
749
|
+
latest.message = message
|
|
750
|
+
}
|
|
751
|
+
if let sessionKey = value?.session_key {
|
|
752
|
+
latest.sessionKey = sessionKey
|
|
753
|
+
}
|
|
754
|
+
if let data = value?.data {
|
|
755
|
+
latest.data = data
|
|
756
|
+
}
|
|
757
|
+
if let manifest = value?.manifest {
|
|
758
|
+
latest.manifest = manifest
|
|
759
|
+
}
|
|
760
|
+
if let link = value?.link {
|
|
761
|
+
latest.link = link
|
|
762
|
+
}
|
|
763
|
+
if let comment = value?.comment {
|
|
764
|
+
latest.comment = comment
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
481
768
|
var parameters: InfoObject = self.createInfoObject()
|
|
482
769
|
if let channel = channel {
|
|
483
770
|
parameters.defaultChannel = channel
|
|
484
771
|
}
|
|
485
|
-
|
|
486
|
-
|
|
772
|
+
guard let request = createRequest(url: url, method: "POST", parameters: parameters.toParameters()) else {
|
|
773
|
+
latest.message = "Error getting Latest"
|
|
774
|
+
latest.error = "request_error"
|
|
775
|
+
latest.kind = "failed"
|
|
776
|
+
return latest
|
|
777
|
+
}
|
|
487
778
|
|
|
488
|
-
request
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
779
|
+
let result = performRequest(request, label: "getLatest")
|
|
780
|
+
latest.statusCode = result.response?.statusCode ?? 0
|
|
781
|
+
|
|
782
|
+
if result.timedOut {
|
|
783
|
+
latest.message = "Error getting Latest"
|
|
784
|
+
latest.error = "timeout_error"
|
|
785
|
+
latest.kind = "failed"
|
|
786
|
+
return latest
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if let error = result.error {
|
|
790
|
+
self.logger.error("Error getting latest version")
|
|
791
|
+
self.logger.debug("Error: \(error.localizedDescription)")
|
|
792
|
+
latest.message = "Error getting Latest"
|
|
793
|
+
latest.error = "response_error"
|
|
794
|
+
latest.kind = "failed"
|
|
795
|
+
return latest
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
guard let data = result.data else {
|
|
799
|
+
self.logger.error("Missing latest version response data")
|
|
800
|
+
latest.message = "Error getting Latest"
|
|
801
|
+
latest.error = "response_error"
|
|
802
|
+
latest.kind = "failed"
|
|
803
|
+
return latest
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if self.checkAndHandleRateLimitResponse(statusCode: latest.statusCode) {
|
|
807
|
+
latest.message = "Rate limit exceeded"
|
|
808
|
+
latest.error = "rate_limit_exceeded"
|
|
809
|
+
latest.kind = "failed"
|
|
810
|
+
return latest
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
guard let responseValue = try? JSONDecoder().decode(AppVersionDec.self, from: data) else {
|
|
814
|
+
self.logger.error("Error decoding latest version")
|
|
815
|
+
latest.message = "Error getting Latest"
|
|
816
|
+
latest.error = "decode_error"
|
|
817
|
+
latest.kind = "failed"
|
|
818
|
+
return latest
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
applyLatestResponse(responseValue)
|
|
822
|
+
|
|
823
|
+
if latest.statusCode < 200 || latest.statusCode >= 300 {
|
|
824
|
+
if latest.message == nil || latest.message?.isEmpty == true {
|
|
825
|
+
latest.message = responseValue.message ?? "Server error: \(latest.statusCode)"
|
|
534
826
|
}
|
|
535
|
-
|
|
827
|
+
if latest.error == nil || latest.error?.isEmpty == true {
|
|
828
|
+
latest.error = responseValue.error ?? "response_error"
|
|
829
|
+
}
|
|
830
|
+
if latest.kind == nil || latest.kind?.isEmpty == true {
|
|
831
|
+
latest.kind = responseValue.kind ?? "failed"
|
|
832
|
+
}
|
|
833
|
+
return latest
|
|
536
834
|
}
|
|
537
|
-
|
|
835
|
+
|
|
538
836
|
return latest
|
|
539
837
|
}
|
|
540
838
|
|
|
@@ -741,13 +1039,13 @@ import UIKit
|
|
|
741
1039
|
userInfo: [NSLocalizedDescriptionKey: "Manifest download failed due to invalid or missing entries"]
|
|
742
1040
|
)
|
|
743
1041
|
// Update bundle status to ERROR if download failed
|
|
744
|
-
let errorBundle = bundleInfo.setStatus(status: BundleStatus.ERROR.
|
|
1042
|
+
let errorBundle = bundleInfo.setStatus(status: BundleStatus.ERROR.storedValue)
|
|
745
1043
|
self.saveBundleInfo(id: id, bundle: errorBundle)
|
|
746
1044
|
throw resolvedError
|
|
747
1045
|
}
|
|
748
1046
|
|
|
749
1047
|
// Update bundle status to PENDING after successful download
|
|
750
|
-
let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.
|
|
1048
|
+
let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.storedValue)
|
|
751
1049
|
self.saveBundleInfo(id: id, bundle: updatedBundle)
|
|
752
1050
|
|
|
753
1051
|
// Send stats for manifest download complete
|
|
@@ -772,85 +1070,105 @@ import UIKit
|
|
|
772
1070
|
version: String,
|
|
773
1071
|
bundleId: String
|
|
774
1072
|
) throws {
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1073
|
+
guard let url = URL(string: downloadUrl) else {
|
|
1074
|
+
throw NSError(
|
|
1075
|
+
domain: "ManifestDownloadError",
|
|
1076
|
+
code: 1,
|
|
1077
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid manifest download URL for file \(fileName): \(downloadUrl)"]
|
|
1078
|
+
)
|
|
1079
|
+
}
|
|
780
1080
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
throw NSError(domain: "StatusCodeError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData) for file \(fileName) at url \(downloadUrl)"])
|
|
789
|
-
} else {
|
|
790
|
-
throw NSError(domain: "StatusCodeError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid for file \(fileName) at url \(downloadUrl)"])
|
|
791
|
-
}
|
|
792
|
-
}
|
|
1081
|
+
guard let request = createRequest(url: url, method: "GET") else {
|
|
1082
|
+
throw NSError(
|
|
1083
|
+
domain: "ManifestDownloadError",
|
|
1084
|
+
code: 2,
|
|
1085
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid manifest request for file \(fileName): \(downloadUrl)"]
|
|
1086
|
+
)
|
|
1087
|
+
}
|
|
793
1088
|
|
|
794
|
-
|
|
795
|
-
var finalData = data
|
|
796
|
-
if !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
797
|
-
let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
|
|
798
|
-
try finalData.write(to: tempFile)
|
|
799
|
-
do {
|
|
800
|
-
try CryptoCipher.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
|
|
801
|
-
} catch {
|
|
802
|
-
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
803
|
-
throw error
|
|
804
|
-
}
|
|
805
|
-
finalData = try Data(contentsOf: tempFile)
|
|
806
|
-
try FileManager.default.removeItem(at: tempFile)
|
|
807
|
-
}
|
|
1089
|
+
let result = performRequest(request, label: "downloadManifestFile \(fileName)")
|
|
808
1090
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1091
|
+
if result.timedOut {
|
|
1092
|
+
self.sendStats(action: "download_manifest_file_fail", versionName: "\(version):\(fileName)")
|
|
1093
|
+
throw NSError(
|
|
1094
|
+
domain: NSURLErrorDomain,
|
|
1095
|
+
code: NSURLErrorTimedOut,
|
|
1096
|
+
userInfo: [NSLocalizedDescriptionKey: "Timed out downloading manifest file \(fileName) at url \(downloadUrl)"]
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
817
1099
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
|
|
825
|
-
if calculatedChecksum != fileHash {
|
|
826
|
-
try? FileManager.default.removeItem(at: destFilePath)
|
|
827
|
-
self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
|
|
828
|
-
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
829
|
-
}
|
|
1100
|
+
if let error = result.error {
|
|
1101
|
+
self.sendStats(action: "download_manifest_file_fail", versionName: "\(version):\(fileName)")
|
|
1102
|
+
self.logger.error("Manifest file download network error")
|
|
1103
|
+
self.logger.debug("Bundle: \(bundleId), File: \(fileName), Error: \(error.localizedDescription)")
|
|
1104
|
+
throw error
|
|
1105
|
+
}
|
|
830
1106
|
|
|
831
|
-
|
|
832
|
-
|
|
1107
|
+
guard let data = result.data else {
|
|
1108
|
+
self.sendStats(action: "download_manifest_file_fail", versionName: "\(version):\(fileName)")
|
|
1109
|
+
throw NSError(
|
|
1110
|
+
domain: "ManifestDownloadError",
|
|
1111
|
+
code: 3,
|
|
1112
|
+
userInfo: [NSLocalizedDescriptionKey: "Manifest file response was empty for \(fileName) at url \(downloadUrl)"]
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
833
1115
|
|
|
834
|
-
|
|
835
|
-
|
|
1116
|
+
let statusCode = result.response?.statusCode ?? 200
|
|
1117
|
+
if statusCode < 200 || statusCode >= 300 {
|
|
1118
|
+
self.sendStats(action: "download_manifest_file_fail", versionName: "\(version):\(fileName)")
|
|
1119
|
+
if let stringData = String(data: data, encoding: .utf8) {
|
|
1120
|
+
throw NSError(domain: "StatusCodeError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData) for file \(fileName) at url \(downloadUrl)"])
|
|
1121
|
+
} else {
|
|
1122
|
+
throw NSError(domain: "StatusCodeError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid for file \(fileName) at url \(downloadUrl)"])
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
836
1125
|
|
|
1126
|
+
do {
|
|
1127
|
+
// Add decryption step if public key is set and sessionKey is provided
|
|
1128
|
+
var finalData = data
|
|
1129
|
+
if !self.publicKey.isEmpty && !sessionKey.isEmpty {
|
|
1130
|
+
let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
|
|
1131
|
+
try finalData.write(to: tempFile)
|
|
1132
|
+
do {
|
|
1133
|
+
try CryptoCipher.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
|
|
837
1134
|
} catch {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
self.logger.debug("Bundle: \(bundleId), File: \(fileName), Error: \(error.localizedDescription)")
|
|
1135
|
+
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
1136
|
+
throw error
|
|
841
1137
|
}
|
|
1138
|
+
finalData = try Data(contentsOf: tempFile)
|
|
1139
|
+
try FileManager.default.removeItem(at: tempFile)
|
|
1140
|
+
}
|
|
842
1141
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
self.
|
|
846
|
-
|
|
847
|
-
|
|
1142
|
+
// Decompress Brotli if needed
|
|
1143
|
+
if isBrotli {
|
|
1144
|
+
guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
|
|
1145
|
+
self.sendStats(action: "download_manifest_brotli_fail", versionName: "\(version):\(destFileName)")
|
|
1146
|
+
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
|
|
1147
|
+
}
|
|
1148
|
+
finalData = decompressedData
|
|
848
1149
|
}
|
|
849
|
-
}
|
|
850
1150
|
|
|
851
|
-
|
|
1151
|
+
// Write to destination
|
|
1152
|
+
try finalData.write(to: destFilePath)
|
|
1153
|
+
|
|
1154
|
+
// Always verify checksum when file_hash is present
|
|
1155
|
+
let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
|
|
1156
|
+
CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
|
|
1157
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
|
|
1158
|
+
if calculatedChecksum != fileHash {
|
|
1159
|
+
try? FileManager.default.removeItem(at: destFilePath)
|
|
1160
|
+
self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
|
|
1161
|
+
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Save to cache
|
|
1165
|
+
try finalData.write(to: cacheFilePath)
|
|
852
1166
|
|
|
853
|
-
|
|
1167
|
+
self.logger.info("Manifest file downloaded and cached")
|
|
1168
|
+
self.logger.debug("Bundle: \(bundleId), File: \(fileName), Brotli: \(isBrotli), Encrypted: \(!self.publicKey.isEmpty && !sessionKey.isEmpty)")
|
|
1169
|
+
} catch {
|
|
1170
|
+
self.logger.error("Manifest file download failed")
|
|
1171
|
+
self.logger.debug("Bundle: \(bundleId), File: \(fileName), Error: \(error.localizedDescription)")
|
|
854
1172
|
throw error
|
|
855
1173
|
}
|
|
856
1174
|
}
|
|
@@ -999,7 +1317,6 @@ import UIKit
|
|
|
999
1317
|
|
|
1000
1318
|
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1001
1319
|
let id: String = self.randomString(length: 10)
|
|
1002
|
-
let semaphore = DispatchSemaphore(value: 0)
|
|
1003
1320
|
// Each download uses its own temp files keyed by bundle ID to prevent collisions
|
|
1004
1321
|
if version != getLocalUpdateVersion(for: id) {
|
|
1005
1322
|
cleanDownloadData(for: id)
|
|
@@ -1011,10 +1328,11 @@ import UIKit
|
|
|
1011
1328
|
try checkDiskSpace()
|
|
1012
1329
|
|
|
1013
1330
|
var checksum = ""
|
|
1014
|
-
var targetSize = -1
|
|
1015
1331
|
var lastSentProgress = 0
|
|
1016
|
-
|
|
1017
|
-
let
|
|
1332
|
+
let totalReceivedBytes: Int64 = loadDownloadProgress(for: id) // Retrieving the amount of already downloaded data if exist, defined at 0 otherwise
|
|
1333
|
+
let tempPath = tempDataPath(for: id)
|
|
1334
|
+
let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum, link: link, comment: comment)
|
|
1335
|
+
self.saveBundleInfo(id: id, bundle: bundleInfo)
|
|
1018
1336
|
|
|
1019
1337
|
// Send stats for zip download start
|
|
1020
1338
|
self.sendStats(action: "download_zip_start", versionName: version)
|
|
@@ -1024,66 +1342,61 @@ import UIKit
|
|
|
1024
1342
|
self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
|
|
1025
1343
|
}
|
|
1026
1344
|
var mainError: NSError?
|
|
1027
|
-
let monitor = ClosureEventMonitor()
|
|
1028
|
-
monitor.requestDidCompleteTaskWithError = { (_, _, error) in
|
|
1029
|
-
if error != nil {
|
|
1030
|
-
self.logger.error("Downloading failed - ClosureEventMonitor activated")
|
|
1031
|
-
mainError = error as NSError?
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
let configuration = URLSessionConfiguration.default
|
|
1035
|
-
configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
|
|
1036
|
-
let session = Session(configuration: configuration, eventMonitors: [monitor])
|
|
1037
1345
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if case .success(let data) = result {
|
|
1047
|
-
self.tempData.append(data)
|
|
1346
|
+
guard var request = createRequest(url: url, method: "GET") else {
|
|
1347
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
1348
|
+
throw NSError(
|
|
1349
|
+
domain: "DownloadError",
|
|
1350
|
+
code: 2,
|
|
1351
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid download request for \(url.absoluteString)"]
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1048
1354
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1355
|
+
if totalReceivedBytes > 0 {
|
|
1356
|
+
request.setValue("bytes=\(totalReceivedBytes)-", forHTTPHeaderField: "Range")
|
|
1357
|
+
}
|
|
1051
1358
|
|
|
1052
|
-
|
|
1359
|
+
let downloadResult = performDownloadRequest(request, label: "download \(version)")
|
|
1053
1360
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1361
|
+
if downloadResult.timedOut {
|
|
1362
|
+
persistPartialDownload(downloadResult, id: id, tempPath: tempPath, existingBytes: totalReceivedBytes)
|
|
1363
|
+
mainError = NSError(
|
|
1364
|
+
domain: NSURLErrorDomain,
|
|
1365
|
+
code: NSURLErrorTimedOut,
|
|
1366
|
+
userInfo: [NSLocalizedDescriptionKey: "Timed out downloading bundle from \(url.absoluteString)"]
|
|
1367
|
+
)
|
|
1368
|
+
} else if let error = downloadResult.error {
|
|
1369
|
+
logger.error("Download failed")
|
|
1370
|
+
persistPartialDownload(downloadResult, id: id, tempPath: tempPath, existingBytes: totalReceivedBytes)
|
|
1371
|
+
mainError = error as NSError
|
|
1372
|
+
} else if let statusCode = downloadResult.response?.statusCode, statusCode < 200 || statusCode >= 300 {
|
|
1373
|
+
logger.error("Download failed")
|
|
1374
|
+
mainError = NSError(
|
|
1375
|
+
domain: "DownloadError",
|
|
1376
|
+
code: statusCode,
|
|
1377
|
+
userInfo: [NSLocalizedDescriptionKey: "Download request failed with status code \(statusCode)"]
|
|
1378
|
+
)
|
|
1379
|
+
} else if let downloadedFileURL = downloadResult.fileURL {
|
|
1380
|
+
do {
|
|
1381
|
+
try storeDownloadedFile(downloadedFileURL, at: tempPath, existingBytes: totalReceivedBytes, response: downloadResult.response)
|
|
1061
1382
|
|
|
1062
|
-
|
|
1063
|
-
self.
|
|
1383
|
+
if lastSentProgress < 70 {
|
|
1384
|
+
self.notifyDownload(id: id, percent: 70, ignoreMultipleOfTen: true)
|
|
1385
|
+
lastSentProgress = 70
|
|
1064
1386
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
1073
|
-
let reachabilityManager = NetworkReachabilityManager()
|
|
1074
|
-
reachabilityManager?.startListening { status in
|
|
1075
|
-
switch status {
|
|
1076
|
-
case .notReachable:
|
|
1077
|
-
// Stop the download request if the network is not reachable
|
|
1078
|
-
request.cancel()
|
|
1079
|
-
mainError = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet, userInfo: nil)
|
|
1080
|
-
semaphore.signal()
|
|
1081
|
-
default:
|
|
1082
|
-
break
|
|
1387
|
+
self.logger.info("Download complete")
|
|
1388
|
+
} catch let error as NSError {
|
|
1389
|
+
mainError = error
|
|
1390
|
+
} catch {
|
|
1391
|
+
mainError = error as NSError
|
|
1083
1392
|
}
|
|
1393
|
+
} else {
|
|
1394
|
+
mainError = NSError(
|
|
1395
|
+
domain: "DownloadError",
|
|
1396
|
+
code: 1,
|
|
1397
|
+
userInfo: [NSLocalizedDescriptionKey: "Downloaded file is missing at \(tempPath.path)"]
|
|
1398
|
+
)
|
|
1084
1399
|
}
|
|
1085
|
-
semaphore.wait()
|
|
1086
|
-
reachabilityManager?.stopListening()
|
|
1087
1400
|
|
|
1088
1401
|
if mainError != nil {
|
|
1089
1402
|
logger.error("Failed to download bundle")
|
|
@@ -1092,7 +1405,6 @@ import UIKit
|
|
|
1092
1405
|
throw mainError!
|
|
1093
1406
|
}
|
|
1094
1407
|
|
|
1095
|
-
let tempPath = tempDataPath(for: id)
|
|
1096
1408
|
let finalPath = tempPath.deletingLastPathComponent().appendingPathComponent("\(id)")
|
|
1097
1409
|
do {
|
|
1098
1410
|
try CryptoCipher.decryptFile(filePath: tempPath, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
|
|
@@ -1313,7 +1625,7 @@ import UIKit
|
|
|
1313
1625
|
if removeInfo {
|
|
1314
1626
|
self.removeBundleInfo(id: id)
|
|
1315
1627
|
} else {
|
|
1316
|
-
self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.
|
|
1628
|
+
self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.storedValue))
|
|
1317
1629
|
}
|
|
1318
1630
|
logger.info("Bundle deleted successfully")
|
|
1319
1631
|
logger.debug("Version: \(deleted.getVersionName())")
|
|
@@ -1668,54 +1980,74 @@ import UIKit
|
|
|
1668
1980
|
setChannel.error = "missing_config"
|
|
1669
1981
|
return setChannel
|
|
1670
1982
|
}
|
|
1671
|
-
let
|
|
1983
|
+
guard let channelURL = URL(string: self.channelUrl) else {
|
|
1984
|
+
logger.error("Invalid channel URL")
|
|
1985
|
+
setChannel.message = "Channel URL is invalid"
|
|
1986
|
+
setChannel.error = "invalid_config"
|
|
1987
|
+
return setChannel
|
|
1988
|
+
}
|
|
1672
1989
|
var parameters: InfoObject = self.createInfoObject()
|
|
1673
1990
|
parameters.channel = channel
|
|
1991
|
+
guard let request = createRequest(url: channelURL, method: "POST", parameters: parameters.toParameters()) else {
|
|
1992
|
+
setChannel.error = "Request failed: invalid request"
|
|
1993
|
+
return setChannel
|
|
1994
|
+
}
|
|
1674
1995
|
|
|
1675
|
-
let
|
|
1996
|
+
let result = performRequest(request, label: "setChannel")
|
|
1676
1997
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
semaphore.signal()
|
|
1683
|
-
return
|
|
1684
|
-
}
|
|
1998
|
+
if self.checkAndHandleRateLimitResponse(statusCode: result.response?.statusCode) {
|
|
1999
|
+
setChannel.message = "Rate limit exceeded"
|
|
2000
|
+
setChannel.error = "rate_limit_exceeded"
|
|
2001
|
+
return setChannel
|
|
2002
|
+
}
|
|
1685
2003
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2004
|
+
if result.timedOut {
|
|
2005
|
+
setChannel.error = "Request timed out"
|
|
2006
|
+
return setChannel
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if let error = result.error {
|
|
2010
|
+
self.logger.error("Error setting channel")
|
|
2011
|
+
self.logger.debug("Error: \(error.localizedDescription)")
|
|
2012
|
+
setChannel.error = "Request failed: \(error.localizedDescription)"
|
|
2013
|
+
return setChannel
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
guard let data = result.data else {
|
|
2017
|
+
setChannel.error = "Request failed: empty response"
|
|
2018
|
+
return setChannel
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
guard let responseValue = try? JSONDecoder().decode(SetChannelDec.self, from: data) else {
|
|
2022
|
+
setChannel.error = "decode_error"
|
|
2023
|
+
return setChannel
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
let statusCode = result.response?.statusCode ?? 0
|
|
2027
|
+
if statusCode < 200 || statusCode >= 300 {
|
|
2028
|
+
setChannel.message = responseValue.message ?? "Server error: \(statusCode)"
|
|
2029
|
+
setChannel.error = responseValue.error ?? "response_error"
|
|
2030
|
+
return setChannel
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
if let error = responseValue.error {
|
|
2034
|
+
setChannel.error = error
|
|
2035
|
+
} else if responseValue.unset == true {
|
|
2036
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelKey)
|
|
2037
|
+
UserDefaults.standard.synchronize()
|
|
2038
|
+
self.logger.info("Public channel requested, channel override removed")
|
|
2039
|
+
|
|
2040
|
+
setChannel.status = responseValue.status ?? "ok"
|
|
2041
|
+
setChannel.message = responseValue.message ?? "Public channel requested, channel override removed. Device will use public channel automatically."
|
|
2042
|
+
} else {
|
|
2043
|
+
self.defaultChannel = channel
|
|
2044
|
+
UserDefaults.standard.set(channel, forKey: defaultChannelKey)
|
|
2045
|
+
UserDefaults.standard.synchronize()
|
|
2046
|
+
self.logger.info("defaultChannel persisted locally: \(channel)")
|
|
2047
|
+
|
|
2048
|
+
setChannel.status = responseValue.status ?? ""
|
|
2049
|
+
setChannel.message = responseValue.message ?? ""
|
|
1717
2050
|
}
|
|
1718
|
-
semaphore.wait()
|
|
1719
2051
|
return setChannel
|
|
1720
2052
|
}
|
|
1721
2053
|
|
|
@@ -1736,49 +2068,77 @@ import UIKit
|
|
|
1736
2068
|
getChannel.error = "missing_config"
|
|
1737
2069
|
return getChannel
|
|
1738
2070
|
}
|
|
1739
|
-
let
|
|
2071
|
+
guard let channelURL = URL(string: self.channelUrl) else {
|
|
2072
|
+
logger.error("Invalid channel URL")
|
|
2073
|
+
getChannel.message = "Channel URL is invalid"
|
|
2074
|
+
getChannel.error = "invalid_config"
|
|
2075
|
+
return getChannel
|
|
2076
|
+
}
|
|
1740
2077
|
let parameters: InfoObject = self.createInfoObject()
|
|
1741
|
-
let request =
|
|
2078
|
+
guard let request = createRequest(url: channelURL, method: "PUT", parameters: parameters.toParameters()) else {
|
|
2079
|
+
getChannel.error = "Request failed: invalid request"
|
|
2080
|
+
return getChannel
|
|
2081
|
+
}
|
|
1742
2082
|
|
|
1743
|
-
request
|
|
1744
|
-
defer {
|
|
1745
|
-
semaphore.signal()
|
|
1746
|
-
}
|
|
2083
|
+
let result = performRequest(request, label: "getChannel")
|
|
1747
2084
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
}
|
|
2085
|
+
if self.checkAndHandleRateLimitResponse(statusCode: result.response?.statusCode) {
|
|
2086
|
+
getChannel.message = "Rate limit exceeded"
|
|
2087
|
+
getChannel.error = "rate_limit_exceeded"
|
|
2088
|
+
return getChannel
|
|
2089
|
+
}
|
|
1754
2090
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1767
|
-
case let .failure(error):
|
|
1768
|
-
if let data = response.data, let bodyString = String(data: data, encoding: .utf8) {
|
|
1769
|
-
if bodyString.contains("channel_not_found") && response.response?.statusCode == 400 && !self.defaultChannel.isEmpty {
|
|
1770
|
-
getChannel.channel = self.defaultChannel
|
|
1771
|
-
getChannel.status = "default"
|
|
1772
|
-
return
|
|
1773
|
-
}
|
|
2091
|
+
if result.timedOut {
|
|
2092
|
+
getChannel.error = "Request timed out"
|
|
2093
|
+
return getChannel
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
if let error = result.error {
|
|
2097
|
+
if let data = result.data, let bodyString = String(data: data, encoding: .utf8) {
|
|
2098
|
+
if bodyString.contains("channel_not_found") && result.response?.statusCode == 400 && !self.defaultChannel.isEmpty {
|
|
2099
|
+
getChannel.channel = self.defaultChannel
|
|
2100
|
+
getChannel.status = "default"
|
|
2101
|
+
return getChannel
|
|
1774
2102
|
}
|
|
2103
|
+
}
|
|
1775
2104
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2105
|
+
self.logger.error("Error getting channel")
|
|
2106
|
+
self.logger.debug("Error: \(error.localizedDescription)")
|
|
2107
|
+
getChannel.error = "Request failed: \(error.localizedDescription)"
|
|
2108
|
+
return getChannel
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
guard let data = result.data else {
|
|
2112
|
+
getChannel.error = "Request failed: empty response"
|
|
2113
|
+
return getChannel
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
guard let responseValue = try? JSONDecoder().decode(GetChannelDec.self, from: data) else {
|
|
2117
|
+
getChannel.error = "decode_error"
|
|
2118
|
+
return getChannel
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
let statusCode = result.response?.statusCode ?? 0
|
|
2122
|
+
if let error = responseValue.error {
|
|
2123
|
+
if error == "channel_not_found", statusCode == 400, !self.defaultChannel.isEmpty {
|
|
2124
|
+
getChannel.channel = self.defaultChannel
|
|
2125
|
+
getChannel.status = "default"
|
|
2126
|
+
return getChannel
|
|
1779
2127
|
}
|
|
2128
|
+
getChannel.error = error
|
|
2129
|
+
getChannel.message = responseValue.message ?? ""
|
|
2130
|
+
return getChannel
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
if statusCode < 200 || statusCode >= 300 {
|
|
2134
|
+
getChannel.message = responseValue.message ?? "Server error: \(statusCode)"
|
|
2135
|
+
getChannel.error = "response_error"
|
|
2136
|
+
} else {
|
|
2137
|
+
getChannel.status = responseValue.status ?? ""
|
|
2138
|
+
getChannel.message = responseValue.message ?? ""
|
|
2139
|
+
getChannel.channel = responseValue.channel ?? ""
|
|
2140
|
+
getChannel.allowSet = responseValue.allowSet ?? true
|
|
1780
2141
|
}
|
|
1781
|
-
semaphore.wait()
|
|
1782
2142
|
return getChannel
|
|
1783
2143
|
}
|
|
1784
2144
|
|
|
@@ -1798,14 +2158,12 @@ import UIKit
|
|
|
1798
2158
|
return listChannels
|
|
1799
2159
|
}
|
|
1800
2160
|
|
|
1801
|
-
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
1802
|
-
|
|
1803
2161
|
// Create info object and convert to query parameters
|
|
1804
2162
|
let infoObject = self.createInfoObject()
|
|
1805
2163
|
|
|
1806
2164
|
// Create query parameters from InfoObject
|
|
1807
2165
|
var urlComponents = URLComponents(string: self.channelUrl)
|
|
1808
|
-
var queryItems: [URLQueryItem] = []
|
|
2166
|
+
var queryItems: [URLQueryItem] = urlComponents?.queryItems ?? []
|
|
1809
2167
|
|
|
1810
2168
|
// Convert InfoObject to dictionary using Mirror
|
|
1811
2169
|
let mirror = Mirror(reflecting: infoObject)
|
|
@@ -1829,47 +2187,62 @@ import UIKit
|
|
|
1829
2187
|
return listChannels
|
|
1830
2188
|
}
|
|
1831
2189
|
|
|
1832
|
-
let request =
|
|
2190
|
+
guard let request = createRequest(url: url, method: "GET", expectsJSONResponse: true) else {
|
|
2191
|
+
listChannels.error = "Invalid channel URL"
|
|
2192
|
+
return listChannels
|
|
2193
|
+
}
|
|
1833
2194
|
|
|
1834
|
-
request
|
|
1835
|
-
defer {
|
|
1836
|
-
semaphore.signal()
|
|
1837
|
-
}
|
|
2195
|
+
let result = performRequest(request, label: "listChannels")
|
|
1838
2196
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
}
|
|
2197
|
+
if self.checkAndHandleRateLimitResponse(statusCode: result.response?.statusCode) {
|
|
2198
|
+
listChannels.error = "rate_limit_exceeded"
|
|
2199
|
+
return listChannels
|
|
2200
|
+
}
|
|
1844
2201
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
if let error = responseValue.error {
|
|
1850
|
-
listChannels.error = error
|
|
1851
|
-
return
|
|
1852
|
-
}
|
|
2202
|
+
if result.timedOut {
|
|
2203
|
+
listChannels.error = "Request timed out"
|
|
2204
|
+
return listChannels
|
|
2205
|
+
}
|
|
1853
2206
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2207
|
+
if let error = result.error {
|
|
2208
|
+
self.logger.error("Error listing channels")
|
|
2209
|
+
self.logger.debug("Error: \(error.localizedDescription)")
|
|
2210
|
+
listChannels.error = "Request failed: \(error.localizedDescription)"
|
|
2211
|
+
return listChannels
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
guard let data = result.data else {
|
|
2215
|
+
listChannels.error = "Request failed: empty response"
|
|
2216
|
+
return listChannels
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
guard let responseValue = try? JSONDecoder().decode(ListChannelsDec.self, from: data) else {
|
|
2220
|
+
listChannels.error = "decode_error"
|
|
2221
|
+
return listChannels
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
let statusCode = result.response?.statusCode ?? 0
|
|
2225
|
+
if let error = responseValue.error {
|
|
2226
|
+
listChannels.error = error
|
|
2227
|
+
return listChannels
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
if statusCode < 200 || statusCode >= 300 {
|
|
2231
|
+
listChannels.error = "response_error"
|
|
2232
|
+
return listChannels
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if let channels = responseValue.channels {
|
|
2236
|
+
listChannels.channels = channels.map { channel in
|
|
2237
|
+
var channelDict: [String: Any] = [:]
|
|
2238
|
+
channelDict["id"] = channel.id ?? ""
|
|
2239
|
+
channelDict["name"] = channel.name ?? ""
|
|
2240
|
+
channelDict["public"] = channel.public ?? false
|
|
2241
|
+
channelDict["allow_self_set"] = channel.allow_self_set ?? false
|
|
2242
|
+
return channelDict
|
|
1870
2243
|
}
|
|
1871
2244
|
}
|
|
1872
|
-
|
|
2245
|
+
|
|
1873
2246
|
return listChannels
|
|
1874
2247
|
}
|
|
1875
2248
|
|
|
@@ -2034,13 +2407,12 @@ import UIKit
|
|
|
2034
2407
|
logger.debug("Bundle ID: \(id), Error: \(error.localizedDescription)")
|
|
2035
2408
|
}
|
|
2036
2409
|
}
|
|
2037
|
-
UserDefaults.standard.synchronize()
|
|
2038
2410
|
}
|
|
2039
2411
|
|
|
2040
2412
|
private func setBundleStatus(id: String, status: BundleStatus) {
|
|
2041
2413
|
logger.info("Setting status for bundle [\(id)] to \(status)")
|
|
2042
2414
|
let info = self.getBundleInfo(id: id)
|
|
2043
|
-
self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.
|
|
2415
|
+
self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.storedValue))
|
|
2044
2416
|
}
|
|
2045
2417
|
|
|
2046
2418
|
public func getCurrentBundle() -> BundleInfo {
|