Skip to content

Commit

Permalink
refactor(api): [torrust#182] Axum API, torrent context, upload torren…
Browse files Browse the repository at this point in the history
…t file
  • Loading branch information
josecelano committed Jun 15, 2023
1 parent d7f1e34 commit 538ebcc
Show file tree
Hide file tree
Showing 22 changed files with 1,030 additions and 78 deletions.
5 changes: 5 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ pub enum ServiceError {
DatabaseError,
}

// Begin ActixWeb error handling
// todo: remove after migration to Axum

#[derive(Serialize, Deserialize)]
pub struct ErrorToResponse {
pub error: String,
Expand All @@ -156,6 +159,8 @@ impl ResponseError for ServiceError {
}
}

// End ActixWeb error handling

impl From<sqlx::Error> for ServiceError {
fn from(e: sqlx::Error) -> Self {
eprintln!("{e:?}");
Expand Down
10 changes: 5 additions & 5 deletions src/web/api/v1/contexts/category/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::common::AppData;
use crate::databases::database::{self, Category};
use crate::errors::ServiceError;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::{self, OkResponse};
use crate::web::api::v1::responses::{self, OkResponseData};

/// It handles the request to get all the categories.
///
Expand All @@ -29,9 +29,9 @@ use crate::web::api::v1::responses::{self, OkResponse};
#[allow(clippy::unused_async)]
pub async fn get_all_handler(
State(app_data): State<Arc<AppData>>,
) -> Result<Json<responses::OkResponse<Vec<Category>>>, database::Error> {
) -> Result<Json<responses::OkResponseData<Vec<Category>>>, database::Error> {
match app_data.category_repository.get_all().await {
Ok(categories) => Ok(Json(responses::OkResponse { data: categories })),
Ok(categories) => Ok(Json(responses::OkResponseData { data: categories })),
Err(error) => Err(error),
}
}
Expand All @@ -49,7 +49,7 @@ pub async fn add_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(category_form): extract::Json<AddCategoryForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
) -> Result<Json<OkResponseData<String>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.category_service.add_category(&category_form.name, &user_id).await {
Expand All @@ -71,7 +71,7 @@ pub async fn delete_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(category_form): extract::Json<DeleteCategoryForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
) -> Result<Json<OkResponseData<String>>, ServiceError> {
// code-review: why do we need to send the whole category object to delete it?
// And we should use the ID instead of the name, because the name could change
// or we could add support for multiple languages.
Expand Down
10 changes: 5 additions & 5 deletions src/web/api/v1/contexts/category/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
//! context.
use axum::Json;

use crate::web::api::v1::responses::OkResponse;
use crate::web::api::v1::responses::OkResponseData;

/// Response after successfully creating a new category.
pub fn added_category(category_name: &str) -> Json<OkResponse<String>> {
Json(OkResponse {
pub fn added_category(category_name: &str) -> Json<OkResponseData<String>> {
Json(OkResponseData {
data: category_name.to_string(),
})
}

/// Response after successfully deleting a new category.
pub fn deleted_category(category_name: &str) -> Json<OkResponse<String>> {
Json(OkResponse {
pub fn deleted_category(category_name: &str) -> Json<OkResponseData<String>> {
Json(OkResponseData {
data: category_name.to_string(),
})
}
18 changes: 9 additions & 9 deletions src/web/api/v1/contexts/settings/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::common::AppData;
use crate::config::{ConfigurationPublic, TorrustBackend};
use crate::errors::ServiceError;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::{self, OkResponse};
use crate::web::api::v1::responses::{self, OkResponseData};

/// Get all settings.
///
Expand All @@ -21,28 +21,28 @@ use crate::web::api::v1::responses::{self, OkResponse};
pub async fn get_all_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
) -> Result<Json<OkResponse<TorrustBackend>>, ServiceError> {
) -> Result<Json<OkResponseData<TorrustBackend>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

let all_settings = app_data.settings_service.get_all(&user_id).await?;

Ok(Json(responses::OkResponse { data: all_settings }))
Ok(Json(responses::OkResponseData { data: all_settings }))
}

/// Get public Settings.
#[allow(clippy::unused_async)]
pub async fn get_public_handler(State(app_data): State<Arc<AppData>>) -> Json<responses::OkResponse<ConfigurationPublic>> {
pub async fn get_public_handler(State(app_data): State<Arc<AppData>>) -> Json<responses::OkResponseData<ConfigurationPublic>> {
let public_settings = app_data.settings_service.get_public().await;

Json(responses::OkResponse { data: public_settings })
Json(responses::OkResponseData { data: public_settings })
}

/// Get website name.
#[allow(clippy::unused_async)]
pub async fn get_site_name_handler(State(app_data): State<Arc<AppData>>) -> Json<responses::OkResponse<String>> {
pub async fn get_site_name_handler(State(app_data): State<Arc<AppData>>) -> Json<responses::OkResponseData<String>> {
let site_name = app_data.settings_service.get_site_name().await;

Json(responses::OkResponse { data: site_name })
Json(responses::OkResponseData { data: site_name })
}

/// Update all the settings.
Expand All @@ -59,10 +59,10 @@ pub async fn update_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(torrust_backend): extract::Json<TorrustBackend>,
) -> Result<Json<OkResponse<TorrustBackend>>, ServiceError> {
) -> Result<Json<OkResponseData<TorrustBackend>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

let new_settings = app_data.settings_service.update_all(torrust_backend, &user_id).await?;

Ok(Json(responses::OkResponse { data: new_settings }))
Ok(Json(responses::OkResponseData { data: new_settings }))
}
10 changes: 5 additions & 5 deletions src/web/api/v1/contexts/tag/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::databases::database;
use crate::errors::ServiceError;
use crate::models::torrent_tag::TorrentTag;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::{self, OkResponse};
use crate::web::api::v1::responses::{self, OkResponseData};

/// It handles the request to get all the tags.
///
Expand All @@ -30,9 +30,9 @@ use crate::web::api::v1::responses::{self, OkResponse};
#[allow(clippy::unused_async)]
pub async fn get_all_handler(
State(app_data): State<Arc<AppData>>,
) -> Result<Json<responses::OkResponse<Vec<TorrentTag>>>, database::Error> {
) -> Result<Json<responses::OkResponseData<Vec<TorrentTag>>>, database::Error> {
match app_data.tag_repository.get_all().await {
Ok(tags) => Ok(Json(responses::OkResponse { data: tags })),
Ok(tags) => Ok(Json(responses::OkResponseData { data: tags })),
Err(error) => Err(error),
}
}
Expand All @@ -50,7 +50,7 @@ pub async fn add_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(add_tag_form): extract::Json<AddTagForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
) -> Result<Json<OkResponseData<String>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.tag_service.add_tag(&add_tag_form.name, &user_id).await {
Expand All @@ -72,7 +72,7 @@ pub async fn delete_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(delete_tag_form): extract::Json<DeleteTagForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
) -> Result<Json<OkResponseData<String>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.tag_service.delete_tag(&delete_tag_form.tag_id, &user_id).await {
Expand Down
10 changes: 5 additions & 5 deletions src/web/api/v1/contexts/tag/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
use axum::Json;

use crate::models::torrent_tag::TagId;
use crate::web::api::v1::responses::OkResponse;
use crate::web::api::v1::responses::OkResponseData;

/// Response after successfully creating a new tag.
pub fn added_tag(tag_name: &str) -> Json<OkResponse<String>> {
Json(OkResponse {
pub fn added_tag(tag_name: &str) -> Json<OkResponseData<String>> {
Json(OkResponseData {
data: tag_name.to_string(),
})
}

/// Response after successfully deleting a tag.
pub fn deleted_tag(tag_id: TagId) -> Json<OkResponse<String>> {
Json(OkResponse {
pub fn deleted_tag(tag_id: TagId) -> Json<OkResponseData<String>> {
Json(OkResponseData {
data: tag_id.to_string(),
})
}
131 changes: 131 additions & 0 deletions src/web/api/v1/contexts/torrent/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! API handlers for the [`torrent`](crate::web::api::v1::contexts::torrent) API
//! context.
use std::io::{Cursor, Write};
use std::sync::Arc;

use axum::extract::{Multipart, State};
use axum::response::{IntoResponse, Response};

use super::responses::new_torrent_response;
use crate::common::AppData;
use crate::errors::ServiceError;
use crate::models::torrent::TorrentRequest;
use crate::models::torrent_tag::TagId;
use crate::routes::torrent::Create;
use crate::utils::parse_torrent;
use crate::web::api::v1::extractors::bearer_token::Extract;

/// Upload a new torrent file to the Index
///
/// # Errors
///
/// This function will return an error if
///
/// - The user does not have permission to upload the torrent file.
/// - The submitted torrent file is not a valid torrent file.
#[allow(clippy::unused_async)]
pub async fn upload_torrent_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
multipart: Multipart,
) -> Response {
let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
Ok(user_id) => user_id,
Err(err) => return err.into_response(),
};

let torrent_request = match get_torrent_request_from_payload(multipart).await {
Ok(torrent_request) => torrent_request,
Err(err) => return err.into_response(),
};

let info_hash = torrent_request.torrent.info_hash().clone();

match app_data.torrent_service.add_torrent(torrent_request, user_id).await {
Ok(torrent_id) => new_torrent_response(torrent_id, &info_hash).into_response(),
Err(error) => error.into_response(),
}
}

/// Extracts the [`TorrentRequest`] from the multipart form payload.
async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result<TorrentRequest, ServiceError> {
let torrent_buffer = vec![0u8];
let mut torrent_cursor = Cursor::new(torrent_buffer);

let mut title = String::new();
let mut description = String::new();
let mut category = String::new();
let mut tags: Vec<TagId> = vec![];

while let Some(mut field) = payload.next_field().await.unwrap() {
let name = field.name().unwrap().clone();

match name {
"title" => {
let data = field.bytes().await.unwrap();
if data.is_empty() {
continue;
};
title = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
}
"description" => {
let data = field.bytes().await.unwrap();
if data.is_empty() {
continue;
};
description = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
}
"category" => {
let data = field.bytes().await.unwrap();
if data.is_empty() {
continue;
};
category = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
}
"tags" => {
let data = field.bytes().await.unwrap();
if data.is_empty() {
continue;
};
let string_data = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
tags = serde_json::from_str(&string_data).map_err(|_| ServiceError::BadRequest)?;
}
"torrent" => {
let content_type = field.content_type().unwrap().clone();

if content_type != "application/x-bittorrent" {
return Err(ServiceError::InvalidFileType);
}

while let Some(chunk) = field.chunk().await.map_err(|_| (ServiceError::BadRequest))? {
torrent_cursor.write_all(&chunk)?;
}
}
_ => {}
}
}

let fields = Create {
title,
description,
category,
tags,
};

fields.verify()?;

let position = usize::try_from(torrent_cursor.position()).map_err(|_| ServiceError::InvalidTorrentFile)?;
let inner = torrent_cursor.get_ref();

let torrent = parse_torrent::decode_torrent(&inner[..position]).map_err(|_| ServiceError::InvalidTorrentFile)?;

// Make sure that the pieces key has a length that is a multiple of 20
// code-review: I think we could put this inside the service.
if let Some(pieces) = torrent.info.pieces.as_ref() {
if pieces.as_ref().len() % 20 != 0 {
return Err(ServiceError::InvalidTorrentPiecesLength);
}
}

Ok(TorrentRequest { fields, torrent })
}
3 changes: 3 additions & 0 deletions src/web/api/v1/contexts/torrent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,6 @@
//!
//! Refer to the [`DeletedTorrentResponse`](crate::models::response::DeletedTorrentResponse)
//! struct for more information about the response attributes.
pub mod handlers;
pub mod responses;
pub mod routes;
22 changes: 22 additions & 0 deletions src/web/api/v1/contexts/torrent/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use axum::Json;
use serde::{Deserialize, Serialize};

use crate::models::torrent::TorrentId;
use crate::web::api::v1::responses::OkResponseData;

#[allow(clippy::module_name_repetitions)]
#[derive(Serialize, Deserialize, Debug)]
pub struct NewTorrentResponseData {
pub torrent_id: TorrentId,
pub info_hash: String,
}

/// Response after successfully uploading a new torrent.
pub fn new_torrent_response(torrent_id: TorrentId, info_hash: &str) -> Json<OkResponseData<NewTorrentResponseData>> {
Json(OkResponseData {
data: NewTorrentResponseData {
torrent_id,
info_hash: info_hash.to_owned(),
},
})
}
15 changes: 15 additions & 0 deletions src/web/api/v1/contexts/torrent/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! API routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
//!
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag).
use std::sync::Arc;

use axum::routing::post;
use axum::Router;

use super::handlers::upload_torrent_handler;
use crate::common::AppData;

/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
pub fn router_for_single_resources(app_data: Arc<AppData>) -> Router {
Router::new().route("/upload", post(upload_torrent_handler).with_state(app_data))
}
Loading

0 comments on commit 538ebcc

Please sign in to comment.