fix(android): segment-file concurrent downloader + harden kill→restart resume (EROFS/ENOSPC)#66
Merged
Merged
Conversation
Replace module-local ConcurrentRangeDownloader with a single shared implementation and adapt consumers. Added a dependency on :onekeyfe_react-native-range-downloader in app-update's build.gradle and removed the duplicate ConcurrentRangeDownloader source from app-update. ReactNativeAppUpdate now imports the shared downloader and detects concurrent segment files when resuming; ReactNativeBundleUpdate cleans up legacy segment files and accounts for ".segN" in stem parsing. The range-downloader implementation was rewritten to stream into per-segment files (<partial>.segN), avoid full-file pre-allocation (fixes EROFS/ENOSPC on near-full devices), resume/concat segments into the final .partial, and adjust ETag/If-Range, cancellation and error handling accordingly.
Switch APK download location from context.cacheDir/apks to context.filesDir/apks to avoid system-reclaimable cache issues (EROFS/ENOSPC) during downloads. Rename getApkCacheDir to getApkDownloadDir, update callers and cleanup to match, and add a doc comment explaining the rationale and FileProvider exposure. Update res/xml/app_update_file_paths.xml to expose the new files-path (apks_files) while retaining the legacy cache-path for transition compatibility.
Pass partialFile.absolutePath to ConcurrentRangeDownloader.download instead of the filename (partialFilePath). The downloader resolves relative paths against the process CWD ("/"), which could cause segment files to be written to the read-only root filesystem (EROFS). Using the absolute path ensures segments go into the app's files dir (e.g. filesDir/apks), matching react-native-bundle-update behavior; added an inline comment explaining this.
Several fixes to make multi-segment downloads more robust and race-safe: - Use OkHttp .use() to ensure responses are closed and avoid resource leaks. - Treat segment files (<dest>.partial.segN) as first-class artifacts: sweep/delete sibling .segN on various error/restart paths in app-update and bundle-update so stale segment bytes can't be mistaken for valid resume data. - Make progress reporting thread-safe by using AtomicInteger + CAS to emit monotonic progress only when it increases. - Add defensive Content-Range handling: validate 206 Content-Range start matches expected resume offset (drop partial and restart if not), and in ConcurrentRangeDownloader verify per-segment Content-Range bounds and reject overlong segments. - Change resume semantics: object identity is no longer pinned by ETag/If-Range. Resume is unconditional; the caller's whole-file SHA256/GPG verify remains the final correctness backstop. Removed ETag plumbing and If-Range logic. - Correct progress accounting to include committed bytes already concatenated into .partial and avoid re-fetching segments whose extent is already covered by .partial. - Improve segment selection: pending segments exclude segments already committed into .partial; keep incomplete segment files for later resume instead of wiping them. - Add parseContentRangeBounds helper and related checks to ensure fetched slices exactly match the requested ranges. - Add DEFAULT_SEGMENT_COUNT and sweep segmented artifacts in ReactNativeRangeDownloader discard/cancel paths. These changes reduce corruption risk from misaligned 206s, stale sibling segments, and concurrent progress races, while keeping resume behavior predictable and recoverable via the final file verification.
Improve robustness of range/partial handling across native modules: - AppUpdate & BundleUpdate: ensure leftover concurrent segment files (<partial>.segN) are removed when promoting/discarding partials, and treat 206 Content-Range start mismatches as retryable errors (close response, delete partial+segments, and throw) to avoid writing mis-aligned bytes. BundleUpdate adds a CONCURRENT_SEGMENT_COUNT constant and defers promotion if concurrent segments exist. - RangeDownloader: replace hardcoded default segment sweep with sweepPartialArtifacts (glob by prefix) to clear any custom segment counts, and make progress reporting thread-safe using an AtomicInteger + CAS to dedupe/monotonize progress events. - AppUpdate: read ASC files with response.use to always close connections, enforce a max ASC size, and avoid leaking response bodies. These changes prevent mixing segmented and single-stream artifacts, avoid corrupted downloads from CDN/proxy 206 bugs, and fix leaked connections and duplicate progress events.
zhaono1
approved these changes
Jun 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
On near-full f2fs devices, the old concurrent downloader pre-allocated the whole file via
RandomAccessFile(partial, "rw").setLength(total), which fails with EROFS/ENOSPC before a single byte is fetched, aborting the entire download. This branch moves the download to a segment-file model and systematically hardens kill → cold-start resume.Core changes
Segment-file model (fixes EROFS/ENOSPC)
<partial>.segN(plain O_WRONLY append, no whole-file pre-allocation); once all segments are present they are concatenated in order into.partial, then renamed. Only sequential append + sequential read throughout; peak footprint ~1x + one segment./resolving to the read-only root).Resume hardening (kill → cold start)
.segNare always reused, never wiped for a missing/changed ETag. A mid-flight object swap is caught by the final SHA256 → clean full re-download (a successful concat always deletes the.segN, so retries are clean and cannot loop)..partial's current length is the committed cursor; after a kill mid-concat, prefix segments already merged into.partialare not re-downloaded even if their.segNwas deleted, restoring the 1x + one-segment footprint.Content-Range, require start/end to exactly match the requested window and the written length to not exceed the segment length, rejecting mis-aligned/over-long 206 responses.Consumer / wrapper cleanup
.partial+.segN; resume branches (RenameFailed / Range-resume / Deferred) deliberately keep the segments..partial.seg*(supports a custom segmentCount); bundle gains thehasConcurrentSegmentsguard and the 206 start check; concurrent progress switched toAtomicIntegerCAS;downloadASC/downloadAscFileResponse wrapped in.use{}to fix leaks; magic number 8 extracted to a constant.Verification
compileDebugKotlinBUILD SUCCESSFUL for all three modules..segN(Phase-2 segment detection hit), final verify passed and the APK installed.Scope
🤖 Generated with Claude Code