@onekeyfe/react-native-range-downloader 3.0.39
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 +21 -0
- package/README.md +36 -0
- package/ReactNativeRangeDownloader.podspec +30 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +132 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ConcurrentRangeDownloader.kt +340 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +233 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloaderPackage.kt +24 -0
- package/ios/ReactNativeRangeDownloader.swift +732 -0
- package/lib/module/ReactNativeRangeDownloader.nitro.js +4 -0
- package/lib/module/ReactNativeRangeDownloader.nitro.js.map +1 -0
- package/lib/module/index.js +15 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +35 -0
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JDownloadChannel.hpp +62 -0
- package/nitrogen/generated/android/c++/JFunc_void_RangeDownloadEvent.hpp +80 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +117 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JRangeDownloadEvent.hpp +75 -0
- package/nitrogen/generated/android/c++/JRangeDownloadOutcome.hpp +59 -0
- package/nitrogen/generated/android/c++/JRangeDownloadParams.hpp +84 -0
- package/nitrogen/generated/android/c++/JRangeDownloadResult.hpp +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/DownloadChannel.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/Func_void_RangeDownloadEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +79 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadEvent.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadOutcome.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadParams.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadResult.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/reactnativerangedownloaderOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativerangedownloader+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativerangedownloader+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativerangedownloaderOnLoad.cpp +46 -0
- package/nitrogen/generated/android/reactnativerangedownloaderOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloader+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.cpp +65 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +246 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +62 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloaderAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloaderAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +123 -0
- package/nitrogen/generated/ios/swift/DownloadChannel.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_RangeDownloadEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_RangeDownloadResult.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +197 -0
- package/nitrogen/generated/ios/swift/RangeDownloadEvent.swift +80 -0
- package/nitrogen/generated/ios/swift/RangeDownloadOutcome.swift +40 -0
- package/nitrogen/generated/ios/swift/RangeDownloadParams.swift +145 -0
- package/nitrogen/generated/ios/swift/RangeDownloadResult.swift +77 -0
- package/nitrogen/generated/shared/c++/DownloadChannel.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +79 -0
- package/nitrogen/generated/shared/c++/RangeDownloadEvent.hpp +93 -0
- package/nitrogen/generated/shared/c++/RangeDownloadOutcome.hpp +76 -0
- package/nitrogen/generated/shared/c++/RangeDownloadParams.hpp +102 -0
- package/nitrogen/generated/shared/c++/RangeDownloadResult.hpp +86 -0
- package/package.json +169 -0
- package/src/ReactNativeRangeDownloader.nitro.ts +60 -0
- package/src/index.tsx +20 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativerangedownloader
|
|
2
|
+
|
|
3
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
4
|
+
import com.margelo.nitro.NitroModules
|
|
5
|
+
import com.margelo.nitro.core.Promise
|
|
6
|
+
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.security.MessageDigest
|
|
9
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
10
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
11
|
+
|
|
12
|
+
// P1: faithful migration of the Android concurrent multi-range downloader.
|
|
13
|
+
//
|
|
14
|
+
// The core algorithm (.partial preallocation + .progress manifest resume +
|
|
15
|
+
// 8-segment thread pool + If-Range/200 fallback) lives unchanged in the
|
|
16
|
+
// in-module ConcurrentRangeDownloader helper, copied byte-for-byte from
|
|
17
|
+
// react-native-bundle-update. This class is the Nitro adapter: it builds the
|
|
18
|
+
// HTTPS-only OkHttpClient, drives the helper, finalizes on COMPLETED (promote
|
|
19
|
+
// .partial -> dest + optional SHA256 self-check), and bridges progress to the
|
|
20
|
+
// shared listener registry as RangeDownloadEvent (tagged with channel/taskId).
|
|
21
|
+
//
|
|
22
|
+
// Android has no background-session concept: `channel` is only an event label
|
|
23
|
+
// + artifact-directory tag and does not change the download mechanism. The
|
|
24
|
+
// on-disk format (.partial + .progress) is kept exactly as shipped so existing
|
|
25
|
+
// interrupted downloads resume cleanly.
|
|
26
|
+
@DoNotStrip
|
|
27
|
+
class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
|
|
28
|
+
|
|
29
|
+
private class Listener(val id: Double, val callback: (RangeDownloadEvent) -> Unit)
|
|
30
|
+
|
|
31
|
+
private val listeners = CopyOnWriteArrayList<Listener>()
|
|
32
|
+
private val nextListenerId = AtomicLong(1)
|
|
33
|
+
|
|
34
|
+
// HTTPS-only client: reject any redirect to a non-HTTPS hop. Mirrors the
|
|
35
|
+
// existing react-native-bundle-update configuration verbatim.
|
|
36
|
+
private val httpClient = okhttp3.OkHttpClient.Builder()
|
|
37
|
+
.addNetworkInterceptor { chain ->
|
|
38
|
+
val req = chain.request()
|
|
39
|
+
if (!req.url.isHttps) {
|
|
40
|
+
throw java.io.IOException("Redirect to non-HTTPS URL is not allowed: ${req.url}")
|
|
41
|
+
}
|
|
42
|
+
chain.proceed(req)
|
|
43
|
+
}
|
|
44
|
+
.build()
|
|
45
|
+
|
|
46
|
+
override fun download(params: RangeDownloadParams): Promise<RangeDownloadResult> {
|
|
47
|
+
return Promise.async {
|
|
48
|
+
val channel = params.channel
|
|
49
|
+
val taskId = params.taskId
|
|
50
|
+
val downloadUrl = params.url
|
|
51
|
+
val destFilePath = params.destFilePath
|
|
52
|
+
val expectedSha256 = params.expectedSha256
|
|
53
|
+
|
|
54
|
+
// HTTPS-only, same gate as the source module.
|
|
55
|
+
if (!downloadUrl.startsWith("https://")) {
|
|
56
|
+
OneKeyLog.error("RangeDownloader", "download: URL is not HTTPS: $downloadUrl")
|
|
57
|
+
sendEvent(channel, taskId, type = "error", message = "Download URL must use HTTPS")
|
|
58
|
+
throw Exception("Download URL must use HTTPS")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Resume support: download to <dest>.partial, promote to <dest> on success.
|
|
62
|
+
val partialFilePath = "$destFilePath.partial"
|
|
63
|
+
val destFile = File(destFilePath)
|
|
64
|
+
|
|
65
|
+
// Optional tuning knobs, defaulting to the shipped behavior (8 segments / 2MB).
|
|
66
|
+
val segmentCount = params.segmentCount?.toInt()?.takeIf { it > 0 } ?: 8
|
|
67
|
+
val minConcurrentBytes = params.minConcurrentBytes?.toLong()?.takeIf { it > 0 }
|
|
68
|
+
?: (2L * 1024 * 1024)
|
|
69
|
+
|
|
70
|
+
sendEvent(channel, taskId, type = "start")
|
|
71
|
+
|
|
72
|
+
var lastProgress = -1
|
|
73
|
+
val outcome = ConcurrentRangeDownloader(
|
|
74
|
+
httpClient = httpClient,
|
|
75
|
+
segmentCount = segmentCount,
|
|
76
|
+
minConcurrentBytes = minConcurrentBytes,
|
|
77
|
+
log = { msg -> OneKeyLog.info("RangeDownloader", msg) },
|
|
78
|
+
).download(downloadUrl, partialFilePath) { transferred, total ->
|
|
79
|
+
if (total > 0) {
|
|
80
|
+
val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
|
|
81
|
+
if (p != lastProgress) {
|
|
82
|
+
sendEvent(channel, taskId, type = "progress", progress = p)
|
|
83
|
+
lastProgress = p
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (outcome == ConcurrentRangeDownloader.Outcome.FALLBACK) {
|
|
89
|
+
// Concurrency unusable. The helper has already cleaned up its own
|
|
90
|
+
// artifacts where appropriate; the caller runs its single-stream path.
|
|
91
|
+
OneKeyLog.info("RangeDownloader", "download: concurrent not used, returning fallback")
|
|
92
|
+
sendEvent(channel, taskId, type = "fallback", message = "concurrent unavailable")
|
|
93
|
+
return@async RangeDownloadResult(
|
|
94
|
+
outcome = RangeDownloadOutcome.FALLBACK,
|
|
95
|
+
filePath = destFilePath,
|
|
96
|
+
fallbackReason = "concurrent unavailable (range unsupported / 200 / too small / single-stream partial)",
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// COMPLETED: `.partial` is fully on disk. Promote -> final, then run the
|
|
101
|
+
// optional in-module SHA256 self-check (mirrors the source finalize path).
|
|
102
|
+
if (destFile.exists()) destFile.delete()
|
|
103
|
+
if (!File(partialFilePath).renameTo(destFile)) {
|
|
104
|
+
OneKeyLog.error("RangeDownloader", "download: rename .partial -> final failed")
|
|
105
|
+
sendEvent(channel, taskId, type = "error", message = "Failed to finalize download")
|
|
106
|
+
throw Exception("Failed to finalize download")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!expectedSha256.isNullOrEmpty()) {
|
|
110
|
+
OneKeyLog.info("RangeDownloader", "download: concurrent finished, verifying SHA256...")
|
|
111
|
+
if (!verifySHA256(destFilePath, expectedSha256)) {
|
|
112
|
+
destFile.delete()
|
|
113
|
+
OneKeyLog.error("RangeDownloader", "download: SHA256 verification failed")
|
|
114
|
+
sendEvent(channel, taskId, type = "error", message = "SHA256 verification failed")
|
|
115
|
+
throw Exception("SHA256 verification failed")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sendEvent(channel, taskId, type = "complete", progress = 100)
|
|
120
|
+
OneKeyLog.info("RangeDownloader", "download: completed channel=$channel taskId=$taskId")
|
|
121
|
+
RangeDownloadResult(
|
|
122
|
+
outcome = RangeDownloadOutcome.COMPLETED,
|
|
123
|
+
filePath = destFilePath,
|
|
124
|
+
fallbackReason = null,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun discardArtifacts(
|
|
130
|
+
channel: DownloadChannel,
|
|
131
|
+
taskId: String,
|
|
132
|
+
destFilePath: String
|
|
133
|
+
): Promise<Unit> {
|
|
134
|
+
return Promise.async {
|
|
135
|
+
// Manifest first so it never outlives the partial it describes.
|
|
136
|
+
File("$destFilePath.partial.progress").delete()
|
|
137
|
+
File("$destFilePath.partial").delete()
|
|
138
|
+
OneKeyLog.info("RangeDownloader", "discardArtifacts: channel=$channel taskId=$taskId")
|
|
139
|
+
Unit
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun addDownloadListener(callback: (event: RangeDownloadEvent) -> Unit): Double {
|
|
144
|
+
val id = nextListenerId.getAndIncrement().toDouble()
|
|
145
|
+
listeners.add(Listener(id, callback))
|
|
146
|
+
OneKeyLog.debug("RangeDownloader", "addDownloadListener: id=$id, totalListeners=${listeners.size}")
|
|
147
|
+
return id
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun removeDownloadListener(id: Double) {
|
|
151
|
+
listeners.removeAll { it.id == id }
|
|
152
|
+
OneKeyLog.debug("RangeDownloader", "removeDownloadListener: id=$id, totalListeners=${listeners.size}")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// App cache directory — an app-owned, writable absolute path resolved at
|
|
156
|
+
// runtime (no hardcoded sandbox path).
|
|
157
|
+
override fun getDownloadsDir(): String {
|
|
158
|
+
val ctx = NitroModules.applicationContext ?: return ""
|
|
159
|
+
return ctx.cacheDir.absolutePath
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Broadcast one event to every registered listener. Listeners filter by
|
|
163
|
+
// channel/taskId on their side (shared registry, per the design).
|
|
164
|
+
private fun sendEvent(
|
|
165
|
+
channel: DownloadChannel,
|
|
166
|
+
taskId: String,
|
|
167
|
+
type: String,
|
|
168
|
+
progress: Int = 0,
|
|
169
|
+
message: String = "",
|
|
170
|
+
) {
|
|
171
|
+
val event = RangeDownloadEvent(
|
|
172
|
+
channel = channel,
|
|
173
|
+
taskId = taskId,
|
|
174
|
+
type = type,
|
|
175
|
+
progress = progress.toDouble(),
|
|
176
|
+
message = message,
|
|
177
|
+
)
|
|
178
|
+
for (listener in listeners) {
|
|
179
|
+
try {
|
|
180
|
+
listener.callback(event)
|
|
181
|
+
} catch (e: Exception) {
|
|
182
|
+
OneKeyLog.error("RangeDownloader", "Error sending event: ${e.message}")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun verifySHA256(filePath: String, expected: String): Boolean {
|
|
188
|
+
val calculated = calculateSHA256(filePath) ?: return false
|
|
189
|
+
return secureCompare(calculated, expected)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun calculateSHA256(filePath: String): String? {
|
|
193
|
+
val file = File(filePath)
|
|
194
|
+
if (!file.exists()) {
|
|
195
|
+
OneKeyLog.error("RangeDownloader", "calculateSHA256: file not found: $filePath")
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
return try {
|
|
199
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
200
|
+
java.io.BufferedInputStream(java.io.FileInputStream(filePath)).use { bis ->
|
|
201
|
+
val buffer = ByteArray(8192)
|
|
202
|
+
var count: Int
|
|
203
|
+
while (bis.read(buffer).also { count = it } > 0) {
|
|
204
|
+
digest.update(buffer, 0, count)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
bytesToHex(digest.digest())
|
|
208
|
+
} catch (e: Exception) {
|
|
209
|
+
OneKeyLog.error("RangeDownloader", "calculateSHA256: ${e.javaClass.simpleName}: ${e.message}")
|
|
210
|
+
null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private fun bytesToHex(bytes: ByteArray): String {
|
|
215
|
+
val sb = StringBuilder(bytes.size * 2)
|
|
216
|
+
for (b in bytes) {
|
|
217
|
+
sb.append(String.format("%02x", b))
|
|
218
|
+
}
|
|
219
|
+
return sb.toString()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Constant-time comparison of two hex SHA256 strings (lower-cased).
|
|
223
|
+
private fun secureCompare(a: String, b: String): Boolean {
|
|
224
|
+
val x = a.lowercase()
|
|
225
|
+
val y = b.lowercase()
|
|
226
|
+
if (x.length != y.length) return false
|
|
227
|
+
var result = 0
|
|
228
|
+
for (i in x.indices) {
|
|
229
|
+
result = result or (x[i].code xor y[i].code)
|
|
230
|
+
}
|
|
231
|
+
return result == 0
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativerangedownloader
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class ReactNativeRangeDownloaderPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
14
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
init {
|
|
19
|
+
System.loadLibrary("reactnativerangedownloader")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|