Cloud Torrent is a self-hosted remote torrent client: you add torrents remotely, they download to the server's disk as sets of files, and you browse or stream those files over HTTP.
This is a ground-up rewrite of the original cloud-torrent — a Go backend (anacrolix/torrent engine, jpillora/velox realtime sync) with a Vue 3 + Vite + TypeScript frontend, all compiled into a single binary.
- Single binary, cross-platform
- Add torrents by magnet,
.torrentURL, or drag-and-drop upload - Embedded torrent search (jpillora/scraper)
- Real-time updates (no refresh)
- Inline media previews (audio / image / video) with spacebar play/pause
- Mobile-friendly
- Fast content server (stream while downloading)
Binaries
Download binaries from GitHub Releases. New releases are published as prereleases for Linux, macOS, and Windows.
Docker
Docker image publishing is disabled. To build and run the current source locally:
docker build -f .dev/Dockerfile -t cloud-torrent .
docker run -d -p 3000:3000 -v /path/to/downloads:/downloads cloud-torrentSource
Go 1.27.3+, Node 24+, and Bun 1.4.4+ are required.
git clone https://github.com/jpillora/cloud-torrent
cd cloud-torrent
./dev.ts build # builds the SPA and dist/cloud-torrent
./dist/cloud-torrent --port 3000$ cloud-torrent --help
Usage: cloud-torrent [options]
Options:
--title, -t Title of this instance (default Cloud Torrent)
--port, -p Listening port (default 3000)
--host, -h Listening interface (default all)
--auth, -a Optional basic auth in form 'user:password'
--config-path, -c Configuration file path (default cloud-torrent.json)
--key-path, -k TLS Key file path
--cert-path TLS Certificate file path
--log, -l Enable request logging
--open, -o Open browser after the server starts
--version, -v display version
--help display help
The download directory and engine options are configured in the UI (gear icon)
and persisted to cloud-torrent.json.
The Go structs are the single source of truth for data shapes; the TypeScript
types are generated from them with tygo.
Build commands use Take via ./dev.ts;
run ./dev.ts --help to list commands. Build and release configuration lives in .dev/.
./dev.ts gen-types # regenerate frontend/src/types/*.gen.ts
./dev.ts check-types # CI guard: regenerate + fail if stale
npm --prefix frontend run dev # Vite dev server (proxies /sync /api -> :3000)
./dev.ts test # backend tests
./dev.ts ci # types, build, Go tests, and browser E2EInstall Chromium once with ./dev.ts e2e --install-browser. On Linux machines
missing browser libraries, add --with-deps (may require sudo). Subsequent runs
use ./dev.ts e2e; this builds the embedded app before testing. Use --skip-build
only when dist/cloud-torrent is already current. CHROMIUM_EXECUTABLE_PATH can
select an existing compatible Chromium installation.
The default Playwright test uses a pinned, locally generated CC0 torrent with a throttled loopback WebSeed. It exercises the real Omni input, metadata/progress updates over SSE, exact downloaded bytes, keyboard folder controls, and ZIP contents after removing the torrent card. It needs no public torrent service; package/browser installation still requires internet. Both the app configuration and downloads live in a temporary directory and are removed on exit, including failures and interrupts. Production services and download folders are never used.
For the public fixture from WebTorrent's free torrents,
run ./dev.ts e2e --public-fixture. This opt-in check downloads only Big Buck
Bunny's poster.jpg (310,380 bytes), verifies its pinned SHA-256, and verifies the
folder archive. It requires outbound internet and allows a longer timeout. The
fixture provenance and refresh procedure are in .dev/e2e/README.md.
GitHub Actions runs the deterministic browser test as a separate Take step;
./dev.ts ci includes it locally as well. The Go public-network poster/content smoke test
remains available with go test -tags e2e -run E2E ./server/....
Powered by anacrolix/torrent. A rewrite of the original Node version.
Operational files (the configured settings file, TLS key/certificate, and private
state directory) are excluded from download listings and archives, and cannot
be retrieved or deleted through the download API. Aliases and parent-folder
removal are checked too. --state-directory reserves a private location outside
the download tree; overlapping directories are rejected. Settings writes use
owner-only permissions.
Outbound HTTP uses a public-destination policy: loopback, private, link-local,
metadata and reserved addresses are denied, including after DNS resolution and
redirects. Trusted operators can grant specific CIDRs with
--outbound-allow-cidrs '192.168.1.10/32'. Local E2E fixtures explicitly grant
127.0.0.1/32. This applies to provider/config/torrent fetching, magnet HTTP
sources, webseeds and HTTP tracker dialing. Ambient HTTP proxy variables are not
used. Peer TCP/uTP sockets and DHT discovery are separate and are not an anonymity
boundary; UDP tracker resolution also uses the destination grants. Library
redirect limits remain bounded (five hops for application/source fetches, the
HTTP client's default ten for library-owned requests). Raw upstream diagnostics
are suppressed because they can contain tracker passkeys; application errors
and typed status remain available without raw URLs or headers.
Provider list and detail requests share a 15-second deadline and four concurrent slots. Browser disconnects cancel upstream work. Responses are limited to 2 MiB of decoded content and 500 results, including gzip responses; failures return an error rather than an empty result list. Excess requests receive HTTP 429.
Torrent sessions survive process restarts, including pending magnets, running or
paused intent, metainfo and file selections. The default private store is
.<settings-filename>.state beside the settings file; use --state-directory
to place it on a separate persistent volume. The directory must be writable by
the server and outside downloads. Only one process may use a store at a time.
State files contain private source URLs, use owner-only permissions, and must
be backed up as secrets. Updates are atomic and synced before actions succeed;
the store is limited to 256 torrents and 16 MiB. Restart verifies actual pieces
rather than trusting completion counters. Invalid or incompatible state stops
startup: restore a backup, or stop the server and move sessions.json aside to
start with an empty session. Downloaded files are retained.
Global DownloadLimitKiB and UploadLimitKiB settings independently cap aggregate
transfer rates in KiB/s (1024 bytes per second). Set a whole number from 1 to
1073741824, or explicitly use 0 for unlimited. Invalid values are rejected without
changing active settings. Each direction permits a fixed 64 KiB token burst;
network buffers and already admitted reads/requests may briefly exceed the
sustained rate. Download limiting covers peer reads and decoded webseed payloads;
upload limiting covers peer payloads. Protocol and other application HTTP traffic
are not upload payload. Limits update in place without replacing torrent clients
or changing paused state and file selections.
The FIFO queue exposes queued, metadata, downloading, paused, completed
and seeding states with one-based waiting positions. MaxActiveDownloads
accepts 0 (unlimited, the compatibility default) through 256. Completed selected
files release their download slot even while seeding. Manual Start joins the
queue and never bypasses capacity. Pause and removal advance waiting work;
capacity changes retain selections and never resume user-paused entries.
MaxActiveMetadata accepts 1–64, with 0 selecting a bounded default of four.
Excess pending magnets wait without a torrent client handle, so HTTP metadata
sources, peer metadata work and waiters are admitted separately from downloads.
Pausing a pending magnet releases its metadata slot. The overall store admits
at most 256 entries, deduplicates identities, and preserves queue order and
intent across restarts.
Optional SeedRatioLimit (0 disabled, otherwise up to 1000) and
SeedTimeMinutes (whole minutes, 0 disabled, up to 525600) stop sharing when
either threshold is reached. They apply after all selected files are verified
complete. The ratio is cumulative peer payload uploaded divided by selected
file sizes, including existing verified data; a zero denominator disables ratio
stopping. Active seeding time excludes pauses, downtime, downloading, and periods
with upload or seeding disabled. Accounting is persisted during state polls and
on actions/shutdown. Policies are evaluated on the one-second server poll;
in-flight transfers and buffering can overshoot a threshold between polls.
A stopped-by-policy reason is shown, with files, selections and sessions retained.
“Resume sharing (ignore limits)” explicitly overrides budgets until the next
Stop; accounting and that override survive restart. Ordinary Start does not
bypass a policy stop. A failure to persist active accounting pauses sharing.
Per-file Skip changes download priority without deleting data. Selected siblings continue, and Resume selected retains the subset after pause or restart; Start all deliberately selects every file. Skipping every file pauses the torrent. Selection is shown separately from active transfer state. BitTorrent pieces can span file boundaries, and admitted writes can finish, so a skipped file may receive incidental bytes. Skip cancels combined webseed requests once so they cannot hold up selected siblings. Deleting a skipped file or its ancestor still requires a whole-torrent pause when an active sibling shares its pieces. File selection actions accept only exact torrent file paths.
Opt in to local torrent import with --watch-directory /private/torrent-inbox.
The operator-configured inbox must be outside downloads and private session state;
it cannot be changed through the web UI. It accepts only regular .torrent
files, never links or subdirectories, and retains every source file. Sources
must remain unchanged for at least one second; writing a temporary file and
renaming it into the inbox is recommended. Imports use the same outbound policy,
queue, AutoStart setting, download root and existing file selections as manual
imports. Inbox status and sanitized errors appear in the browser.
The watcher scans once a second, accepts at most 256 directory entries and imports at most two files per scan, with a 10 MiB per-file limit. Move old files out when the entry limit is reached. A full torrent queue retries automatically when its size changes; unchanged invalid files or failed session saves require correcting the problem and touching or renaming the source. SHA-256 receipts persist atomically with acceptance, including after torrent removal, so retained files do not resurrect removed torrents after restart. The private store retains at most 4096 accepted-file receipts; after that use manual import. Inbox files cannot select a download destination or run commands.
Use --search-config /private/providers.json for a private operator-controlled
search catalog; this disables remote catalog refresh. Keep credentials in that
file's headers, protect it with filesystem permissions and place it outside
served downloads. Browser provider descriptors contain only a name and origin.
Existing HTML list/result extraction remains supported. JSON providers use
explicit JSON pointers (RFC 6901), including array indices and ~0/~1 escapes:
{
"legal": {
"name": "Legal fixtures",
"type": "json",
"url": "https://primary.example/search?q={{query}}&page={{page:1}}",
"mirrors": ["https://mirror.example/search?q={{query}}&page={{page:1}}"],
"json": {
"list": "/data/items",
"fields": {"name": "/title", "torrent": "/links/0", "seeds": "/stats/seeds"}
}
}
}Set JSON list to "" for a root array, or omit it for a single detail object
(e.g. a legal/item endpoint). Pointers allow at most 16 segments; extracted
fields must be scalars. Providers may return name, url, path, torrent,
magnet, infohash, size, seeds and peers. Links resolve against the
successful response URL, including mirror redirects. Unsafe link schemes and
invalid identities fail the attempt; extracted trackers are never added to
constructed magnets.
Only the private operator file can opt in to mirrors (up to two per endpoint). Each mirror is a complete URL template and receives the configured headers, so include only origins trusted with those credentials. Attempts run sequentially within one 15-second request deadline and one concurrency slot, using the shared outbound policy, 2 MiB decoded-body and 500-result limits. Empty valid results succeed without fallback; network, status, parsing and validation failures can try the next configured mirror. Invalid catalog refreshes preserve the entire last-known-good catalog. Restart to apply edits to a private operator file.
Inbound HTTP requests have a 5-second header deadline and a 60-second idle
keep-alive timeout. Request bodies have a total read budget of 15 seconds, or
60 seconds for /api/torrentfile, and a 10 MiB byte limit (including chunked
uploads). The budget starts after headers arrive and includes authentication;
stalling uploads return 408, oversized bodies return 413, and failed body reads
close the connection. These are fixed server defaults. Deployments needing
slower uploads should also account for any reverse proxy's own request limits.
A fully consumed body clears its read deadline before action processing. There
is no global response write timeout: active /sync connections, file/ZIP
transfers and range responses can continue beyond the upload deadlines. Sync
retains its separate stalled-writer protection. Authentication and compression
still wrap the same routes.
Torrent cards distinguish Verified content present on disk from Received network payload. Download/upload rates measure payload bytes per second (not protocol overhead or disk verification), sampled at intervals of at least 250 ms. Idle and paused transfers show zero rates. Received and its useful subset cover the current torrent client-handle session, including peer and webseed payload; they reset on restart or client recreation during reconfiguration. Existing verified files do not increase received counters. Uploaded, ratio and seeding time reuse durable session accounting and survive restart. Ratio uses the selected content size, rather than network-received bytes; zero-size selections show zero.
Cards show active connected peers and total known peers without peer addresses, tracker URLs, passkeys or raw transport statistics. Webseeds contribute to payload counters but not peer counts. States distinguish queued, fetching metadata, downloading, seeding, completed, paused and stopped by policy. A seeding-budget stop includes its reason and explicit sharing override action. Download/seeding state describes admitted work; rates and peers show whether that work is currently moving payload.
