Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor API folder structure #237

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/apis/context/auth_key/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use axum::extract::{Path, State};
use axum::response::Response;
use serde::Deserialize;

use super::responses::{
auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response,
};
use crate::apis::context::auth_key::resources::AuthKey;
use crate::apis::responses::{invalid_auth_key_param_response, ok_response};
use crate::tracker::auth::Key;
use crate::tracker::Tracker;

pub async fn generate_auth_key_handler(State(tracker): State<Arc<Tracker>>, Path(seconds_valid_or_key): Path<u64>) -> Response {
let seconds_valid = seconds_valid_or_key;
match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await {
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
Err(e) => failed_to_generate_key_response(e),
}
}

#[derive(Deserialize)]
pub struct KeyParam(String);

pub async fn delete_auth_key_handler(
State(tracker): State<Arc<Tracker>>,
Path(seconds_valid_or_key): Path<KeyParam>,
) -> Response {
match Key::from_str(&seconds_valid_or_key.0) {
Err(_) => invalid_auth_key_param_response(&seconds_valid_or_key.0),
Ok(key) => match tracker.remove_auth_key(&key.to_string()).await {
Ok(_) => ok_response(),
Err(e) => failed_to_delete_key_response(e),
},
}
}

pub async fn reload_keys_handler(State(tracker): State<Arc<Tracker>>) -> Response {
match tracker.load_keys_from_database().await {
Ok(_) => ok_response(),
Err(e) => failed_to_reload_keys_response(e),
}
}
4 changes: 4 additions & 0 deletions src/apis/context/auth_key/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod handlers;
pub mod resources;
pub mod responses;
pub mod routes;
File renamed without changes.
35 changes: 35 additions & 0 deletions src/apis/context/auth_key/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::error::Error;

use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};

use crate::apis::context::auth_key::resources::AuthKey;
use crate::apis::responses::unhandled_rejection_response;

/// # Panics
///
/// Will panic if it can't convert the `AuthKey` resource to json
#[must_use]
pub fn auth_key_response(auth_key: &AuthKey) -> Response {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json; charset=utf-8")],
serde_json::to_string(auth_key).unwrap(),
)
.into_response()
}

#[must_use]
pub fn failed_to_generate_key_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to generate key: {e}"))
}

#[must_use]
pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to delete key: {e}"))
}

#[must_use]
pub fn failed_to_reload_keys_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to reload keys: {e}"))
}
25 changes: 25 additions & 0 deletions src/apis/context/auth_key/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::sync::Arc;

use axum::routing::{get, post};
use axum::Router;

use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler};
use crate::tracker::Tracker;

pub fn add(router: Router, tracker: Arc<Tracker>) -> Router {
// Keys
router
.route(
// code-review: Axum does not allow two routes with the same path but different path variable name.
// In the new major API version, `seconds_valid` should be a POST form field so that we will have two paths:
// POST /api/key
// DELETE /api/key/:key
"/api/key/:seconds_valid_or_key",
post(generate_auth_key_handler)
.with_state(tracker.clone())
.delete(delete_auth_key_handler)
.with_state(tracker.clone()),
)
// Keys command
.route("/api/keys/reload", get(reload_keys_handler).with_state(tracker))
}
2 changes: 1 addition & 1 deletion src/apis/resources/mod.rs → src/apis/context/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub mod auth_key;
pub mod peer;
pub mod stats;
pub mod torrent;
pub mod whitelist;
13 changes: 13 additions & 0 deletions src/apis/context/stats/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use std::sync::Arc;

use axum::extract::State;
use axum::response::Json;

use super::resources::Stats;
use super::responses::stats_response;
use crate::tracker::services::statistics::get_metrics;
use crate::tracker::Tracker;

pub async fn get_stats_handler(State(tracker): State<Arc<Tracker>>) -> Json<Stats> {
stats_response(get_metrics(tracker.clone()).await)
}
4 changes: 4 additions & 0 deletions src/apis/context/stats/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod handlers;
pub mod resources;
pub mod responses;
pub mod routes;
File renamed without changes.
8 changes: 8 additions & 0 deletions src/apis/context/stats/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use axum::response::Json;

use super::resources::Stats;
use crate::tracker::services::statistics::TrackerMetrics;

pub fn stats_response(tracker_metrics: TrackerMetrics) -> Json<Stats> {
Json(Stats::from(tracker_metrics))
}
11 changes: 11 additions & 0 deletions src/apis/context/stats/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use std::sync::Arc;

use axum::routing::get;
use axum::Router;

use super::handlers::get_stats_handler;
use crate::tracker::Tracker;

pub fn add(router: Router, tracker: Arc<Tracker>) -> Router {
router.route("/api/stats", get(get_stats_handler).with_state(tracker))
}
59 changes: 59 additions & 0 deletions src/apis/context/torrent/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::response::{IntoResponse, Json, Response};
use serde::{de, Deserialize, Deserializer};

use super::resources::torrent::ListItem;
use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response};
use crate::apis::responses::invalid_info_hash_param_response;
use crate::apis::InfoHashParam;
use crate::protocol::info_hash::InfoHash;
use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination};
use crate::tracker::Tracker;

pub async fn get_torrent_handler(State(tracker): State<Arc<Tracker>>, Path(info_hash): Path<InfoHashParam>) -> Response {
match InfoHash::from_str(&info_hash.0) {
Err(_) => invalid_info_hash_param_response(&info_hash.0),
Ok(info_hash) => match get_torrent_info(tracker.clone(), &info_hash).await {
Some(info) => torrent_info_response(info).into_response(),
None => torrent_not_known_response(),
},
}
}

