@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.
@@ -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
- private func populateDeltaCache(for id: String) {
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 checksum = CryptoCipher.calcChecksum(filePath: fileURL)
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 = Bundle.main.bundleURL.appendingPathComponent("public")
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 = Bundle.main.bundleURL.appendingPathComponent("public")
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 ?? "Public channel requested, channel override removed. Device will use public channel automatically."
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.51.1",
3
+ "version": "8.51.3",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",