Skip to content

About

☁️ Cloud Torrent: a self-hosted remote torrent client

Topics

Resources

Stars

6.2k stars

Watchers

237 watching

Forks

Repository files navigation

screenshot

Cloud Torrent

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.

Features

  • Single binary, cross-platform
  • Add torrents by magnet, .torrent URL, 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)

Install

Binaries

Releases

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-torrent

Source

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

Usage

$ 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.

Development

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 E2E

Browser end-to-end checks

Install 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/....

Credits

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.

About

☁️ Cloud Torrent: a self-hosted remote torrent client

Topics

Resources

Stars

6.2k stars

Watchers

237 watching

Forks

Releases

Packages

Used by

Contributors

Languages