@capgo/capacitor-updater 8.51.1 → 8.51.3
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 +49 -46
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +111 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +0 -1
- package/dist/docs.json +18 -2
- package/dist/esm/definitions.d.ts +22 -6
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +506 -22
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +61 -9
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +8 -0
- package/package.json +1 -1
|
@@ -707,13 +707,42 @@ import UIKit
|
|
|
707
707
|
}
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
-
private func populateDeltaCacheAsync(for id: String) {
|
|
710
|
+
private func populateDeltaCacheAsync(for id: String, manifest: [ManifestEntry]? = nil, sessionKey: String = "") {
|
|
711
711
|
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
712
|
-
self?.populateDeltaCache(for: id)
|
|
712
|
+
self?.populateDeltaCache(for: id, manifest: manifest, sessionKey: sessionKey)
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
-
|
|
716
|
+
struct ManifestLookupEntry {
|
|
717
|
+
let hash: String
|
|
718
|
+
/// The manifest's own file name, `.br` suffix included when present. The
|
|
719
|
+
/// built-in bundle stores files under this exact name (see
|
|
720
|
+
/// isManifestEntryAvailableLocally), unlike the extracted/cached copy which
|
|
721
|
+
/// is always named without the suffix.
|
|
722
|
+
let originalFileName: String
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/// Keys are stripped of the `.br` suffix so they match the extracted file names on
|
|
726
|
+
/// disk, not the manifest's (possibly brotli-compressed) original file names.
|
|
727
|
+
func manifestHashLookup(manifest: [ManifestEntry]?, sessionKey: String) -> [String: ManifestLookupEntry] {
|
|
728
|
+
guard let manifest else {
|
|
729
|
+
return [:]
|
|
730
|
+
}
|
|
731
|
+
var lookup: [String: ManifestLookupEntry] = [:]
|
|
732
|
+
for entry in manifest {
|
|
733
|
+
guard let fileName = entry.file_name,
|
|
734
|
+
let hash = resolveManifestFileHash(entry: entry, sessionKey: sessionKey) else {
|
|
735
|
+
continue
|
|
736
|
+
}
|
|
737
|
+
let destFileName = fileName.hasSuffix(".br") ? String(fileName.dropLast(3)) : fileName
|
|
738
|
+
lookup[destFileName] = ManifestLookupEntry(hash: hash, originalFileName: fileName)
|
|
739
|
+
}
|
|
740
|
+
return lookup
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/// `manifest` must only contain entries the caller already checksum-verified
|
|
744
|
+
/// (as `downloadManifest` does) — the hashes are trusted as-is, not re-checked.
|
|
745
|
+
func populateDeltaCache(for id: String, manifest: [ManifestEntry]? = nil, sessionKey: String = "") {
|
|
717
746
|
let bundleDir = self.getBundleDirectory(id: id)
|
|
718
747
|
let fileManager = FileManager.default
|
|
719
748
|
|
|
@@ -733,17 +762,33 @@ import UIKit
|
|
|
733
762
|
return
|
|
734
763
|
}
|
|
735
764
|
|
|
765
|
+
let knownEntries = manifestHashLookup(manifest: manifest, sessionKey: sessionKey)
|
|
766
|
+
let builtinFolder = self.builtinFolderURL()
|
|
767
|
+
|
|
736
768
|
for case let fileURL as URL in enumerator {
|
|
737
769
|
let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey])
|
|
738
770
|
if resourceValues?.isDirectory == true {
|
|
739
771
|
continue
|
|
740
772
|
}
|
|
741
773
|
|
|
742
|
-
let
|
|
774
|
+
let relativePath = String(fileURL.path.dropFirst(bundleDir.path.count + 1))
|
|
775
|
+
let knownEntry = knownEntries[relativePath]
|
|
776
|
+
|
|
777
|
+
let checksum = knownEntry?.hash ?? CryptoCipher.calcChecksum(filePath: fileURL)
|
|
743
778
|
if checksum.isEmpty {
|
|
744
779
|
continue
|
|
745
780
|
}
|
|
746
781
|
|
|
782
|
+
// Builtin is already a permanent reuse source (see isManifestEntryAvailableLocally),
|
|
783
|
+
// so there's no need to also duplicate this file into the delta cache
|
|
784
|
+
let builtinRelativePath = knownEntry?.originalFileName ?? relativePath
|
|
785
|
+
let builtinFilePath = builtinFolder.appendingPathComponent(builtinRelativePath)
|
|
786
|
+
let isBuiltinOrigin = fileManager.fileExists(atPath: builtinFilePath.path) &&
|
|
787
|
+
verifyChecksum(file: builtinFilePath, expectedHash: checksum)
|
|
788
|
+
if isBuiltinOrigin {
|
|
789
|
+
continue
|
|
790
|
+
}
|
|
791
|
+
|
|
747
792
|
let cacheFile = cacheFolder.appendingPathComponent("\(checksum)_\(fileURL.lastPathComponent)")
|
|
748
793
|
if fileManager.fileExists(atPath: cacheFile.path) {
|
|
749
794
|
continue
|
|
@@ -931,6 +976,12 @@ import UIKit
|
|
|
931
976
|
return actualHash == expectedHash
|
|
932
977
|
}
|
|
933
978
|
|
|
979
|
+
/// Overridable so tests can point it at a writable directory instead of the
|
|
980
|
+
/// real (read-only) app bundle.
|
|
981
|
+
func builtinFolderURL() -> URL {
|
|
982
|
+
Bundle.main.bundleURL.appendingPathComponent("public")
|
|
983
|
+
}
|
|
984
|
+
|
|
934
985
|
private func resolveManifestFileHash(entry: ManifestEntry, sessionKey: String) -> String? {
|
|
935
986
|
guard var fileHash = entry.file_hash, !fileHash.isEmpty else {
|
|
936
987
|
return nil
|
|
@@ -953,7 +1004,7 @@ import UIKit
|
|
|
953
1004
|
return false
|
|
954
1005
|
}
|
|
955
1006
|
|
|
956
|
-
let builtinFolder =
|
|
1007
|
+
let builtinFolder = self.builtinFolderURL()
|
|
957
1008
|
let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
|
|
958
1009
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
959
1010
|
return true
|
|
@@ -1066,7 +1117,7 @@ import UIKit
|
|
|
1066
1117
|
let id = self.randomString(length: 10)
|
|
1067
1118
|
logger.info("downloadManifest start \(id)")
|
|
1068
1119
|
let destFolder = self.getBundleDirectory(id: id)
|
|
1069
|
-
let builtinFolder =
|
|
1120
|
+
let builtinFolder = self.builtinFolderURL()
|
|
1070
1121
|
|
|
1071
1122
|
// Check disk space before starting manifest download (estimate 100KB per file, minimum 50MB)
|
|
1072
1123
|
let estimatedSize = Int64(max(manifest.count * 100 * 1024, 50 * 1024 * 1024))
|
|
@@ -1253,6 +1304,8 @@ import UIKit
|
|
|
1253
1304
|
// Send stats for manifest download complete
|
|
1254
1305
|
self.sendStats(action: "download_manifest_complete", versionName: version)
|
|
1255
1306
|
|
|
1307
|
+
self.populateDeltaCacheAsync(for: id, manifest: manifest, sessionKey: sessionKey)
|
|
1308
|
+
|
|
1256
1309
|
self.notifyDownload(id: id, percent: 100, bundle: updatedBundle)
|
|
1257
1310
|
logger.info("downloadManifest done \(id)")
|
|
1258
1311
|
return updatedBundle
|
|
@@ -2439,7 +2492,8 @@ import UIKit
|
|
|
2439
2492
|
self.logger.info("Public channel requested, channel override removed")
|
|
2440
2493
|
|
|
2441
2494
|
setChannel.status = responseValue.status ?? "ok"
|
|
2442
|
-
setChannel.message = responseValue.message
|
|
2495
|
+
setChannel.message = responseValue.message
|
|
2496
|
+
?? "Public channel requested, channel override removed. Device will use public channel automatically."
|
|
2443
2497
|
} else {
|
|
2444
2498
|
self.defaultChannel = channel
|
|
2445
2499
|
UserDefaults.standard.set(channel, forKey: defaultChannelKey)
|
|
@@ -2454,7 +2508,6 @@ import UIKit
|
|
|
2454
2508
|
|
|
2455
2509
|
func getChannel(defaultChannelKey: String? = nil) -> GetChannel {
|
|
2456
2510
|
let getChannel: GetChannel = GetChannel()
|
|
2457
|
-
|
|
2458
2511
|
// Check if rate limit was exceeded
|
|
2459
2512
|
if CapgoUpdater.rateLimitExceeded {
|
|
2460
2513
|
logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.")
|
|
@@ -2853,7 +2906,6 @@ import UIKit
|
|
|
2853
2906
|
}
|
|
2854
2907
|
}
|
|
2855
2908
|
|
|
2856
|
-
|
|
2857
2909
|
private func setBundleStatus(id: String, status: BundleStatus) {
|
|
2858
2910
|
logger.info("Setting status for bundle [\(id)] to \(status)")
|
|
2859
2911
|
let info = self.getBundleInfo(id: id)
|
|
@@ -668,6 +668,14 @@ extension UIWindow {
|
|
|
668
668
|
}
|
|
669
669
|
return
|
|
670
670
|
}
|
|
671
|
+
guard plugin.persistDefaultChannelStateFromDefaults() else {
|
|
672
|
+
DispatchQueue.main.async {
|
|
673
|
+
progressAlert.dismiss(animated: true) {
|
|
674
|
+
self.showError(message: "Channel set to \(name), but local persistence failed.", plugin: plugin)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return
|
|
678
|
+
}
|
|
671
679
|
|
|
672
680
|
// Update progress message
|
|
673
681
|
DispatchQueue.main.async {
|