@hot-updater/react-native 0.20.14 → 0.21.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/HotUpdater.podspec +7 -4
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +78 -21
- package/android/src/main/java/com/hotupdater/DecompressService.kt +83 -0
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +26 -0
- package/android/src/main/java/com/hotupdater/HashUtils.kt +47 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +3 -3
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +3 -1
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +293 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +105 -0
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +117 -0
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +175 -0
- package/android/src/newarch/HotUpdaterModule.kt +2 -0
- package/android/src/oldarch/HotUpdaterModule.kt +2 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +133 -60
- package/ios/HotUpdater/Internal/DecompressService.swift +89 -0
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +22 -0
- package/ios/HotUpdater/Internal/HashUtils.swift +63 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -2
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +4 -4
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +23 -10
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +229 -0
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +177 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +73 -7
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +165 -0
- package/lib/commonjs/checkForUpdate.js +1 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/fetchUpdateInfo.js +1 -1
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
- package/lib/commonjs/native.js +3 -1
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +1 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/fetchUpdateInfo.js +1 -1
- package/lib/module/fetchUpdateInfo.js.map +1 -1
- package/lib/module/native.js +3 -1
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +5 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +5 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/checkForUpdate.ts +1 -0
- package/src/fetchUpdateInfo.ts +1 -1
- package/src/native.ts +6 -0
- package/src/specs/NativeHotUpdater.ts +5 -0
- package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +0 -98
- package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +0 -74
- package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +0 -25
package/HotUpdater.podspec
CHANGED
|
@@ -11,18 +11,21 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.license = package["license"]
|
|
12
12
|
s.authors = package["author"]
|
|
13
13
|
|
|
14
|
-
s.platforms = { :ios =>
|
|
14
|
+
s.platforms = { :ios => "13.4" }
|
|
15
15
|
s.source = { :git => "https://github.com/gronxb/hot-updater.git", :tag => "#{s.version}" }
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
17
|
s.public_header_files = "ios/HotUpdater/Public/*.h"
|
|
18
18
|
s.private_header_files = "ios/HotUpdater/Internal/*.h"
|
|
19
|
-
s.exclude_files = "ios/HotUpdater/Package.swift", "ios/HotUpdater/Test/**/*.{swift,h,m,mm}"
|
|
19
|
+
s.exclude_files = ["ios/HotUpdater/Package.swift", "ios/HotUpdater/Test/**/*.{swift,h,m,mm}"]
|
|
20
20
|
|
|
21
21
|
s.pod_target_xcconfig = {
|
|
22
22
|
"DEFINES_MODULE" => "YES",
|
|
23
23
|
"OTHER_SWIFT_FLAGS" => "-enable-experimental-feature AccessLevelOnImport"
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
# SWCompression dependency for ZIP/TAR/GZIP/Brotli extraction support
|
|
27
|
+
# Native Compression framework is used for GZIP and Brotli decompression
|
|
28
|
+
s.dependency "SWCompression", "~> 4.8.0"
|
|
26
29
|
|
|
27
30
|
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
28
31
|
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
@@ -45,5 +48,5 @@ Pod::Spec.new do |s|
|
|
|
45
48
|
s.dependency "RCTTypeSafety"
|
|
46
49
|
s.dependency "ReactCommon/turbomodule/core"
|
|
47
50
|
end
|
|
48
|
-
end
|
|
51
|
+
end
|
|
49
52
|
end
|
package/android/build.gradle
CHANGED
|
@@ -130,6 +130,9 @@ dependencies {
|
|
|
130
130
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
|
|
131
131
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
|
|
132
132
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
133
|
+
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
134
|
+
implementation "org.brotli:dec:0.1.2"
|
|
135
|
+
implementation "org.apache.commons:commons-compress:1.24.0"
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
if (isNewArchitectureEnabled()) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
|
+
import android.os.StatFs
|
|
3
4
|
import android.util.Log
|
|
4
5
|
import kotlinx.coroutines.Dispatchers
|
|
5
6
|
import kotlinx.coroutines.withContext
|
|
@@ -39,12 +40,14 @@ interface BundleStorageService {
|
|
|
39
40
|
* Updates the bundle from the specified URL
|
|
40
41
|
* @param bundleId ID of the bundle to update
|
|
41
42
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
43
|
+
* @param fileHash SHA256 hash of the bundle file for verification (nullable)
|
|
42
44
|
* @param progressCallback Callback for download progress updates
|
|
43
45
|
* @return true if the update was successful
|
|
44
46
|
*/
|
|
45
47
|
suspend fun updateBundle(
|
|
46
48
|
bundleId: String,
|
|
47
49
|
fileUrl: String?,
|
|
50
|
+
fileHash: String?,
|
|
48
51
|
progressCallback: (Double) -> Unit,
|
|
49
52
|
): Boolean
|
|
50
53
|
}
|
|
@@ -55,7 +58,7 @@ interface BundleStorageService {
|
|
|
55
58
|
class BundleFileStorageService(
|
|
56
59
|
private val fileSystem: FileSystemService,
|
|
57
60
|
private val downloadService: DownloadService,
|
|
58
|
-
private val
|
|
61
|
+
private val decompressService: DecompressService,
|
|
59
62
|
private val preferences: PreferencesService,
|
|
60
63
|
) : BundleStorageService {
|
|
61
64
|
override fun setBundleURL(localPath: String?): Boolean {
|
|
@@ -84,9 +87,10 @@ class BundleFileStorageService(
|
|
|
84
87
|
override suspend fun updateBundle(
|
|
85
88
|
bundleId: String,
|
|
86
89
|
fileUrl: String?,
|
|
90
|
+
fileHash: String?,
|
|
87
91
|
progressCallback: (Double) -> Unit,
|
|
88
92
|
): Boolean {
|
|
89
|
-
Log.d("BundleStorage", "updateBundle bundleId $bundleId fileUrl $fileUrl")
|
|
93
|
+
Log.d("BundleStorage", "updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash")
|
|
90
94
|
|
|
91
95
|
// If no URL is provided, reset to fallback
|
|
92
96
|
if (fileUrl.isNullOrEmpty()) {
|
|
@@ -131,18 +135,46 @@ class BundleFileStorageService(
|
|
|
131
135
|
}
|
|
132
136
|
tempDir.mkdirs()
|
|
133
137
|
|
|
134
|
-
val tempZipFile = File(tempDir, "bundle.zip")
|
|
135
|
-
|
|
136
138
|
return withContext(Dispatchers.IO) {
|
|
137
139
|
val downloadUrl = URL(fileUrl)
|
|
138
140
|
|
|
139
|
-
//
|
|
141
|
+
// Determine bundle filename from URL
|
|
142
|
+
val bundleFileName =
|
|
143
|
+
if (downloadUrl.path.isNotEmpty()) {
|
|
144
|
+
File(downloadUrl.path).name.ifEmpty { "bundle.zip" }
|
|
145
|
+
} else {
|
|
146
|
+
"bundle.zip"
|
|
147
|
+
}
|
|
148
|
+
val tempBundleFile = File(tempDir, bundleFileName)
|
|
149
|
+
|
|
150
|
+
// Check file size before downloading
|
|
151
|
+
val fileSize = downloadService.getFileSize(downloadUrl)
|
|
152
|
+
if (fileSize > 0 && baseDir != null) {
|
|
153
|
+
// Check available disk space
|
|
154
|
+
val stat = StatFs(baseDir.absolutePath)
|
|
155
|
+
val availableBytes = stat.availableBlocksLong * stat.blockSizeLong
|
|
156
|
+
val requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
157
|
+
|
|
158
|
+
Log.d("BundleStorage", "File size: $fileSize bytes, Available: $availableBytes bytes, Required: $requiredSpace bytes")
|
|
159
|
+
|
|
160
|
+
if (availableBytes < requiredSpace) {
|
|
161
|
+
val errorMsg = "Insufficient disk space: need $requiredSpace bytes, available $availableBytes bytes"
|
|
162
|
+
Log.d("BundleStorage", errorMsg)
|
|
163
|
+
return@withContext false
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
Log.d("BundleStorage", "Unable to determine file size, proceeding with download")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Download the file (0% - 80%)
|
|
140
170
|
val downloadResult =
|
|
141
171
|
downloadService.downloadFile(
|
|
142
172
|
downloadUrl,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
tempBundleFile,
|
|
174
|
+
) { downloadProgress ->
|
|
175
|
+
// Map download progress to 0.0 - 0.8
|
|
176
|
+
progressCallback(downloadProgress * 0.8)
|
|
177
|
+
}
|
|
146
178
|
|
|
147
179
|
when (downloadResult) {
|
|
148
180
|
is DownloadResult.Error -> {
|
|
@@ -151,23 +183,43 @@ class BundleFileStorageService(
|
|
|
151
183
|
return@withContext false
|
|
152
184
|
}
|
|
153
185
|
is DownloadResult.Success -> {
|
|
154
|
-
|
|
186
|
+
Log.d("BundleStorage", "Download successful")
|
|
187
|
+
// 1) Verify file hash if provided
|
|
188
|
+
if (!fileHash.isNullOrEmpty()) {
|
|
189
|
+
Log.d("BundleStorage", "Verifying file hash...")
|
|
190
|
+
if (!HashUtils.verifyHash(tempBundleFile, fileHash)) {
|
|
191
|
+
Log.d("BundleStorage", "Hash mismatch! Deleting and aborting.")
|
|
192
|
+
tempDir.deleteRecursively()
|
|
193
|
+
tempBundleFile.delete()
|
|
194
|
+
return@withContext false
|
|
195
|
+
}
|
|
196
|
+
Log.d("BundleStorage", "Hash verification passed")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 2) Create a .tmp directory under bundle-store (to avoid colliding with an existing bundleId folder)
|
|
155
200
|
val tmpDir = File(bundleStoreDir, "$bundleId.tmp")
|
|
156
201
|
if (tmpDir.exists()) {
|
|
157
202
|
tmpDir.deleteRecursively()
|
|
158
203
|
}
|
|
159
204
|
tmpDir.mkdirs()
|
|
160
205
|
|
|
161
|
-
//
|
|
162
|
-
Log.d("BundleStorage", "
|
|
163
|
-
if (!
|
|
164
|
-
|
|
206
|
+
// 3) Extract archive into tmpDir (80% - 100%)
|
|
207
|
+
Log.d("BundleStorage", "Extracting $tempBundleFile → $tmpDir")
|
|
208
|
+
if (!decompressService.extractZipFile(
|
|
209
|
+
tempBundleFile.absolutePath,
|
|
210
|
+
tmpDir.absolutePath,
|
|
211
|
+
) { unzipProgress ->
|
|
212
|
+
// Map unzip progress (0.0 - 1.0) to overall progress (0.8 - 1.0)
|
|
213
|
+
progressCallback(0.8 + (unzipProgress * 0.2))
|
|
214
|
+
}
|
|
215
|
+
) {
|
|
216
|
+
Log.d("BundleStorage", "Failed to extract archive into tmpDir.")
|
|
165
217
|
tempDir.deleteRecursively()
|
|
166
218
|
tmpDir.deleteRecursively()
|
|
167
219
|
return@withContext false
|
|
168
220
|
}
|
|
169
221
|
|
|
170
|
-
//
|
|
222
|
+
// 4) Find index.android.bundle inside tmpDir
|
|
171
223
|
val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
|
|
172
224
|
if (extractedIndex == null) {
|
|
173
225
|
Log.d("BundleStorage", "index.android.bundle not found in tmpDir.")
|
|
@@ -176,12 +228,16 @@ class BundleFileStorageService(
|
|
|
176
228
|
return@withContext false
|
|
177
229
|
}
|
|
178
230
|
|
|
179
|
-
//
|
|
231
|
+
// 5) Log extracted bundle file size
|
|
232
|
+
val bundleSize = extractedIndex.length()
|
|
233
|
+
Log.d("BundleStorage", "Extracted bundle size: $bundleSize bytes")
|
|
234
|
+
|
|
235
|
+
// 6) If the realDir (bundle-store/<bundleId>) exists, delete it
|
|
180
236
|
if (finalBundleDir.exists()) {
|
|
181
237
|
finalBundleDir.deleteRecursively()
|
|
182
238
|
}
|
|
183
239
|
|
|
184
|
-
//
|
|
240
|
+
// 7) Attempt to rename tmpDir → finalBundleDir (atomic within the same parent folder)
|
|
185
241
|
val renamed = tmpDir.renameTo(finalBundleDir)
|
|
186
242
|
if (!renamed) {
|
|
187
243
|
// If rename fails, use moveItem or copyItem
|
|
@@ -191,7 +247,7 @@ class BundleFileStorageService(
|
|
|
191
247
|
}
|
|
192
248
|
}
|
|
193
249
|
|
|
194
|
-
//
|
|
250
|
+
// 8) Verify index.android.bundle exists inside finalBundleDir
|
|
195
251
|
val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
196
252
|
if (finalIndexFile == null) {
|
|
197
253
|
Log.d("BundleStorage", "index.android.bundle not found in realDir.")
|
|
@@ -200,21 +256,22 @@ class BundleFileStorageService(
|
|
|
200
256
|
return@withContext false
|
|
201
257
|
}
|
|
202
258
|
|
|
203
|
-
//
|
|
259
|
+
// 9) Update finalBundleDir's last modified time
|
|
204
260
|
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
205
261
|
|
|
206
|
-
//
|
|
262
|
+
// 10) Save the new bundle path in Preferences
|
|
207
263
|
val bundlePath = finalIndexFile.absolutePath
|
|
208
264
|
Log.d("BundleStorage", "Setting bundle URL: $bundlePath")
|
|
209
265
|
setBundleURL(bundlePath)
|
|
210
266
|
|
|
211
|
-
//
|
|
267
|
+
// 11) Clean up temporary and download folders
|
|
212
268
|
tempDir.deleteRecursively()
|
|
213
269
|
|
|
214
|
-
//
|
|
270
|
+
// 12) Remove old bundles
|
|
215
271
|
cleanupOldBundles(bundleStoreDir, currentBundleId, bundleId)
|
|
216
272
|
|
|
217
273
|
Log.d("BundleStorage", "Downloaded and activated bundle successfully.")
|
|
274
|
+
// Progress already at 1.0 from unzip completion
|
|
218
275
|
return@withContext true
|
|
219
276
|
}
|
|
220
277
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import java.io.File
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unified decompression service that uses Strategy pattern to handle multiple compression formats.
|
|
8
|
+
* Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
|
|
9
|
+
*/
|
|
10
|
+
class DecompressService {
|
|
11
|
+
companion object {
|
|
12
|
+
private const val TAG = "DecompressService"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Array of available strategies in order of detection priority
|
|
16
|
+
// Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
|
|
17
|
+
private val strategies =
|
|
18
|
+
listOf(
|
|
19
|
+
ZipDecompressionStrategy(),
|
|
20
|
+
TarGzDecompressionStrategy(),
|
|
21
|
+
TarBrDecompressionStrategy(),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extracts a compressed file to the destination directory.
|
|
26
|
+
* Automatically detects compression format by trying each strategy's validation.
|
|
27
|
+
* @param filePath Path to the compressed file
|
|
28
|
+
* @param destinationPath Path to the destination directory
|
|
29
|
+
* @param progressCallback Callback for progress updates (0.0 - 1.0)
|
|
30
|
+
* @return true if extraction was successful, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
fun extractZipFile(
|
|
33
|
+
filePath: String,
|
|
34
|
+
destinationPath: String,
|
|
35
|
+
progressCallback: (Double) -> Unit,
|
|
36
|
+
): Boolean {
|
|
37
|
+
// Collect file information for better error messages
|
|
38
|
+
val file = File(filePath)
|
|
39
|
+
val fileName = file.name
|
|
40
|
+
val fileSize = if (file.exists()) file.length() else 0L
|
|
41
|
+
|
|
42
|
+
// Try each strategy's validation
|
|
43
|
+
for (strategy in strategies) {
|
|
44
|
+
if (strategy.isValid(filePath)) {
|
|
45
|
+
Log.d(TAG, "Using strategy for $fileName")
|
|
46
|
+
return strategy.decompress(filePath, destinationPath, progressCallback)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// No valid strategy found - provide detailed error message
|
|
51
|
+
val errorMessage =
|
|
52
|
+
"""
|
|
53
|
+
Failed to decompress file: $fileName ($fileSize bytes)
|
|
54
|
+
|
|
55
|
+
Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
|
|
56
|
+
|
|
57
|
+
Supported formats:
|
|
58
|
+
- ZIP archives (.zip)
|
|
59
|
+
- GZIP compressed TAR archives (.tar.gz)
|
|
60
|
+
- Brotli compressed TAR archives (.tar.br)
|
|
61
|
+
|
|
62
|
+
Please verify the file is not corrupted and matches one of the supported formats.
|
|
63
|
+
""".trimIndent()
|
|
64
|
+
|
|
65
|
+
Log.e(TAG, errorMessage)
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates if a file is a valid compressed archive.
|
|
71
|
+
* @param filePath Path to the file to validate
|
|
72
|
+
* @return true if the file is a valid compressed archive
|
|
73
|
+
*/
|
|
74
|
+
fun isValidZipFile(filePath: String): Boolean {
|
|
75
|
+
for (strategy in strategies) {
|
|
76
|
+
if (strategy.isValid(filePath)) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
Log.d(TAG, "No valid strategy found for file: $filePath")
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface for decompression strategies
|
|
5
|
+
*/
|
|
6
|
+
interface DecompressionStrategy {
|
|
7
|
+
/**
|
|
8
|
+
* Validates if a file can be decompressed by this strategy
|
|
9
|
+
* @param filePath Path to the file to validate
|
|
10
|
+
* @return true if the file is valid for this strategy
|
|
11
|
+
*/
|
|
12
|
+
fun isValid(filePath: String): Boolean
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Decompresses a file to the destination directory
|
|
16
|
+
* @param filePath Path to the compressed file
|
|
17
|
+
* @param destinationPath Path to the destination directory
|
|
18
|
+
* @param progressCallback Callback for progress updates (0.0 - 1.0)
|
|
19
|
+
* @return true if decompression was successful, false otherwise
|
|
20
|
+
*/
|
|
21
|
+
fun decompress(
|
|
22
|
+
filePath: String,
|
|
23
|
+
destinationPath: String,
|
|
24
|
+
progressCallback: (Double) -> Unit,
|
|
25
|
+
): Boolean
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import java.io.File
|
|
5
|
+
import java.security.MessageDigest
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utility class for file hash operations
|
|
9
|
+
*/
|
|
10
|
+
object HashUtils {
|
|
11
|
+
/**
|
|
12
|
+
* Calculates SHA256 hash of a file
|
|
13
|
+
* @param file The file to hash
|
|
14
|
+
* @return Hex string of the hash (lowercase)
|
|
15
|
+
*/
|
|
16
|
+
fun calculateSHA256(file: File): String {
|
|
17
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
18
|
+
file.inputStream().use { input ->
|
|
19
|
+
val buffer = ByteArray(8192)
|
|
20
|
+
var bytesRead: Int
|
|
21
|
+
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
22
|
+
digest.update(buffer, 0, bytesRead)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return digest.digest().joinToString("") { "%02x".format(it) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Verifies file hash
|
|
30
|
+
* @param file File to verify
|
|
31
|
+
* @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
|
|
32
|
+
* @return true if hash matches
|
|
33
|
+
*/
|
|
34
|
+
fun verifyHash(
|
|
35
|
+
file: File,
|
|
36
|
+
expectedHash: String,
|
|
37
|
+
): Boolean {
|
|
38
|
+
val actualHash = calculateSHA256(file)
|
|
39
|
+
val matches = actualHash.equals(expectedHash, ignoreCase = true)
|
|
40
|
+
|
|
41
|
+
if (!matches) {
|
|
42
|
+
Log.d("HashUtils", "Hash mismatch - Expected: $expectedHash, Actual: $actualHash")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return matches
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -49,6 +49,7 @@ class HotUpdater {
|
|
|
49
49
|
* @param context Application context
|
|
50
50
|
* @param bundleId ID of the bundle to update
|
|
51
51
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
52
|
+
* @param fileHash SHA256 hash of the bundle file for verification (nullable)
|
|
52
53
|
* @param progressCallback Callback for download progress updates
|
|
53
54
|
* @return true if the update was successful
|
|
54
55
|
*/
|
|
@@ -56,11 +57,13 @@ class HotUpdater {
|
|
|
56
57
|
context: Context,
|
|
57
58
|
bundleId: String,
|
|
58
59
|
fileUrl: String?,
|
|
60
|
+
fileHash: String?,
|
|
59
61
|
progressCallback: (Double) -> Unit,
|
|
60
62
|
): Boolean =
|
|
61
63
|
HotUpdaterFactory.getInstance(context).updateBundle(
|
|
62
64
|
bundleId,
|
|
63
65
|
fileUrl,
|
|
66
|
+
fileHash,
|
|
64
67
|
progressCallback,
|
|
65
68
|
)
|
|
66
69
|
|
|
@@ -33,15 +33,15 @@ object HotUpdaterFactory {
|
|
|
33
33
|
// Create services
|
|
34
34
|
val fileSystem = FileManagerService(appContext)
|
|
35
35
|
val preferences = VersionedPreferencesService(appContext, isolationKey)
|
|
36
|
-
val downloadService =
|
|
37
|
-
val
|
|
36
|
+
val downloadService = OkHttpDownloadService()
|
|
37
|
+
val decompressService = DecompressService()
|
|
38
38
|
|
|
39
39
|
// Create bundle storage with dependencies
|
|
40
40
|
val bundleStorage =
|
|
41
41
|
BundleFileStorageService(
|
|
42
42
|
fileSystem,
|
|
43
43
|
downloadService,
|
|
44
|
-
|
|
44
|
+
decompressService,
|
|
45
45
|
preferences,
|
|
46
46
|
)
|
|
47
47
|
|
|
@@ -185,14 +185,16 @@ class HotUpdaterImpl(
|
|
|
185
185
|
* Updates the bundle from the specified URL
|
|
186
186
|
* @param bundleId ID of the bundle to update
|
|
187
187
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
188
|
+
* @param fileHash SHA256 hash of the bundle file for verification (nullable)
|
|
188
189
|
* @param progressCallback Callback for download progress updates
|
|
189
190
|
* @return true if the update was successful
|
|
190
191
|
*/
|
|
191
192
|
suspend fun updateBundle(
|
|
192
193
|
bundleId: String,
|
|
193
194
|
fileUrl: String?,
|
|
195
|
+
fileHash: String?,
|
|
194
196
|
progressCallback: (Double) -> Unit,
|
|
195
|
-
): Boolean = bundleStorage.updateBundle(bundleId, fileUrl, progressCallback)
|
|
197
|
+
): Boolean = bundleStorage.updateBundle(bundleId, fileUrl, fileHash, progressCallback)
|
|
196
198
|
|
|
197
199
|
/**
|
|
198
200
|
* Reloads the React Native application
|