@hot-updater/react-native 0.30.12 → 0.31.1
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/HotUpdater.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
- package/android/src/newarch/HotUpdater.kt +14 -2
- package/android/src/newarch/HotUpdaterModule.kt +112 -1
- package/android/src/oldarch/HotUpdater.kt +14 -2
- package/android/src/oldarch/HotUpdaterModule.kt +107 -3
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
- package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
- package/lib/commonjs/DefaultResolver.js +6 -2
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +79 -3
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +6 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +53 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +117 -1
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +40 -16
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +119 -3
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +8 -0
- package/lib/commonjs/sdkVersion.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +72 -5
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.spec.js +265 -8
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/wrap.js +19 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +74 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/DefaultResolver.js +6 -2
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +78 -3
- package/lib/module/DefaultResolver.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +6 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +53 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +117 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +40 -16
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +119 -3
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +4 -0
- package/lib/module/sdkVersion.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +72 -5
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.spec.js +265 -8
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/wrap.js +19 -5
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +74 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +36 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +56 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +5 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +43 -29
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +36 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +56 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +2 -0
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +5 -0
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +43 -29
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +20 -10
- package/src/DefaultResolver.spec.ts +103 -3
- package/src/DefaultResolver.ts +9 -2
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +149 -1
- package/src/index.ts +99 -40
- package/src/native.spec.ts +139 -1
- package/src/native.ts +145 -31
- package/src/sdkVersion.ts +1 -0
- package/src/specs/NativeHotUpdater.ts +28 -0
- package/src/store.spec.ts +324 -9
- package/src/store.ts +133 -7
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +79 -42
|
@@ -4,10 +4,52 @@ import android.os.StatFs
|
|
|
4
4
|
import android.util.Log
|
|
5
5
|
import kotlinx.coroutines.Dispatchers
|
|
6
6
|
import kotlinx.coroutines.withContext
|
|
7
|
+
import org.brotli.dec.BrotliInputStream
|
|
7
8
|
import org.json.JSONObject
|
|
8
9
|
import java.io.File
|
|
10
|
+
import java.io.FileInputStream
|
|
11
|
+
import java.io.FileOutputStream
|
|
9
12
|
import java.net.URL
|
|
10
13
|
|
|
14
|
+
data class ChangedAssetDescriptor(
|
|
15
|
+
val fileUrl: String?,
|
|
16
|
+
val fileHash: String,
|
|
17
|
+
val fileCompression: String? = null,
|
|
18
|
+
val patch: BsdiffPatchDescriptor? = null,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
data class BsdiffPatchDescriptor(
|
|
22
|
+
val algorithm: String,
|
|
23
|
+
val baseBundleId: String,
|
|
24
|
+
val baseFileHash: String,
|
|
25
|
+
val patchFileHash: String,
|
|
26
|
+
val patchUrl: String,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
data class UpdateProgressPayload(
|
|
30
|
+
val progress: Double,
|
|
31
|
+
val artifactType: String,
|
|
32
|
+
val downloadedBytes: Long? = null,
|
|
33
|
+
val totalBytes: Long? = null,
|
|
34
|
+
val details: DiffProgressDetails? = null,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
data class DiffProgressFileSnapshot(
|
|
38
|
+
val path: String,
|
|
39
|
+
val downloadPath: String = path,
|
|
40
|
+
val status: String,
|
|
41
|
+
val progress: Double,
|
|
42
|
+
val order: Int,
|
|
43
|
+
val downloadedBytes: Long? = null,
|
|
44
|
+
val totalBytes: Long? = null,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
data class DiffProgressDetails(
|
|
48
|
+
val totalFilesCount: Int,
|
|
49
|
+
val completedFilesCount: Int,
|
|
50
|
+
val files: List<DiffProgressFileSnapshot> = emptyList(),
|
|
51
|
+
)
|
|
52
|
+
|
|
11
53
|
/**
|
|
12
54
|
* Interface for bundle storage operations
|
|
13
55
|
*/
|
|
@@ -50,7 +92,10 @@ interface BundleStorageService {
|
|
|
50
92
|
bundleId: String,
|
|
51
93
|
fileUrl: String?,
|
|
52
94
|
fileHash: String?,
|
|
53
|
-
|
|
95
|
+
manifestUrl: String?,
|
|
96
|
+
manifestFileHash: String?,
|
|
97
|
+
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
98
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
54
99
|
)
|
|
55
100
|
|
|
56
101
|
/**
|
|
@@ -127,6 +172,303 @@ class BundleFileStorageService(
|
|
|
127
172
|
private const val TAG = "BundleStorage"
|
|
128
173
|
}
|
|
129
174
|
|
|
175
|
+
private fun emitArchiveProgress(
|
|
176
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
177
|
+
progress: Double,
|
|
178
|
+
downloadedBytes: Long? = null,
|
|
179
|
+
totalBytes: Long? = null,
|
|
180
|
+
) {
|
|
181
|
+
progressCallback(
|
|
182
|
+
UpdateProgressPayload(
|
|
183
|
+
progress = progress.coerceIn(0.0, 1.0),
|
|
184
|
+
artifactType = "archive",
|
|
185
|
+
downloadedBytes = downloadedBytes,
|
|
186
|
+
totalBytes = totalBytes,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private fun createDiffProgressFiles(changedAssets: Map<String, ChangedAssetDescriptor>): MutableList<DiffProgressFileSnapshot> =
|
|
192
|
+
changedAssets.keys
|
|
193
|
+
.sorted()
|
|
194
|
+
.mapIndexed { order, path ->
|
|
195
|
+
DiffProgressFileSnapshot(
|
|
196
|
+
path = path,
|
|
197
|
+
downloadPath = path,
|
|
198
|
+
status = "pending",
|
|
199
|
+
progress = 0.0,
|
|
200
|
+
order = order,
|
|
201
|
+
)
|
|
202
|
+
}.toMutableList()
|
|
203
|
+
|
|
204
|
+
private fun updateDiffProgressFile(
|
|
205
|
+
files: MutableList<DiffProgressFileSnapshot>,
|
|
206
|
+
assetPath: String,
|
|
207
|
+
status: String,
|
|
208
|
+
progress: Double,
|
|
209
|
+
downloadPath: String? = null,
|
|
210
|
+
downloadedBytes: Long? = null,
|
|
211
|
+
totalBytes: Long? = null,
|
|
212
|
+
) {
|
|
213
|
+
val fileIndex = files.indexOfFirst { it.path == assetPath }
|
|
214
|
+
if (fileIndex == -1) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
files[fileIndex] =
|
|
219
|
+
files[fileIndex].copy(
|
|
220
|
+
status = status,
|
|
221
|
+
progress = progress.coerceIn(0.0, 1.0),
|
|
222
|
+
downloadPath = downloadPath ?: files[fileIndex].downloadPath,
|
|
223
|
+
downloadedBytes = downloadedBytes ?: files[fileIndex].downloadedBytes,
|
|
224
|
+
totalBytes = totalBytes ?: files[fileIndex].totalBytes,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private fun calculateDiffOverallProgress(
|
|
229
|
+
phase: String,
|
|
230
|
+
files: List<DiffProgressFileSnapshot>,
|
|
231
|
+
manifestProgress: Double = 0.0,
|
|
232
|
+
): Double {
|
|
233
|
+
val normalizedManifestProgress = manifestProgress.coerceIn(0.0, 1.0)
|
|
234
|
+
return when (phase) {
|
|
235
|
+
"manifest" -> {
|
|
236
|
+
normalizedManifestProgress * 0.15
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
"downloading" -> {
|
|
240
|
+
if (files.isEmpty()) {
|
|
241
|
+
0.92
|
|
242
|
+
} else {
|
|
243
|
+
val completedFilesCount = files.count { it.status == "downloaded" }
|
|
244
|
+
val activeProgressUnits =
|
|
245
|
+
files
|
|
246
|
+
.filter { it.status == "downloading" }
|
|
247
|
+
.sumOf { it.progress.coerceIn(0.0, 1.0) }
|
|
248
|
+
(0.2 + ((completedFilesCount + activeProgressUnits) / files.size) * 0.72)
|
|
249
|
+
.coerceIn(0.2, 0.92)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
"finalizing" -> {
|
|
254
|
+
0.97
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
"completed" -> {
|
|
258
|
+
1.0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
else -> {
|
|
262
|
+
0.0
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun emitDiffProgress(
|
|
268
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
269
|
+
phase: String,
|
|
270
|
+
files: List<DiffProgressFileSnapshot>,
|
|
271
|
+
manifestProgress: Double = 0.0,
|
|
272
|
+
) {
|
|
273
|
+
progressCallback(
|
|
274
|
+
UpdateProgressPayload(
|
|
275
|
+
progress =
|
|
276
|
+
calculateDiffOverallProgress(
|
|
277
|
+
phase = phase,
|
|
278
|
+
files = files,
|
|
279
|
+
manifestProgress = manifestProgress,
|
|
280
|
+
),
|
|
281
|
+
artifactType = "diff",
|
|
282
|
+
details =
|
|
283
|
+
DiffProgressDetails(
|
|
284
|
+
totalFilesCount = files.size,
|
|
285
|
+
completedFilesCount = files.count { it.status == "downloaded" },
|
|
286
|
+
files = files.toList(),
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun resetDiffProgressFile(
|
|
293
|
+
files: MutableList<DiffProgressFileSnapshot>,
|
|
294
|
+
assetPath: String,
|
|
295
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
296
|
+
) {
|
|
297
|
+
updateDiffProgressFile(
|
|
298
|
+
files = files,
|
|
299
|
+
assetPath = assetPath,
|
|
300
|
+
status = "pending",
|
|
301
|
+
progress = 0.0,
|
|
302
|
+
downloadPath = assetPath,
|
|
303
|
+
)
|
|
304
|
+
emitDiffProgress(
|
|
305
|
+
progressCallback = progressCallback,
|
|
306
|
+
phase = "downloading",
|
|
307
|
+
files = files,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private fun patchTempFile(
|
|
312
|
+
tempDir: File,
|
|
313
|
+
assetPath: String,
|
|
314
|
+
): File {
|
|
315
|
+
val safeName = assetPath.replace("/", "__").replace("\\", "__")
|
|
316
|
+
val patchDir = File(tempDir, "patches")
|
|
317
|
+
patchDir.mkdirs()
|
|
318
|
+
return File(patchDir, "$safeName.bsdiff")
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private fun patchDownloadPath(assetPath: String): String = "$assetPath.bsdiff"
|
|
322
|
+
|
|
323
|
+
private fun downloadPathForChangedAsset(
|
|
324
|
+
assetPath: String,
|
|
325
|
+
changedAsset: ChangedAssetDescriptor,
|
|
326
|
+
): String =
|
|
327
|
+
if (changedAsset.fileCompression == "br") {
|
|
328
|
+
"$assetPath.br"
|
|
329
|
+
} else {
|
|
330
|
+
assetPath
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private fun compressedTempFile(
|
|
334
|
+
tempDir: File,
|
|
335
|
+
assetPath: String,
|
|
336
|
+
compression: String,
|
|
337
|
+
): File {
|
|
338
|
+
val safeName = assetPath.replace("/", "__").replace("\\", "__")
|
|
339
|
+
val compressedDir = File(tempDir, "compressed")
|
|
340
|
+
compressedDir.mkdirs()
|
|
341
|
+
return File(compressedDir, "$safeName.$compression")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private fun decompressBrotliFile(
|
|
345
|
+
sourceFile: File,
|
|
346
|
+
targetFile: File,
|
|
347
|
+
) {
|
|
348
|
+
targetFile.parentFile?.mkdirs()
|
|
349
|
+
val tempOutputFile = File(targetFile.parentFile, "${targetFile.name}.tmp")
|
|
350
|
+
|
|
351
|
+
if (tempOutputFile.exists()) {
|
|
352
|
+
tempOutputFile.delete()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
FileInputStream(sourceFile).use { fileInputStream ->
|
|
357
|
+
BrotliInputStream(fileInputStream).use { brotliInputStream ->
|
|
358
|
+
FileOutputStream(tempOutputFile).use { outputStream ->
|
|
359
|
+
brotliInputStream.copyTo(outputStream)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (targetFile.exists()) {
|
|
365
|
+
targetFile.delete()
|
|
366
|
+
}
|
|
367
|
+
if (!tempOutputFile.renameTo(targetFile)) {
|
|
368
|
+
tempOutputFile.copyTo(targetFile, overwrite = true)
|
|
369
|
+
tempOutputFile.delete()
|
|
370
|
+
}
|
|
371
|
+
} catch (error: Throwable) {
|
|
372
|
+
tempOutputFile.delete()
|
|
373
|
+
throw error
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private suspend fun applyPatchAssetIfPossible(
|
|
378
|
+
assetPath: String,
|
|
379
|
+
changedAsset: ChangedAssetDescriptor,
|
|
380
|
+
currentBundleId: String?,
|
|
381
|
+
activeBundleDir: File?,
|
|
382
|
+
targetFile: File,
|
|
383
|
+
expectedHash: String,
|
|
384
|
+
tempDir: File,
|
|
385
|
+
diffFiles: MutableList<DiffProgressFileSnapshot>,
|
|
386
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
387
|
+
): Boolean {
|
|
388
|
+
val patch = changedAsset.patch ?: return false
|
|
389
|
+
if (patch.algorithm != "bsdiff" || currentBundleId != patch.baseBundleId) {
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
val sourceDir = activeBundleDir ?: return false
|
|
394
|
+
val sourceFile = File(sourceDir, assetPath)
|
|
395
|
+
if (!sourceFile.exists() || !HashUtils.verifyHash(sourceFile, patch.baseFileHash)) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
val patchFile = patchTempFile(tempDir, assetPath)
|
|
400
|
+
|
|
401
|
+
return try {
|
|
402
|
+
when (
|
|
403
|
+
val patchDownloadResult =
|
|
404
|
+
downloadService.downloadFile(
|
|
405
|
+
URL(patch.patchUrl),
|
|
406
|
+
patchFile,
|
|
407
|
+
) { downloadProgress ->
|
|
408
|
+
updateDiffProgressFile(
|
|
409
|
+
files = diffFiles,
|
|
410
|
+
assetPath = assetPath,
|
|
411
|
+
status = "downloading",
|
|
412
|
+
progress = downloadProgress.progress,
|
|
413
|
+
downloadPath = patchDownloadPath(assetPath),
|
|
414
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
415
|
+
totalBytes = downloadProgress.totalBytes,
|
|
416
|
+
)
|
|
417
|
+
emitDiffProgress(
|
|
418
|
+
progressCallback = progressCallback,
|
|
419
|
+
phase = "downloading",
|
|
420
|
+
files = diffFiles,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
) {
|
|
424
|
+
is DownloadResult.Error -> {
|
|
425
|
+
false
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
is DownloadResult.Success -> {
|
|
429
|
+
if (!HashUtils.verifyHash(patchDownloadResult.file, patch.patchFileHash)) {
|
|
430
|
+
false
|
|
431
|
+
} else {
|
|
432
|
+
BsdiffPatch.apply(sourceFile, patchDownloadResult.file, targetFile)
|
|
433
|
+
HashUtils.verifyHash(targetFile, expectedHash).also { patched ->
|
|
434
|
+
if (patched) {
|
|
435
|
+
Log.d(
|
|
436
|
+
TAG,
|
|
437
|
+
"HotUpdaterBsdiffPatchApplied asset=$assetPath baseBundleId=${patch.baseBundleId}",
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch (_: Exception) {
|
|
445
|
+
false
|
|
446
|
+
} finally {
|
|
447
|
+
patchFile.delete()
|
|
448
|
+
if (!targetFile.exists() || !HashUtils.verifyHash(targetFile, expectedHash)) {
|
|
449
|
+
targetFile.delete()
|
|
450
|
+
}
|
|
451
|
+
}.also { patched ->
|
|
452
|
+
if (!patched) {
|
|
453
|
+
resetDiffProgressFile(
|
|
454
|
+
files = diffFiles,
|
|
455
|
+
assetPath = assetPath,
|
|
456
|
+
progressCallback = progressCallback,
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private data class ParsedBundleManifest(
|
|
463
|
+
val bundleId: String,
|
|
464
|
+
val assets: Map<String, ParsedManifestAsset>,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
private data class ParsedManifestAsset(
|
|
468
|
+
val fileHash: String,
|
|
469
|
+
val signature: String?,
|
|
470
|
+
)
|
|
471
|
+
|
|
130
472
|
private data class ActiveBundleMetadataSnapshot(
|
|
131
473
|
val activeBundleId: String,
|
|
132
474
|
val bundleId: String?,
|
|
@@ -380,6 +722,138 @@ class BundleFileStorageService(
|
|
|
380
722
|
else -> value
|
|
381
723
|
}
|
|
382
724
|
|
|
725
|
+
private fun parseBundleManifestFromMap(manifest: Map<String, Any?>): ParsedBundleManifest? {
|
|
726
|
+
val manifestBundleId =
|
|
727
|
+
(manifest["bundleId"] as? String)
|
|
728
|
+
?.trim()
|
|
729
|
+
?.takeIf { it.isNotEmpty() }
|
|
730
|
+
?: return null
|
|
731
|
+
val assetsValue = manifest["assets"] as? Map<*, *> ?: return null
|
|
732
|
+
val assets = linkedMapOf<String, ParsedManifestAsset>()
|
|
733
|
+
|
|
734
|
+
for ((assetPath, assetValue) in assetsValue) {
|
|
735
|
+
if (assetPath !is String) {
|
|
736
|
+
return null
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
val assetMap = assetValue as? Map<*, *> ?: return null
|
|
740
|
+
val fileHash = assetMap["fileHash"] as? String ?: return null
|
|
741
|
+
val signature = assetMap["signature"] as? String
|
|
742
|
+
assets[assetPath] =
|
|
743
|
+
ParsedManifestAsset(
|
|
744
|
+
fileHash = fileHash,
|
|
745
|
+
signature = signature?.takeIf { it.isNotBlank() },
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return ParsedBundleManifest(
|
|
750
|
+
bundleId = manifestBundleId,
|
|
751
|
+
assets = assets,
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private fun parseBundleManifestFromFile(manifestFile: File): ParsedBundleManifest? {
|
|
756
|
+
if (!manifestFile.exists()) {
|
|
757
|
+
return null
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return try {
|
|
761
|
+
val parsed = JSONObject(manifestFile.readText()).let(::jsonObjectToMap)
|
|
762
|
+
parseBundleManifestFromMap(parsed)
|
|
763
|
+
} catch (e: Exception) {
|
|
764
|
+
Log.w(TAG, "Failed to parse manifest ${manifestFile.absolutePath}: ${e.message}")
|
|
765
|
+
null
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private fun writeBundleManifestFile(
|
|
770
|
+
destination: File,
|
|
771
|
+
manifest: ParsedBundleManifest,
|
|
772
|
+
) {
|
|
773
|
+
destination.parentFile?.mkdirs()
|
|
774
|
+
val assetsObject = JSONObject()
|
|
775
|
+
|
|
776
|
+
manifest.assets.toSortedMap().forEach { (assetPath, asset) ->
|
|
777
|
+
val assetObject = JSONObject().put("fileHash", asset.fileHash)
|
|
778
|
+
if (!asset.signature.isNullOrBlank()) {
|
|
779
|
+
assetObject.put("signature", asset.signature)
|
|
780
|
+
}
|
|
781
|
+
assetsObject.put(
|
|
782
|
+
assetPath,
|
|
783
|
+
assetObject,
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
val manifestObject =
|
|
788
|
+
JSONObject()
|
|
789
|
+
.put("bundleId", manifest.bundleId)
|
|
790
|
+
.put("assets", assetsObject)
|
|
791
|
+
|
|
792
|
+
destination.writeText("${manifestObject}\n")
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private fun getActiveBundleDir(): File? {
|
|
796
|
+
val activeBundleId = getActiveBundleId() ?: return null
|
|
797
|
+
val bundleDir = File(getBundleStoreDir(), activeBundleId)
|
|
798
|
+
return bundleDir.takeIf { it.exists() }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private fun canUseManifestDrivenInstall(): Boolean {
|
|
802
|
+
val activeBundleDir = getActiveBundleDir() ?: return false
|
|
803
|
+
if (!activeBundleDir.exists()) {
|
|
804
|
+
return false
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
val currentManifest =
|
|
808
|
+
getActiveBundleMetadataSnapshot()
|
|
809
|
+
?.manifest
|
|
810
|
+
?.let(::parseBundleManifestFromMap) ?: return false
|
|
811
|
+
|
|
812
|
+
return currentManifest.assets.isNotEmpty()
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private fun copyBundleFile(
|
|
816
|
+
source: File,
|
|
817
|
+
destination: File,
|
|
818
|
+
) {
|
|
819
|
+
destination.parentFile?.mkdirs()
|
|
820
|
+
source.copyTo(destination, overwrite = true)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private fun verifyManifestAssetFile(
|
|
824
|
+
file: File,
|
|
825
|
+
asset: ParsedManifestAsset,
|
|
826
|
+
) {
|
|
827
|
+
val actualHash =
|
|
828
|
+
HashUtils.calculateSHA256(file)
|
|
829
|
+
?: throw SignatureVerificationException.FileReadFailed()
|
|
830
|
+
|
|
831
|
+
if (!actualHash.equals(asset.fileHash, ignoreCase = true)) {
|
|
832
|
+
throw SignatureVerificationException.FileHashMismatch()
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (!SignatureVerifier.isSigningEnabled(context)) {
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
val signature =
|
|
840
|
+
asset.signature
|
|
841
|
+
?: throw SignatureVerificationException.InvalidSignatureFormat()
|
|
842
|
+
|
|
843
|
+
SignatureVerifier.verifyHashSignature(context, asset.fileHash, signature)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private fun verifyManifestAssetFileOrThrow(
|
|
847
|
+
file: File,
|
|
848
|
+
asset: ParsedManifestAsset,
|
|
849
|
+
) {
|
|
850
|
+
try {
|
|
851
|
+
verifyManifestAssetFile(file, asset)
|
|
852
|
+
} catch (e: SignatureVerificationException) {
|
|
853
|
+
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
383
857
|
/**
|
|
384
858
|
* Checks if isolationKey has changed and cleans up old bundles if needed.
|
|
385
859
|
* This handles migration when isolationKey format changes.
|
|
@@ -639,11 +1113,14 @@ class BundleFileStorageService(
|
|
|
639
1113
|
bundleId: String,
|
|
640
1114
|
fileUrl: String?,
|
|
641
1115
|
fileHash: String?,
|
|
642
|
-
|
|
1116
|
+
manifestUrl: String?,
|
|
1117
|
+
manifestFileHash: String?,
|
|
1118
|
+
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
1119
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
643
1120
|
) {
|
|
644
1121
|
Log.d(
|
|
645
1122
|
TAG,
|
|
646
|
-
"updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash",
|
|
1123
|
+
"updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash manifestUrl $manifestUrl",
|
|
647
1124
|
)
|
|
648
1125
|
|
|
649
1126
|
// If no URL is provided, reset to fallback and clean up all bundles
|
|
@@ -721,6 +1198,40 @@ class BundleFileStorageService(
|
|
|
721
1198
|
}
|
|
722
1199
|
}
|
|
723
1200
|
|
|
1201
|
+
val hasManifestDrivenArtifacts =
|
|
1202
|
+
!manifestUrl.isNullOrEmpty() &&
|
|
1203
|
+
!manifestFileHash.isNullOrEmpty() &&
|
|
1204
|
+
changedAssets != null
|
|
1205
|
+
|
|
1206
|
+
if (hasManifestDrivenArtifacts && canUseManifestDrivenInstall()) {
|
|
1207
|
+
try {
|
|
1208
|
+
updateBundleFromManifest(
|
|
1209
|
+
bundleId = bundleId,
|
|
1210
|
+
manifestUrl = manifestUrl!!,
|
|
1211
|
+
manifestFileHash = manifestFileHash!!,
|
|
1212
|
+
changedAssets = changedAssets!!,
|
|
1213
|
+
bundleStoreDir = bundleStoreDir,
|
|
1214
|
+
finalBundleDir = finalBundleDir,
|
|
1215
|
+
progressCallback = progressCallback,
|
|
1216
|
+
)
|
|
1217
|
+
return
|
|
1218
|
+
} catch (e: Exception) {
|
|
1219
|
+
if (fileUrl.isNullOrEmpty()) {
|
|
1220
|
+
throw e
|
|
1221
|
+
}
|
|
1222
|
+
Log.w(
|
|
1223
|
+
TAG,
|
|
1224
|
+
"Manifest-driven install failed for $bundleId. Falling back to archive: ${e.message}",
|
|
1225
|
+
e,
|
|
1226
|
+
)
|
|
1227
|
+
}
|
|
1228
|
+
} else if (hasManifestDrivenArtifacts) {
|
|
1229
|
+
Log.d(
|
|
1230
|
+
TAG,
|
|
1231
|
+
"Skipping manifest-driven install for $bundleId because no active OTA manifest is available. Using archive.",
|
|
1232
|
+
)
|
|
1233
|
+
}
|
|
1234
|
+
|
|
724
1235
|
val tempDirName = "bundle-temp"
|
|
725
1236
|
val tempDir = File(baseDir, tempDirName)
|
|
726
1237
|
if (tempDir.exists()) {
|
|
@@ -772,7 +1283,12 @@ class BundleFileStorageService(
|
|
|
772
1283
|
},
|
|
773
1284
|
) { downloadProgress ->
|
|
774
1285
|
// Map download progress to 0.0 - 0.8
|
|
775
|
-
|
|
1286
|
+
emitArchiveProgress(
|
|
1287
|
+
progressCallback,
|
|
1288
|
+
downloadProgress.progress * 0.8,
|
|
1289
|
+
downloadProgress.downloadedBytes,
|
|
1290
|
+
downloadProgress.totalBytes,
|
|
1291
|
+
)
|
|
776
1292
|
}
|
|
777
1293
|
|
|
778
1294
|
// Check for disk space error first before processing download result
|
|
@@ -827,7 +1343,10 @@ class BundleFileStorageService(
|
|
|
827
1343
|
tmpDir.absolutePath,
|
|
828
1344
|
) { unzipProgress ->
|
|
829
1345
|
// Map unzip progress (0.0 - 1.0) to overall progress (0.8 - 1.0)
|
|
830
|
-
|
|
1346
|
+
emitArchiveProgress(
|
|
1347
|
+
progressCallback,
|
|
1348
|
+
0.8 + (unzipProgress * 0.2),
|
|
1349
|
+
)
|
|
831
1350
|
}
|
|
832
1351
|
) {
|
|
833
1352
|
Log.d("BundleStorage", "Failed to extract archive into tmpDir.")
|
|
@@ -916,6 +1435,367 @@ class BundleFileStorageService(
|
|
|
916
1435
|
/**
|
|
917
1436
|
* Removes old bundles except for the specified bundle IDs, and any leftover .tmp directories
|
|
918
1437
|
*/
|
|
1438
|
+
private suspend fun updateBundleFromManifest(
|
|
1439
|
+
bundleId: String,
|
|
1440
|
+
manifestUrl: String,
|
|
1441
|
+
manifestFileHash: String,
|
|
1442
|
+
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1443
|
+
bundleStoreDir: File,
|
|
1444
|
+
finalBundleDir: File,
|
|
1445
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1446
|
+
) {
|
|
1447
|
+
val activeBundleDir = getActiveBundleDir()
|
|
1448
|
+
val currentBundleId = getBundleId()
|
|
1449
|
+
val currentManifest =
|
|
1450
|
+
getActiveBundleMetadataSnapshot()?.manifest?.let(::parseBundleManifestFromMap)
|
|
1451
|
+
val baseDir =
|
|
1452
|
+
fileSystem.getInternalFilesDir() ?: bundleStoreDir.parentFile ?: bundleStoreDir
|
|
1453
|
+
val tempDir = File(baseDir, "bundle-manifest-temp")
|
|
1454
|
+
val tmpDir = File(bundleStoreDir, "$bundleId.tmp")
|
|
1455
|
+
|
|
1456
|
+
if (tempDir.exists()) {
|
|
1457
|
+
tempDir.deleteRecursively()
|
|
1458
|
+
}
|
|
1459
|
+
tempDir.mkdirs()
|
|
1460
|
+
|
|
1461
|
+
val diffFiles = createDiffProgressFiles(changedAssets)
|
|
1462
|
+
|
|
1463
|
+
try {
|
|
1464
|
+
val manifestFile = File(tempDir, "manifest.json")
|
|
1465
|
+
emitDiffProgress(
|
|
1466
|
+
progressCallback = progressCallback,
|
|
1467
|
+
phase = "manifest",
|
|
1468
|
+
files = diffFiles,
|
|
1469
|
+
manifestProgress = 0.0,
|
|
1470
|
+
)
|
|
1471
|
+
when (
|
|
1472
|
+
val manifestDownloadResult =
|
|
1473
|
+
downloadService.downloadFile(
|
|
1474
|
+
URL(manifestUrl),
|
|
1475
|
+
manifestFile,
|
|
1476
|
+
) { downloadProgress ->
|
|
1477
|
+
emitDiffProgress(
|
|
1478
|
+
progressCallback = progressCallback,
|
|
1479
|
+
phase = "manifest",
|
|
1480
|
+
files = diffFiles,
|
|
1481
|
+
manifestProgress = downloadProgress.progress,
|
|
1482
|
+
)
|
|
1483
|
+
}
|
|
1484
|
+
) {
|
|
1485
|
+
is DownloadResult.Error -> {
|
|
1486
|
+
if (manifestDownloadResult.exception is IncompleteDownloadException) {
|
|
1487
|
+
val incompleteEx =
|
|
1488
|
+
manifestDownloadResult.exception as IncompleteDownloadException
|
|
1489
|
+
throw HotUpdaterException.incompleteDownload(
|
|
1490
|
+
incompleteEx.expectedSize,
|
|
1491
|
+
incompleteEx.actualSize,
|
|
1492
|
+
)
|
|
1493
|
+
}
|
|
1494
|
+
throw HotUpdaterException.downloadFailed(manifestDownloadResult.exception)
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
is DownloadResult.Success -> {
|
|
1498
|
+
Unit
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
try {
|
|
1503
|
+
SignatureVerifier.verifyBundle(context, manifestFile, manifestFileHash)
|
|
1504
|
+
} catch (e: SignatureVerificationException) {
|
|
1505
|
+
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
val targetManifest = parseBundleManifestFromFile(manifestFile) ?: throw HotUpdaterException.invalidBundle()
|
|
1509
|
+
if (targetManifest.bundleId != bundleId) {
|
|
1510
|
+
throw HotUpdaterException.invalidBundle()
|
|
1511
|
+
}
|
|
1512
|
+
emitDiffProgress(
|
|
1513
|
+
progressCallback = progressCallback,
|
|
1514
|
+
phase = if (diffFiles.isEmpty()) "finalizing" else "downloading",
|
|
1515
|
+
files = diffFiles,
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
if (tmpDir.exists()) {
|
|
1519
|
+
tmpDir.deleteRecursively()
|
|
1520
|
+
}
|
|
1521
|
+
tmpDir.mkdirs()
|
|
1522
|
+
|
|
1523
|
+
val targetEntries = targetManifest.assets.entries.toList()
|
|
1524
|
+
targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
|
|
1525
|
+
val expectedHash = expectedAsset.fileHash
|
|
1526
|
+
val targetFile = File(tmpDir, assetPath)
|
|
1527
|
+
val currentAsset = currentManifest?.assets?.get(assetPath)
|
|
1528
|
+
|
|
1529
|
+
if (currentAsset?.fileHash == expectedHash) {
|
|
1530
|
+
val sourceDir =
|
|
1531
|
+
activeBundleDir
|
|
1532
|
+
?: throw HotUpdaterException.downloadFailed(
|
|
1533
|
+
IllegalStateException("Current bundle directory unavailable for reused asset: $assetPath"),
|
|
1534
|
+
)
|
|
1535
|
+
val sourceFile = File(sourceDir, assetPath)
|
|
1536
|
+
if (!sourceFile.exists() || !HashUtils.verifyHash(sourceFile, expectedHash)) {
|
|
1537
|
+
throw HotUpdaterException.downloadFailed(
|
|
1538
|
+
IllegalStateException("Reusable asset missing or corrupted: $assetPath"),
|
|
1539
|
+
)
|
|
1540
|
+
}
|
|
1541
|
+
copyBundleFile(sourceFile, targetFile)
|
|
1542
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1543
|
+
return@forEachIndexed
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
val changedAsset =
|
|
1547
|
+
changedAssets[assetPath]
|
|
1548
|
+
?: run {
|
|
1549
|
+
updateDiffProgressFile(
|
|
1550
|
+
files = diffFiles,
|
|
1551
|
+
assetPath = assetPath,
|
|
1552
|
+
status = "failed",
|
|
1553
|
+
progress = 0.0,
|
|
1554
|
+
)
|
|
1555
|
+
emitDiffProgress(
|
|
1556
|
+
progressCallback = progressCallback,
|
|
1557
|
+
phase = "downloading",
|
|
1558
|
+
files = diffFiles,
|
|
1559
|
+
)
|
|
1560
|
+
throw HotUpdaterException.downloadFailed(
|
|
1561
|
+
IllegalStateException("Changed asset missing from update response: $assetPath"),
|
|
1562
|
+
)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (!changedAsset.fileHash.equals(expectedHash, ignoreCase = true)) {
|
|
1566
|
+
updateDiffProgressFile(
|
|
1567
|
+
files = diffFiles,
|
|
1568
|
+
assetPath = assetPath,
|
|
1569
|
+
status = "failed",
|
|
1570
|
+
progress = 0.0,
|
|
1571
|
+
)
|
|
1572
|
+
emitDiffProgress(
|
|
1573
|
+
progressCallback = progressCallback,
|
|
1574
|
+
phase = "downloading",
|
|
1575
|
+
files = diffFiles,
|
|
1576
|
+
)
|
|
1577
|
+
throw HotUpdaterException.signatureVerificationFailed(
|
|
1578
|
+
SignatureVerificationException.FileHashMismatch(),
|
|
1579
|
+
)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
val patched =
|
|
1583
|
+
applyPatchAssetIfPossible(
|
|
1584
|
+
assetPath = assetPath,
|
|
1585
|
+
changedAsset = changedAsset,
|
|
1586
|
+
currentBundleId = currentBundleId,
|
|
1587
|
+
activeBundleDir = activeBundleDir,
|
|
1588
|
+
targetFile = targetFile,
|
|
1589
|
+
expectedHash = expectedHash,
|
|
1590
|
+
tempDir = tempDir,
|
|
1591
|
+
diffFiles = diffFiles,
|
|
1592
|
+
progressCallback = progressCallback,
|
|
1593
|
+
)
|
|
1594
|
+
if (patched) {
|
|
1595
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1596
|
+
updateDiffProgressFile(
|
|
1597
|
+
files = diffFiles,
|
|
1598
|
+
assetPath = assetPath,
|
|
1599
|
+
status = "downloaded",
|
|
1600
|
+
progress = 1.0,
|
|
1601
|
+
)
|
|
1602
|
+
emitDiffProgress(
|
|
1603
|
+
progressCallback = progressCallback,
|
|
1604
|
+
phase =
|
|
1605
|
+
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1606
|
+
"finalizing"
|
|
1607
|
+
} else {
|
|
1608
|
+
"downloading"
|
|
1609
|
+
},
|
|
1610
|
+
files = diffFiles,
|
|
1611
|
+
)
|
|
1612
|
+
return@forEachIndexed
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
val changedAssetFileUrl =
|
|
1616
|
+
changedAsset.fileUrl
|
|
1617
|
+
?: run {
|
|
1618
|
+
updateDiffProgressFile(
|
|
1619
|
+
files = diffFiles,
|
|
1620
|
+
assetPath = assetPath,
|
|
1621
|
+
status = "failed",
|
|
1622
|
+
progress = 0.0,
|
|
1623
|
+
)
|
|
1624
|
+
emitDiffProgress(
|
|
1625
|
+
progressCallback = progressCallback,
|
|
1626
|
+
phase = "downloading",
|
|
1627
|
+
files = diffFiles,
|
|
1628
|
+
)
|
|
1629
|
+
throw HotUpdaterException.downloadFailed(
|
|
1630
|
+
IllegalStateException("Changed asset fileUrl missing and patch could not be applied: $assetPath"),
|
|
1631
|
+
)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
val downloadPath = downloadPathForChangedAsset(assetPath, changedAsset)
|
|
1635
|
+
val downloadedAssetFile =
|
|
1636
|
+
if (changedAsset.fileCompression == "br") {
|
|
1637
|
+
compressedTempFile(tempDir, assetPath, "br")
|
|
1638
|
+
} else {
|
|
1639
|
+
targetFile
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
when (
|
|
1643
|
+
val assetDownloadResult =
|
|
1644
|
+
downloadService.downloadFile(
|
|
1645
|
+
URL(changedAssetFileUrl),
|
|
1646
|
+
downloadedAssetFile,
|
|
1647
|
+
) { downloadProgress ->
|
|
1648
|
+
updateDiffProgressFile(
|
|
1649
|
+
files = diffFiles,
|
|
1650
|
+
assetPath = assetPath,
|
|
1651
|
+
status = "downloading",
|
|
1652
|
+
progress = downloadProgress.progress,
|
|
1653
|
+
downloadPath = downloadPath,
|
|
1654
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
1655
|
+
totalBytes = downloadProgress.totalBytes,
|
|
1656
|
+
)
|
|
1657
|
+
emitDiffProgress(
|
|
1658
|
+
progressCallback = progressCallback,
|
|
1659
|
+
phase = "downloading",
|
|
1660
|
+
files = diffFiles,
|
|
1661
|
+
)
|
|
1662
|
+
}
|
|
1663
|
+
) {
|
|
1664
|
+
is DownloadResult.Error -> {
|
|
1665
|
+
updateDiffProgressFile(
|
|
1666
|
+
files = diffFiles,
|
|
1667
|
+
assetPath = assetPath,
|
|
1668
|
+
status = "failed",
|
|
1669
|
+
progress =
|
|
1670
|
+
diffFiles
|
|
1671
|
+
.firstOrNull { it.path == assetPath }
|
|
1672
|
+
?.progress ?: 0.0,
|
|
1673
|
+
)
|
|
1674
|
+
emitDiffProgress(
|
|
1675
|
+
progressCallback = progressCallback,
|
|
1676
|
+
phase = "downloading",
|
|
1677
|
+
files = diffFiles,
|
|
1678
|
+
)
|
|
1679
|
+
if (assetDownloadResult.exception is IncompleteDownloadException) {
|
|
1680
|
+
val incompleteEx =
|
|
1681
|
+
assetDownloadResult.exception as IncompleteDownloadException
|
|
1682
|
+
throw HotUpdaterException.incompleteDownload(
|
|
1683
|
+
incompleteEx.expectedSize,
|
|
1684
|
+
incompleteEx.actualSize,
|
|
1685
|
+
)
|
|
1686
|
+
}
|
|
1687
|
+
throw HotUpdaterException.downloadFailed(assetDownloadResult.exception)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
is DownloadResult.Success -> {
|
|
1691
|
+
try {
|
|
1692
|
+
if (changedAsset.fileCompression == "br") {
|
|
1693
|
+
decompressBrotliFile(
|
|
1694
|
+
assetDownloadResult.file,
|
|
1695
|
+
targetFile,
|
|
1696
|
+
)
|
|
1697
|
+
}
|
|
1698
|
+
verifyManifestAssetFileOrThrow(
|
|
1699
|
+
targetFile,
|
|
1700
|
+
expectedAsset,
|
|
1701
|
+
)
|
|
1702
|
+
} catch (e: Exception) {
|
|
1703
|
+
updateDiffProgressFile(
|
|
1704
|
+
files = diffFiles,
|
|
1705
|
+
assetPath = assetPath,
|
|
1706
|
+
status = "failed",
|
|
1707
|
+
progress = 1.0,
|
|
1708
|
+
)
|
|
1709
|
+
emitDiffProgress(
|
|
1710
|
+
progressCallback = progressCallback,
|
|
1711
|
+
phase = "downloading",
|
|
1712
|
+
files = diffFiles,
|
|
1713
|
+
)
|
|
1714
|
+
if (e is HotUpdaterException) {
|
|
1715
|
+
throw e
|
|
1716
|
+
}
|
|
1717
|
+
throw HotUpdaterException.downloadFailed(e)
|
|
1718
|
+
} finally {
|
|
1719
|
+
if (changedAsset.fileCompression == "br") {
|
|
1720
|
+
assetDownloadResult.file.delete()
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
updateDiffProgressFile(
|
|
1724
|
+
files = diffFiles,
|
|
1725
|
+
assetPath = assetPath,
|
|
1726
|
+
status = "downloaded",
|
|
1727
|
+
progress = 1.0,
|
|
1728
|
+
)
|
|
1729
|
+
emitDiffProgress(
|
|
1730
|
+
progressCallback = progressCallback,
|
|
1731
|
+
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
1732
|
+
files = diffFiles,
|
|
1733
|
+
)
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
emitDiffProgress(
|
|
1739
|
+
progressCallback = progressCallback,
|
|
1740
|
+
phase = "finalizing",
|
|
1741
|
+
files = diffFiles,
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
writeBundleManifestFile(File(tmpDir, "manifest.json"), targetManifest)
|
|
1745
|
+
|
|
1746
|
+
val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
|
|
1747
|
+
if (extractedIndex == null) {
|
|
1748
|
+
throw HotUpdaterException.invalidBundle()
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (finalBundleDir.exists()) {
|
|
1752
|
+
finalBundleDir.deleteRecursively()
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
val renamed = tmpDir.renameTo(finalBundleDir)
|
|
1756
|
+
if (!renamed) {
|
|
1757
|
+
if (!fileSystem.moveItem(tmpDir.absolutePath, finalBundleDir.absolutePath)) {
|
|
1758
|
+
if (!fileSystem.copyItem(tmpDir.absolutePath, finalBundleDir.absolutePath)) {
|
|
1759
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
1760
|
+
}
|
|
1761
|
+
tmpDir.deleteRecursively()
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
1766
|
+
if (finalIndexFile == null) {
|
|
1767
|
+
finalBundleDir.deleteRecursively()
|
|
1768
|
+
throw HotUpdaterException.invalidBundle()
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
1772
|
+
|
|
1773
|
+
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
1774
|
+
val updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
1775
|
+
saveMetadata(updatedMetadata)
|
|
1776
|
+
setBundleURL(finalIndexFile.absolutePath)
|
|
1777
|
+
|
|
1778
|
+
tempDir.deleteRecursively()
|
|
1779
|
+
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
1780
|
+
progressCallback(
|
|
1781
|
+
UpdateProgressPayload(
|
|
1782
|
+
progress = 1.0,
|
|
1783
|
+
artifactType = "diff",
|
|
1784
|
+
details =
|
|
1785
|
+
DiffProgressDetails(
|
|
1786
|
+
totalFilesCount = diffFiles.size,
|
|
1787
|
+
completedFilesCount = diffFiles.count { it.status == "downloaded" },
|
|
1788
|
+
files = diffFiles.toList(),
|
|
1789
|
+
),
|
|
1790
|
+
),
|
|
1791
|
+
)
|
|
1792
|
+
} catch (e: Exception) {
|
|
1793
|
+
tempDir.deleteRecursively()
|
|
1794
|
+
tmpDir.deleteRecursively()
|
|
1795
|
+
throw e
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
919
1799
|
private fun cleanupOldBundles(
|
|
920
1800
|
bundleStoreDir: File,
|
|
921
1801
|
currentBundleId: String?,
|