jm-tech-power-official/Flash Download
A desktop download manager that opens up to 32 connections against one file and can send each of them out a different proxy route.
Public
Uncategorized
Clone
jm-tech-power-official/flash-download.jmproj
README.md
# Flash Download
A desktop download manager that opens up to 32 connections against one file and
can send each of them out a different proxy route.
Built with Electron. The UI follows the "Instrument panel" design direction in
[`design/instrument-panel.html`](design/instrument-panel.html).
## Run it
```bash
npm install
npm start
```
For development there is a local origin server with range support plus two
proxies, so you can exercise the engine without hammering a real host:
```bash
npm run fixture # origin on :18080, proxies on :18081 and :18082
```
Then add `http://127.0.0.1:18080/ubuntu-24.04.3-live-server-amd64.iso`.
```bash
npm test # engine tests: splitting, proxies, retries, resume, limits
```
## Install it
Download an artifact from `dist/`, or build them yourself:
```bash
npm run dist:linux # AppImage + .deb
npm run dist:win # NSIS installer + portable .exe (needs wine on Linux)
npm run dist:mac:zip # macOS .zip for arm64 + x64 (builds anywhere)
npm run dist:mac # adds the .dmg — that target must run on macOS
npm run pack # unpacked app only, no installer
```
| Platform | Artifacts |
| --- | --- |
| Linux | `.AppImage` (chmod +x and run), `.deb` (`sudo apt install ./flash-download_*.deb`) |
| Windows | `.exe` installer (per-user, choose your own directory) and a portable `.exe` |
| macOS | `.zip` per architecture (arm64 / x64), macOS 11+ — unsigned, so Gatekeeper needs right-click → Open, or `xattr -dr com.apple.quarantine` |
### Linux: the AppImage and the Chromium sandbox
Prefer the **.deb** on Debian/Ubuntu. Its postinst tests whether user namespaces
work and configures `chrome-sandbox` accordingly, so the app runs with Chromium's
full sandbox.
The AppImage cannot do that. It mounts over FUSE with `nosuid` and is built as a
normal user, so the `chrome-sandbox` helper inside it can never be root-owned
`4755`. On a machine where the namespace sandbox is also unavailable, Chromium
falls back to that helper and aborts outright:
```
FATAL:setuid_sandbox_host.cc(163)] The SUID sandbox helper binary was found,
but is not configured correctly.
```
The bundled desktop entry launches it as `AppRun --no-sandbox %U`, so starting it
from your applications menu works. Running the file directly from a terminal does
not pass that flag — AppRun forwards only the arguments you give it — so use:
```bash
./flash-download-0.1.1-x86_64.AppImage --no-sandbox
```
That disables the Chromium sandbox for the AppImage only. The renderer loads no
remote content (local files under a `self` CSP, `contextIsolation` on, no node
integration), but the .deb is still the better install if you want the sandbox.
Installed builds register a desktop entry that accepts links, so a browser or
file manager can hand a URL straight to the app. One copy runs at a time: a
second launch passes its argument to the running window and exits, rather than
starting a rival downloader with its own queue.
```bash
flash-download "https://example.com/big.iso" # opens the add dialog in the running app
```
Builds are unsigned: shipping to real users needs a Windows Authenticode
certificate and an Apple Developer ID, and there is no auto-update channel
configured.
## Microsoft Store
The Store package is an AppX, built with `npm run dist:store`. **It only builds on
Windows** — electron-builder refuses elsewhere with *"AppX is supported only on
Windows 10 or Windows Server 2012 R2"*, so this one target cannot be produced on
the Linux box that builds everything else.
AppX rather than MSIX because electron-builder has no MSIX target at all. The two
share the same container and the same `schemas.microsoft.com/appx/manifest/...`
namespaces, this app uses no MSIX-only feature, and Partner Center accepts `.appx`.
For a literal `.msix`, run the Windows SDK's `makeappx pack` yourself, or feed the
NSIS installer to the MSIX Packaging Tool.
One real advantage: **the Store signs the package**, so this route needs no
Authenticode certificate — unlike submitting the `.exe` as a Win32 app, which
requires a signed installer.
### Before it will build
`build.appx` in `package.json` carries three placeholders that Partner Center
issues once the app name is reserved. `build/check-appx-identity.js` runs first
and refuses to build until they are replaced:
| Field | Partner Center → your app → Product identity |
| --- | --- |
| `identityName` | Package/Identity/Name |
| `publisher` | Package/Identity/Publisher — the full `CN=…` string |
| `publisherDisplayName` | Package/Properties/PublisherDisplayName |
Tile assets are already in `build/appx/` (store logo, square 44/71/150/310, wide
310x150, splash). Listing copy is drafted in
[`docs/STORE-LISTING.md`](docs/STORE-LISTING.md) and the privacy policy in
[`docs/PRIVACY.md`](docs/PRIVACY.md) — the latter still needs hosting somewhere
public, since Partner Center requires a URL. Screenshots and the age rating
questionnaire are still outstanding.
## The idea
A multi-threaded downloader splits a file into byte ranges and fills them in
parallel. A single aggregate percentage averages all of that away — but the
download finishes when the **slowest lane** finishes.
So there is no aggregate progress bar anywhere in this app. Progress is a
**segment lane strip**: one row per connection, width proportional to the bytes
that connection has taken, its own rate beside it. When a proxy goes bad you see
a short hatched lane with the route name on it, instead of a number that drifts.
## How a download runs
1. **Probe** — a one-byte ranged GET establishes size, whether the server honours
`Range`, the ETag, and the real filename. A server that refuses ranges gets a
single honest stream rather than a fake parallel one.
2. **Plan** — the file is split into N contiguous ranges, one per connection.
The output file is preallocated so every lane can write at its own offset.
3. **Pull** — each lane streams its range through its assigned route and writes
positionally into the shared file descriptor. No temp parts, no stitching pass.
4. **Rebalance** — a finished connection takes the back half of whatever range
has the longest time left, live, by shortening that lane mid-flight. This runs
continuously rather than once, so a connection that had nothing worth taking
when it finished keeps looking; otherwise throughput collapses at the tail as
connections retire one by one. How small a tail is worth handing over scales
with how fast the lane is actually moving — a fixed byte threshold is too
coarse near the end. A lane that wedges is taken over whole rather than
waited out.
5. **Verify** — SHA256 over the finished file, compared against an expected
checksum when one was supplied.
Progress is mirrored to a `.fdpart` sidecar every 2s, so pausing or quitting
resumes from the exact byte offsets. A resume is refused if the ETag moved.
## Routes
A route is `direct`, an HTTP proxy, or SOCKS4/5. Downloads either pin to one
route or **spread across the pool**, where each connection takes a different
healthy route — this is where multi-threading and proxying compound.
Routes are latency-tested at launch. Three route-level failures inside a minute
marks a route degraded and moves its lanes elsewhere without interrupting the
download.
## Layout
```
src/main/engine/
net.js request + redirects + probe, with typed errors that know
whether a fault is the route's or the origin's
agents.js route -> proxy agent, cached per route for socket reuse
download.js Lane and Download: splitting, pumping, stalls, retries,
rebalancing, resume sidecar, verification
manager.js downloads, queue, route pool, settings, persistence
limiter.js token bucket; a speed limit applies to the whole app, not per lane
secrets.js proxy credentials encrypted via an injected OS codec, kept out
of state.json; the codec is injected so the engine stays testable
src/main/main.js window, IPC, clipboard capture, single-instance lock,
safeStorage codec for secrets.js
src/preload/ contextBridge: named channels only, no node in the renderer
src/renderer/ UI; app.js reconciles lanes in place so fills never reset
```
The renderer runs with `contextIsolation`, `sandbox`, no node integration, and a
CSP limited to `self`. Fonts are bundled, so nothing is fetched at runtime —
Archivo and IBM Plex Mono, both SIL OFL 1.1, with their licence texts and
attribution in [`src/renderer/fonts/`](src/renderer/fonts/README.md).
Proxy credentials are encrypted with Electron `safeStorage` (DPAPI / Keychain /
libsecret) and written to `credentials.enc`, never to `state.json`. A state.json
left by an older version is migrated and scrubbed on first launch. Where no
keyring is available the credentials are session-only and never hit the disk as
plain text. See `test/secrets.test.js`.
## Known limits
- HTTP and HTTPS only; no torrent or metalink sources yet.
- No scheduler, no browser-extension capture, no per-site route rules.
- Above 32 connections the strip would need binning; the cap is 32 for now.
- A wedged connection still costs the 3s it takes to classify it as stalled.
- Builds are unsigned and there is no auto-updater.