diff --git a/src/api/resource/torrent.rs b/src/api/resource/torrent.rs index 924b61b8..bec82a13 100644 --- a/src/api/resource/torrent.rs +++ b/src/api/resource/torrent.rs @@ -1,11 +1,14 @@ use serde::{Deserialize, Serialize}; +use super::peer; +use crate::tracker::services::torrent::Info; + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Torrent { pub info_hash: String, - pub seeders: u32, - pub completed: u32, - pub leechers: u32, + pub seeders: u64, + pub completed: u64, + pub leechers: u64, #[serde(skip_serializing_if = "Option::is_none")] pub peers: Option>, } @@ -19,3 +22,64 @@ pub struct ListItem { // todo: this is always None. Remove field from endpoint? pub peers: Option>, } + +impl From for Torrent { + fn from(info: Info) -> Self { + Self { + info_hash: info.info_hash.to_string(), + seeders: info.seeders, + completed: info.completed, + leechers: info.leechers, + peers: info + .peers + .map(|peers| peers.iter().map(|peer| peer::Peer::from(*peer)).collect()), + } + } +} + +#[cfg(test)] +mod tests { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::str::FromStr; + + use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + + use crate::api::resource::peer::Peer; + use crate::api::resource::torrent::Torrent; + use crate::protocol::clock::DurationSinceUnixEpoch; + use crate::protocol::info_hash::InfoHash; + use crate::tracker::peer; + use crate::tracker::services::torrent::Info; + + fn sample_peer() -> peer::Peer { + peer::Peer { + peer_id: peer::Id(*b"-qB00000000000000000"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Started, + } + } + + #[test] + fn torrent_resource_should_be_converted_from_torrent_info() { + assert_eq!( + Torrent::from(Info { + info_hash: InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), + seeders: 1, + completed: 2, + leechers: 3, + peers: Some(vec![sample_peer()]), + }), + Torrent { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, + completed: 2, + leechers: 3, + peers: Some(vec![Peer::from(sample_peer())]), + } + ); + } +} diff --git a/src/api/routes.rs b/src/api/routes.rs index 73f1269e..b29023f2 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -124,9 +124,9 @@ pub fn routes(tracker: &Arc) -> impl Filter>) -> Json { - Json(json!(Stats::from(get_metrics(tracker.clone()).await))) +pub async fn get_stats(State(tracker): State>) -> Json { + Json(Stats::from(get_metrics(tracker.clone()).await)) +} + +/// # Panics +/// +/// Will panic if the torrent does not exist. +pub async fn get_torrent(State(tracker): State>, Path(info_hash): Path) -> Json { + let info = get_torrent_info(tracker.clone(), &InfoHash::from_str(&info_hash).unwrap()) + .await + .unwrap(); + // todo: return "not found" if the torrent does not exist + Json(Torrent::from(info)) } diff --git a/src/apis/server.rs b/src/apis/server.rs index 668959cd..dcd0924c 100644 --- a/src/apis/server.rs +++ b/src/apis/server.rs @@ -10,12 +10,13 @@ use log::info; use warp::hyper; use super::middlewares::auth::auth; -use super::routes::get_stats; +use super::routes::{get_stats, get_torrent}; use crate::tracker; pub fn start(socket_addr: SocketAddr, tracker: &Arc) -> impl Future> { let app = Router::new() .route("/stats", get(get_stats).with_state(tracker.clone())) + .route("/torrent/:info_hash", get(get_torrent).with_state(tracker.clone())) .layer(middleware::from_fn_with_state(tracker.config.clone(), auth)); let server = axum::Server::bind(&socket_addr).serve(app.into_make_service()); diff --git a/src/tracker/services/common.rs b/src/tracker/services/common.rs new file mode 100644 index 00000000..8757e6a2 --- /dev/null +++ b/src/tracker/services/common.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use crate::config::Configuration; +use crate::tracker::statistics::Keeper; +use crate::tracker::Tracker; + +/// # Panics +/// +/// Will panic if tracker cannot be instantiated. +#[must_use] +pub fn tracker_factory(configuration: &Arc) -> Tracker { + // todo: the tracker initialization is duplicated in many places. + + // Initialize stats tracker + let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); + + // Initialize Torrust tracker + match Tracker::new(configuration, Some(stats_event_sender), stats_repository) { + Ok(tracker) => tracker, + Err(error) => { + panic!("{}", error) + } + } +} diff --git a/src/tracker/services/mod.rs b/src/tracker/services/mod.rs index 3449ec7b..ffa5bb25 100644 --- a/src/tracker/services/mod.rs +++ b/src/tracker/services/mod.rs @@ -1 +1,3 @@ pub mod statistics; +pub mod torrent; +pub mod common; diff --git a/src/tracker/services/statistics.rs b/src/tracker/services/statistics.rs index bbc069dd..745f5563 100644 --- a/src/tracker/services/statistics.rs +++ b/src/tracker/services/statistics.rs @@ -36,37 +36,18 @@ pub async fn get_metrics(tracker: Arc) -> TrackerMetrics { mod tests { use std::sync::Arc; - use super::Tracker; use crate::config::{ephemeral_configuration, Configuration}; use crate::tracker; + use crate::tracker::services::common::tracker_factory; use crate::tracker::services::statistics::{get_metrics, TrackerMetrics}; - use crate::tracker::statistics::Keeper; pub fn tracker_configuration() -> Arc { Arc::new(ephemeral_configuration()) } - pub fn tracker_factory() -> Tracker { - // code-review: the tracker initialization is duplicated in many places. Consider make this function public. - - // Configuration - let configuration = tracker_configuration(); - - // Initialize stats tracker - let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); - - // Initialize Torrust tracker - match Tracker::new(&configuration, Some(stats_event_sender), stats_repository) { - Ok(tracker) => tracker, - Err(error) => { - panic!("{}", error) - } - } - } - #[tokio::test] async fn the_statistics_service_should_return_the_tracker_metrics() { - let tracker = Arc::new(tracker_factory()); + let tracker = Arc::new(tracker_factory(&tracker_configuration())); let tracker_metrics = get_metrics(tracker.clone()).await; diff --git a/src/tracker/services/torrent.rs b/src/tracker/services/torrent.rs new file mode 100644 index 00000000..da7d24ce --- /dev/null +++ b/src/tracker/services/torrent.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use crate::protocol::info_hash::InfoHash; +use crate::tracker::peer::Peer; +use crate::tracker::Tracker; + +#[derive(Debug, PartialEq)] +pub struct Info { + pub info_hash: InfoHash, + pub seeders: u64, + pub completed: u64, + pub leechers: u64, + pub peers: Option>, +} + +pub async fn get_torrent_info(tracker: Arc, info_hash: &InfoHash) -> Option { + let db = tracker.get_torrents().await; + + let torrent_entry_option = db.get(info_hash); + + let torrent_entry = match torrent_entry_option { + Some(torrent_entry) => torrent_entry, + None => { + return None; + } + }; + + let (seeders, completed, leechers) = torrent_entry.get_stats(); + + let peers = torrent_entry.get_peers(None); + + let peers = Some(peers.iter().map(|peer| (**peer)).collect()); + + Some(Info { + info_hash: *info_hash, + seeders: u64::from(seeders), + completed: u64::from(completed), + leechers: u64::from(leechers), + peers, + }) +} + +#[cfg(test)] +mod tests { + + mod getting_a_torrent_info { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::str::FromStr; + use std::sync::Arc; + + use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + + use crate::config::{ephemeral_configuration, Configuration}; + use crate::protocol::clock::DurationSinceUnixEpoch; + use crate::protocol::info_hash::InfoHash; + use crate::tracker::peer; + use crate::tracker::services::common::tracker_factory; + use crate::tracker::services::torrent::{get_torrent_info, Info}; + + pub fn tracker_configuration() -> Arc { + Arc::new(ephemeral_configuration()) + } + + fn sample_peer() -> peer::Peer { + peer::Peer { + peer_id: peer::Id(*b"-qB00000000000000000"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Started, + } + } + + #[tokio::test] + async fn should_return_none_if_the_tracker_does_not_have_the_torrent() { + let tracker = Arc::new(tracker_factory(&tracker_configuration())); + + let torrent_info = get_torrent_info( + tracker.clone(), + &InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(), + ) + .await; + + assert!(torrent_info.is_none()); + } + + #[tokio::test] + async fn should_return_the_torrent_info_if_the_tracker_has_the_torrent() { + let tracker = Arc::new(tracker_factory(&tracker_configuration())); + + let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); + let info_hash = InfoHash::from_str(&hash).unwrap(); + + tracker + .update_torrent_with_peer_and_get_stats(&info_hash, &sample_peer()) + .await; + + let torrent_info = get_torrent_info(tracker.clone(), &InfoHash::from_str(&hash).unwrap()) + .await + .unwrap(); + + assert_eq!( + torrent_info, + Info { + info_hash: InfoHash::from_str(&hash).unwrap(), + seeders: 1, + completed: 0, + leechers: 0, + peers: Some(vec![sample_peer()]), + } + ); + } + } +} diff --git a/tests/tracker_api.rs b/tests/tracker_api.rs index 25d747f2..78f8efbb 100644 --- a/tests/tracker_api.rs +++ b/tests/tracker_api.rs @@ -645,4 +645,67 @@ mod tracker_apis { assert_unauthorized(response).await; } } + + mod for_torrent_resources { + use std::str::FromStr; + + use torrust_tracker::api::resource; + use torrust_tracker::api::resource::torrent::Torrent; + use torrust_tracker::protocol::info_hash::InfoHash; + + use crate::api::asserts::{assert_token_not_valid, assert_unauthorized}; + use crate::api::client::Client; + use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; + use crate::api::fixtures::sample_peer; + use crate::api::server::start_default_api; + use crate::api::Version; + + #[tokio::test] + async fn should_allow_getting_a_torrent_info() { + let api_server = start_default_api(&Version::Axum).await; + + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + + let peer = sample_peer(); + + api_server.add_torrent(&info_hash, &peer).await; + + let response = Client::new(api_server.get_connection_info(), &Version::Axum) + .get_torrent(&info_hash.to_string()) + .await; + + assert_eq!(response.status(), 200); + assert_eq!( + response.json::().await.unwrap(), + Torrent { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), + seeders: 1, + completed: 0, + leechers: 0, + peers: Some(vec![resource::peer::Peer::from(peer)]) + } + ); + } + + #[tokio::test] + async fn should_not_allow_getting_a_torrent_info_for_unauthenticated_users() { + let api_server = start_default_api(&Version::Axum).await; + + let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); + + api_server.add_torrent(&info_hash, &sample_peer()).await; + + let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address()), &Version::Axum) + .get_torrent(&info_hash.to_string()) + .await; + + assert_token_not_valid(response).await; + + let response = Client::new(connection_with_no_token(&api_server.get_bind_address()), &Version::Axum) + .get_torrent(&info_hash.to_string()) + .await; + + assert_unauthorized(response).await; + } + } }