@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.
- package/android/src/main/java/com/otakit/updater/BundleStore.java +11 -0
- package/android/src/main/java/com/otakit/updater/DeltaAssembler.java +416 -0
- package/android/src/main/java/com/otakit/updater/HashUtils.java +16 -0
- package/android/src/main/java/com/otakit/updater/ManifestClient.java +69 -4
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +180 -1
- package/dist/esm/definitions.d.ts +6 -4
- package/dist/esm/definitions.d.ts.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +17 -0
- package/ios/Sources/UpdaterPlugin/DeltaAssembler.swift +316 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +54 -6
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +166 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|