@onekeyfe/react-native-range-downloader 6.7.0-alpha.310 → 6.7.0-alpha.313
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.
|
@@ -100,7 +100,10 @@ class ConcurrentRangeDownloader(
|
|
|
100
100
|
* segment files, so no in-flight worker resurrects a deleted file.
|
|
101
101
|
*/
|
|
102
102
|
class CancelHandle {
|
|
103
|
+
// Only an explicit caller cancellation sets aborted. Worker failures use
|
|
104
|
+
// workerStop to stop sibling segments without changing that intent.
|
|
103
105
|
val aborted = AtomicBoolean(false)
|
|
106
|
+
internal val workerStop = AtomicBoolean(false)
|
|
104
107
|
|
|
105
108
|
@Volatile
|
|
106
109
|
private var pool: java.util.concurrent.ExecutorService? = null
|
|
@@ -108,7 +111,7 @@ class ConcurrentRangeDownloader(
|
|
|
108
111
|
internal fun attach(pool: java.util.concurrent.ExecutorService) {
|
|
109
112
|
this.pool = pool
|
|
110
113
|
// If cancel() already raced in before the pool was attached, honor it.
|
|
111
|
-
if (
|
|
114
|
+
if (workerStop.get()) pool.shutdownNow()
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/**
|
|
@@ -121,6 +124,7 @@ class ConcurrentRangeDownloader(
|
|
|
121
124
|
*/
|
|
122
125
|
fun cancel() {
|
|
123
126
|
aborted.set(true)
|
|
127
|
+
workerStop.set(true)
|
|
124
128
|
pool?.let { p ->
|
|
125
129
|
p.shutdownNow()
|
|
126
130
|
try {
|
|
@@ -148,12 +152,15 @@ class ConcurrentRangeDownloader(
|
|
|
148
152
|
* Fills [partialFilePath] completely with the resource at [url] using
|
|
149
153
|
* concurrent ranges. See [Outcome]. Throws on a transient/IO error after
|
|
150
154
|
* per-segment retries, leaving the segment files in place so a later attempt
|
|
151
|
-
* resumes.
|
|
155
|
+
* resumes. [maxTotalBytes] rejects a known oversized probe before workers
|
|
156
|
+
* or artifact writes; callers with unknown single-stream sizes enforce their
|
|
157
|
+
* own streaming limit.
|
|
152
158
|
*/
|
|
153
159
|
fun download(
|
|
154
160
|
url: String,
|
|
155
161
|
partialFilePath: String,
|
|
156
162
|
cancelHandle: CancelHandle? = null,
|
|
163
|
+
maxTotalBytes: Long = Long.MAX_VALUE,
|
|
157
164
|
onProgress: (transferred: Long, total: Long) -> Unit,
|
|
158
165
|
): Outcome {
|
|
159
166
|
val partialFile = File(partialFilePath)
|
|
@@ -168,6 +175,9 @@ class ConcurrentRangeDownloader(
|
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
val probe = probe(url) ?: return Outcome.FALLBACK
|
|
178
|
+
if (probe.totalSize > maxTotalBytes) {
|
|
179
|
+
throw SecurityException("Range download exceeds caller size limit")
|
|
180
|
+
}
|
|
171
181
|
if (!probe.supportsRange || probe.totalSize < minConcurrentBytes) {
|
|
172
182
|
log("concurrent: not eligible (supportsRange=${probe.supportsRange}, size=${probe.totalSize})")
|
|
173
183
|
return Outcome.FALLBACK
|
|
@@ -215,9 +225,9 @@ class ConcurrentRangeDownloader(
|
|
|
215
225
|
)
|
|
216
226
|
onProgress(transferred.get(), total)
|
|
217
227
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
val aborted = cancelHandle?.
|
|
228
|
+
// External cancel also stops workers; internal fallback/failure must not
|
|
229
|
+
// mark the caller's explicit-cancellation flag.
|
|
230
|
+
val aborted = cancelHandle?.workerStop ?: AtomicBoolean(false)
|
|
221
231
|
val fallback = AtomicBoolean(false)
|
|
222
232
|
val firstError = AtomicReference<Exception?>(null)
|
|
223
233
|
|
|
@@ -237,11 +247,11 @@ class ConcurrentRangeDownloader(
|
|
|
237
247
|
}
|
|
238
248
|
} catch (e: FallbackException) {
|
|
239
249
|
fallback.set(true)
|
|
240
|
-
aborted.set(true)
|
|
241
250
|
firstError.compareAndSet(null, e)
|
|
242
|
-
} catch (e: Exception) {
|
|
243
251
|
aborted.set(true)
|
|
252
|
+
} catch (e: Exception) {
|
|
244
253
|
firstError.compareAndSet(null, e)
|
|
254
|
+
aborted.set(true)
|
|
245
255
|
}
|
|
246
256
|
}
|
|
247
257
|
}
|
|
@@ -2,6 +2,9 @@ package com.margelo.nitro.reactnativerangedownloader
|
|
|
2
2
|
|
|
3
3
|
import okhttp3.OkHttpClient
|
|
4
4
|
import okhttp3.mockwebserver.MockWebServer
|
|
5
|
+
import okhttp3.mockwebserver.Dispatcher
|
|
6
|
+
import okhttp3.mockwebserver.MockResponse
|
|
7
|
+
import okhttp3.mockwebserver.RecordedRequest
|
|
5
8
|
import org.junit.After
|
|
6
9
|
import org.junit.Assert.assertEquals
|
|
7
10
|
import org.junit.Assert.assertFalse
|
|
@@ -82,6 +85,52 @@ class ConcurrentRangeDownloaderOcdsTest {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
@Test
|
|
89
|
+
fun oversizedProbedTotalRejectsBeforeRangeWorkers() {
|
|
90
|
+
val cap = content.size.toLong()
|
|
91
|
+
server = MockWebServer()
|
|
92
|
+
server.dispatcher = object : Dispatcher() {
|
|
93
|
+
override fun dispatch(request: RecordedRequest) = MockResponse()
|
|
94
|
+
.setResponseCode(206)
|
|
95
|
+
.addHeader("Content-Range", "bytes 0-0/${cap + 1}")
|
|
96
|
+
.setBody("x")
|
|
97
|
+
}
|
|
98
|
+
server.start()
|
|
99
|
+
|
|
100
|
+
val partial = partialPath()
|
|
101
|
+
val failure = runCatching {
|
|
102
|
+
newDownloader().download(
|
|
103
|
+
url = url(),
|
|
104
|
+
partialFilePath = partial,
|
|
105
|
+
maxTotalBytes = cap,
|
|
106
|
+
onProgress = { _, _ -> },
|
|
107
|
+
)
|
|
108
|
+
}.exceptionOrNull()
|
|
109
|
+
|
|
110
|
+
assertTrue("oversized total must reject", failure is SecurityException)
|
|
111
|
+
assertEquals("only the probe may reach the server", 1, server.requestCount)
|
|
112
|
+
assertEquals("bytes=0-0", server.takeRequest().getHeader("Range"))
|
|
113
|
+
assertNoArtifactsLeft(partial)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
fun probedTotalAtLimitCanComplete() {
|
|
118
|
+
val fs = RangeFaultServer(content)
|
|
119
|
+
startServer(fs)
|
|
120
|
+
|
|
121
|
+
val partial = partialPath()
|
|
122
|
+
val outcome = newDownloader().download(
|
|
123
|
+
url = url(),
|
|
124
|
+
partialFilePath = partial,
|
|
125
|
+
maxTotalBytes = content.size.toLong(),
|
|
126
|
+
onProgress = { _, _ -> },
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assertEquals(ConcurrentRangeDownloader.Outcome.COMPLETED, outcome)
|
|
130
|
+
assertEquals(content.size.toLong(), File(partial).length())
|
|
131
|
+
assertEquals(sha256(content), sha256(File(partial)))
|
|
132
|
+
}
|
|
133
|
+
|
|
85
134
|
// ------------------------------------------------------------------------
|
|
86
135
|
// OCDS-T1: drop one segment's connection mid-body a couple of times.
|
|
87
136
|
// Only that segment is re-fetched (retries stay in its own window); the
|
|
@@ -244,6 +293,67 @@ class ConcurrentRangeDownloaderOcdsTest {
|
|
|
244
293
|
assertNoArtifactsLeft(partial)
|
|
245
294
|
}
|
|
246
295
|
|
|
296
|
+
@Test
|
|
297
|
+
fun internalFallbackDoesNotMarkCallerCancelled() {
|
|
298
|
+
val target = parts[2]
|
|
299
|
+
startServer(RangeFaultServer(content).arm(
|
|
300
|
+
RangeFaultServer.FaultMode.STATUS_200, target.start, target.end, times = Int.MAX_VALUE,
|
|
301
|
+
))
|
|
302
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
303
|
+
|
|
304
|
+
val outcome = newDownloader().download(url(), partialPath(), handle) { _, _ -> }
|
|
305
|
+
|
|
306
|
+
assertEquals(ConcurrentRangeDownloader.Outcome.FALLBACK, outcome)
|
|
307
|
+
assertFalse(handle.aborted.get())
|
|
308
|
+
assertNoArtifactsLeft(partialPath())
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@Test
|
|
312
|
+
fun segment403PreservesHttpStatusWithoutMarkingCallerCancelled() {
|
|
313
|
+
val target = parts[2]
|
|
314
|
+
val normal = RangeFaultServer(content)
|
|
315
|
+
server = MockWebServer()
|
|
316
|
+
server.dispatcher = object : Dispatcher() {
|
|
317
|
+
override fun dispatch(request: RecordedRequest): MockResponse =
|
|
318
|
+
if (request.getHeader("Range") == "bytes=${target.start}-${target.end}") {
|
|
319
|
+
MockResponse().setResponseCode(403)
|
|
320
|
+
} else {
|
|
321
|
+
normal.dispatcher.dispatch(request)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
server.start()
|
|
325
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
326
|
+
|
|
327
|
+
val failure = runCatching {
|
|
328
|
+
newDownloader().download(url(), partialPath(), handle) { _, _ -> }
|
|
329
|
+
}.exceptionOrNull()
|
|
330
|
+
|
|
331
|
+
assertTrue(failure is ConcurrentRangeDownloader.PermanentHttpException)
|
|
332
|
+
assertEquals(403, (failure as ConcurrentRangeDownloader.PermanentHttpException).code)
|
|
333
|
+
assertFalse(handle.aborted.get())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@Test
|
|
337
|
+
fun exhaustedSocketTimeoutDoesNotMarkCallerCancelled() {
|
|
338
|
+
val target = parts[2]
|
|
339
|
+
startServer(RangeFaultServer(content).arm(
|
|
340
|
+
RangeFaultServer.FaultMode.STALL, target.start, target.end, times = Int.MAX_VALUE,
|
|
341
|
+
))
|
|
342
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
343
|
+
val client = OkHttpClient.Builder().readTimeout(200, TimeUnit.MILLISECONDS).build()
|
|
344
|
+
val downloader = ConcurrentRangeDownloader(client, maxPartRetry = 0)
|
|
345
|
+
|
|
346
|
+
val failure = runCatching {
|
|
347
|
+
downloader.download(url(), partialPath(), handle) { _, _ -> }
|
|
348
|
+
}.exceptionOrNull()
|
|
349
|
+
|
|
350
|
+
assertTrue(
|
|
351
|
+
"expected a socket timeout, got ${failure?.javaClass?.name}: ${failure?.message}",
|
|
352
|
+
failure is java.net.SocketTimeoutException,
|
|
353
|
+
)
|
|
354
|
+
assertFalse(handle.aborted.get())
|
|
355
|
+
}
|
|
356
|
+
|
|
247
357
|
// ------------------------------------------------------------------------
|
|
248
358
|
// OCDS-T4: 429 (+Retry-After: 0) once on a segment → transient retry succeeds.
|
|
249
359
|
// SHA correct; the affected segment requested twice; no from-0 restart.
|
|
@@ -483,6 +593,7 @@ class ConcurrentRangeDownloaderOcdsTest {
|
|
|
483
593
|
worker.shutdownNow()
|
|
484
594
|
|
|
485
595
|
assertTrue("cancel must abort promptly (took ${elapsed}ms)", elapsed < 12_000)
|
|
596
|
+
assertTrue("explicit cancellation must mark caller intent", handle.aborted.get())
|
|
486
597
|
|
|
487
598
|
// cancel-then-delete: now that workers are terminated, deleting the artifacts
|
|
488
599
|
// must STICK — no straggler worker resurrects a `.segN`.
|
package/package.json
CHANGED