JM Tech Power

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.