Skip to content

Commit

Permalink
feat(api): [torrust#143] axum api, WIP. GET /api/torrent/:info_hash e…
Browse files Browse the repository at this point in the history
…ndpoint

Not all cases finished yet. Not found case is pending.
  • Loading branch information
josecelano committed Jan 5, 2023
1 parent fe4303c commit 16d438d
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 33 deletions.
70 changes: 67 additions & 3 deletions src/api/resource/torrent.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<super::peer::Peer>>,
}
Expand All @@ -19,3 +22,64 @@ pub struct ListItem {
// todo: this is always None. Remove field from endpoint?
pub peers: Option<Vec<super::peer::Peer>>,
}

impl From<Info> 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())]),
}
);
}
}
6 changes: 3 additions & 3 deletions src/api/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ pub fn routes(tracker: &Arc<tracker::Tracker>) -> impl Filter<Extract = impl war

Ok(reply::json(&Torrent {
info_hash: info_hash.to_string(),
seeders,
completed,
leechers,
seeders: u64::from(seeders),
completed: u64::from(completed),
leechers: u64::from(leechers),
peers: Some(peer_resources),
}))
});
Expand Down
23 changes: 18 additions & 5 deletions src/apis/routes.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
use std::str::FromStr;
use std::sync::Arc;

use axum::extract::State;
use axum::extract::{Path, State};
use axum::response::Json;
use serde_json::{json, Value};

use crate::api::resource::stats::Stats;
use crate::api::resource::torrent::Torrent;
use crate::protocol::info_hash::InfoHash;
use crate::tracker::services::statistics::get_metrics;
use crate::tracker::services::torrent::get_torrent_info;
use crate::tracker::Tracker;

#[allow(clippy::unused_async)]
pub async fn get_stats(State(tracker): State<Arc<Tracker>>) -> Json<Value> {
Json(json!(Stats::from(get_metrics(tracker.clone()).await)))
pub async fn get_stats(State(tracker): State<Arc<Tracker>>) -> Json<Stats> {
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<Arc<Tracker>>, Path(info_hash): Path<String>) -> Json<Torrent> {
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))
}
3 changes: 2 additions & 1 deletion src/apis/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<tracker::Tracker>) -> impl Future<Output = hyper::Result<()>> {
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());
Expand Down
24 changes: 24 additions & 0 deletions src/tracker/services/common.rs
Original file line number Diff line number Diff line change
@@ -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<Configuration>) -> 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)
}
}
}
2 changes: 2 additions & 0 deletions src/tracker/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod statistics;
pub mod torrent;
pub mod common;
23 changes: 2 additions & 21 deletions src/tracker/services/statistics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,18 @@ pub async fn get_metrics(tracker: Arc<Tracker>) -> 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<Configuration> {
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;

Expand Down
116 changes: 116 additions & 0 deletions src/tracker/services/torrent.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<Peer>>,
}

pub async fn get_torrent_info(tracker: Arc<Tracker>, info_hash: &InfoHash) -> Option<Info> {
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<Configuration> {
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()]),
}
);
}
}
}
63 changes: 63 additions & 0 deletions tests/tracker_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Torrent>().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;
}
}
}

0 comments on commit 16d438d

Please sign in to comment.