diff --git a/Cargo.lock b/Cargo.lock index 20bd0d0e..ab566b55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/rust/downloads/Cargo.toml b/rust/downloads/Cargo.toml index cc3a34b8..5c432628 100644 --- a/rust/downloads/Cargo.toml +++ b/rust/downloads/Cargo.toml @@ -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" diff --git a/rust/downloads/src/bencode.rs b/rust/downloads/src/bencode.rs index 8b137891..dc18df53 100644 --- a/rust/downloads/src/bencode.rs +++ b/rust/downloads/src/bencode.rs @@ -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, +} + +/// 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), + List(Vec), + Dict(BTreeMap, 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 { + let mut buf = Vec::new(); + self.encode_into(&mut buf); + buf + } + + /// Encode into an existing buffer + pub fn encode_into(&self, buf: &mut Vec) { + 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"); + } +} diff --git a/rust/downloads/src/lib.rs b/rust/downloads/src/lib.rs index 4f286a1a..e936535e 100644 --- a/rust/downloads/src/lib.rs +++ b/rust/downloads/src/lib.rs @@ -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}; diff --git a/rust/downloads/src/session.rs b/rust/downloads/src/session.rs index bc1930d6..8d1404e3 100644 --- a/rust/downloads/src/session.rs +++ b/rust/downloads/src/session.rs @@ -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, api: Arc, session_dir: PathBuf, - torrents: Arc>>, + torrents: Arc>>>, } 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, }) } diff --git a/rust/downloads/src/tracker.rs b/rust/downloads/src/tracker.rs index 8b137891..513d5a94 100644 --- a/rust/downloads/src/tracker.rs +++ b/rust/downloads/src/tracker.rs @@ -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, +} + +/// 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> { + // 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(¶ms, &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(¶ms, &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(¶ms, &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(¶ms, &topology).unwrap(); + + // Should still be valid bencoded response with empty peers + assert!(response.starts_with(b"d")); + assert!(response.ends_with(b"e")); + } +} diff --git a/rust/exo_pyo3_bindings/src/downloads.rs b/rust/exo_pyo3_bindings/src/downloads.rs index 4e1e9663..6d23de6a 100644 --- a/rust/exo_pyo3_bindings/src/downloads.rs +++ b/rust/exo_pyo3_bindings/src/downloads.rs @@ -208,7 +208,7 @@ impl TorrentSessionHandle { /// Info hash as hex string fn add_torrent( &self, - py: Python<'_>, + _py: Python<'_>, torrent_data: Vec, save_path: String, file_indices: Option>, @@ -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> { 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> { + fn list_torrents(&self, _py: Python<'_>) -> PyResult> { 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) }) } }