@hot-updater/react-native 0.24.7 → 0.25.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/LICENSE +12 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +144 -20
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +27 -2
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +17 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +11 -38
- package/android/src/newarch/HotUpdaterModule.kt +5 -0
- package/android/src/oldarch/HotUpdaterModule.kt +6 -0
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +168 -34
- package/ios/HotUpdater/Internal/BundleMetadata.swift +17 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +14 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +20 -3
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +24 -40
- package/lib/commonjs/global.d.js +6 -0
- package/lib/commonjs/global.d.js.map +1 -0
- package/lib/commonjs/index.js +25 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +17 -1
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/wrap.js +1 -2
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/global.d.js +8 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/index.js +26 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +15 -0
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +8 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +8 -0
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/global.d.ts +23 -0
- package/src/index.ts +26 -0
- package/src/native.ts +15 -0
- package/src/specs/NativeHotUpdater.ts +9 -0
package/LICENSE
CHANGED
|
@@ -19,3 +19,15 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
ADDITIONAL DISCLAIMER
|
|
26
|
+
|
|
27
|
+
The authors of this software shall not be held responsible for any legal issues,
|
|
28
|
+
regulatory violations, policy violations, or disputes arising from the use,
|
|
29
|
+
distribution, or integration of this software.
|
|
30
|
+
|
|
31
|
+
Users are solely responsible for ensuring compliance with all applicable laws,
|
|
32
|
+
regulations, and third-party policies (including but not limited to app store
|
|
33
|
+
policies).
|
|
@@ -70,6 +70,12 @@ interface BundleStorageService {
|
|
|
70
70
|
* @return true if clearing was successful
|
|
71
71
|
*/
|
|
72
72
|
fun clearCrashHistory(): Boolean
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the base URL for the current active bundle directory
|
|
76
|
+
* @return Base URL string (e.g., "file:///data/.../bundle-store/abc123") or empty string
|
|
77
|
+
*/
|
|
78
|
+
fun getBaseURL(): String
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
/**
|
|
@@ -81,11 +87,20 @@ class BundleFileStorageService(
|
|
|
81
87
|
private val downloadService: DownloadService,
|
|
82
88
|
private val decompressService: DecompressService,
|
|
83
89
|
private val preferences: PreferencesService,
|
|
90
|
+
private val isolationKey: String,
|
|
84
91
|
) : BundleStorageService {
|
|
85
92
|
companion object {
|
|
86
93
|
private const val TAG = "BundleStorage"
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
init {
|
|
97
|
+
// Ensure bundle store directory exists
|
|
98
|
+
getBundleStoreDir().mkdirs()
|
|
99
|
+
|
|
100
|
+
// Clean up old bundles if isolationKey format changed
|
|
101
|
+
checkAndCleanupIfIsolationKeyChanged()
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
// Session-only rollback tracking (in-memory)
|
|
90
105
|
private var sessionRollbackBundleId: String? = null
|
|
91
106
|
|
|
@@ -102,9 +117,12 @@ class BundleFileStorageService(
|
|
|
102
117
|
|
|
103
118
|
// MARK: - Metadata Operations
|
|
104
119
|
|
|
105
|
-
private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile())
|
|
120
|
+
private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile(), isolationKey)
|
|
106
121
|
|
|
107
|
-
private fun saveMetadata(metadata: BundleMetadata): Boolean
|
|
122
|
+
private fun saveMetadata(metadata: BundleMetadata): Boolean {
|
|
123
|
+
val updatedMetadata = metadata.copy(isolationKey = isolationKey)
|
|
124
|
+
return updatedMetadata.saveToFile(getMetadataFile())
|
|
125
|
+
}
|
|
108
126
|
|
|
109
127
|
private fun createInitialMetadata(): BundleMetadata {
|
|
110
128
|
val currentBundleId = extractBundleIdFromCurrentURL()
|
|
@@ -124,6 +142,67 @@ class BundleFileStorageService(
|
|
|
124
142
|
return regex.find(currentUrl)?.groupValues?.get(1)
|
|
125
143
|
}
|
|
126
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Checks if isolationKey has changed and cleans up old bundles if needed.
|
|
147
|
+
* This handles migration when isolationKey format changes.
|
|
148
|
+
*/
|
|
149
|
+
private fun checkAndCleanupIfIsolationKeyChanged() {
|
|
150
|
+
val metadataFile = getMetadataFile()
|
|
151
|
+
|
|
152
|
+
if (!metadataFile.exists()) {
|
|
153
|
+
// First launch - no cleanup needed
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Read metadata without validation to get stored isolationKey
|
|
159
|
+
val jsonString = metadataFile.readText()
|
|
160
|
+
val json = org.json.JSONObject(jsonString)
|
|
161
|
+
val storedIsolationKey = json.optString("isolationKey", null)
|
|
162
|
+
|
|
163
|
+
if (storedIsolationKey != null && storedIsolationKey != isolationKey) {
|
|
164
|
+
// isolationKey changed - migration needed
|
|
165
|
+
Log.d(TAG, "isolationKey changed: $storedIsolationKey -> $isolationKey")
|
|
166
|
+
Log.d(TAG, "Cleaning up old bundles for migration")
|
|
167
|
+
cleanupAllBundlesForMigration()
|
|
168
|
+
}
|
|
169
|
+
} catch (e: Exception) {
|
|
170
|
+
Log.e(TAG, "Error checking isolationKey: ${e.message}")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Removes all bundle directories during migration.
|
|
176
|
+
* Called when isolationKey format changes.
|
|
177
|
+
*/
|
|
178
|
+
private fun cleanupAllBundlesForMigration() {
|
|
179
|
+
val bundleStoreDir = getBundleStoreDir()
|
|
180
|
+
|
|
181
|
+
if (!bundleStoreDir.exists()) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
var cleanedCount = 0
|
|
187
|
+
bundleStoreDir.listFiles()?.forEach { file ->
|
|
188
|
+
if (file.isDirectory) {
|
|
189
|
+
try {
|
|
190
|
+
if (file.deleteRecursively()) {
|
|
191
|
+
cleanedCount++
|
|
192
|
+
Log.d(TAG, "Migration: removed old bundle ${file.name}")
|
|
193
|
+
}
|
|
194
|
+
} catch (e: Exception) {
|
|
195
|
+
Log.e(TAG, "Error removing bundle ${file.name}: ${e.message}")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Log.d(TAG, "Migration cleanup complete: removed $cleanedCount bundles")
|
|
201
|
+
} catch (e: Exception) {
|
|
202
|
+
Log.e(TAG, "Error during migration cleanup: ${e.message}")
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
127
206
|
// MARK: - State Machine
|
|
128
207
|
|
|
129
208
|
private fun isVerificationPending(metadata: BundleMetadata): Boolean = metadata.verificationPending && metadata.stagingBundleId != null
|
|
@@ -483,34 +562,48 @@ class BundleFileStorageService(
|
|
|
483
562
|
}
|
|
484
563
|
val tempBundleFile = File(tempDir, bundleFileName)
|
|
485
564
|
|
|
486
|
-
// Check file size before downloading
|
|
487
|
-
val fileSize = downloadService.getFileSize(downloadUrl)
|
|
488
|
-
if (fileSize > 0 && baseDir != null) {
|
|
489
|
-
// Check available disk space
|
|
490
|
-
val stat = StatFs(baseDir.absolutePath)
|
|
491
|
-
val availableBytes = stat.availableBlocksLong * stat.blockSizeLong
|
|
492
|
-
val requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
493
|
-
|
|
494
|
-
Log.d("BundleStorage", "File size: $fileSize bytes, Available: $availableBytes bytes, Required: $requiredSpace bytes")
|
|
495
|
-
|
|
496
|
-
if (availableBytes < requiredSpace) {
|
|
497
|
-
Log.d("BundleStorage", "Insufficient disk space: need $requiredSpace bytes, available $availableBytes bytes")
|
|
498
|
-
throw HotUpdaterException.insufficientDiskSpace(requiredSpace, availableBytes)
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
Log.d("BundleStorage", "Unable to determine file size, proceeding with download")
|
|
502
|
-
}
|
|
503
|
-
|
|
504
565
|
// Download the file (0% - 80%)
|
|
566
|
+
// Disk space check will be performed in fileSizeCallback
|
|
567
|
+
var diskSpaceError: HotUpdaterException? = null
|
|
568
|
+
|
|
505
569
|
val downloadResult =
|
|
506
570
|
downloadService.downloadFile(
|
|
507
571
|
downloadUrl,
|
|
508
572
|
tempBundleFile,
|
|
573
|
+
fileSizeCallback = { fileSize ->
|
|
574
|
+
// Perform disk space check when file size is known
|
|
575
|
+
if (baseDir != null) {
|
|
576
|
+
val stat = StatFs(baseDir.absolutePath)
|
|
577
|
+
val availableBytes = stat.availableBlocksLong * stat.blockSizeLong
|
|
578
|
+
val requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
579
|
+
|
|
580
|
+
Log.d(
|
|
581
|
+
"BundleStorage",
|
|
582
|
+
"File size: $fileSize bytes, Available: $availableBytes bytes, Required: $requiredSpace bytes",
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if (availableBytes < requiredSpace) {
|
|
586
|
+
Log.d(
|
|
587
|
+
TAG,
|
|
588
|
+
"Insufficient disk space detected: need $requiredSpace bytes, available $availableBytes bytes",
|
|
589
|
+
)
|
|
590
|
+
// Store error to be thrown after download completes/cancels
|
|
591
|
+
diskSpaceError = HotUpdaterException.insufficientDiskSpace(requiredSpace, availableBytes)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
},
|
|
509
595
|
) { downloadProgress ->
|
|
510
596
|
// Map download progress to 0.0 - 0.8
|
|
511
597
|
progressCallback(downloadProgress * 0.8)
|
|
512
598
|
}
|
|
513
599
|
|
|
600
|
+
// Check for disk space error first before processing download result
|
|
601
|
+
diskSpaceError?.let {
|
|
602
|
+
Log.d(TAG, "Throwing disk space error")
|
|
603
|
+
tempDir.deleteRecursively()
|
|
604
|
+
throw it
|
|
605
|
+
}
|
|
606
|
+
|
|
514
607
|
when (downloadResult) {
|
|
515
608
|
is DownloadResult.Error -> {
|
|
516
609
|
Log.d("BundleStorage", "Download failed: ${downloadResult.exception.message}")
|
|
@@ -705,4 +798,35 @@ class BundleFileStorageService(
|
|
|
705
798
|
Log.e(TAG, "Error during cleanup: ${e.message}")
|
|
706
799
|
}
|
|
707
800
|
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Gets the base URL for the current active bundle directory.
|
|
804
|
+
* Returns the file:// URL to the bundle directory without trailing slash.
|
|
805
|
+
* This is used for Expo DOM components to construct full asset paths.
|
|
806
|
+
* @return Base URL string (e.g., "file:///data/.../bundle-store/abc123") or empty string
|
|
807
|
+
*/
|
|
808
|
+
override fun getBaseURL(): String {
|
|
809
|
+
return try {
|
|
810
|
+
val metadata = loadMetadataOrNull()
|
|
811
|
+
val activeBundleId =
|
|
812
|
+
when {
|
|
813
|
+
metadata?.verificationPending == true && metadata.stagingBundleId != null ->
|
|
814
|
+
metadata.stagingBundleId
|
|
815
|
+
metadata?.stableBundleId != null -> metadata.stableBundleId
|
|
816
|
+
else -> extractBundleIdFromCurrentURL()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (activeBundleId != null) {
|
|
820
|
+
val bundleDir = File(getBundleStoreDir(), activeBundleId)
|
|
821
|
+
if (bundleDir.exists()) {
|
|
822
|
+
return "file://${bundleDir.absolutePath}"
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
""
|
|
827
|
+
} catch (e: Exception) {
|
|
828
|
+
Log.e(TAG, "Error getting base URL: ${e.message}")
|
|
829
|
+
""
|
|
830
|
+
}
|
|
831
|
+
}
|
|
708
832
|
}
|
|
@@ -10,6 +10,7 @@ import java.io.File
|
|
|
10
10
|
*/
|
|
11
11
|
data class BundleMetadata(
|
|
12
12
|
val schema: String = SCHEMA_VERSION,
|
|
13
|
+
val isolationKey: String? = null,
|
|
13
14
|
val stableBundleId: String? = null,
|
|
14
15
|
val stagingBundleId: String? = null,
|
|
15
16
|
val verificationPending: Boolean = false,
|
|
@@ -25,6 +26,12 @@ data class BundleMetadata(
|
|
|
25
26
|
fun fromJson(json: JSONObject): BundleMetadata =
|
|
26
27
|
BundleMetadata(
|
|
27
28
|
schema = json.optString("schema", SCHEMA_VERSION),
|
|
29
|
+
isolationKey =
|
|
30
|
+
if (json.has("isolationKey") && !json.isNull("isolationKey")) {
|
|
31
|
+
json.getString("isolationKey").takeIf { it.isNotEmpty() }
|
|
32
|
+
} else {
|
|
33
|
+
null
|
|
34
|
+
},
|
|
28
35
|
stableBundleId =
|
|
29
36
|
if (json.has("stableBundleId") && !json.isNull("stableBundleId")) {
|
|
30
37
|
json.getString("stableBundleId").takeIf { it.isNotEmpty() }
|
|
@@ -53,7 +60,10 @@ data class BundleMetadata(
|
|
|
53
60
|
updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
|
|
54
61
|
)
|
|
55
62
|
|
|
56
|
-
fun loadFromFile(
|
|
63
|
+
fun loadFromFile(
|
|
64
|
+
file: File,
|
|
65
|
+
expectedIsolationKey: String,
|
|
66
|
+
): BundleMetadata? {
|
|
57
67
|
return try {
|
|
58
68
|
if (!file.exists()) {
|
|
59
69
|
Log.d(TAG, "Metadata file does not exist: ${file.absolutePath}")
|
|
@@ -61,7 +71,21 @@ data class BundleMetadata(
|
|
|
61
71
|
}
|
|
62
72
|
val jsonString = file.readText()
|
|
63
73
|
val json = JSONObject(jsonString)
|
|
64
|
-
fromJson(json)
|
|
74
|
+
val metadata = fromJson(json)
|
|
75
|
+
|
|
76
|
+
// Validate isolation key
|
|
77
|
+
val metadataKey = metadata.isolationKey
|
|
78
|
+
if (metadataKey != null) {
|
|
79
|
+
if (metadataKey != expectedIsolationKey) {
|
|
80
|
+
Log.d(TAG, "Isolation key mismatch: expected=$expectedIsolationKey, got=$metadataKey")
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
Log.d(TAG, "Missing isolation key in metadata, treating as invalid")
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
metadata
|
|
65
89
|
} catch (e: Exception) {
|
|
66
90
|
Log.e(TAG, "Failed to load metadata from file", e)
|
|
67
91
|
null
|
|
@@ -72,6 +96,7 @@ data class BundleMetadata(
|
|
|
72
96
|
fun toJson(): JSONObject =
|
|
73
97
|
JSONObject().apply {
|
|
74
98
|
put("schema", schema)
|
|
99
|
+
put("isolationKey", isolationKey ?: JSONObject.NULL)
|
|
75
100
|
put("stableBundleId", stableBundleId ?: JSONObject.NULL)
|
|
76
101
|
put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
|
|
77
102
|
put("verificationPending", verificationPending)
|
|
@@ -72,6 +72,7 @@ class HotUpdaterImpl {
|
|
|
72
72
|
val preferences = createPreferences(appContext)
|
|
73
73
|
val downloadService = OkHttpDownloadService()
|
|
74
74
|
val decompressService = DecompressService()
|
|
75
|
+
val isolationKey = getIsolationKey(appContext)
|
|
75
76
|
|
|
76
77
|
return BundleFileStorageService(
|
|
77
78
|
appContext,
|
|
@@ -79,6 +80,7 @@ class HotUpdaterImpl {
|
|
|
79
80
|
downloadService,
|
|
80
81
|
decompressService,
|
|
81
82
|
preferences,
|
|
83
|
+
isolationKey,
|
|
82
84
|
)
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -110,8 +112,13 @@ class HotUpdaterImpl {
|
|
|
110
112
|
val appVersion = getAppVersion(context) ?: "unknown"
|
|
111
113
|
val appChannel = getChannel(context)
|
|
112
114
|
|
|
113
|
-
//
|
|
114
|
-
val baseKey =
|
|
115
|
+
// Include both fingerprint hash and app version for complete isolation
|
|
116
|
+
val baseKey =
|
|
117
|
+
if (!fingerprintHash.isNullOrEmpty()) {
|
|
118
|
+
"${fingerprintHash}_$appVersion"
|
|
119
|
+
} else {
|
|
120
|
+
appVersion
|
|
121
|
+
}
|
|
115
122
|
|
|
116
123
|
return "HotUpdaterPrefs_${baseKey}_$appChannel"
|
|
117
124
|
}
|
|
@@ -290,4 +297,12 @@ class HotUpdaterImpl {
|
|
|
290
297
|
* @return true if clearing was successful
|
|
291
298
|
*/
|
|
292
299
|
fun clearCrashHistory(): Boolean = bundleStorage.clearCrashHistory()
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Gets the base URL for the current active bundle directory.
|
|
303
|
+
* Returns the file:// URL to the bundle directory with trailing slash.
|
|
304
|
+
* This is used for Expo DOM components to construct full asset paths.
|
|
305
|
+
* @return Base URL string (e.g., "file:///data/.../bundle-store/abc123/") or empty string
|
|
306
|
+
*/
|
|
307
|
+
fun getBaseURL(): String = bundleStorage.getBaseURL()
|
|
293
308
|
}
|
|
@@ -45,23 +45,18 @@ sealed class DownloadResult {
|
|
|
45
45
|
* Interface for download operations
|
|
46
46
|
*/
|
|
47
47
|
interface DownloadService {
|
|
48
|
-
/**
|
|
49
|
-
* Gets the file size from the URL without downloading
|
|
50
|
-
* @param fileUrl The URL to check
|
|
51
|
-
* @return File size in bytes, or -1 if unavailable
|
|
52
|
-
*/
|
|
53
|
-
suspend fun getFileSize(fileUrl: URL): Long
|
|
54
|
-
|
|
55
48
|
/**
|
|
56
49
|
* Downloads a file from a URL
|
|
57
50
|
* @param fileUrl The URL to download from
|
|
58
51
|
* @param destination The local file to save to
|
|
52
|
+
* @param fileSizeCallback Optional callback called when file size is known
|
|
59
53
|
* @param progressCallback Callback for download progress updates
|
|
60
54
|
* @return Result indicating success or failure
|
|
61
55
|
*/
|
|
62
56
|
suspend fun downloadFile(
|
|
63
57
|
fileUrl: URL,
|
|
64
58
|
destination: File,
|
|
59
|
+
fileSizeCallback: ((Long) -> Unit)? = null,
|
|
65
60
|
progressCallback: (Double) -> Unit,
|
|
66
61
|
): DownloadResult
|
|
67
62
|
}
|
|
@@ -128,34 +123,10 @@ class OkHttpDownloadService : DownloadService {
|
|
|
128
123
|
.writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
|
129
124
|
.build()
|
|
130
125
|
|
|
131
|
-
override suspend fun getFileSize(fileUrl: URL): Long =
|
|
132
|
-
withContext(Dispatchers.IO) {
|
|
133
|
-
try {
|
|
134
|
-
val request =
|
|
135
|
-
Request
|
|
136
|
-
.Builder()
|
|
137
|
-
.url(fileUrl)
|
|
138
|
-
.head()
|
|
139
|
-
.build()
|
|
140
|
-
client.newCall(request).execute().use { response ->
|
|
141
|
-
if (response.isSuccessful) {
|
|
142
|
-
val contentLength = response.header("Content-Length")?.toLongOrNull() ?: -1L
|
|
143
|
-
Log.d(TAG, "File size from HEAD request: $contentLength bytes")
|
|
144
|
-
contentLength
|
|
145
|
-
} else {
|
|
146
|
-
Log.d(TAG, "HEAD request failed: ${response.code}")
|
|
147
|
-
-1L
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch (e: Exception) {
|
|
151
|
-
Log.d(TAG, "Failed to get file size: ${e.message}")
|
|
152
|
-
-1L
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
126
|
override suspend fun downloadFile(
|
|
157
127
|
fileUrl: URL,
|
|
158
128
|
destination: File,
|
|
129
|
+
fileSizeCallback: ((Long) -> Unit)?,
|
|
159
130
|
progressCallback: (Double) -> Unit,
|
|
160
131
|
): DownloadResult =
|
|
161
132
|
withContext(Dispatchers.IO) {
|
|
@@ -167,6 +138,7 @@ class OkHttpDownloadService : DownloadService {
|
|
|
167
138
|
return@withContext attemptDownload(
|
|
168
139
|
fileUrl,
|
|
169
140
|
destination,
|
|
141
|
+
fileSizeCallback,
|
|
170
142
|
progressCallback,
|
|
171
143
|
)
|
|
172
144
|
} catch (e: Exception) {
|
|
@@ -193,6 +165,7 @@ class OkHttpDownloadService : DownloadService {
|
|
|
193
165
|
private suspend fun attemptDownload(
|
|
194
166
|
fileUrl: URL,
|
|
195
167
|
destination: File,
|
|
168
|
+
fileSizeCallback: ((Long) -> Unit)?,
|
|
196
169
|
progressCallback: (Double) -> Unit,
|
|
197
170
|
): DownloadResult =
|
|
198
171
|
withContext(Dispatchers.IO) {
|
|
@@ -231,14 +204,14 @@ class OkHttpDownloadService : DownloadService {
|
|
|
231
204
|
// Get total file size
|
|
232
205
|
val totalSize = body.contentLength()
|
|
233
206
|
|
|
234
|
-
if (totalSize
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
207
|
+
if (totalSize > 0) {
|
|
208
|
+
// Notify file size to caller for disk space check
|
|
209
|
+
fileSizeCallback?.invoke(totalSize)
|
|
210
|
+
Log.d(TAG, "Starting download: $totalSize bytes")
|
|
211
|
+
} else {
|
|
212
|
+
Log.d(TAG, "Content-Length not available ($totalSize), proceeding without disk space check")
|
|
238
213
|
}
|
|
239
214
|
|
|
240
|
-
Log.d(TAG, "Starting download: $totalSize bytes")
|
|
241
|
-
|
|
242
215
|
try {
|
|
243
216
|
// Wrap response body with progress tracking
|
|
244
217
|
val progressBody =
|
|
@@ -162,6 +162,11 @@ class HotUpdaterModule internal constructor(
|
|
|
162
162
|
return impl.clearCrashHistory()
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
override fun getBaseURL(): String {
|
|
166
|
+
val impl = getInstance()
|
|
167
|
+
return impl.getBaseURL()
|
|
168
|
+
}
|
|
169
|
+
|
|
165
170
|
companion object {
|
|
166
171
|
const val NAME = "HotUpdater"
|
|
167
172
|
}
|
|
@@ -170,6 +170,12 @@ class HotUpdaterModule internal constructor(
|
|
|
170
170
|
return impl.clearCrashHistory()
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
174
|
+
override fun getBaseURL(): String {
|
|
175
|
+
val impl = getInstance()
|
|
176
|
+
return impl.getBaseURL()
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
companion object {
|
|
174
180
|
const val NAME = "HotUpdater"
|
|
175
181
|
}
|