@otakit/capacitor-updater 2.2.0 → 2.3.0

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.
@@ -1040,10 +1040,13 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1040
1040
  ) -> [String: Any] {
1041
1041
  var payload: [String: Any] = [
1042
1042
  "version": latest.version,
1043
- "url": latest.url,
1044
1043
  "sha256": latest.sha256,
1045
1044
  "size": latest.size,
1045
+ "strategy": latest.strategy,
1046
1046
  ]
1047
+ if let url = latest.url {
1048
+ payload["url"] = url
1049
+ }
1047
1050
  if let runtimeVersion = latest.runtimeVersion {
1048
1051
  payload["runtimeVersion"] = runtimeVersion
1049
1052
  }
@@ -1056,7 +1059,11 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1056
1059
  _ manifest: LatestManifest,
1057
1060
  targetChannel: String?
1058
1061
  ) async throws -> BundleInfo {
1059
- guard let url = URL(string: manifest.url) else {
1062
+ if manifest.strategy == "deltas" {
1063
+ return try await assembleAndStage(manifest: manifest, targetChannel: targetChannel)
1064
+ }
1065
+
1066
+ guard let urlString = manifest.url, let url = URL(string: urlString) else {
1060
1067
  throw NSError(
1061
1068
  domain: "OtaKit",
1062
1069
  code: 1,
@@ -1076,6 +1083,163 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1076
1083
  )
1077
1084
  }
1078
1085
 
1086
+ private static let bundleFileListName = "otakit_files.json"
1087
+
1088
+ /// Deltas strategy: fill content-cache misses and assemble the bundle from
1089
+ /// the cache, then hand it to the same staging path the zip flow uses.
1090
+ private func assembleAndStage(
1091
+ manifest: LatestManifest,
1092
+ targetChannel: String?
1093
+ ) async throws -> BundleInfo {
1094
+ // Same conservative disk-space guard as the zip path.
1095
+ let requiredSpace = Int64(Double(manifest.size) * 2.5)
1096
+ if getFreeDiskSpace() < requiredSpace {
1097
+ sendDeviceEvent(
1098
+ action: .downloadError,
1099
+ bundleVersion: manifest.version,
1100
+ runtimeVersion: manifest.runtimeVersion,
1101
+ channel: targetChannel,
1102
+ releaseId: manifest.releaseId,
1103
+ detail: "insufficient_disk_space"
1104
+ )
1105
+ emitEvent(
1106
+ "downloadFailed",
1107
+ failureEventData(
1108
+ version: manifest.version,
1109
+ runtimeVersion: manifest.runtimeVersion,
1110
+ channel: targetChannel,
1111
+ releaseId: manifest.releaseId,
1112
+ reason: "insufficient_disk_space"
1113
+ )
1114
+ )
1115
+ throw NSError(
1116
+ domain: "OtaKit",
1117
+ code: 1,
1118
+ userInfo: [NSLocalizedDescriptionKey: "Insufficient disk space"]
1119
+ )
1120
+ }
1121
+
1122
+ let assembleDirectory = fileManager.temporaryDirectory
1123
+ .appendingPathComponent("otakit-assemble-\(UUID().uuidString)", isDirectory: true)
1124
+ defer {
1125
+ try? fileManager.removeItem(at: assembleDirectory)
1126
+ }
1127
+
1128
+ do {
1129
+ guard let files = manifest.files, !files.isEmpty else {
1130
+ throw NSError(
1131
+ domain: "OtaKit",
1132
+ code: 1,
1133
+ userInfo: [NSLocalizedDescriptionKey: "Delta manifest is missing its file list"]
1134
+ )
1135
+ }
1136
+
1137
+ let assembler = DeltaAssembler(
1138
+ cacheDirectory: store.filesCacheDirectory,
1139
+ downloader: downloader
1140
+ )
1141
+ try assembler.validate(files, expectedFilesHash: manifest.sha256)
1142
+
1143
+ if let builtinDirectory = Bundle.main.resourceURL?
1144
+ .appendingPathComponent("public", isDirectory: true) {
1145
+ assembler.seedFromBuiltinIfNeeded(
1146
+ builtinDirectory: builtinDirectory,
1147
+ nativeBuild: coordinator.nativeBuild
1148
+ )
1149
+ }
1150
+
1151
+ try await assembler.assemble(entries: files, into: assembleDirectory)
1152
+
1153
+ let bundleId = buildBundleId(
1154
+ version: manifest.version,
1155
+ releaseId: manifest.releaseId,
1156
+ sha256: manifest.sha256
1157
+ )
1158
+ let destination = coordinator.bundleDirectory(for: bundleId)
1159
+ if fileManager.fileExists(atPath: destination.path) {
1160
+ try fileManager.removeItem(at: destination)
1161
+ }
1162
+ try fileManager.moveItem(at: assembleDirectory, to: destination)
1163
+
1164
+ // Record this bundle's content hashes for cache pruning.
1165
+ let hashes = files.map { $0.sha256.lowercased() }
1166
+ if let data = try? JSONSerialization.data(withJSONObject: hashes) {
1167
+ try? data.write(
1168
+ to: destination.appendingPathComponent(UpdaterPlugin.bundleFileListName),
1169
+ options: .atomic
1170
+ )
1171
+ }
1172
+
1173
+ let info = BundleInfo(
1174
+ id: bundleId,
1175
+ version: manifest.version,
1176
+ runtimeVersion: manifest.runtimeVersion,
1177
+ status: .pending,
1178
+ downloadedAt: Date(),
1179
+ sha256: manifest.sha256,
1180
+ path: destination.path,
1181
+ channel: targetChannel,
1182
+ releaseId: manifest.releaseId
1183
+ )
1184
+
1185
+ let cleanupBundleIds = try coordinator.stageDownloadedBundle(info)
1186
+ coordinator.cleanupBundles(cleanupBundleIds)
1187
+
1188
+ pruneDeltaCache(assembler: assembler)
1189
+
1190
+ sendDeviceEvent(
1191
+ action: .downloaded,
1192
+ bundleVersion: manifest.version,
1193
+ runtimeVersion: manifest.runtimeVersion,
1194
+ channel: targetChannel,
1195
+ releaseId: manifest.releaseId
1196
+ )
1197
+ emitEvent("updateStaged", ["bundle": info.toDictionary()])
1198
+ return info
1199
+ } catch {
1200
+ sendDeviceEvent(
1201
+ action: .downloadError,
1202
+ bundleVersion: manifest.version,
1203
+ runtimeVersion: manifest.runtimeVersion,
1204
+ channel: targetChannel,
1205
+ releaseId: manifest.releaseId,
1206
+ detail: error.localizedDescription
1207
+ )
1208
+ emitEvent(
1209
+ "downloadFailed",
1210
+ failureEventData(
1211
+ version: manifest.version,
1212
+ runtimeVersion: manifest.runtimeVersion,
1213
+ channel: targetChannel,
1214
+ releaseId: manifest.releaseId,
1215
+ reason: failureReason(from: error)
1216
+ )
1217
+ )
1218
+ throw error
1219
+ }
1220
+ }
1221
+
1222
+ /// Keep only cache entries referenced by live bundles (plus the builtin seed).
1223
+ private func pruneDeltaCache(assembler: DeltaAssembler) {
1224
+ var referenced = Set<String>()
1225
+ let liveBundleIds = [
1226
+ store.getCurrentBundleId(),
1227
+ store.getFallbackBundleId(),
1228
+ store.getStagedBundleId(),
1229
+ ]
1230
+ for bundleId in liveBundleIds {
1231
+ guard let bundleId else { continue }
1232
+ let listURL = store.bundleDirectory(for: bundleId)
1233
+ .appendingPathComponent(UpdaterPlugin.bundleFileListName)
1234
+ guard let data = try? Data(contentsOf: listURL),
1235
+ let hashes = try? JSONSerialization.jsonObject(with: data) as? [String] else {
1236
+ continue
1237
+ }
1238
+ referenced.formUnion(hashes.map { $0.lowercased() })
1239
+ }
1240
+ assembler.pruneCache(referencedHashes: referenced)
1241
+ }
1242
+
1079
1243
  private func pruneIncompatibleBundles() {
1080
1244
  let cleanupBundleIds = coordinator.pruneIncompatibleBundles(
1081
1245
  isCompatibleRuntime: isCompatibleRuntime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otakit/capacitor-updater",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Capacitor plugin for OTA updates",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",