diff --git a/CHANGELOG.md b/CHANGELOG.md index b154bca2..ba21cf46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,31 @@ upgrade notes in each release describe what to back up and check. ## [Unreleased] +## [0.2.0-beta.5] - 2026-11-03 + +### Added + +- qbit web-seed endpoints, tracker tier and downloaded-count reporting, app + network and shutdown endpoints, torrent metadata controls, and in-flight + piece-state reporting. +- Per-file priorities, per-category share limits, torrent location and rename + controls, missing-payload diagnosis, and richer rtbit-fluent torrent and + filter management. + +### Changed + +- Rechecks now report progress, enforce concurrent and throughput limits, and + persist the related advanced settings. Session recovery and legacy session + directory migration are more robust. +- External URL fetches are hardened, shutdown persistence is smoke-tested, and + release artifacts receive fluent-client smoke coverage. + +### Fixed + +- Tracker and DHT retry backoff, low-disk protection, crash-safe state writes, + and graceful shutdown behavior are now enforced across the daemon and UI. + + ### Changed - The desktop client is now the gpui-based `rtbit-fluent`. Releases ship @@ -347,7 +372,8 @@ persistent session directory before upgrading. - The Docker build and web UI were consolidated so the shipped interface is built from the reviewed source tree. -[Unreleased]: https://github.com/TheDancingDeveloper-org/rustTorrent/compare/v0.2.0-beta.4...HEAD +[0.2.0-beta.5]: https://github.com/TheDancingDeveloper-org/rustTorrent/releases/tag/v0.2.0-beta.5 +[Unreleased]: https://github.com/TheDancingDeveloper-org/rustTorrent/compare/v0.2.0-beta.5...HEAD [0.2.0-beta.4]: https://github.com/TheDancingDeveloper-org/rustTorrent/releases/tag/v0.2.0-beta.4 [0.2.0-beta.3]: https://github.com/TheDancingDeveloper-org/rustTorrent/releases/tag/v0.2.0-beta.3 [0.2.0-beta.1]: https://github.com/TheDancingDeveloper-org/rustTorrent/releases/tag/v0.2.0-beta.1 diff --git a/Cargo.lock b/Cargo.lock index 81e12c6c..f4d5e1d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7451,7 +7451,7 @@ dependencies = [ [[package]] name = "rtbit" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bytes", @@ -7485,7 +7485,7 @@ dependencies = [ [[package]] name = "rtbit-client" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "http", "reqwest", @@ -7499,7 +7499,7 @@ dependencies = [ [[package]] name = "rtbit-desktop" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "directories", @@ -7531,11 +7531,11 @@ dependencies = [ [[package]] name = "rtbit-desktop-menu" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" [[package]] name = "rtbit-fluent" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "fluent-app", "fluent-core", @@ -7551,7 +7551,7 @@ dependencies = [ [[package]] name = "rtbit-ui-core" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "rtbit-client", "secret-service", @@ -8778,7 +8778,7 @@ dependencies = [ [[package]] name = "swarmforge" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "arc-swap", @@ -8867,7 +8867,7 @@ dependencies = [ [[package]] name = "swarmforge-bencode" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "arrayvec", @@ -8882,7 +8882,7 @@ dependencies = [ [[package]] name = "swarmforge-buffers" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "bytes", "serde", @@ -8892,14 +8892,14 @@ dependencies = [ [[package]] name = "swarmforge-clone-to-owned" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "bytes", ] [[package]] name = "swarmforge-core" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bytes", @@ -8929,7 +8929,7 @@ dependencies = [ [[package]] name = "swarmforge-dht" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "aws-lc-rs", @@ -8961,7 +8961,7 @@ dependencies = [ [[package]] name = "swarmforge-lsd" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "atoi 3.1.1", @@ -8981,7 +8981,7 @@ dependencies = [ [[package]] name = "swarmforge-peer-protocol" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bitvec", @@ -9001,7 +9001,7 @@ dependencies = [ [[package]] name = "swarmforge-sha1-wrapper" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "assert_cfg", "aws-lc-rs", @@ -9011,7 +9011,7 @@ dependencies = [ [[package]] name = "swarmforge-tracker-comms" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "async-stream", @@ -9038,7 +9038,7 @@ dependencies = [ [[package]] name = "swarmforge-upnp" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bstr", @@ -9059,7 +9059,7 @@ dependencies = [ [[package]] name = "swarmforge-upnp-serve" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index fe411769..09b3c539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ all = { level = "warn", priority = -1 } unused = "warn" [workspace.package] -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" [profile.dev] panic = "abort" @@ -53,18 +53,18 @@ debug = true [workspace.dependencies] # Internal crates. Version-plus-path keeps publishing metadata while making this # monorepo's sources authoritative for all local builds. -librtbit = { version = "0.2.0-beta.4", path = "crates/librtbit", package = "swarmforge", default-features = false } -bencode = { version = "0.2.0-beta.4", path = "crates/librtbit-bencode", package = "swarmforge-bencode", default-features = false } -buffers = { version = "0.2.0-beta.4", path = "crates/librtbit-buffers", package = "swarmforge-buffers" } -clone_to_owned = { version = "0.2.0-beta.4", path = "crates/librtbit-clone-to-owned", package = "swarmforge-clone-to-owned" } -librtbit-core = { version = "0.2.0-beta.4", path = "crates/librtbit-core", package = "swarmforge-core", default-features = false } -dht = { version = "0.2.0-beta.4", path = "crates/librtbit-dht", package = "swarmforge-dht", default-features = false } -librtbit-lsd = { version = "0.2.0-beta.4", path = "crates/librtbit-lsd", package = "swarmforge-lsd", default-features = false } -peer_binary_protocol = { version = "0.2.0-beta.4", path = "crates/librtbit-peer-protocol", package = "swarmforge-peer-protocol", default-features = false } -sha1w = { version = "0.2.0-beta.4", path = "crates/librtbit-sha1-wrapper", package = "swarmforge-sha1-wrapper", default-features = false } -tracker_comms = { version = "0.2.0-beta.4", path = "crates/librtbit-tracker-comms", package = "swarmforge-tracker-comms", default-features = false } -librtbit-upnp = { version = "0.2.0-beta.4", path = "crates/librtbit-upnp", package = "swarmforge-upnp" } -upnp-serve = { version = "0.2.0-beta.4", path = "crates/librtbit-upnp-serve", package = "swarmforge-upnp-serve", default-features = false } +librtbit = { version = "0.2.0-beta.5", path = "crates/librtbit", package = "swarmforge", default-features = false } +bencode = { version = "0.2.0-beta.5", path = "crates/librtbit-bencode", package = "swarmforge-bencode", default-features = false } +buffers = { version = "0.2.0-beta.5", path = "crates/librtbit-buffers", package = "swarmforge-buffers" } +clone_to_owned = { version = "0.2.0-beta.5", path = "crates/librtbit-clone-to-owned", package = "swarmforge-clone-to-owned" } +librtbit-core = { version = "0.2.0-beta.5", path = "crates/librtbit-core", package = "swarmforge-core", default-features = false } +dht = { version = "0.2.0-beta.5", path = "crates/librtbit-dht", package = "swarmforge-dht", default-features = false } +librtbit-lsd = { version = "0.2.0-beta.5", path = "crates/librtbit-lsd", package = "swarmforge-lsd", default-features = false } +peer_binary_protocol = { version = "0.2.0-beta.5", path = "crates/librtbit-peer-protocol", package = "swarmforge-peer-protocol", default-features = false } +sha1w = { version = "0.2.0-beta.5", path = "crates/librtbit-sha1-wrapper", package = "swarmforge-sha1-wrapper", default-features = false } +tracker_comms = { version = "0.2.0-beta.5", path = "crates/librtbit-tracker-comms", package = "swarmforge-tracker-comms", default-features = false } +librtbit-upnp = { version = "0.2.0-beta.5", path = "crates/librtbit-upnp", package = "swarmforge-upnp" } +upnp-serve = { version = "0.2.0-beta.5", path = "crates/librtbit-upnp-serve", package = "swarmforge-upnp-serve", default-features = false } # External crates anyhow = "1" diff --git a/SECURITY.md b/SECURITY.md index a1f6d274..58942f00 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,7 @@ you in the advisory if you wish. ## Supported versions Security fixes are made against the latest release candidate or release tag -(currently `0.2.0-beta.4`) and the `main` branch. Earlier beta releases are not +(currently `0.2.0-beta.5`) and the `main` branch. Earlier beta releases are not supported; upgrade to the latest tag to receive fixes. ## Deployment notes diff --git a/benchv2/Cargo.lock b/benchv2/Cargo.lock index f90f32a9..ceb940d5 100644 --- a/benchv2/Cargo.lock +++ b/benchv2/Cargo.lock @@ -102,7 +102,7 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "benchv2" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bollard", diff --git a/benchv2/Cargo.toml b/benchv2/Cargo.toml index d9e22fa4..74775113 100644 --- a/benchv2/Cargo.toml +++ b/benchv2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "benchv2" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2021" [dependencies] diff --git a/crates/librtbit-bencode/Cargo.toml b/crates/librtbit-bencode/Cargo.toml index b0dd00f3..2e493d2d 100644 --- a/crates/librtbit-bencode/Cargo.toml +++ b/crates/librtbit-bencode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-bencode" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Bencode serialization and deserialization using Serde" license = "Apache-2.0" diff --git a/crates/librtbit-buffers/Cargo.toml b/crates/librtbit-buffers/Cargo.toml index 8acefd1e..5a702192 100644 --- a/crates/librtbit-buffers/Cargo.toml +++ b/crates/librtbit-buffers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-buffers" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Utils to work with byte buffers in librtbit source code" license = "Apache-2.0" diff --git a/crates/librtbit-clone-to-owned/Cargo.toml b/crates/librtbit-clone-to-owned/Cargo.toml index d95164f9..9d2b29aa 100644 --- a/crates/librtbit-clone-to-owned/Cargo.toml +++ b/crates/librtbit-clone-to-owned/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-clone-to-owned" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Util traits to represent something that can be made owned and change type at the same time" license = "Apache-2.0" diff --git a/crates/librtbit-core/Cargo.toml b/crates/librtbit-core/Cargo.toml index 40da37a0..8ca0d79a 100644 --- a/crates/librtbit-core/Cargo.toml +++ b/crates/librtbit-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-core" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Core types and utilities for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit-dht/Cargo.toml b/crates/librtbit-dht/Cargo.toml index abedc011..a6cc461a 100644 --- a/crates/librtbit-dht/Cargo.toml +++ b/crates/librtbit-dht/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-dht" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Kademlia DHT implementation for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit-lsd/Cargo.toml b/crates/librtbit-lsd/Cargo.toml index 9a0c50e9..7950cdf8 100644 --- a/crates/librtbit-lsd/Cargo.toml +++ b/crates/librtbit-lsd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-lsd" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "BEP 14 Local Service Discovery for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit-peer-protocol/Cargo.toml b/crates/librtbit-peer-protocol/Cargo.toml index 85122116..413d7576 100644 --- a/crates/librtbit-peer-protocol/Cargo.toml +++ b/crates/librtbit-peer-protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-peer-protocol" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "BitTorrent peer wire protocol implementation for the rtbit client" license = "Apache-2.0" diff --git a/crates/librtbit-sha1-wrapper/Cargo.toml b/crates/librtbit-sha1-wrapper/Cargo.toml index 972113e4..d446c570 100644 --- a/crates/librtbit-sha1-wrapper/Cargo.toml +++ b/crates/librtbit-sha1-wrapper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-sha1-wrapper" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Pluggable SHA1/SHA256 abstraction for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit-tracker-comms/Cargo.toml b/crates/librtbit-tracker-comms/Cargo.toml index f0508bc0..cb24c568 100644 --- a/crates/librtbit-tracker-comms/Cargo.toml +++ b/crates/librtbit-tracker-comms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-tracker-comms" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "HTTP and UDP tracker communication for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit-upnp-serve/Cargo.toml b/crates/librtbit-upnp-serve/Cargo.toml index ddfb8616..52db9cbd 100644 --- a/crates/librtbit-upnp-serve/Cargo.toml +++ b/crates/librtbit-upnp-serve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-upnp-serve" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Simple UPnP MediaServer implementation for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit-upnp/Cargo.toml b/crates/librtbit-upnp/Cargo.toml index ab90576c..2ab345a8 100644 --- a/crates/librtbit-upnp/Cargo.toml +++ b/crates/librtbit-upnp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge-upnp" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "UPnP port forwarding client for the rtbit BitTorrent client" license = "Apache-2.0" diff --git a/crates/librtbit/Cargo.toml b/crates/librtbit/Cargo.toml index e75c4648..2a948c6e 100644 --- a/crates/librtbit/Cargo.toml +++ b/crates/librtbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swarmforge" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" edition = "2024" description = "Core BitTorrent client library for the rtbit torrent client" license = "Apache-2.0" diff --git a/crates/librtbit/src/api.rs b/crates/librtbit/src/api.rs index fc4a52fd..252ff400 100644 --- a/crates/librtbit/src/api.rs +++ b/crates/librtbit/src/api.rs @@ -398,6 +398,45 @@ impl Api { Ok(Default::default()) } + pub async fn api_torrent_action_add_web_seeds( + &self, + idx: TorrentIdOrHash, + urls: Vec, + ) -> Result { + let handle = self.mgr_handle(idx)?; + self.session + .add_web_seeds(&handle, urls) + .await + .with_status(StatusCode::BAD_REQUEST)?; + Ok(Default::default()) + } + pub async fn api_torrent_action_remove_web_seeds( + &self, + idx: TorrentIdOrHash, + urls: Vec, + ) -> Result { + let handle = self.mgr_handle(idx)?; + let removed = self + .session + .remove_web_seeds(&handle, &urls) + .await + .with_status(StatusCode::BAD_REQUEST)?; + Ok(RemoveTrackersResponse { removed }) + } + pub async fn api_torrent_action_edit_web_seed( + &self, + idx: TorrentIdOrHash, + orig: String, + new: String, + ) -> Result { + let handle = self.mgr_handle(idx)?; + self.session + .edit_web_seed(&handle, &orig, &new) + .await + .with_status(StatusCode::BAD_REQUEST)?; + Ok(Default::default()) + } + pub async fn api_torrent_action_add_trackers( &self, idx: TorrentIdOrHash, diff --git a/crates/librtbit/src/http_api/handlers/qbit/mod.rs b/crates/librtbit/src/http_api/handlers/qbit/mod.rs index 39e1cf5c..9710510d 100644 --- a/crates/librtbit/src/http_api/handlers/qbit/mod.rs +++ b/crates/librtbit/src/http_api/handlers/qbit/mod.rs @@ -626,6 +626,9 @@ pub(crate) fn make_qbit_router(api_state: ApiState) -> Router { .route("/renameFile", post(h_torrents_rename_file)) .route("/renameFolder", post(h_torrents_rename_folder)) .route("/delete", post(h_torrents_delete)) + .route("/addWebSeeds", post(h_torrents_add_web_seeds)) + .route("/removeWebSeeds", post(h_torrents_remove_web_seeds)) + .route("/editWebSeed", post(h_torrents_edit_web_seed)) .route("/addTrackers", post(h_torrents_add_trackers)) .route("/removeTrackers", post(h_torrents_remove_trackers)) .route("/editTracker", post(h_torrents_edit_tracker)) diff --git a/crates/librtbit/src/http_api/handlers/qbit/tests.rs b/crates/librtbit/src/http_api/handlers/qbit/tests.rs index 7b3088b4..5f1d5f4d 100644 --- a/crates/librtbit/src/http_api/handlers/qbit/tests.rs +++ b/crates/librtbit/src/http_api/handlers/qbit/tests.rs @@ -560,6 +560,112 @@ async fn queue_and_force_start_routes_succeed() { } } +#[tokio::test] +async fn web_seed_routes_round_trip_and_reject_invalid_urls() { + let (state, session, output) = qbit_state().await; + let payload = output.path().join("web-seed-payload.bin"); + std::fs::write(&payload, vec![0x72; 32 * 1024]).unwrap(); + let torrent = create_torrent( + &payload, + CreateTorrentOptions { + piece_length: Some(16_384), + ..Default::default() + }, + &BlockingSpawner::new(1), + ) + .await + .unwrap() + .as_bytes() + .unwrap() + .to_vec(); + let handle = session + .add_torrent( + AddTorrent::from_bytes(torrent), + Some(AddTorrentOptions { + paused: true, + overwrite: true, + output_folder: Some(output.path().join("dl").to_string_lossy().into_owned()), + ..Default::default() + }), + ) + .await + .unwrap() + .into_handle() + .unwrap(); + handle.wait_until_initialized().await.unwrap(); + let hash = handle.info_hash().as_string(); + let router = super::make_qbit_router(state.api_state.clone()); + let post = |action: &str, body: String| { + http::Request::builder() + .method("POST") + .uri(format!("/torrents/{action}")) + .header("content-type", "application/x-www-form-urlencoded") + .body(axum::body::Body::from(body)) + .unwrap() + }; + let list = || { + let router = router.clone(); + let hash = hash.clone(); + async move { + let response = router + .oneshot( + http::Request::builder() + .uri(format!("/torrents/webseeds?hash={hash}")) + .body(axum::body::Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + serde_json::from_slice::>(&body).unwrap() + } + }; + + let response = router + .clone() + .oneshot(post( + "addWebSeeds", + format!("hash={hash}&urls=https%3A%2F%2Fseed.example%2Fa"), + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(list().await[0]["url"], "https://seed.example/a"); + + let response = router + .clone() + .oneshot(post( + "editWebSeed", + format!( + "hash={hash}&origUrl=https%3A%2F%2Fseed.example%2Fa&newUrl=https%3A%2F%2Fseed.example%2Fb" + ), + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(list().await[0]["url"], "https://seed.example/b"); + + let response = router + .clone() + .oneshot(post( + "removeWebSeeds", + format!("hash={hash}&urls=https%3A%2F%2Fseed.example%2Fb"), + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert!(list().await.is_empty()); + + let response = router + .oneshot(post("addWebSeeds", format!("hash={hash}&urls=not-a-url"))) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); +} + #[tokio::test] async fn recheck_endpoint_accepts_qbit_hash_form() { let (state, session, output) = qbit_state().await; diff --git a/crates/librtbit/src/http_api/handlers/qbit/torrent_actions.rs b/crates/librtbit/src/http_api/handlers/qbit/torrent_actions.rs index 9d9cea41..45053a7e 100644 --- a/crates/librtbit/src/http_api/handlers/qbit/torrent_actions.rs +++ b/crates/librtbit/src/http_api/handlers/qbit/torrent_actions.rs @@ -812,6 +812,108 @@ pub(super) async fn h_torrents_set_super_seeding( (StatusCode::OK, "Ok.") } +#[derive(Deserialize, Default)] +pub(super) struct AddWebSeedsForm { + #[serde(default)] + pub(super) hash: String, + #[serde(default)] + pub(super) urls: String, +} +#[derive(Deserialize, Default)] +pub(super) struct RemoveWebSeedsForm { + #[serde(default)] + pub(super) hash: String, + #[serde(default)] + pub(super) urls: String, +} +#[derive(Deserialize, Default)] +pub(super) struct EditWebSeedForm { + #[serde(default)] + pub(super) hash: String, + #[serde(default, alias = "origUrl")] + pub(super) orig_url: String, + #[serde(default, alias = "newUrl")] + pub(super) new_url: String, +} + +pub(super) async fn h_torrents_add_web_seeds( + State(state): State>, + body: Bytes, +) -> impl IntoResponse { + let form: AddWebSeedsForm = serde_urlencoded::from_bytes(&body).unwrap_or_default(); + if form.hash.is_empty() { + return qbit_error(StatusCode::BAD_REQUEST, "hash is required"); + } + let Ok(idx) = TorrentIdOrHash::parse(&form.hash) else { + return qbit_error(StatusCode::NOT_FOUND, "Not found"); + }; + let urls = form + .urls + .lines() + .map(str::trim) + .filter(|url| !url.is_empty()) + .map(str::to_owned) + .collect(); + match state + .api_state + .api + .api_torrent_action_add_web_seeds(idx, urls) + .await + { + Ok(_) => (StatusCode::OK, "Ok.").into_response(), + Err(_error) => qbit_error(StatusCode::BAD_REQUEST, "invalid web seed URL"), + } +} +pub(super) async fn h_torrents_remove_web_seeds( + State(state): State>, + body: Bytes, +) -> impl IntoResponse { + let form: RemoveWebSeedsForm = serde_urlencoded::from_bytes(&body).unwrap_or_default(); + if form.hash.is_empty() { + return qbit_error(StatusCode::BAD_REQUEST, "hash is required"); + } + let Ok(idx) = TorrentIdOrHash::parse(&form.hash) else { + return qbit_error(StatusCode::NOT_FOUND, "Not found"); + }; + let urls = form + .urls + .split('|') + .map(str::trim) + .filter(|url| !url.is_empty()) + .map(str::to_owned) + .collect(); + match state + .api_state + .api + .api_torrent_action_remove_web_seeds(idx, urls) + .await + { + Ok(_) => (StatusCode::OK, "Ok.").into_response(), + Err(_) => qbit_error(StatusCode::BAD_REQUEST, "invalid web seed URL"), + } +} +pub(super) async fn h_torrents_edit_web_seed( + State(state): State>, + body: Bytes, +) -> impl IntoResponse { + let form: EditWebSeedForm = serde_urlencoded::from_bytes(&body).unwrap_or_default(); + if form.hash.is_empty() { + return qbit_error(StatusCode::BAD_REQUEST, "hash is required"); + } + let Ok(idx) = TorrentIdOrHash::parse(&form.hash) else { + return qbit_error(StatusCode::NOT_FOUND, "Not found"); + }; + match state + .api_state + .api + .api_torrent_action_edit_web_seed(idx, form.orig_url, form.new_url) + .await + { + Ok(_) => (StatusCode::OK, "Ok.").into_response(), + Err(_) => qbit_error(StatusCode::BAD_REQUEST, "invalid web seed URL"), + } +} + #[derive(Deserialize, Default)] pub(super) struct AddTrackersForm { #[serde(default)] diff --git a/crates/librtbit/src/http_api/handlers/qbit/torrents.rs b/crates/librtbit/src/http_api/handlers/qbit/torrents.rs index 5e6fe428..6eb59610 100644 --- a/crates/librtbit/src/http_api/handlers/qbit/torrents.rs +++ b/crates/librtbit/src/http_api/handlers/qbit/torrents.rs @@ -868,6 +868,7 @@ pub(super) async fn h_torrents_webseeds( let seeds: Vec = handle .shared() .web_seed_urls + .read() .iter() .map(|url| QbitWebSeed { url: url.clone() }) .collect(); diff --git a/crates/librtbit/src/http_api/handlers/qbit_parity_spec.json b/crates/librtbit/src/http_api/handlers/qbit_parity_spec.json index 88cfafbf..e3d21c90 100644 --- a/crates/librtbit/src/http_api/handlers/qbit_parity_spec.json +++ b/crates/librtbit/src/http_api/handlers/qbit_parity_spec.json @@ -645,7 +645,10 @@ { "endpoint": "torrents/addWebSeeds", "method": "POST", - "status": "missing" + "status": "full", + "response": { + "content": "text" + } }, { "endpoint": "torrents/bottomPrio", @@ -759,7 +762,10 @@ { "endpoint": "torrents/editWebSeed", "method": "POST", - "status": "missing" + "status": "full", + "response": { + "content": "text" + } }, { "endpoint": "torrents/export", @@ -1063,7 +1069,10 @@ { "endpoint": "torrents/removeWebSeeds", "method": "POST", - "status": "missing" + "status": "full", + "response": { + "content": "text" + } }, { "endpoint": "torrents/rename", diff --git a/crates/librtbit/src/session/mod.rs b/crates/librtbit/src/session/mod.rs index 7491561b..8d006a6f 100644 --- a/crates/librtbit/src/session/mod.rs +++ b/crates/librtbit/src/session/mod.rs @@ -1459,7 +1459,7 @@ impl Session { connector: self.connector.clone(), session: Arc::downgrade(self), magnet_name: name, - web_seed_urls, + web_seed_urls: web_seed_urls.into(), category: RwLock::new(opts.category.clone()), tags: RwLock::new(crate::tag::normalize_tags(&opts.tags)), ratelimit_override: RwLock::new(opts.ratelimits), @@ -1780,6 +1780,83 @@ impl Session { Ok(()) } + pub async fn add_web_seeds( + &self, + handle: &ManagedTorrentHandle, + urls: Vec, + ) -> anyhow::Result<()> { + let parsed = urls + .into_iter() + .map(|url| { + url::Url::parse(&url) + .with_context(|| format!("invalid web seed URL {url:?}")) + .and_then(|parsed| { + if matches!(parsed.scheme(), "http" | "https") { + Ok(url) + } else { + bail!("unsupported web seed URL scheme {:?}", parsed.scheme()) + } + }) + }) + .collect::>>()?; + { + let mut seeds = handle.shared().web_seed_urls.write(); + for url in parsed { + if !seeds.contains(&url) { + seeds.push(url); + } + } + } + self.try_update_persistence_metadata(handle).await; + Ok(()) + } + + pub async fn remove_web_seeds( + &self, + handle: &ManagedTorrentHandle, + urls: &[String], + ) -> anyhow::Result { + let removed = { + let mut seeds = handle.shared().web_seed_urls.write(); + let before = seeds.len(); + seeds.retain(|url| !urls.iter().any(|candidate| candidate == url)); + before - seeds.len() + }; + if removed > 0 { + self.try_update_persistence_metadata(handle).await; + } + Ok(removed) + } + + pub async fn edit_web_seed( + &self, + handle: &ManagedTorrentHandle, + orig: &str, + new: &str, + ) -> anyhow::Result<()> { + url::Url::parse(new) + .with_context(|| format!("invalid web seed URL {new:?}")) + .and_then(|parsed| { + if matches!(parsed.scheme(), "http" | "https") { + Ok(()) + } else { + bail!("unsupported web seed URL scheme {:?}", parsed.scheme()) + } + })?; + { + let mut seeds = handle.shared().web_seed_urls.write(); + let Some(index) = seeds.iter().position(|url| url == orig) else { + bail!("web seed {orig:?} is not configured") + }; + if seeds.iter().any(|url| url == new) { + bail!("web seed {new:?} is already configured") + } + seeds[index] = new.to_owned(); + } + self.try_update_persistence_metadata(handle).await; + Ok(()) + } + pub async fn add_trackers( self: &Arc, handle: &ManagedTorrentHandle, diff --git a/crates/librtbit/src/session_persistence/json.rs b/crates/librtbit/src/session_persistence/json.rs index be3f4e6d..319236ec 100644 --- a/crates/librtbit/src/session_persistence/json.rs +++ b/crates/librtbit/src/session_persistence/json.rs @@ -160,6 +160,7 @@ impl JsonSessionPersistenceStore { let (policy, share_limits, uploaded_bytes, part_files) = runtime_fields(torrent); let (output_folder, output_folder_root) = super::location_fields(torrent); let st = SerializedTorrent { + web_seed_urls: Some(torrent.shared().web_seed_urls.read().clone()), trackers: torrent .shared() .trackers @@ -712,6 +713,26 @@ mod tests { assert_eq!(opts.tags, vec!["hd", "tv"]); } + #[test] + fn serialized_torrent_empty_web_seed_override_survives_restart() { + let legacy = serde_json::json!({ + "info_hash": "0000000000000000000000000000000000000000", + "trackers": [], + "output_folder": "/tmp", + "only_files": null, + "is_paused": false, + }); + let legacy_state: SerializedTorrent = serde_json::from_value(legacy.clone()).unwrap(); + assert!(legacy_state.restored_state().web_seed_urls.is_none()); + + let mut removed_all = legacy; + removed_all["web_seed_urls"] = serde_json::json!([]); + let saved: SerializedTorrent = serde_json::from_value(removed_all).unwrap(); + let restarted: SerializedTorrent = + serde_json::from_value(serde_json::to_value(saved).unwrap()).unwrap(); + assert_eq!(restarted.restored_state().web_seed_urls, Some(Vec::new())); + } + #[test] fn serialized_torrent_file_renames_round_trip_and_default_empty() { let legacy = serde_json::json!({ diff --git a/crates/librtbit/src/session_persistence/mod.rs b/crates/librtbit/src/session_persistence/mod.rs index 0271ac4a..1908cd20 100644 --- a/crates/librtbit/src/session_persistence/mod.rs +++ b/crates/librtbit/src/session_persistence/mod.rs @@ -224,6 +224,11 @@ pub struct SerializedTorrent { #[serde(skip)] torrent_bytes: Bytes, trackers: HashSet, + /// BEP 19 web seed URLs, including runtime qbit API changes. `None` means + /// no runtime override was saved; `Some(empty)` means all seeds were + /// explicitly removed. The option also keeps older state files compatible. + #[serde(default, skip_serializing_if = "Option::is_none")] + web_seed_urls: Option>, output_folder: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] output_folder_root: Option, @@ -335,6 +340,7 @@ impl SerializedTorrent { comment: self.comment.clone(), auto_management: self.auto_management, super_seeding: self.super_seeding, + web_seed_urls: self.web_seed_urls.clone(), } } @@ -417,10 +423,14 @@ pub(crate) struct RestoredState { comment: Option, auto_management: bool, super_seeding: bool, + web_seed_urls: Option>, } impl RestoredState { pub(crate) fn apply(self, shared: &ManagedTorrentShared) { + if let Some(web_seed_urls) = self.web_seed_urls { + *shared.web_seed_urls.write() = web_seed_urls; + } self.policy.store(shared); *shared.share_limits.write() = self.share_limits; shared diff --git a/crates/librtbit/src/session_persistence/postgres.rs b/crates/librtbit/src/session_persistence/postgres.rs index 0c0e09a3..37a5026c 100644 --- a/crates/librtbit/src/session_persistence/postgres.rs +++ b/crates/librtbit/src/session_persistence/postgres.rs @@ -24,6 +24,7 @@ struct TorrentsTableRecord { info_hash: Vec, torrent_bytes: Vec, trackers: Vec, + web_seed_urls: Option>, output_folder: String, output_folder_root: Option, only_files: Option>, @@ -76,6 +77,7 @@ impl TorrentsTableRecord { info_hash: Id20::from_bytes(&self.info_hash).ok()?, torrent_bytes: self.torrent_bytes.into(), trackers: self.trackers.into_iter().collect(), + web_seed_urls: self.web_seed_urls, output_folder: PathBuf::from(self.output_folder), output_folder_root: self.output_folder_root.map(PathBuf::from), only_files: self @@ -171,6 +173,7 @@ impl PostgresSessionStorage { "ALTER TABLE torrents ADD COLUMN IF NOT EXISTS uploaded_bytes BIGINT NOT NULL DEFAULT 0" ); exec!("ALTER TABLE torrents ADD COLUMN IF NOT EXISTS part_files BOOLEAN"); + exec!("ALTER TABLE torrents ADD COLUMN IF NOT EXISTS web_seed_urls TEXT[]"); Ok(Self { pool }) } @@ -200,6 +203,7 @@ impl SessionPersistenceStore for PostgresSessionStorage { .iter() .map(|tracker| tracker.to_string()) .collect::>(); + let web_seed_urls = Some(torrent.shared().web_seed_urls.read().clone()); let ( share_max_ratio, share_max_seeding_secs, @@ -207,14 +211,15 @@ impl SessionPersistenceStore for PostgresSessionStorage { uploaded_bytes, part_files, ) = runtime_columns(torrent)?; - let q = "INSERT INTO torrents (id, info_hash, torrent_bytes, trackers, output_folder, output_folder_root, only_files, is_paused, share_max_ratio, share_max_seeding_secs, share_max_inactive_secs, uploaded_bytes, part_files) - VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + let q = "INSERT INTO torrents (id, info_hash, torrent_bytes, trackers, web_seed_urls, output_folder, output_folder_root, only_files, is_paused, share_max_ratio, share_max_seeding_secs, share_max_inactive_secs, uploaded_bytes, part_files) + VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT(id) DO NOTHING"; sqlx::query(q) .bind::(id.try_into()?) .bind(&torrent.info_hash().0[..]) .bind(torrent_bytes.as_ref()) .bind(trackers) + .bind(web_seed_urls) .bind( torrent .shared() @@ -282,6 +287,7 @@ impl SessionPersistenceStore for PostgresSessionStorage { .iter() .map(|tracker| tracker.to_string()) .collect::>(); + let web_seed_urls = Some(torrent.shared().web_seed_urls.read().clone()); let (policy, _, _, _) = runtime_fields(torrent); let (output_folder, output_folder_root) = super::location_fields(torrent); let ( @@ -292,9 +298,10 @@ impl SessionPersistenceStore for PostgresSessionStorage { part_files, ) = runtime_columns(torrent)?; sqlx::query( - "UPDATE torrents SET trackers = $1, only_files = $2, is_paused = $3, queued = $4, held_for_disk = $5, share_paused = $6, share_max_ratio = $7, share_max_seeding_secs = $8, share_max_inactive_secs = $9, uploaded_bytes = $10, part_files = $11, output_folder = $12, output_folder_root = $13, file_renames = $14 WHERE id = $15", + "UPDATE torrents SET trackers = $1, web_seed_urls = $2, only_files = $3, is_paused = $4, queued = $5, held_for_disk = $6, share_paused = $7, share_max_ratio = $8, share_max_seeding_secs = $9, share_max_inactive_secs = $10, uploaded_bytes = $11, part_files = $12, output_folder = $13, output_folder_root = $14, file_renames = $15 WHERE id = $16", ) .bind(trackers) + .bind(web_seed_urls) .bind(torrent.only_files().map(|v| { v.into_iter() .filter_map(|f| f.try_into().ok()) @@ -496,6 +503,35 @@ impl BitVFactory for PostgresSessionStorage { #[cfg(test)] mod tests { + use super::*; + + #[test] + fn postgres_record_round_trips_empty_web_seed_override() { + let record = TorrentsTableRecord { + id: 1, + info_hash: vec![0; 20], + torrent_bytes: Vec::new(), + trackers: Vec::new(), + web_seed_urls: Some(Vec::new()), + output_folder: "/tmp".to_owned(), + output_folder_root: None, + only_files: None, + is_paused: false, + queued: false, + held_for_disk: false, + share_paused: false, + share_max_ratio: None, + share_max_seeding_secs: None, + share_max_inactive_secs: None, + uploaded_bytes: 0, + part_files: None, + file_renames: None, + }; + + let (_, serialized) = record.into_serialized_torrent().unwrap(); + assert_eq!(serialized.restored_state().web_seed_urls, Some(Vec::new())); + } + /// Each query in `store` and `update_metadata` binds exactly one value per /// placeholder. Postgres rejects a mismatch at runtime, and CI has no /// Postgres, so check the source. diff --git a/crates/librtbit/src/torrent_state/live/mod.rs b/crates/librtbit/src/torrent_state/live/mod.rs index 34909858..2b87860e 100644 --- a/crates/librtbit/src/torrent_state/live/mod.rs +++ b/crates/librtbit/src/torrent_state/live/mod.rs @@ -395,8 +395,8 @@ impl TorrentStateLive { state.clone().task_peer_health_monitor(), ); - if !state.shared.web_seed_urls.is_empty() { - info!(urls = ?state.shared.web_seed_urls, "torrent has webseed URLs"); + if !state.shared.web_seed_urls.read().is_empty() { + info!(urls = ?state.shared.web_seed_urls.read(), "torrent has webseed URLs"); } Ok(state) diff --git a/crates/librtbit/src/torrent_state/mod.rs b/crates/librtbit/src/torrent_state/mod.rs index 7535f574..deb7cc24 100644 --- a/crates/librtbit/src/torrent_state/mod.rs +++ b/crates/librtbit/src/torrent_state/mod.rs @@ -298,7 +298,7 @@ pub struct ManagedTorrentShared { pub(crate) magnet_name: Option, /// BEP 19: WebSeed URLs parsed from the torrent's url-list field. - pub web_seed_urls: Vec, + pub web_seed_urls: RwLock>, /// Category assigned to this torrent. pub category: RwLock>, diff --git a/crates/rtbit-client/Cargo.toml b/crates/rtbit-client/Cargo.toml index 11714bdb..1abec4c0 100644 --- a/crates/rtbit-client/Cargo.toml +++ b/crates/rtbit-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtbit-client" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" authors = ["AusAgentSmith "] edition = "2024" description = "HTTP client for the rustTorrent daemon API." diff --git a/crates/rtbit-fluent/Cargo.toml b/crates/rtbit-fluent/Cargo.toml index cf584a66..58dc0ce6 100644 --- a/crates/rtbit-fluent/Cargo.toml +++ b/crates/rtbit-fluent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtbit-fluent" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" authors = ["AusAgentSmith "] edition = "2024" description = "rustTorrent native client on the fluent-gpui design system (scaffold)." diff --git a/crates/rtbit-ui-core/Cargo.toml b/crates/rtbit-ui-core/Cargo.toml index fde28c6e..40b1c799 100644 --- a/crates/rtbit-ui-core/Cargo.toml +++ b/crates/rtbit-ui-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtbit-ui-core" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" authors = ["AusAgentSmith "] edition = "2024" description = "Toolkit-neutral core of the native rustTorrent client." diff --git a/crates/rtbit/Cargo.toml b/crates/rtbit/Cargo.toml index 0fadb0ae..a7fff8be 100644 --- a/crates/rtbit/Cargo.toml +++ b/crates/rtbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtbit" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" authors = ["AusAgentSmith "] edition = "2024" description = "A bittorrent command line client and server." diff --git a/desktop/menu/Cargo.toml b/desktop/menu/Cargo.toml index 679b9886..b3809cdc 100644 --- a/desktop/menu/Cargo.toml +++ b/desktop/menu/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rtbit-desktop-menu" edition = "2024" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" description = "Native menu bar description for the rtbit desktop wrapper" publish = false diff --git a/desktop/package.json b/desktop/package.json index a89ee414..a3439b40 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "rtbit-desktop", "private": true, - "version": "0.2.0-beta.4", + "version": "0.2.0-beta.5", "scripts": { "tauri": "tauri" }, diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 542b72b1..85c60b4e 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rtbit-desktop" edition = "2024" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" description = "rtbit torrent client" license = "Apache-2.0" repository = "" diff --git a/docs/QBIT-API-PARITY.md b/docs/QBIT-API-PARITY.md index b691b3f5..d2f065fb 100644 --- a/docs/QBIT-API-PARITY.md +++ b/docs/QBIT-API-PARITY.md @@ -9,7 +9,7 @@ and enforced, and where we currently stand. ## Current standing (upstream WebAPI 2.16.2, refreshed 2026-10-17) -**86 of 95 in-scope endpoints routed (~91%)** — 42 full, 44 partial — plus 2 legacy aliases (`torrents/pause`, `torrents/resume`) that upstream removed in WebAPI 2.11. +**92 of 95 in-scope endpoints routed (~97%)** — 48 full, 44 partial — plus 2 legacy aliases (`torrents/pause`, `torrents/resume`) that upstream removed in WebAPI 2.11. 35 of the 130 upstream endpoints are descoped (since 2026-08-30; `clientdata/*` was brought back in scope for WI-17) diff --git a/docs/REPOSITORY-LAYOUT.md b/docs/REPOSITORY-LAYOUT.md index 328c225f..325291d7 100644 --- a/docs/REPOSITORY-LAYOUT.md +++ b/docs/REPOSITORY-LAYOUT.md @@ -18,7 +18,7 @@ similar are kept separate. Architecture notes live in the other files under | `benchv2/` | Rust-native benchmark harness comparing rtbit with another client. Outside the workspace, with its own `Cargo.toml`, `Dockerfile` and `docker-compose.yml`. | All crates and packages share the version in the release notes, currently -`0.2.0-beta.4`. `scripts/check-repo-hygiene.sh` (run with `make check-repo`) fails +`0.2.0-beta.5`. `scripts/check-repo-hygiene.sh` (run with `make check-repo`) fails if a manifest drifts from it. ## Container images and compose files diff --git a/docs/ReleaseProcess.md b/docs/ReleaseProcess.md index 1bc6116f..6d24d5fc 100644 --- a/docs/ReleaseProcess.md +++ b/docs/ReleaseProcess.md @@ -5,11 +5,11 @@ rusttorrent.dev website. The historical Forgejo/Woodpecker path is retired. ## Versioning -The current pre-release is `0.2.0-beta.4`. +The current pre-release is `0.2.0-beta.5`. - Crate version: `crates/rtbit/Cargo.toml` -- Release tag: `v0.2.0-beta.4` -- Docker tag: `ghcr.io/thedancingdeveloper-org/rusttorrent:v0.2.0-beta.4` +- Release tag: `v0.2.0-beta.5` +- Docker tag: `ghcr.io/thedancingdeveloper-org/rusttorrent:v0.2.0-beta.5` ## Branching model diff --git a/docs/releases/v0.2.0-beta.5.md b/docs/releases/v0.2.0-beta.5.md new file mode 100644 index 00000000..67639403 --- /dev/null +++ b/docs/releases/v0.2.0-beta.5.md @@ -0,0 +1,37 @@ +# rustTorrent v0.2.0-beta.5 + +## Summary + +rustTorrent 0.2.0-beta.5 is a feature and hardening build of the daemon, Web +UI, and `rtbit-fluent` desktop client. It expands WebUI API v2 compatibility, +adds torrent and category management controls, improves recheck and recovery +behavior, and hardens state persistence and external URL handling. + +This remains beta software. Back up the persistent session directory and the +PostgreSQL database before upgrading, and avoid using it for data that cannot +be downloaded again. + +## Highlights + +- **WebUI API v2 compatibility.** Web-seed, tracker-tier, downloaded-count, + app network/shutdown, torrent metadata, and in-flight piece-state endpoints + are available. +- **Torrent management.** Per-file priorities, category share limits, torrent + location and rename controls, missing-payload diagnosis, and richer Fluent + filters and controls are included. +- **Reliability.** Recheck progress, concurrency and throughput limits, + crash-safe state writes, legacy session migration, low-disk protection, + retry backoff, and bounded shutdown persistence are improved. +- **Safety and delivery.** External URL fetching is hardened and the release + workflow smoke-tests fluent desktop artifacts. + +## Upgrade notes + +- Back up `/home/rtbit/db`, `/home/rtbit/cache` and the credentials directory + before upgrading. +- No configuration changes are required. + +## Downloads + +Release artifacts are attached to the canonical GitHub release and are also +published at `https://dl.rusttorrent.dev/v0.2.0-beta.5/`. diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 075d5073..5ff95a14 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -980,7 +980,7 @@ dependencies = [ [[package]] name = "rusttorrent-fuzz" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "libfuzzer-sys", "swarmforge-bencode", @@ -1085,7 +1085,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "swarmforge-bencode" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "arrayvec", @@ -1100,7 +1100,7 @@ dependencies = [ [[package]] name = "swarmforge-buffers" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "bytes", "serde", @@ -1110,14 +1110,14 @@ dependencies = [ [[package]] name = "swarmforge-clone-to-owned" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "bytes", ] [[package]] name = "swarmforge-core" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bytes", @@ -1145,7 +1145,7 @@ dependencies = [ [[package]] name = "swarmforge-dht" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "aws-lc-rs", @@ -1176,7 +1176,7 @@ dependencies = [ [[package]] name = "swarmforge-peer-protocol" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "anyhow", "bitvec", @@ -1195,7 +1195,7 @@ dependencies = [ [[package]] name = "swarmforge-sha1-wrapper" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" dependencies = [ "assert_cfg", "crypto-hash", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f54dcc13..f61693bc 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rusttorrent-fuzz" -version = "0.2.0-beta.4" +version = "0.2.0-beta.5" publish = false edition = "2024" diff --git a/package-lock.json b/package-lock.json index fba66740..986f607f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ }, "desktop": { "name": "rtbit-desktop", - "version": "0.2.0-beta.4", + "version": "0.2.0-beta.5", "devDependencies": { "@tauri-apps/cli": "^2.12.4" } diff --git a/scripts/publish.sh b/scripts/publish.sh index 33452a3f..a706756e 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -20,7 +20,7 @@ if $resume && [[ -n "$publish_flag" ]]; then exit 2 fi -version="0.2.0-beta.4" +version="0.2.0-beta.5" # Pre-release versions (a '-' suffix such as 0.1.0-rc.1) are never published to # crates.io from this script. Exit 0 so a release pipeline that calls it for every