fix(downloads): validate partials before resuming

This commit is contained in:
4gray committed 2026-08-01 20:56:19 +02:00
1 parent b3c3119070
commit 8b7ecb02e3
10 files changed
+116 -27

No files matched your search

+6 -4
View File
@@ -12,7 +12,7 @@ variants, contextual buttons, and theme-aware styling.
- **Queue control (`apps/electron-backend/src/app/events/database/download-runtime.ts`)**
`DownloadTask` mirrors a row of the shared `downloads` table (type `Download` in `libs/shared/database/src/lib/schema.ts`) plus transient cancel/pause/progress helpers (shared task types live in `download-task.ts`). Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`. The byte transfer itself lives in `download-transfer.ts`, finalization and retained-partial persistence in `download-finalize.ts`, and the renderer update broadcast in `download-broadcast.ts`.
- **Range-aware transfer (`download-transfer.ts`)**
The transfer streams the response through the backend's validated Axios redirect helper instead of `electron-dl`. Headers (user agent, referer, origin) are persisted in `request_headers` and re-applied through the same allowlist when read back on retry/resume. Xtream VOD downloads use the playlist's configured User-Agent when present and otherwise share the provider-compatible `XTREAM_CLIENT_USER_AGENT` used by Xtream API requests and stream probes. Retry, resume, and missing-file recovery resolve the owning playlist type and add that fallback to legacy Xtream rows without a stored User-Agent; Stalker rows are left unchanged. Active pause/cancel operations abort the current request with `AbortController`; pause keeps the partial file and cancel removes it. Resume checks the existing `.part` size (rejecting anything that is not a regular file, so a symlink planted while paused is never followed) and sends `Range: bytes=<offset>-` plus `If-Range` with the stored entity validator. The first response's strong `ETag` (or `Last-Modified`) is persisted in `resume_validator` for exactly this purpose. A `206 Partial Content` answer must start at the requested offset (`Content-Range` is verified) before bytes are appended; any other 2xx answer — the server ignoring `Range`, or `If-Range` detecting that the remote file changed — restarts the transfer from byte zero over the same `.part` instead of failing the download.
The transfer streams the response through the backend's validated Axios redirect helper instead of `electron-dl`. Headers (user agent, referer, origin) are persisted in `request_headers` and re-applied through the same allowlist when read back on retry/resume. Xtream VOD downloads use the playlist's configured User-Agent when present and otherwise share the provider-compatible `XTREAM_CLIENT_USER_AGENT` used by Xtream API requests and stream probes. Retry, resume, and missing-file recovery resolve the owning playlist type and add that fallback to legacy Xtream rows without a stored User-Agent; known Stalker rows are left unchanged. Download rows deliberately outlive individually deleted playlists, so a headerless legacy row whose source no longer exists receives the same IPTV-player fallback because its original provider type cannot be recovered. Active pause/cancel operations abort the current request with `AbortController`; pause keeps the partial file and cancel removes it. Resume checks the existing `.part` size (rejecting anything that is not a regular file, so a symlink planted while paused is never followed). The first response's strong `ETag` (or `Last-Modified`) is persisted in `resume_validator`; only a partial carrying that validator may send `Range: bytes=<offset>-` plus `If-Range` and append bytes. A retained partial without a validator restarts from byte zero and overwrites its `.part`, so a changed remote representation can never be joined to an unverified prefix. A `206 Partial Content` answer must start at the requested offset (`Content-Range` is verified) before bytes are appended; any other 2xx answer — the server ignoring `Range`, or `If-Range` detecting that the remote file changed — restarts the transfer from byte zero over the same `.part` instead of failing the download.
- **Destination collision policy**
Existing destination files are never overwritten, inspected, or deleted.
Before starting a new transfer, the backend atomically reserves a free
@@ -25,9 +25,11 @@ variants, contextual buttons, and theme-aware styling.
`unlink()`. Completion creates the final `filePath` from the `.part` without
overwriting an existing file; cancel and non-recoverable transfer failures
remove the `.part`, while finalization failures, completed-partial failures,
and allowlisted network interruptions after bytes reached disk deliberately
retain it (the row keeps `filePath` so a later retry can finish without
re-downloading); pause and restart recovery keep it for a later resume.
and allowlisted network interruptions after bytes reached disk with a stored
representation validator deliberately retain it (the row keeps `filePath`
so a later retry can finish without re-downloading); pause and restart
recovery keep partials, but a later retry starts over when no validator was
available.
Re-downloading such a failed row from a detail page
(`DOWNLOADS_START`) deletes the retained `.part` before the row is reset.
- **Derived file readiness and recovery**
@@ -102,7 +102,11 @@ Electron's `XTREAM_REQUEST` and stream-probe handlers plus Xtream VOD download
requests share the exported `XTREAM_CLIENT_USER_AGENT` fallback. A playlist's
explicit User-Agent still wins for its stream probe and download. Legacy
download rows without a stored User-Agent receive the fallback when retrying,
resuming, or recovering a missing completed file. Some Xtream
resuming, or recovering a missing completed file. Download rows intentionally
survive individual source deletion; when the playlist row is already gone and
its type can no longer be recovered, a headerless legacy download receives the
same IPTV-player fallback, while a still-identifiable Stalker row remains
unchanged. Some Xtream
panels sit behind a WAF (e.g. Cloudflare) configured to challenge
generic/incomplete browser-looking User-Agents while allowlisting known IPTV
player clients; a player-style User-Agent (currently a VLC signature) avoids