@capgo/capacitor-updater 8.0.0-alpha → 8.0.0-alpha.2
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 +102 -12
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +60 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +75 -36
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +41 -104
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +84 -75
- package/dist/docs.json +105 -8
- package/dist/esm/definitions.d.ts +97 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +37 -10
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +26 -8
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +68 -71
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +4 -0
- package/package.json +2 -1
|
@@ -372,6 +372,12 @@ import UIKit
|
|
|
372
372
|
if let manifest = response.value?.manifest {
|
|
373
373
|
latest.manifest = manifest
|
|
374
374
|
}
|
|
375
|
+
if let link = response.value?.link {
|
|
376
|
+
latest.link = link
|
|
377
|
+
}
|
|
378
|
+
if let comment = response.value?.comment {
|
|
379
|
+
latest.comment = comment
|
|
380
|
+
}
|
|
375
381
|
case let .failure(error):
|
|
376
382
|
self.logger.error("Error getting Latest \(response.value.debugDescription) \(error)")
|
|
377
383
|
latest.message = "Error getting Latest \(String(describing: response.value))"
|
|
@@ -400,11 +406,11 @@ import UIKit
|
|
|
400
406
|
private var tempData = Data()
|
|
401
407
|
|
|
402
408
|
private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
|
|
403
|
-
let actualHash =
|
|
409
|
+
let actualHash = CryptoCipher.calcChecksum(filePath: file)
|
|
404
410
|
return actualHash == expectedHash
|
|
405
411
|
}
|
|
406
412
|
|
|
407
|
-
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
|
|
413
|
+
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
408
414
|
let id = self.randomString(length: 10)
|
|
409
415
|
logger.info("downloadManifest start \(id)")
|
|
410
416
|
let destFolder = self.getBundleDirectory(id: id)
|
|
@@ -414,7 +420,7 @@ import UIKit
|
|
|
414
420
|
try FileManager.default.createDirectory(at: destFolder, withIntermediateDirectories: true, attributes: nil)
|
|
415
421
|
|
|
416
422
|
// Create and save BundleInfo before starting the download process
|
|
417
|
-
let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "")
|
|
423
|
+
let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "", link: link, comment: comment)
|
|
418
424
|
self.saveBundleInfo(id: id, bundle: bundleInfo)
|
|
419
425
|
|
|
420
426
|
// Send stats for manifest download start
|
|
@@ -445,10 +451,16 @@ import UIKit
|
|
|
445
451
|
}
|
|
446
452
|
}
|
|
447
453
|
|
|
454
|
+
// Check if file has .br extension for Brotli decompression
|
|
448
455
|
let fileNameWithoutPath = (fileName as NSString).lastPathComponent
|
|
449
456
|
let cacheFileName = "\(fileHash)_\(fileNameWithoutPath)"
|
|
450
457
|
let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
|
|
451
|
-
|
|
458
|
+
|
|
459
|
+
// Check if file is Brotli compressed and remove .br extension from destination
|
|
460
|
+
let isBrotli = fileName.hasSuffix(".br")
|
|
461
|
+
let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
462
|
+
|
|
463
|
+
let destFilePath = destFolder.appendingPathComponent(destFileName)
|
|
452
464
|
let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
|
|
453
465
|
|
|
454
466
|
// Create necessary subdirectories in the destination folder
|
|
@@ -457,16 +469,26 @@ import UIKit
|
|
|
457
469
|
dispatchGroup.enter()
|
|
458
470
|
|
|
459
471
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
472
|
+
do {
|
|
473
|
+
try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
|
|
474
|
+
logger.info("downloadManifest \(fileName) using builtin file \(id)")
|
|
475
|
+
completedFiles += 1
|
|
476
|
+
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
477
|
+
} catch {
|
|
478
|
+
downloadError = error
|
|
479
|
+
logger.error("Failed to copy builtin file \(fileName): \(error.localizedDescription)")
|
|
480
|
+
}
|
|
464
481
|
dispatchGroup.leave()
|
|
465
482
|
} else if FileManager.default.fileExists(atPath: cacheFilePath.path) && verifyChecksum(file: cacheFilePath, expectedHash: fileHash) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
483
|
+
do {
|
|
484
|
+
try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
|
|
485
|
+
logger.info("downloadManifest \(fileName) copy from cache \(id)")
|
|
486
|
+
completedFiles += 1
|
|
487
|
+
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
488
|
+
} catch {
|
|
489
|
+
downloadError = error
|
|
490
|
+
logger.error("Failed to copy cached file \(fileName): \(error.localizedDescription)")
|
|
491
|
+
}
|
|
470
492
|
dispatchGroup.leave()
|
|
471
493
|
} else {
|
|
472
494
|
// File not in cache, download, decompress, and save to both cache and destination
|
|
@@ -502,15 +524,11 @@ import UIKit
|
|
|
502
524
|
try FileManager.default.removeItem(at: tempFile)
|
|
503
525
|
}
|
|
504
526
|
|
|
505
|
-
//
|
|
506
|
-
let isBrotli = fileName.hasSuffix(".br")
|
|
507
|
-
let finalFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
508
|
-
let destFilePath = destFolder.appendingPathComponent(finalFileName)
|
|
509
|
-
|
|
527
|
+
// Use the isBrotli and destFilePath already computed above
|
|
510
528
|
if isBrotli {
|
|
511
529
|
// Decompress the Brotli data
|
|
512
530
|
guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
|
|
513
|
-
self.sendStats(action: "download_manifest_brotli_fail", versionName: "\(version):\(
|
|
531
|
+
self.sendStats(action: "download_manifest_brotli_fail", versionName: "\(version):\(destFileName)")
|
|
514
532
|
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
|
|
515
533
|
}
|
|
516
534
|
finalData = decompressedData
|
|
@@ -523,7 +541,9 @@ import UIKit
|
|
|
523
541
|
CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
|
|
524
542
|
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
|
|
525
543
|
if calculatedChecksum != fileHash {
|
|
526
|
-
|
|
544
|
+
// Delete the corrupt file before throwing error
|
|
545
|
+
try? FileManager.default.removeItem(at: destFilePath)
|
|
546
|
+
self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
|
|
527
547
|
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
528
548
|
}
|
|
529
549
|
}
|
|
@@ -681,7 +701,7 @@ import UIKit
|
|
|
681
701
|
return status == COMPRESSION_STATUS_END ? decompressedData : nil
|
|
682
702
|
}
|
|
683
703
|
|
|
684
|
-
public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
|
|
704
|
+
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
685
705
|
let id: String = self.randomString(length: 10)
|
|
686
706
|
let semaphore = DispatchSemaphore(value: 0)
|
|
687
707
|
if version != getLocalUpdateVersion() {
|
|
@@ -748,7 +768,7 @@ import UIKit
|
|
|
748
768
|
semaphore.signal()
|
|
749
769
|
}
|
|
750
770
|
}
|
|
751
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
|
|
771
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
752
772
|
let reachabilityManager = NetworkReachabilityManager()
|
|
753
773
|
reachabilityManager?.startListening { status in
|
|
754
774
|
switch status {
|
|
@@ -766,7 +786,7 @@ import UIKit
|
|
|
766
786
|
|
|
767
787
|
if mainError != nil {
|
|
768
788
|
logger.error("Failed to download: \(String(describing: mainError))")
|
|
769
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
789
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
770
790
|
throw mainError!
|
|
771
791
|
}
|
|
772
792
|
|
|
@@ -776,7 +796,7 @@ import UIKit
|
|
|
776
796
|
try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
|
|
777
797
|
} catch {
|
|
778
798
|
logger.error("Failed decrypt file : \(error)")
|
|
779
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
799
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
780
800
|
cleanDownloadData()
|
|
781
801
|
throw error
|
|
782
802
|
}
|
|
@@ -789,7 +809,7 @@ import UIKit
|
|
|
789
809
|
|
|
790
810
|
} catch {
|
|
791
811
|
logger.error("Failed to unzip file: \(error)")
|
|
792
|
-
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
812
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
|
|
793
813
|
// Best-effort cleanup of the decrypted zip file when unzip fails
|
|
794
814
|
do {
|
|
795
815
|
if FileManager.default.fileExists(atPath: finalPath.path) {
|
|
@@ -804,7 +824,7 @@ import UIKit
|
|
|
804
824
|
|
|
805
825
|
self.notifyDownload(id: id, percent: 90)
|
|
806
826
|
logger.info("Downloading: 90% (wrapping up)")
|
|
807
|
-
let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
|
|
827
|
+
let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum, link: link, comment: comment)
|
|
808
828
|
self.saveBundleInfo(id: id, bundle: info)
|
|
809
829
|
self.cleanDownloadData()
|
|
810
830
|
|
|
@@ -1111,60 +1131,31 @@ import UIKit
|
|
|
1111
1131
|
self.setBundleStatus(id: bundle.getId(), status: BundleStatus.ERROR)
|
|
1112
1132
|
}
|
|
1113
1133
|
|
|
1114
|
-
func unsetChannel() -> SetChannel {
|
|
1134
|
+
func unsetChannel(defaultChannelKey: String, configDefaultChannel: String) -> SetChannel {
|
|
1115
1135
|
let setChannel: SetChannel = SetChannel()
|
|
1116
1136
|
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
return setChannel
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (self.channelUrl ).isEmpty {
|
|
1126
|
-
logger.error("Channel URL is not set")
|
|
1127
|
-
setChannel.message = "Channel URL is not set"
|
|
1128
|
-
setChannel.error = "missing_config"
|
|
1129
|
-
return setChannel
|
|
1130
|
-
}
|
|
1131
|
-
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
|
|
1132
|
-
let parameters: InfoObject = self.createInfoObject()
|
|
1133
|
-
|
|
1134
|
-
let request = alamofireSession.request(self.channelUrl, method: .delete, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
|
|
1135
|
-
|
|
1136
|
-
request.validate().responseDecodable(of: SetChannelDec.self) { response in
|
|
1137
|
-
// Check for 429 rate limit
|
|
1138
|
-
if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
|
|
1139
|
-
setChannel.message = "Rate limit exceeded"
|
|
1140
|
-
setChannel.error = "rate_limit_exceeded"
|
|
1141
|
-
semaphore.signal()
|
|
1142
|
-
return
|
|
1143
|
-
}
|
|
1137
|
+
// Clear persisted defaultChannel and revert to config value
|
|
1138
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelKey)
|
|
1139
|
+
UserDefaults.standard.synchronize()
|
|
1140
|
+
self.defaultChannel = configDefaultChannel
|
|
1141
|
+
self.logger.info("Persisted defaultChannel cleared, reverted to config value: \(configDefaultChannel)")
|
|
1144
1142
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if let responseValue = response.value {
|
|
1148
|
-
if let error = responseValue.error {
|
|
1149
|
-
setChannel.error = error
|
|
1150
|
-
} else {
|
|
1151
|
-
setChannel.status = responseValue.status ?? ""
|
|
1152
|
-
setChannel.message = responseValue.message ?? ""
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
case let .failure(error):
|
|
1156
|
-
self.logger.error("Error unset Channel \(error)")
|
|
1157
|
-
setChannel.error = "Request failed: \(error.localizedDescription)"
|
|
1158
|
-
}
|
|
1159
|
-
semaphore.signal()
|
|
1160
|
-
}
|
|
1161
|
-
semaphore.wait()
|
|
1143
|
+
setChannel.status = "ok"
|
|
1144
|
+
setChannel.message = "Channel override removed"
|
|
1162
1145
|
return setChannel
|
|
1163
1146
|
}
|
|
1164
1147
|
|
|
1165
|
-
func setChannel(channel: String) -> SetChannel {
|
|
1148
|
+
func setChannel(channel: String, defaultChannelKey: String, allowSetDefaultChannel: Bool) -> SetChannel {
|
|
1166
1149
|
let setChannel: SetChannel = SetChannel()
|
|
1167
1150
|
|
|
1151
|
+
// Check if setting defaultChannel is allowed
|
|
1152
|
+
if !allowSetDefaultChannel {
|
|
1153
|
+
logger.error("setChannel is disabled by allowSetDefaultChannel config")
|
|
1154
|
+
setChannel.message = "setChannel is disabled by configuration"
|
|
1155
|
+
setChannel.error = "disabled_by_config"
|
|
1156
|
+
return setChannel
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1168
1159
|
// Check if rate limit was exceeded
|
|
1169
1160
|
if CapgoUpdater.rateLimitExceeded {
|
|
1170
1161
|
logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.")
|
|
@@ -1200,6 +1191,12 @@ import UIKit
|
|
|
1200
1191
|
if let error = responseValue.error {
|
|
1201
1192
|
setChannel.error = error
|
|
1202
1193
|
} else {
|
|
1194
|
+
// Success - persist defaultChannel
|
|
1195
|
+
self.defaultChannel = channel
|
|
1196
|
+
UserDefaults.standard.set(channel, forKey: defaultChannelKey)
|
|
1197
|
+
UserDefaults.standard.synchronize()
|
|
1198
|
+
self.logger.info("defaultChannel persisted locally: \(channel)")
|
|
1199
|
+
|
|
1203
1200
|
setChannel.status = responseValue.status ?? ""
|
|
1204
1201
|
setChannel.message = responseValue.message ?? ""
|
|
1205
1202
|
}
|
|
@@ -160,6 +160,8 @@ struct AppVersionDec: Decodable {
|
|
|
160
160
|
let breaking: Bool?
|
|
161
161
|
let data: [String: String]?
|
|
162
162
|
let manifest: [ManifestEntry]?
|
|
163
|
+
let link: String?
|
|
164
|
+
let comment: String?
|
|
163
165
|
// The HTTP status code is captured separately in CapgoUpdater; this struct only mirrors JSON.
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -174,6 +176,8 @@ public class AppVersion: NSObject {
|
|
|
174
176
|
var breaking: Bool?
|
|
175
177
|
var data: [String: String]?
|
|
176
178
|
var manifest: [ManifestEntry]?
|
|
179
|
+
var link: String?
|
|
180
|
+
var comment: String?
|
|
177
181
|
var statusCode: Int = 0
|
|
178
182
|
}
|
|
179
183
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-updater",
|
|
3
|
-
"version": "8.0.0-alpha",
|
|
3
|
+
"version": "8.0.0-alpha.2",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Live update for capacitor apps",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"ionic",
|
|
38
38
|
"appflow alternative",
|
|
39
39
|
"capawesome alternative",
|
|
40
|
+
"@capawesome/capacitor-live-update",
|
|
40
41
|
"native"
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|