Skip to content

Commit 538ebcc

Browse files
committed
refactor(api): [#182] Axum API, torrent context, upload torrent file
1 parent d7f1e34 commit 538ebcc

File tree

22 files changed

+1030
-78
lines changed

22 files changed

+1030
-78
lines changed

src/errors.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ pub enum ServiceError {
139139
DatabaseError,
140140
}
141141

142+
// Begin ActixWeb error handling
143+
// todo: remove after migration to Axum
144+
142145
#[derive(Serialize, Deserialize)]
143146
pub struct ErrorToResponse {
144147
pub error: String,
@@ -156,6 +159,8 @@ impl ResponseError for ServiceError {
156159
}
157160
}
158161

162+
// End ActixWeb error handling
163+
159164
impl From<sqlx::Error> for ServiceError {
160165
fn from(e: sqlx::Error) -> Self {
161166
eprintln!("{e:?}");

src/web/api/v1/contexts/category/handlers.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::common::AppData;
1111
use crate::databases::database::{self, Category};
1212
use crate::errors::ServiceError;
1313
use crate::web::api::v1::extractors::bearer_token::Extract;
14-
use crate::web::api::v1::responses::{self, OkResponse};
14+
use crate::web::api::v1::responses::{self, OkResponseData};
1515

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

5555
match app_data.category_service.add_category(&category_form.name, &user_id).await {
@@ -71,7 +71,7 @@ pub async fn delete_handler(
7171
State(app_data): State<Arc<AppData>>,
7272
Extract(maybe_bearer_token): Extract,
7373
extract::Json(category_form): extract::Json<DeleteCategoryForm>,
74-
) -> Result<Json<OkResponse<String>>, ServiceError> {
74+
) -> Result<Json<OkResponseData<String>>, ServiceError> {
7575
// code-review: why do we need to send the whole category object to delete it?
7676
// And we should use the ID instead of the name, because the name could change
7777
// or we could add support for multiple languages.

src/web/api/v1/contexts/category/responses.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22
//! context.
33
use axum::Json;
44

5-
use crate::web::api::v1::responses::OkResponse;
5+
use crate::web::api::v1::responses::OkResponseData;
66

77
/// Response after successfully creating a new category.
8-
pub fn added_category(category_name: &str) -> Json<OkResponse<String>> {
9-
Json(OkResponse {
8+
pub fn added_category(category_name: &str) -> Json<OkResponseData<String>> {
9+
Json(OkResponseData {
1010
data: category_name.to_string(),
1111
})
1212
}
1313

1414
/// Response after successfully deleting a new category.
15-
pub fn deleted_category(category_name: &str) -> Json<OkResponse<String>> {
16-
Json(OkResponse {
15+
pub fn deleted_category(category_name: &str) -> Json<OkResponseData<String>> {
16+
Json(OkResponseData {
1717
data: category_name.to_string(),
1818
})
1919
}

src/web/api/v1/contexts/settings/handlers.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::common::AppData;
99
use crate::config::{ConfigurationPublic, TorrustBackend};
1010
use crate::errors::ServiceError;
1111
use crate::web::api::v1::extractors::bearer_token::Extract;
12-
use crate::web::api::v1::responses::{self, OkResponse};
12+
use crate::web::api::v1::responses::{self, OkResponseData};
1313

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

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

29-
Ok(Json(responses::OkResponse { data: all_settings }))
29+
Ok(Json(responses::OkResponseData { data: all_settings }))
3030
}
3131

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

37-
Json(responses::OkResponse { data: public_settings })
37+
Json(responses::OkResponseData { data: public_settings })
3838
}
3939

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

45-
Json(responses::OkResponse { data: site_name })
45+
Json(responses::OkResponseData { data: site_name })
4646
}
4747

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

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

67-
Ok(Json(responses::OkResponse { data: new_settings }))
67+
Ok(Json(responses::OkResponseData { data: new_settings }))
6868
}

src/web/api/v1/contexts/tag/handlers.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::databases::database;
1212
use crate::errors::ServiceError;
1313
use crate::models::torrent_tag::TorrentTag;
1414
use crate::web::api::v1::extractors::bearer_token::Extract;
15-
use crate::web::api::v1::responses::{self, OkResponse};
15+
use crate::web::api::v1::responses::{self, OkResponseData};
1616

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

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

7878
match app_data.tag_service.delete_tag(&delete_tag_form.tag_id, &user_id).await {

src/web/api/v1/contexts/tag/responses.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
use axum::Json;
44

55
use crate::models::torrent_tag::TagId;
6-
use crate::web::api::v1::responses::OkResponse;
6+
use crate::web::api::v1::responses::OkResponseData;
77

88
/// Response after successfully creating a new tag.
9-
pub fn added_tag(tag_name: &str) -> Json<OkResponse<String>> {
10-
Json(OkResponse {
9+
pub fn added_tag(tag_name: &str) -> Json<OkResponseData<String>> {
10+
Json(OkResponseData {
1111
data: tag_name.to_string(),
1212
})
1313
}
1414

1515
/// Response after successfully deleting a tag.
16-
pub fn deleted_tag(tag_id: TagId) -> Json<OkResponse<String>> {
17-
Json(OkResponse {
16+
pub fn deleted_tag(tag_id: TagId) -> Json<OkResponseData<String>> {
17+
Json(OkResponseData {
1818
data: tag_id.to_string(),
1919
})
2020
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//! API handlers for the [`torrent`](crate::web::api::v1::contexts::torrent) API
2+
//! context.
3+
use std::io::{Cursor, Write};
4+
use std::sync::Arc;
5+
6+
use axum::extract::{Multipart, State};
7+
use axum::response::{IntoResponse, Response};
8+
9+
use super::responses::new_torrent_response;
10+
use crate::common::AppData;
11+
use crate::errors::ServiceError;
12+
use crate::models::torrent::TorrentRequest;
13+
use crate::models::torrent_tag::TagId;
14+
use crate::routes::torrent::Create;
15+
use crate::utils::parse_torrent;
16+
use crate::web::api::v1::extractors::bearer_token::Extract;
17+
18+
/// Upload a new torrent file to the Index
19+
///
20+
/// # Errors
21+
///
22+
/// This function will return an error if
23+
///
24+
/// - The user does not have permission to upload the torrent file.
25+
/// - The submitted torrent file is not a valid torrent file.
26+
#[allow(clippy::unused_async)]
27+
pub async fn upload_torrent_handler(
28+
State(app_data): State<Arc<AppData>>,
29+
Extract(maybe_bearer_token): Extract,
30+
multipart: Multipart,
31+
) -> Response {
32+
let user_id = match app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await {
33+
Ok(user_id) => user_id,
34+
Err(err) => return err.into_response(),
35+
};
36+
37+
let torrent_request = match get_torrent_request_from_payload(multipart).await {
38+
Ok(torrent_request) => torrent_request,
39+
Err(err) => return err.into_response(),
40+
};
41+
42+
let info_hash = torrent_request.torrent.info_hash().clone();
43+
44+
match app_data.torrent_service.add_torrent(torrent_request, user_id).await {
45+
Ok(torrent_id) => new_torrent_response(torrent_id, &info_hash).into_response(),
46+
Err(error) => error.into_response(),
47+
}
48+
}
49+
50+
/// Extracts the [`TorrentRequest`] from the multipart form payload.
51+
async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result<TorrentRequest, ServiceError> {
52+
let torrent_buffer = vec![0u8];
53+
let mut torrent_cursor = Cursor::new(torrent_buffer);
54+
55+
let mut title = String::new();
56+
let mut description = String::new();
57+
let mut category = String::new();
58+
let mut tags: Vec<TagId> = vec![];
59+
60+
while let Some(mut field) = payload.next_field().await.unwrap() {
61+
let name = field.name().unwrap().clone();
62+
63+
match name {
64+
"title" => {
65+
let data = field.bytes().await.unwrap();
66+
if data.is_empty() {
67+
continue;
68+
};
69+
title = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
70+
}
71+
"description" => {
72+
let data = field.bytes().await.unwrap();
73+
if data.is_empty() {
74+
continue;
75+
};
76+
description = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
77+
}
78+
"category" => {
79+
let data = field.bytes().await.unwrap();
80+
if data.is_empty() {
81+
continue;
82+
};
83+
category = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
84+
}
85+
"tags" => {
86+
let data = field.bytes().await.unwrap();
87+
if data.is_empty() {
88+
continue;
89+
};
90+
let string_data = String::from_utf8(data.to_vec()).map_err(|_| ServiceError::BadRequest)?;
91+
tags = serde_json::from_str(&string_data).map_err(|_| ServiceError::BadRequest)?;
92+
}
93+
"torrent" => {
94+
let content_type = field.content_type().unwrap().clone();
95+
96+
if content_type != "application/x-bittorrent" {
97+
return Err(ServiceError::InvalidFileType);
98+
}
99+
100+
while let Some(chunk) = field.chunk().await.map_err(|_| (ServiceError::BadRequest))? {
101+
torrent_cursor.write_all(&chunk)?;
102+
}
103+
}
104+
_ => {}
105+
}
106+
}
107+
108+
let fields = Create {
109+
title,
110+
description,
111+
category,
112+
tags,
113+
};
114+
115+
fields.verify()?;
116+
117+
let position = usize::try_from(torrent_cursor.position()).map_err(|_| ServiceError::InvalidTorrentFile)?;
118+
let inner = torrent_cursor.get_ref();
119+
120+
let torrent = parse_torrent::decode_torrent(&inner[..position]).map_err(|_| ServiceError::InvalidTorrentFile)?;
121+
122+
// Make sure that the pieces key has a length that is a multiple of 20
123+
// code-review: I think we could put this inside the service.
124+
if let Some(pieces) = torrent.info.pieces.as_ref() {
125+
if pieces.as_ref().len() % 20 != 0 {
126+
return Err(ServiceError::InvalidTorrentPiecesLength);
127+
}
128+
}
129+
130+
Ok(TorrentRequest { fields, torrent })
131+
}

src/web/api/v1/contexts/torrent/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,6 @@
328328
//!
329329
//! Refer to the [`DeletedTorrentResponse`](crate::models::response::DeletedTorrentResponse)
330330
//! struct for more information about the response attributes.
331+
pub mod handlers;
332+
pub mod responses;
333+
pub mod routes;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use axum::Json;
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::models::torrent::TorrentId;
5+
use crate::web::api::v1::responses::OkResponseData;
6+
7+
#[allow(clippy::module_name_repetitions)]
8+
#[derive(Serialize, Deserialize, Debug)]
9+
pub struct NewTorrentResponseData {
10+
pub torrent_id: TorrentId,
11+
pub info_hash: String,
12+
}
13+
14+
/// Response after successfully uploading a new torrent.
15+
pub fn new_torrent_response(torrent_id: TorrentId, info_hash: &str) -> Json<OkResponseData<NewTorrentResponseData>> {
16+
Json(OkResponseData {
17+
data: NewTorrentResponseData {
18+
torrent_id,
19+
info_hash: info_hash.to_owned(),
20+
},
21+
})
22+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! API routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
2+
//!
3+
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag).
4+
use std::sync::Arc;
5+
6+
use axum::routing::post;
7+
use axum::Router;
8+
9+
use super::handlers::upload_torrent_handler;
10+
use crate::common::AppData;
11+
12+
/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
13+
pub fn router_for_single_resources(app_data: Arc<AppData>) -> Router {
14+
Router::new().route("/upload", post(upload_torrent_handler).with_state(app_data))
15+
}

0 commit comments

Comments
 (0)