feat(downloads): switch to rqbit fork and implement fake tracker

- Update librqbit dependency to JakeHillion/rqbit fork (v9.0.0-beta.1)
- Update session.rs for new librqbit API:
  - SessionPersistenceConfig instead of bool
  - AddTorrentResponse enum handling
  - Updated TorrentStats field access
- Implement bencode module with AnnounceParams and BencodeValue encoding
- Implement fake tracker that generates bencoded announce responses
  from Exo topology data (PeerInfo, TopologyData, handle_announce)
- Fix PyO3 bindings for new librqbit types (remove allow_threads)
This commit is contained in:
Jake Hillion
2026-01-12 15:12:56 +00:00
parent ba708c2ccd
commit 1ca732da23
7 changed files with 767 additions and 120 deletions
Generated
+368 -59
View File
@@ -296,6 +296,15 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -321,14 +330,87 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backoff"
version = "0.4.0"
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"getrandom 0.2.16",
"instant",
"rand 0.8.5",
"axum-core",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
dependencies = [
"axum",
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde_core",
"serde_html_form",
"serde_path_to_error",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "backon"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
dependencies = [
"fastrand",
"gloo-timers 0.3.0",
"tokio",
]
[[package]]
@@ -384,15 +466,6 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "2.0.1"
@@ -576,6 +649,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "chardetng"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.42"
@@ -1068,6 +1152,18 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "dontfrag"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117949f5a8b25ba471b4dfea927a07a2f8730c585532a2eb4294e18f5155397"
dependencies = [
"cfg-if",
"libc",
"tokio",
"windows-sys 0.52.0",
]
[[package]]
name = "downloads"
version = "0.0.1"
@@ -1162,6 +1258,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -1466,7 +1571,7 @@ version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
dependencies = [
"gloo-timers",
"gloo-timers 0.2.6",
"send_wrapper 0.4.0",
]
@@ -1577,6 +1682,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "governor"
version = "0.10.4"
@@ -1810,6 +1927,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
@@ -1824,6 +1947,7 @@ dependencies = [
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
@@ -2129,15 +2253,6 @@ dependencies = [
"generic-array 0.14.7",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "internment"
version = "0.8.6"
@@ -3056,18 +3171,18 @@ dependencies = [
[[package]]
name = "librqbit"
version = "8.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dadca8f521242010a4c846ef5f224c217009c92e272709cdc08ba9cdabe62983"
version = "9.0.0-beta.1"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"arc-swap",
"async-compression",
"async-stream",
"async-trait",
"backoff",
"axum-extra",
"backon",
"base64",
"bincode 2.0.1",
"bincode",
"bitvec",
"byteorder",
"bytes",
@@ -3083,22 +3198,29 @@ dependencies = [
"librqbit-clone-to-owned",
"librqbit-core",
"librqbit-dht",
"librqbit-dualstack-sockets",
"librqbit-lsd",
"librqbit-peer-protocol",
"librqbit-sha1-wrapper",
"librqbit-tracker-comms",
"librqbit-upnp",
"librqbit-utp",
"memmap2",
"mime_guess",
"nix 0.30.1",
"parking_lot",
"rand 0.9.2",
"regex",
"reqwest",
"rlimit",
"serde",
"serde_derive",
"serde_json",
"serde_urlencoded",
"serde_with",
"size_format",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tokio-socks",
"tokio-stream",
@@ -3108,38 +3230,40 @@ dependencies = [
"urlencoding",
"uuid",
"walkdir",
"windows 0.62.2",
]
[[package]]
name = "librqbit-bencode"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606dff526ba81e3eca33e2bb28b53afa2bc0b2c41d252333fa44e6c11abb37da"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"arrayvec",
"atoi",
"bytes",
"librqbit-buffers",
"librqbit-clone-to-owned",
"librqbit-sha1-wrapper",
"serde",
"serde_derive",
"thiserror 2.0.17",
]
[[package]]
name = "librqbit-buffers"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c78b907d6171a7191c162b2b60db46d254ebde6a95282b77372af556c1463"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"bytes",
"librqbit-clone-to-owned",
"serde",
"serde_derive",
]
[[package]]
name = "librqbit-clone-to-owned"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd1e66d773ba9c475ff89286dc1d6f9d167cbb898603797467dd0ea6844c445"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"bytes",
]
@@ -3147,22 +3271,26 @@ dependencies = [
[[package]]
name = "librqbit-core"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55a02cc6fce6743ad38661ccd6fafc6cf1ae5e0106a9922836b0524dbe752378"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"assert_cfg",
"bytes",
"chardetng",
"data-encoding",
"directories",
"encoding_rs",
"hex 0.4.3",
"itertools 0.14.0",
"librqbit-bencode",
"librqbit-buffers",
"librqbit-clone-to-owned",
"librqbit-sha1-wrapper",
"memchr",
"parking_lot",
"rand 0.9.2",
"serde",
"serde_derive",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tracing",
@@ -3171,27 +3299,28 @@ dependencies = [
[[package]]
name = "librqbit-dht"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7cc129194337771a86b0399956c4d9bf1cd97c5f24d14a50be38e170f76a54b"
version = "5.3.0"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"backoff",
"byteorder",
"backon",
"bytes",
"chrono",
"dashmap",
"futures",
"hex 0.4.3",
"indexmap 2.12.1",
"leaky-bucket",
"librqbit-bencode",
"librqbit-buffers",
"librqbit-clone-to-owned",
"librqbit-core",
"librqbit-dualstack-sockets",
"parking_lot",
"rand 0.9.2",
"serde",
"serde_derive",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-util",
@@ -3199,13 +3328,48 @@ dependencies = [
]
[[package]]
name = "librqbit-peer-protocol"
version = "4.3.0"
name = "librqbit-dualstack-sockets"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a73129497b500505f33d1dc0426319b6a6a208f13fdfaae56224ab8c2346a773"
checksum = "52135d1583bc772b74d5dc72a75ca8ed810463159c028d0b28609f597fffa816"
dependencies = [
"axum",
"backon",
"futures",
"libc",
"network-interface",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
]
[[package]]
name = "librqbit-lsd"
version = "0.1.0"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"atoi",
"bstr",
"futures",
"httparse",
"librqbit-core",
"librqbit-dualstack-sockets",
"librqbit-sha1-wrapper",
"parking_lot",
"rand 0.9.2",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "librqbit-peer-protocol"
version = "4.3.0"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"bincode 1.3.3",
"bitvec",
"byteorder",
"bytes",
@@ -3215,13 +3379,15 @@ dependencies = [
"librqbit-clone-to-owned",
"librqbit-core",
"serde",
"serde_derive",
"thiserror 2.0.17",
"tracing",
]
[[package]]
name = "librqbit-sha1-wrapper"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79373a02db73159e4de7ca5d27b6eeae2d540df66c6801db2b01c5513d087524"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"assert_cfg",
"crypto-hash",
@@ -3230,20 +3396,24 @@ dependencies = [
[[package]]
name = "librqbit-tracker-comms"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08204944c5be677a5de8e1230e0249fce5c14abef23048e26452c6fb03f1b260"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"async-stream",
"backon",
"byteorder",
"futures",
"itertools 0.14.0",
"librqbit-bencode",
"librqbit-buffers",
"librqbit-core",
"librqbit-dualstack-sockets",
"parking_lot",
"rand 0.9.2",
"reqwest",
"serde",
"serde_derive",
"serde_with",
"tokio",
"tokio-util",
"tracing",
@@ -3254,22 +3424,47 @@ dependencies = [
[[package]]
name = "librqbit-upnp"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545aad6124c97201055983137e12a19f34acad565120c3cd30596cbd72e8fa86"
source = "git+https://github.com/JakeHillion/rqbit?rev=c4e2ecf81d03bd8acd96a0803d06a70b34d5da19#c4e2ecf81d03bd8acd96a0803d06a70b34d5da19"
dependencies = [
"anyhow",
"bstr",
"futures",
"httparse",
"librqbit-dualstack-sockets",
"network-interface",
"quick-xml",
"reqwest",
"serde",
"serde_derive",
"socket2 0.6.1",
"tokio",
"tracing",
"url",
]
[[package]]
name = "librqbit-utp"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bf54992c5b2c053c50782f6d14164439f46cd889fb97de25ee0bdee04fd5b33"
dependencies = [
"bitvec",
"dontfrag",
"lazy_static",
"libc",
"librqbit-dualstack-sockets",
"metrics",
"parking_lot",
"rand 0.9.2",
"ringbuf",
"rustc-hash 2.1.1",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -3338,6 +3533,12 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "matrixmultiply"
version = "0.3.10"
@@ -3382,6 +3583,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "metrics"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8"
dependencies = [
"ahash",
"portable-atomic",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -3638,6 +3849,18 @@ dependencies = [
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
@@ -4367,9 +4590,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.37.5"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
"serde",
@@ -4744,6 +4967,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "ringbuf"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
dependencies = [
"crossbeam-utils",
"portable-atomic",
"portable-atomic-util",
]
[[package]]
name = "rlimit"
version = "0.10.2"
@@ -4766,7 +5000,7 @@ dependencies = [
"netlink-packet-utils",
"netlink-proto",
"netlink-sys",
"nix",
"nix 0.26.4",
"thiserror 1.0.69",
"tokio",
]
@@ -5084,6 +5318,19 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "serde_html_form"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
dependencies = [
"form_urlencoded",
"indexmap 2.12.1",
"itoa",
"ryu",
"serde_core",
]
[[package]]
name = "serde_json"
version = "1.0.145"
@@ -5097,6 +5344,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
@@ -6274,6 +6532,27 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core 0.62.2",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.53.0"
@@ -6309,6 +6588,17 @@ dependencies = [
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
@@ -6359,6 +6649,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -6470,6 +6770,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
+1 -1
View File
@@ -31,7 +31,7 @@ itertools = { workspace = true }
log = { workspace = true }
# BitTorrent library
librqbit = "8.1.1"
librqbit = { git = "https://github.com/JakeHillion/rqbit", rev = "c4e2ecf81d03bd8acd96a0803d06a70b34d5da19" }
# Embed torrent files
include_dir = "0.7"
+161
View File
@@ -1 +1,162 @@
//! Bencode encoding for BitTorrent tracker responses
//!
//! Implements the subset of bencoding needed for tracker announce responses.
use std::collections::BTreeMap;
/// Parameters from a tracker announce request
#[derive(Debug, Clone)]
pub struct AnnounceParams {
/// 20-byte info hash of the torrent
pub info_hash: [u8; 20],
/// 20-byte peer ID of the client
pub peer_id: [u8; 20],
/// Port the client is listening on
pub port: u16,
/// Total bytes uploaded
pub uploaded: u64,
/// Total bytes downloaded
pub downloaded: u64,
/// Bytes remaining to download
pub left: u64,
/// Whether to return compact peer list (6 bytes per peer)
pub compact: bool,
/// Optional event (started, stopped, completed)
pub event: Option<AnnounceEvent>,
}
/// Announce event types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnounceEvent {
Started,
Stopped,
Completed,
}
/// A bencoded value
#[derive(Debug, Clone)]
pub enum BencodeValue {
Integer(i64),
Bytes(Vec<u8>),
List(Vec<BencodeValue>),
Dict(BTreeMap<Vec<u8>, BencodeValue>),
}
impl BencodeValue {
/// Create a string value from a &str
#[inline]
pub fn string(s: &str) -> Self {
Self::Bytes(s.as_bytes().to_vec())
}
/// Create an integer value
#[inline]
pub fn integer(i: i64) -> Self {
Self::Integer(i)
}
/// Create an empty list
#[inline]
pub fn list() -> Self {
Self::List(Vec::new())
}
/// Create an empty dict
#[inline]
pub fn dict() -> Self {
Self::Dict(BTreeMap::new())
}
/// Add an item to a list (builder pattern)
#[inline]
pub fn push(mut self, value: BencodeValue) -> Self {
if let Self::List(ref mut list) = self {
list.push(value);
}
self
}
/// Insert a key-value pair into a dict (builder pattern)
#[inline]
pub fn insert(mut self, key: &str, value: BencodeValue) -> Self {
if let Self::Dict(ref mut dict) = self {
dict.insert(key.as_bytes().to_vec(), value);
}
self
}
/// Encode to bencoded bytes
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
self.encode_into(&mut buf);
buf
}
/// Encode into an existing buffer
pub fn encode_into(&self, buf: &mut Vec<u8>) {
match self {
Self::Integer(i) => {
buf.push(b'i');
buf.extend_from_slice(i.to_string().as_bytes());
buf.push(b'e');
}
Self::Bytes(bytes) => {
buf.extend_from_slice(bytes.len().to_string().as_bytes());
buf.push(b':');
buf.extend_from_slice(bytes);
}
Self::List(list) => {
buf.push(b'l');
for item in list {
item.encode_into(buf);
}
buf.push(b'e');
}
Self::Dict(dict) => {
buf.push(b'd');
// BTreeMap keeps keys sorted
for (key, value) in dict {
buf.extend_from_slice(key.len().to_string().as_bytes());
buf.push(b':');
buf.extend_from_slice(key);
value.encode_into(buf);
}
buf.push(b'e');
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_integer() {
assert_eq!(BencodeValue::integer(42).encode(), b"i42e");
assert_eq!(BencodeValue::integer(-1).encode(), b"i-1e");
assert_eq!(BencodeValue::integer(0).encode(), b"i0e");
}
#[test]
fn test_encode_string() {
assert_eq!(BencodeValue::string("spam").encode(), b"4:spam");
assert_eq!(BencodeValue::string("").encode(), b"0:");
}
#[test]
fn test_encode_list() {
let list = BencodeValue::list()
.push(BencodeValue::string("spam"))
.push(BencodeValue::integer(42));
assert_eq!(list.encode(), b"l4:spami42ee");
}
#[test]
fn test_encode_dict() {
let dict = BencodeValue::dict()
.insert("bar", BencodeValue::string("spam"))
.insert("foo", BencodeValue::integer(42));
assert_eq!(dict.encode(), b"d3:bar4:spam3:fooi42ee");
}
}
+2 -1
View File
@@ -14,6 +14,7 @@ pub mod progress;
pub mod session;
pub mod tracker;
pub use bencode::AnnounceParams;
pub use embedded::get_embedded_torrent;
pub use session::{DownloadProgress, TorrentSession};
pub use tracker::handle_announce;
pub use tracker::{handle_announce, PeerInfo, TopologyData};
+17 -13
View File
@@ -4,7 +4,7 @@
//! with persistent seeding and selective file downloads.
use anyhow::{Context, Result};
use librqbit::{AddTorrent, AddTorrentOptions, Api, ManagedTorrentHandle, Session, SessionOptions};
use librqbit::{AddTorrent, AddTorrentOptions, AddTorrentResponse, Api, ManagedTorrent, Session, SessionOptions, SessionPersistenceConfig};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
@@ -27,7 +27,7 @@ pub struct TorrentSession {
session: Arc<Session>,
api: Arc<Api>,
session_dir: PathBuf,
torrents: Arc<RwLock<HashMap<String, ManagedTorrentHandle>>>,
torrents: Arc<RwLock<HashMap<String, Arc<ManagedTorrent>>>>,
}
impl TorrentSession {
@@ -42,7 +42,7 @@ impl TorrentSession {
disable_dht: false,
disable_dht_persistence: false,
dht_config: None,
persistence: true,
persistence: Some(SessionPersistenceConfig::Json { folder: None }),
fastresume: true,
..Default::default()
};
@@ -54,7 +54,7 @@ impl TorrentSession {
let api = Api::new(Arc::clone(&session), None);
Ok(Self {
session: Arc::new(session),
session,
api: Arc::new(api),
session_dir,
torrents: Arc::new(RwLock::new(HashMap::new())),
@@ -79,20 +79,25 @@ impl TorrentSession {
let opts = AddTorrentOptions {
overwrite: false,
only_files_regex: None,
only_files: file_indices
.map(|indices| librqbit::AddTorrentOptions::only_files_from_vec(indices)),
only_files: file_indices,
output_folder: Some(save_path.to_string_lossy().to_string()),
..Default::default()
};
let add_torrent = AddTorrent::from_bytes(torrent_data);
let handle = self
let response = self
.session
.add_torrent(add_torrent, Some(opts))
.await
.context("Failed to add torrent")?;
let handle = match response {
AddTorrentResponse::Added(_, handle) => handle,
AddTorrentResponse::AlreadyManaged(_, handle) => handle,
AddTorrentResponse::ListOnly(_) => anyhow::bail!("Torrent was list-only, not added"),
};
let info_hash = handle.info_hash().as_string();
self.torrents
@@ -109,15 +114,14 @@ impl TorrentSession {
let handle = torrents.get(info_hash).context("Torrent not found")?;
let stats = handle.stats();
let state = handle.state();
Ok(DownloadProgress {
downloaded_bytes: stats.downloaded_bytes,
downloaded_bytes: stats.progress_bytes,
total_bytes: stats.total_bytes,
download_speed: stats.download_speed,
upload_speed: stats.upload_speed,
peers_connected: stats.peers_connected,
is_finished: state.is_finished(),
download_speed: stats.live.as_ref().map_or(0.0, |l| l.download_speed.mbps * 1024.0 * 1024.0),
upload_speed: stats.live.as_ref().map_or(0.0, |l| l.upload_speed.mbps * 1024.0 * 1024.0),
peers_connected: stats.live.as_ref().map_or(0, |l| l.snapshot.peer_stats.live as usize),
is_finished: stats.finished,
})
}
+184
View File
@@ -1 +1,185 @@
//! Fake tracker implementation for Exo topology-based peer discovery
//!
//! Instead of contacting real BitTorrent trackers, this module generates
//! tracker announce responses using Exo's cluster topology data.
use std::net::Ipv4Addr;
use anyhow::Result;
use crate::bencode::{AnnounceParams, BencodeValue};
/// Information about a peer in the Exo topology
#[derive(Debug, Clone)]
pub struct PeerInfo {
/// Unique node identifier in the Exo cluster
pub node_id: String,
/// IPv4 address of the peer
pub ip: Ipv4Addr,
/// BitTorrent listening port
pub port: u16,
/// Whether this peer has the complete torrent
pub has_complete: bool,
/// Priority for peer selection (higher = prefer)
pub priority: i32,
}
/// Topology data containing available peers
#[derive(Debug, Clone)]
pub struct TopologyData {
/// List of peers in the topology
pub peers: Vec<PeerInfo>,
}
/// Default announce interval in seconds
const DEFAULT_INTERVAL: i64 = 1800;
/// Handle a tracker announce request using Exo topology data
///
/// Returns a bencoded tracker response containing peers from the topology.
///
/// # Arguments
/// * `params` - Announce request parameters
/// * `topology` - Current Exo cluster topology
///
/// # Returns
/// Bencoded announce response as bytes
pub fn handle_announce(params: &AnnounceParams, topology: &TopologyData) -> Result<Vec<u8>> {
// Sort peers by priority (descending) for better peer selection
let mut peers: Vec<_> = topology.peers.iter().collect();
peers.sort_by(|a, b| b.priority.cmp(&a.priority));
let response = if params.compact {
// Compact format: 6 bytes per peer (4 IP + 2 port)
let mut peer_data = Vec::with_capacity(peers.len() * 6);
for peer in &peers {
peer_data.extend_from_slice(&peer.ip.octets());
peer_data.extend_from_slice(&peer.port.to_be_bytes());
}
BencodeValue::dict()
.insert("interval", BencodeValue::integer(DEFAULT_INTERVAL))
.insert("peers", BencodeValue::Bytes(peer_data))
} else {
// Non-compact format: list of dicts
let mut peer_list = BencodeValue::list();
for peer in &peers {
let peer_dict = BencodeValue::dict()
.insert("ip", BencodeValue::string(&peer.ip.to_string()))
.insert("port", BencodeValue::integer(i64::from(peer.port)))
.insert("peer id", BencodeValue::Bytes(vec![0u8; 20])); // Placeholder peer ID
peer_list = peer_list.push(peer_dict);
}
BencodeValue::dict()
.insert("interval", BencodeValue::integer(DEFAULT_INTERVAL))
.insert("peers", peer_list)
};
Ok(response.encode())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_params(compact: bool) -> AnnounceParams {
AnnounceParams {
info_hash: [0u8; 20],
peer_id: [0u8; 20],
port: 6881,
uploaded: 0,
downloaded: 0,
left: 1000,
compact,
event: None,
}
}
fn make_test_topology() -> TopologyData {
TopologyData {
peers: vec![
PeerInfo {
node_id: "node1".to_string(),
ip: Ipv4Addr::new(192, 168, 1, 1),
port: 6881,
has_complete: true,
priority: 10,
},
PeerInfo {
node_id: "node2".to_string(),
ip: Ipv4Addr::new(192, 168, 1, 2),
port: 6882,
has_complete: false,
priority: 5,
},
],
}
}
#[test]
fn test_compact_response() {
let params = make_test_params(true);
let topology = make_test_topology();
let response = handle_announce(&params, &topology).unwrap();
// Should contain "interval" and "peers" keys
assert!(response.starts_with(b"d"));
assert!(response.ends_with(b"e"));
// Verify we have 12 bytes of peer data (2 peers * 6 bytes)
// The compact peers field should be "12:<12 bytes>"
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("8:interval"));
assert!(response_str.contains("5:peers"));
}
#[test]
fn test_non_compact_response() {
let params = make_test_params(false);
let topology = make_test_topology();
let response = handle_announce(&params, &topology).unwrap();
// Should contain peers as a list
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("8:interval"));
assert!(response_str.contains("5:peers"));
assert!(response_str.contains("2:ip"));
assert!(response_str.contains("4:port"));
}
#[test]
fn test_peer_priority_ordering() {
let params = make_test_params(true);
let topology = make_test_topology();
let response = handle_announce(&params, &topology).unwrap();
// In compact format, first peer should be node1 (priority 10)
// which is 192.168.1.1:6881
// Look for the peer data after "5:peers12:"
let peers_marker = b"5:peers12:";
let pos = response
.windows(peers_marker.len())
.position(|w| w == peers_marker)
.unwrap();
let peer_data = &response[pos + peers_marker.len()..pos + peers_marker.len() + 6];
// First peer should be 192.168.1.1 (node1 with higher priority)
assert_eq!(&peer_data[0..4], &[192, 168, 1, 1]);
}
#[test]
fn test_empty_topology() {
let params = make_test_params(true);
let topology = TopologyData { peers: vec![] };
let response = handle_announce(&params, &topology).unwrap();
// Should still be valid bencoded response with empty peers
assert!(response.starts_with(b"d"));
assert!(response.ends_with(b"e"));
}
}
+34 -46
View File
@@ -208,7 +208,7 @@ impl TorrentSessionHandle {
/// Info hash as hex string
fn add_torrent(
&self,
py: Python<'_>,
_py: Python<'_>,
torrent_data: Vec<u8>,
save_path: String,
file_indices: Option<Vec<usize>>,
@@ -216,18 +216,16 @@ impl TorrentSessionHandle {
let session = Arc::clone(&self.session);
let save_path = PathBuf::from(save_path);
py.allow_threads(|| {
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async {
session
.lock()
.await
.add_torrent(torrent_data, save_path, file_indices)
.await
})
.pyerr()
})
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async {
session
.lock()
.await
.add_torrent(torrent_data, save_path, file_indices)
.await
})
.pyerr()
}
/// Get download progress for a torrent
@@ -240,12 +238,10 @@ impl TorrentSessionHandle {
fn get_progress(&self, py: Python<'_>, info_hash: String) -> PyResult<Py<PyDict>> {
let session = Arc::clone(&self.session);
let progress: DownloadProgress = py.allow_threads(|| {
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.get_progress(&info_hash).await })
.pyerr()
})?;
let progress: DownloadProgress = tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.get_progress(&info_hash).await })
.pyerr()?;
let dict = PyDict::new(py);
dict.set_item("downloaded_bytes", progress.downloaded_bytes)?;
@@ -262,59 +258,51 @@ impl TorrentSessionHandle {
///
/// Args:
/// info_hash: Torrent info hash
fn wait_until_completed(&self, py: Python<'_>, info_hash: String) -> PyResult<()> {
fn wait_until_completed(&self, _py: Python<'_>, info_hash: String) -> PyResult<()> {
let session = Arc::clone(&self.session);
py.allow_threads(|| {
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.wait_until_completed(&info_hash).await })
.pyerr()
})
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.wait_until_completed(&info_hash).await })
.pyerr()
}
/// Enable seeding for a torrent
///
/// Args:
/// info_hash: Torrent info hash
fn enable_seeding(&self, py: Python<'_>, info_hash: String) -> PyResult<()> {
fn enable_seeding(&self, _py: Python<'_>, info_hash: String) -> PyResult<()> {
let session = Arc::clone(&self.session);
py.allow_threads(|| {
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.enable_seeding(&info_hash).await })
.pyerr()
})
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.enable_seeding(&info_hash).await })
.pyerr()
}
/// Remove a torrent from the session
///
/// Args:
/// info_hash: Torrent info hash
fn remove_torrent(&self, py: Python<'_>, info_hash: String) -> PyResult<()> {
fn remove_torrent(&self, _py: Python<'_>, info_hash: String) -> PyResult<()> {
let session = Arc::clone(&self.session);
py.allow_threads(|| {
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.remove_torrent(&info_hash).await })
.pyerr()
})
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { session.lock().await.remove_torrent(&info_hash).await })
.pyerr()
}
/// List all torrents in the session
///
/// Returns:
/// List of info hashes
fn list_torrents(&self, py: Python<'_>) -> PyResult<Vec<String>> {
fn list_torrents(&self, _py: Python<'_>) -> PyResult<Vec<String>> {
let session = Arc::clone(&self.session);
py.allow_threads(|| {
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { Ok(session.lock().await.list_torrents().await) })
})
tokio::runtime::Runtime::new()
.pyerr()?
.block_on(async { Ok(session.lock().await.list_torrents().await) })
}
}