#[derive(Deserialize)]
pub struct PaginationParams {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub offset: Option<u32>,
pub limit: Option<u32>,
}

pub async fn get_torrents_handler(
State(tracker): State<Arc<Tracker>>,
pagination: Query<PaginationParams>,
) -> Json<Vec<ListItem>> {
torrent_list_response(
&get_torrents(
tracker.clone(),
&Pagination::new_with_options(pagination.0.offset, pagination.0.limit),
)
.await,
)
}

/// Serde deserialization decorator to map empty Strings to None,
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
let opt = Option::<String>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
}
}
4 changes: 4 additions & 0 deletions src/apis/context/torrent/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod handlers;
pub mod resources;
pub mod responses;
pub mod routes;
2 changes: 2 additions & 0 deletions src/apis/context/torrent/resources/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod peer;
pub mod torrent;
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ mod tests {

use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes};

use crate::apis::resources::peer::Peer;
use crate::apis::resources::torrent::{ListItem, Torrent};
use super::Torrent;
use crate::apis::context::torrent::resources::peer::Peer;
use crate::apis::context::torrent::resources::torrent::ListItem;
use crate::protocol::clock::DurationSinceUnixEpoch;
use crate::protocol::info_hash::InfoHash;
use crate::tracker::peer;
Expand Down
18 changes: 18 additions & 0 deletions src/apis/context/torrent/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use axum::response::{IntoResponse, Json, Response};
use serde_json::json;

use super::resources::torrent::{ListItem, Torrent};
use crate::tracker::services::torrent::{BasicInfo, Info};

pub fn torrent_list_response(basic_infos: &[BasicInfo]) -> Json<Vec<ListItem>> {
Json(ListItem::new_vec(basic_infos))
}

pub fn torrent_info_response(info: Info) -> Json<Torrent> {
Json(Torrent::from(info))
}

#[must_use]
pub fn torrent_not_known_response() -> Response {
Json(json!("torrent not known")).into_response()
}
17 changes: 17 additions & 0 deletions src/apis/context/torrent/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::sync::Arc;

use axum::routing::get;
use axum::Router;

use super::handlers::{get_torrent_handler, get_torrents_handler};
use crate::tracker::Tracker;

pub fn add(router: Router, tracker: Arc<Tracker>) -> Router {
// Torrents
router
.route(
"/api/torrent/:info_hash",
get(get_torrent_handler).with_state(tracker.clone()),
)
.route("/api/torrents", get(get_torrents_handler).with_state(tracker))
}
46 changes: 46 additions & 0 deletions src/apis/context/whitelist/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::str::FromStr;
use std::sync::Arc;

use axum::extract::{Path, State};
use axum::response::Response;

use super::responses::{
failed_to_reload_whitelist_response, failed_to_remove_torrent_from_whitelist_response, failed_to_whitelist_torrent_response,
};
use crate::apis::responses::{invalid_info_hash_param_response, ok_response};
use crate::apis::InfoHashParam;
use crate::protocol::info_hash::InfoHash;
use crate::tracker::Tracker;

pub async fn add_torrent_to_whitelist_handler(
State(tracker): State<Arc<Tracker>>,
Path(info_hash): Path<InfoHashParam>,
) -> Response {
match InfoHash::from_str(&info_hash.0) {
Err(_) => invalid_info_hash_param_response(&info_hash.0),
Ok(info_hash) => match tracker.add_torrent_to_whitelist(&info_hash).await {
Ok(_) => ok_response(),
Err(e) => failed_to_whitelist_torrent_response(e),
},
}
}

pub async fn remove_torrent_from_whitelist_handler(
State(tracker): State<Arc<Tracker>>,
Path(info_hash): Path<InfoHashParam>,
) -> Response {
match InfoHash::from_str(&info_hash.0) {
Err(_) => invalid_info_hash_param_response(&info_hash.0),
Ok(info_hash) => match tracker.remove_torrent_from_whitelist(&info_hash).await {
Ok(_) => ok_response(),
Err(e) => failed_to_remove_torrent_from_whitelist_response(e),
},
}
}

pub async fn reload_whitelist_handler(State(tracker): State<Arc<Tracker>>) -> Response {
match tracker.load_whitelist_from_database().await {
Ok(_) => ok_response(),
Err(e) => failed_to_reload_whitelist_response(e),
}
}
3 changes: 3 additions & 0 deletions src/apis/context/whitelist/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod handlers;
pub mod responses;
pub mod routes;
20 changes: 20 additions & 0 deletions src/apis/context/whitelist/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::error::Error;

use axum::response::Response;

use crate::apis::responses::unhandled_rejection_response;

#[must_use]
pub fn failed_to_remove_torrent_from_whitelist_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to remove torrent from whitelist: {e}"))
}

#[must_use]
pub fn failed_to_whitelist_torrent_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to whitelist torrent: {e}"))
}

#[must_use]
pub fn failed_to_reload_whitelist_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to reload whitelist: {e}"))
}
22 changes: 22 additions & 0 deletions src/apis/context/whitelist/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::sync::Arc;

use axum::routing::{delete, get, post};
use axum::Router;

use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler};
use crate::tracker::Tracker;

pub fn add(router: Router, tracker: Arc<Tracker>) -> Router {
router
// Whitelisted torrents
.route(
"/api/whitelist/:info_hash",
post(add_torrent_to_whitelist_handler).with_state(tracker.clone()),
)
.route(
"/api/whitelist/:info_hash",
delete(remove_torrent_from_whitelist_handler).with_state(tracker.clone()),
)
// Whitelist commands
.route("/api/whitelist/reload", get(reload_whitelist_handler).with_state(tracker))
}
Loading