Skip to content

seed a path: make a torrent of files you already have, and share it - #184

Merged
baairon merged 1 commit into
baairon:mainfrom
profullstack:seed-a-local-path
Aug 29, 2026
Merged

seed a path: make a torrent of files you already have, and share it#184
baairon merged 1 commit into
baairon:mainfrom
profullstack:seed-a-local-path

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Every way into torlink starts from a torrent somebody else made — search finds one, the watch folder is handed one, the API is posted one. None of that helps when the content is yours and no torrent for it exists yet, and that was the one thing the client could not do.

torlnk seed ./album

Hashes the files, writes album.torrent beside them, prints the magnet, and starts seeding. Takes --seed-time, --delete-files and --daemon like the other headless modes.

The interesting part isn't the creating

It's what gets added afterwards, and it's a bug I only found by running the thing.

addInput turns a .torrent into a magnet and hands the queue that, and startEngine passed item.magnet to webtorrent unconditionally. So the engine had no piece hashes and could not verify a byte until the swarm sent metadata back — and for a torrent created from local content that swarm is empty by definition, because nobody has ever seen the info hash. It sat at zero per cent forever, waiting on peers for data already on the disk underneath it.

The queue already knew the answer. startSeeding has always preferred the stored .torrent over the magnet — "verifies the local file immediately, no swarm needed" — for restoring a seed across a restart. startEngine now does the same:

const source = torrentMetaExists(item.id) ? torrentMetaPath(item.id) : item.magnet;

One line, following a convention already in the file rather than inventing one, and it helps every re-add of something already downloaded — not just this feature. seed writes the metadata to the store before adding, so the engine verifies locally and completes at once.

serve takes a .torrent too

Same flexibility on the way in: POST /add now accepts an uploaded .torrent as well as a magnet, base64 or a data: URI as FileReader hands it over.

POST /add {"magnet":"magnet:?xt=..."}
POST /add {"torrent":"<base64>"}

A .torrent is tried first because it is strictly more information. The bytes travel in the request rather than a path to them: the existing refusal to let a network caller name a local file (allowTorrentPath) is worth keeping, and "add this torrent" must not double as "read this file off your disk". Buffer.from(..., "base64") ignores what it cannot decode instead of throwing, so the leading bencode d is what turns garbage into a 400 rather than a short buffer.

Verified by running it

Not only by the suite. Seeding a directory of real files produced seeds.json with status: "seeding" and an empty download queue. Before the metadata fix, the same run left a queue entry at downloading / progress 0 — that's the failure this fixes, observed directly.

  • npm run typecheck — clean
  • npm test — 372 passing (11 new)

Notes

  • create-torrent moves from a transitive dependency of webtorrent to a direct one, with a local .d.ts matching how parse-torrent and webtorrent are already declared here.
  • The seed root is the content's parent, since a torrent names its own top-level entry. Point a client at the content itself and it looks for album/album, finds nothing, and downloads a second copy beside the first. That calculation has its own test.
  • Happy to split the startEngine line into its own PR if you'd rather take that separately — it stands on its own as a fix.

Every way into torlink starts from a torrent somebody else made — search finds
one, the watch folder is handed one, the API is posted one. None of that helps
when the content is yours and no torrent for it exists yet, which is the one
thing the client could not do.

    torlnk seed ./album

Hashes the files, writes album.torrent beside them, prints the magnet, and
starts seeding.

The interesting part is not the creating, it is what gets added afterwards.
addInput turns a .torrent into a magnet and hands the queue that, and
startEngine passed item.magnet to webtorrent unconditionally — so the engine
had no piece hashes and could not verify a byte until the swarm sent metadata
back. For a torrent created from local content that swarm is empty by
definition: nobody has ever seen the info hash. It sat at zero per cent
forever, waiting on peers for data already on the disk underneath it. I watched
it do exactly that before fixing it.

The queue already knew the answer. startSeeding has always preferred the stored
.torrent over the magnet — "verifies the local file immediately, no swarm
needed" — for restoring a seed across a restart. startEngine now does the same,
which is a one-line change that follows an existing convention rather than
inventing one, and it helps every re-add of something already downloaded, not
just this feature. So `seed` writes the metadata to the store before adding,
and the engine verifies locally and completes at once.

serve gained the same flexibility on the way in: POST /add now takes an
uploaded .torrent as well as a magnet, base64 or a data: URI as FileReader
hands it over. A .torrent is tried first because it is strictly more
information. The bytes travel in the request rather than a path to them — the
existing refusal to let a network caller name a local file is worth keeping,
and "add this torrent" must not double as "read this file off your disk".

Verified by running it, not only by the suite: seeding a directory of real
files produced seeds.json with status "seeding" and an empty download queue.
Before the metadata fix the same run left a queue entry at downloading /
progress 0.

npm run typecheck clean, npm test 372 passing.
@baairon
baairon merged commit 4c52452 into baairon:main Aug 29, 2026
7 of 9 checks passed
@ralyodio
ralyodio deleted the seed-a-local-path branch August 29, 2026 18:01
baairon added a commit that referenced this pull request Aug 29, 2026
Follow-up to #184.

runSeed returned as soon as the torrent was added, so the process stayed
alive only because webtorrent's handles did and queue.suspend() never ran on
shutdown. watch and serve both hold themselves open and suspend on both
signals; seed now does the same, and the CLI exits once that flush returns.

POST /add accepts a base64 .torrent, which is one 20-byte hash per piece and
grows by a third again in base64. The 64KB body cap was sized for magnets, so
it fit every torrent small enough to test with and answered 413 on a large
multi-file release.

startEngine prefers the stored .torrent, which does not carry the announce
URLs that mergeMagnetTrackers folds onto a row assembled from several
sources. Metadata is saved as soon as it arrives, so resuming a partial
download dropped them; the magnet's trackers are now passed as announce
regardless of which source wins.

The new path tests asserted POSIX literals, which resolve drive-qualified on
Windows and failed the suite there. Also plainer seed wording in the README
and --help, and readParsed re-indented after its extraction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants