From eb26c8d5d8a98c1c8f02337ee3f6c7e45139db83 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 18 Sep 2023 15:46:53 +0100 Subject: [PATCH 1/7] fix: tag name for random tag in tests --- tests/common/contexts/tag/fixtures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common/contexts/tag/fixtures.rs b/tests/common/contexts/tag/fixtures.rs index 39ac3081..6012497f 100644 --- a/tests/common/contexts/tag/fixtures.rs +++ b/tests/common/contexts/tag/fixtures.rs @@ -1,7 +1,7 @@ use rand::Rng; pub fn random_tag_name() -> String { - format!("category name {}", random_id()) + format!("tag name {}", random_id()) } fn random_id() -> u64 { From e9476fcc5ef3937273925d2367309aefcd9ccea2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 18 Sep 2023 15:33:45 +0100 Subject: [PATCH 2/7] feat: [#296] persist torrent comment and add it to the API responses. - Torrent details endpoint - Torrent list endpoint - Include the comment in the downloaded torrent --- ...4_torrust_add_comment_field_to_torrent.sql | 1 + ...4_torrust_add_comment_field_to_torrent.sql | 1 + src/databases/mysql.rs | 147 +++++++++++------- src/databases/sqlite.rs | 145 ++++++++++------- src/models/response.rs | 2 + src/models/torrent.rs | 1 + src/models/torrent_file.rs | 6 +- src/services/torrent_file.rs | 4 + tests/common/contexts/torrent/responses.rs | 2 + .../web/api/v1/contexts/torrent/contract.rs | 1 + 10 files changed, 204 insertions(+), 106 deletions(-) create mode 100644 migrations/mysql/20230918103654_torrust_add_comment_field_to_torrent.sql create mode 100644 migrations/sqlite3/20230918103654_torrust_add_comment_field_to_torrent.sql diff --git a/migrations/mysql/20230918103654_torrust_add_comment_field_to_torrent.sql b/migrations/mysql/20230918103654_torrust_add_comment_field_to_torrent.sql new file mode 100644 index 00000000..2ecee2a9 --- /dev/null +++ b/migrations/mysql/20230918103654_torrust_add_comment_field_to_torrent.sql @@ -0,0 +1 @@ +ALTER TABLE torrust_torrents ADD COLUMN comment TEXT NULL; diff --git a/migrations/sqlite3/20230918103654_torrust_add_comment_field_to_torrent.sql b/migrations/sqlite3/20230918103654_torrust_add_comment_field_to_torrent.sql new file mode 100644 index 00000000..ff8774e2 --- /dev/null +++ b/migrations/sqlite3/20230918103654_torrust_add_comment_field_to_torrent.sql @@ -0,0 +1 @@ +ALTER TABLE "torrust_torrents" ADD COLUMN "comment" TEXT NULL; \ No newline at end of file diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index 8a044fb4..550a3d7d 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -300,7 +300,8 @@ impl Database for Mysql { }) } - // TODO: refactor this + // todo: refactor this + #[allow(clippy::too_many_lines)] async fn get_torrents_search_sorted_paginated( &self, search: &Option, @@ -375,7 +376,17 @@ impl Database for Mysql { }; let mut query_string = format!( - "SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, tt.size AS file_size, tt.name, + "SELECT + tt.torrent_id, + tp.username AS uploader, + tt.info_hash, + ti.title, + ti.description, + tt.category_id, + DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, + tt.size AS file_size, + tt.name, + tt.comment, CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders, CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers FROM torrust_torrents tt @@ -443,31 +454,47 @@ impl Database for Mysql { }; // add torrent - let torrent_id = query("INSERT INTO torrust_torrents (uploader_id, category_id, info_hash, size, name, pieces, piece_length, private, root_hash, `source`, date_uploaded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())") - .bind(uploader_id) - .bind(category_id) - .bind(info_hash.to_lowercase()) - .bind(torrent.file_size()) - .bind(torrent.info.name.to_string()) - .bind(pieces) - .bind(torrent.info.piece_length) - .bind(torrent.info.private) - .bind(root_hash) - .bind(torrent.info.source.clone()) - .execute(&mut tx) - .await - .map(|v| i64::try_from(v.last_insert_id()).expect("last ID is larger than i64")) - .map_err(|e| match e { - sqlx::Error::Database(err) => { - log::error!("DB error: {:?}", err); - if err.message().contains("Duplicate entry") && err.message().contains("info_hash") { - database::Error::TorrentAlreadyExists - } else { - database::Error::Error - } + let torrent_id = query( + "INSERT INTO torrust_torrents ( + uploader_id, + category_id, + info_hash, + size, + name, + pieces, + piece_length, + private, + root_hash, + `source`, + comment, + date_uploaded + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())", + ) + .bind(uploader_id) + .bind(category_id) + .bind(info_hash.to_lowercase()) + .bind(torrent.file_size()) + .bind(torrent.info.name.to_string()) + .bind(pieces) + .bind(torrent.info.piece_length) + .bind(torrent.info.private) + .bind(root_hash) + .bind(torrent.info.source.clone()) + .bind(torrent.comment.clone()) + .execute(&mut tx) + .await + .map(|v| i64::try_from(v.last_insert_id()).expect("last ID is larger than i64")) + .map_err(|e| match e { + sqlx::Error::Database(err) => { + log::error!("DB error: {:?}", err); + if err.message().contains("Duplicate entry") && err.message().contains("info_hash") { + database::Error::TorrentAlreadyExists + } else { + database::Error::Error } - _ => database::Error::Error - })?; + } + _ => database::Error::Error, + })?; // add torrent canonical infohash @@ -650,23 +677,19 @@ impl Database for Mysql { } async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { - query_as::<_, DbTorrentInfo>( - "SELECT torrent_id, info_hash, name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?", - ) - .bind(torrent_id) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE torrent_id = ?") + .bind(torrent_id) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result { - query_as::<_, DbTorrentInfo>( - "SELECT torrent_id, info_hash, name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE info_hash = ?", - ) - .bind(info_hash.to_hex_string().to_lowercase()) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE info_hash = ?") + .bind(info_hash.to_hex_string().to_lowercase()) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_torrent_files_from_id(&self, torrent_id: i64) -> Result, database::Error> { @@ -705,7 +728,17 @@ impl Database for Mysql { async fn get_torrent_listing_from_id(&self, torrent_id: i64) -> Result { query_as::<_, TorrentListing>( - "SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, tt.size AS file_size, tt.name, + "SELECT + tt.torrent_id, + tp.username AS uploader, + tt.info_hash, + ti.title, + ti.description, + tt.category_id, + DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, + tt.size AS file_size, + tt.name, + tt.comment, CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders, CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers FROM torrust_torrents tt @@ -713,17 +746,27 @@ impl Database for Mysql { INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id WHERE tt.torrent_id = ? - GROUP BY torrent_id" + GROUP BY torrent_id", ) - .bind(torrent_id) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + .bind(torrent_id) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_torrent_listing_from_info_hash(&self, info_hash: &InfoHash) -> Result { query_as::<_, TorrentListing>( - "SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, tt.size AS file_size, tt.name, + "SELECT + tt.torrent_id, + tp.username AS uploader, + tt.info_hash, + ti.title, + ti.description, + tt.category_id, + DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, + tt.size AS file_size, + tt.name, + tt.comment, CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders, CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers FROM torrust_torrents tt @@ -731,12 +774,12 @@ impl Database for Mysql { INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id WHERE tt.info_hash = ? - GROUP BY torrent_id" + GROUP BY torrent_id", ) - .bind(info_hash.to_hex_string().to_lowercase()) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + .bind(info_hash.to_hex_string().to_lowercase()) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_all_torrents_compact(&self) -> Result, database::Error> { diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 86b71570..9a0dae7a 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -290,7 +290,8 @@ impl Database for Sqlite { }) } - // TODO: refactor this + // todo: refactor this + #[allow(clippy::too_many_lines)] async fn get_torrents_search_sorted_paginated( &self, search: &Option, @@ -365,7 +366,17 @@ impl Database for Sqlite { }; let mut query_string = format!( - "SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, tt.date_uploaded, tt.size AS file_size, tt.name, + "SELECT + tt.torrent_id, + tp.username AS uploader, + tt.info_hash, + ti.title, + ti.description, + tt.category_id, + tt.date_uploaded, + tt.size AS file_size, + tt.name, + tt.comment, CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders, CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers FROM torrust_torrents tt @@ -433,31 +444,47 @@ impl Database for Sqlite { }; // add torrent - let torrent_id = query("INSERT INTO torrust_torrents (uploader_id, category_id, info_hash, size, name, pieces, piece_length, private, root_hash, `source`, date_uploaded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')))") - .bind(uploader_id) - .bind(category_id) - .bind(info_hash.to_lowercase()) - .bind(torrent.file_size()) - .bind(torrent.info.name.to_string()) - .bind(pieces) - .bind(torrent.info.piece_length) - .bind(torrent.info.private) - .bind(root_hash) - .bind(torrent.info.source.clone()) - .execute(&mut tx) - .await - .map(|v| v.last_insert_rowid()) - .map_err(|e| match e { - sqlx::Error::Database(err) => { - log::error!("DB error: {:?}", err); - if err.message().contains("UNIQUE") && err.message().contains("info_hash") { - database::Error::TorrentAlreadyExists - } else { - database::Error::Error - } + let torrent_id = query( + "INSERT INTO torrust_torrents ( + uploader_id, + category_id, + info_hash, + size, + name, + pieces, + piece_length, + private, + root_hash, + `source`, + comment, + date_uploaded + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')))", + ) + .bind(uploader_id) + .bind(category_id) + .bind(info_hash.to_lowercase()) + .bind(torrent.file_size()) + .bind(torrent.info.name.to_string()) + .bind(pieces) + .bind(torrent.info.piece_length) + .bind(torrent.info.private) + .bind(root_hash) + .bind(torrent.info.source.clone()) + .bind(torrent.comment.clone()) + .execute(&mut tx) + .await + .map(|v| v.last_insert_rowid()) + .map_err(|e| match e { + sqlx::Error::Database(err) => { + log::error!("DB error: {:?}", err); + if err.message().contains("UNIQUE") && err.message().contains("info_hash") { + database::Error::TorrentAlreadyExists + } else { + database::Error::Error } - _ => database::Error::Error - })?; + } + _ => database::Error::Error, + })?; // add torrent canonical infohash @@ -640,23 +667,19 @@ impl Database for Sqlite { } async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { - query_as::<_, DbTorrentInfo>( - "SELECT torrent_id, info_hash, name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?", - ) - .bind(torrent_id) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE torrent_id = ?") + .bind(torrent_id) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result { - query_as::<_, DbTorrentInfo>( - "SELECT torrent_id, info_hash, name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE info_hash = ?", - ) - .bind(info_hash.to_hex_string().to_lowercase()) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE info_hash = ?") + .bind(info_hash.to_hex_string().to_lowercase()) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_torrent_files_from_id(&self, torrent_id: i64) -> Result, database::Error> { @@ -695,7 +718,16 @@ impl Database for Sqlite { async fn get_torrent_listing_from_id(&self, torrent_id: i64) -> Result { query_as::<_, TorrentListing>( - "SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, tt.date_uploaded, tt.size AS file_size, tt.name, + "SELECT + tt.torrent_id, + tp.username AS uploader, + tt.info_hash, ti.title, + ti.description, + tt.category_id, + tt.date_uploaded, + tt.size AS file_size, + tt.name, + tt.comment, CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders, CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers FROM torrust_torrents tt @@ -703,17 +735,26 @@ impl Database for Sqlite { INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id WHERE tt.torrent_id = ? - GROUP BY ts.torrent_id" + GROUP BY ts.torrent_id", ) - .bind(torrent_id) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + .bind(torrent_id) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_torrent_listing_from_info_hash(&self, info_hash: &InfoHash) -> Result { query_as::<_, TorrentListing>( - "SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, tt.date_uploaded, tt.size AS file_size, tt.name, + "SELECT + tt.torrent_id, + tp.username AS uploader, + tt.info_hash, ti.title, + ti.description, + tt.category_id, + tt.date_uploaded, + tt.size AS file_size, + tt.name, + tt.comment, CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders, CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers FROM torrust_torrents tt @@ -721,12 +762,12 @@ impl Database for Sqlite { INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id WHERE tt.info_hash = ? - GROUP BY ts.torrent_id" + GROUP BY ts.torrent_id", ) - .bind(info_hash.to_string().to_lowercase()) - .fetch_one(&self.pool) - .await - .map_err(|_| database::Error::TorrentNotFound) + .bind(info_hash.to_string().to_lowercase()) + .fetch_one(&self.pool) + .await + .map_err(|_| database::Error::TorrentNotFound) } async fn get_all_torrents_compact(&self) -> Result, database::Error> { diff --git a/src/models/response.rs b/src/models/response.rs index adb1de07..7d408b79 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -62,6 +62,7 @@ pub struct TorrentResponse { pub magnet_link: String, pub tags: Vec, pub name: String, + pub comment: Option, } impl TorrentResponse { @@ -83,6 +84,7 @@ impl TorrentResponse { magnet_link: String::new(), tags: vec![], name: torrent_listing.name, + comment: torrent_listing.comment, } } } diff --git a/src/models/torrent.rs b/src/models/torrent.rs index eb2bcde2..150d2bba 100644 --- a/src/models/torrent.rs +++ b/src/models/torrent.rs @@ -21,6 +21,7 @@ pub struct TorrentListing { pub seeders: i64, pub leechers: i64, pub name: String, + pub comment: Option, } #[derive(Debug, Deserialize)] diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index c8849170..effd0f48 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -98,7 +98,7 @@ pub struct Torrent { #[serde(default)] #[serde(rename = "creation date")] pub creation_date: Option, - #[serde(rename = "comment")] + #[serde(default)] pub comment: Option, #[serde(default)] #[serde(rename = "created by")] @@ -171,7 +171,7 @@ impl Torrent { httpseeds: None, announce_list: Some(torrent_info.announce_urls), creation_date: None, - comment: None, + comment: torrent_info.comment, created_by: None, } } @@ -191,6 +191,7 @@ impl Torrent { root_hash: torrent_info.root_hash, files: torrent_files, announce_urls: torrent_announce_urls, + comment: torrent_info.comment, }; Torrent::from_new_torrent_info_request(torrent_info_request) } @@ -296,6 +297,7 @@ pub struct DbTorrentInfo { #[serde(default)] pub private: Option, pub root_hash: i64, + pub comment: Option, } #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] diff --git a/src/services/torrent_file.rs b/src/services/torrent_file.rs index dfa72dbd..dbfa72f5 100644 --- a/src/services/torrent_file.rs +++ b/src/services/torrent_file.rs @@ -9,13 +9,16 @@ use crate::services::hasher::sha1; /// It's not the full in-memory representation of a torrent file. The full /// in-memory representation is the `Torrent` struct. pub struct NewTorrentInfoRequest { + // The `info` dictionary fields pub name: String, pub pieces: String, pub piece_length: i64, pub private: Option, pub root_hash: i64, pub files: Vec, + // Other fields of the root level metainfo dictionary pub announce_urls: Vec>, + pub comment: Option, } /// It generates a random single-file torrent for testing purposes. @@ -48,6 +51,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent { root_hash: 0, files: torrent_files, announce_urls: torrent_announce_urls, + comment: None, }; Torrent::from_new_torrent_info_request(torrent_info_request) diff --git a/tests/common/contexts/torrent/responses.rs b/tests/common/contexts/torrent/responses.rs index ee08c2dc..f95d67ce 100644 --- a/tests/common/contexts/torrent/responses.rs +++ b/tests/common/contexts/torrent/responses.rs @@ -40,6 +40,7 @@ pub struct ListItem { pub seeders: i64, pub leechers: i64, pub name: String, + pub comment: Option, } #[derive(Deserialize, PartialEq, Debug)] @@ -64,6 +65,7 @@ pub struct TorrentDetails { pub magnet_link: String, pub tags: Vec, pub name: String, + pub comment: Option, } #[derive(Deserialize, PartialEq, Debug)] diff --git a/tests/e2e/web/api/v1/contexts/torrent/contract.rs b/tests/e2e/web/api/v1/contexts/torrent/contract.rs index 5ed440b1..3e577b37 100644 --- a/tests/e2e/web/api/v1/contexts/torrent/contract.rs +++ b/tests/e2e/web/api/v1/contexts/torrent/contract.rs @@ -211,6 +211,7 @@ mod for_guests { ), tags: vec![], name: test_torrent.index_info.name.clone(), + comment: test_torrent.file_info.comment.clone(), }; assert_expected_torrent_details(&torrent_details_response.data, &expected_torrent); From f0ad6a442f93560c61eda5296b1cfe26f48299a3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 18 Sep 2023 17:18:48 +0100 Subject: [PATCH 3/7] refactor: rename structs and reorganize mods --- src/databases/database.rs | 18 +- src/databases/mysql.rs | 10 +- src/databases/sqlite.rs | 10 +- src/models/torrent_file.rs | 249 +++++++++--------- src/services/torrent.rs | 4 +- src/services/torrent_file.rs | 75 +++++- .../databases/sqlite_v2_0_0.rs | 4 +- 7 files changed, 215 insertions(+), 155 deletions(-) diff --git a/src/databases/database.rs b/src/databases/database.rs index 8fc10d79..e5778649 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -8,7 +8,7 @@ use crate::models::category::CategoryId; use crate::models::info_hash::InfoHash; use crate::models::response::TorrentsResponse; use crate::models::torrent::TorrentListing; -use crate::models::torrent_file::{DbTorrentInfo, Torrent, TorrentFile}; +use crate::models::torrent_file::{DbTorrent, Torrent, TorrentFile}; use crate::models::torrent_tag::{TagId, TorrentTag}; use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile}; @@ -209,11 +209,7 @@ pub trait Database: Sync + Send { let torrent_announce_urls = self.get_torrent_announce_urls_from_id(torrent_info.torrent_id).await?; - Ok(Torrent::from_db_info_files_and_announce_urls( - torrent_info, - torrent_files, - torrent_announce_urls, - )) + Ok(Torrent::from_database(torrent_info, torrent_files, torrent_announce_urls)) } /// Get `Torrent` from `torrent_id`. @@ -224,11 +220,7 @@ pub trait Database: Sync + Send { let torrent_announce_urls = self.get_torrent_announce_urls_from_id(torrent_id).await?; - Ok(Torrent::from_db_info_files_and_announce_urls( - torrent_info, - torrent_files, - torrent_announce_urls, - )) + Ok(Torrent::from_database(torrent_info, torrent_files, torrent_announce_urls)) } /// It returns the list of all infohashes producing the same canonical @@ -257,10 +249,10 @@ pub trait Database: Sync + Send { async fn add_info_hash_to_canonical_info_hash_group(&self, original: &InfoHash, canonical: &InfoHash) -> Result<(), Error>; /// Get torrent's info as `DbTorrentInfo` from `torrent_id`. - async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result; + async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result; /// Get torrent's info as `DbTorrentInfo` from torrent `InfoHash`. - async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result; + async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result; /// Get all torrent's files as `Vec` from `torrent_id`. async fn get_torrent_files_from_id(&self, torrent_id: i64) -> Result, Error>; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index 550a3d7d..f793e5cb 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -13,7 +13,7 @@ use crate::models::category::CategoryId; use crate::models::info_hash::InfoHash; use crate::models::response::TorrentsResponse; use crate::models::torrent::TorrentListing; -use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrentInfo, Torrent, TorrentFile}; +use crate::models::torrent_file::{DbTorrent, DbTorrentAnnounceUrl, DbTorrentFile, Torrent, TorrentFile}; use crate::models::torrent_tag::{TagId, TorrentTag}; use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile}; @@ -676,16 +676,16 @@ impl Database for Mysql { .map_err(|err| database::Error::ErrorWithText(err.to_string())) } - async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { - query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE torrent_id = ?") + async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { + query_as::<_, DbTorrent>("SELECT * FROM torrust_torrents WHERE torrent_id = ?") .bind(torrent_id) .fetch_one(&self.pool) .await .map_err(|_| database::Error::TorrentNotFound) } - async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result { - query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE info_hash = ?") + async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result { + query_as::<_, DbTorrent>("SELECT * FROM torrust_torrents WHERE info_hash = ?") .bind(info_hash.to_hex_string().to_lowercase()) .fetch_one(&self.pool) .await diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 9a0dae7a..980e7a3b 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -13,7 +13,7 @@ use crate::models::category::CategoryId; use crate::models::info_hash::InfoHash; use crate::models::response::TorrentsResponse; use crate::models::torrent::TorrentListing; -use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrentInfo, Torrent, TorrentFile}; +use crate::models::torrent_file::{DbTorrent, DbTorrentAnnounceUrl, DbTorrentFile, Torrent, TorrentFile}; use crate::models::torrent_tag::{TagId, TorrentTag}; use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile}; @@ -666,16 +666,16 @@ impl Database for Sqlite { .map_err(|err| database::Error::ErrorWithText(err.to_string())) } - async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { - query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE torrent_id = ?") + async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { + query_as::<_, DbTorrent>("SELECT * FROM torrust_torrents WHERE torrent_id = ?") .bind(torrent_id) .fetch_one(&self.pool) .await .map_err(|_| database::Error::TorrentNotFound) } - async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result { - query_as::<_, DbTorrentInfo>("SELECT * FROM torrust_torrents WHERE info_hash = ?") + async fn get_torrent_info_from_info_hash(&self, info_hash: &InfoHash) -> Result { + query_as::<_, DbTorrent>("SELECT * FROM torrust_torrents WHERE info_hash = ?") .bind(info_hash.to_hex_string().to_lowercase()) .fetch_one(&self.pool) .await diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index effd0f48..ace9f9fa 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -5,22 +5,38 @@ use sha1::{Digest, Sha1}; use super::info_hash::InfoHash; use crate::config::Configuration; -use crate::services::torrent_file::NewTorrentInfoRequest; +use crate::services::torrent_file::CreateTorrentRequest; use crate::utils::hex::{from_bytes, into_bytes}; -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -pub struct TorrentNode(String, i64); - -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -pub struct TorrentFile { - pub path: Vec, - pub length: i64, +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct Torrent { + pub info: TorrentInfoDictionary, // #[serde(default)] - pub md5sum: Option, + pub announce: Option, + #[serde(default)] + pub nodes: Option>, + #[serde(default)] + pub encoding: Option, + #[serde(default)] + pub httpseeds: Option>, + #[serde(default)] + #[serde(rename = "announce-list")] + pub announce_list: Option>>, + #[serde(default)] + #[serde(rename = "creation date")] + pub creation_date: Option, + #[serde(default)] + pub comment: Option, + #[serde(default)] + #[serde(rename = "created by")] + pub created_by: Option, } #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -pub struct TorrentInfo { +pub struct TorrentNode(String, i64); + +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct TorrentInfoDictionary { pub name: String, #[serde(default)] pub pieces: Option, @@ -43,108 +59,79 @@ pub struct TorrentInfo { pub source: Option, } -impl TorrentInfo { - /// torrent file can only hold a pieces key or a root hash key: - /// [BEP 39](http://www.bittorrent.org/beps/bep_0030.html) - #[must_use] - pub fn get_pieces_as_string(&self) -> String { - match &self.pieces { - None => String::new(), - Some(byte_buf) => from_bytes(byte_buf.as_ref()), - } - } +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct TorrentFile { + pub path: Vec, + pub length: i64, + #[serde(default)] + pub md5sum: Option, +} - /// It returns the root hash as a `i64` value. +impl Torrent { + /// It builds a `Torrent` from a request. /// /// # Panics /// - /// This function will panic if the root hash cannot be converted into a - /// `i64` value. + /// This function will panic if the `torrent_info.pieces` is not a valid hex string. #[must_use] - pub fn get_root_hash_as_i64(&self) -> i64 { - match &self.root_hash { - None => 0i64, - Some(root_hash) => root_hash - .parse::() - .expect("variable `root_hash` cannot be converted into a `i64`"), - } - } + pub fn from_request(create_torrent_req: CreateTorrentRequest) -> Self { + let info_dict = create_torrent_req.build_info_dictionary(); - #[must_use] - pub fn is_a_single_file_torrent(&self) -> bool { - self.length.is_some() - } - - #[must_use] - pub fn is_a_multiple_file_torrent(&self) -> bool { - self.files.is_some() + Self { + info: info_dict, + announce: None, + nodes: None, + encoding: None, + httpseeds: None, + announce_list: Some(create_torrent_req.announce_urls), + creation_date: None, + comment: create_torrent_req.comment, + created_by: None, + } } -} -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] -pub struct Torrent { - pub info: TorrentInfo, // - #[serde(default)] - pub announce: Option, - #[serde(default)] - pub nodes: Option>, - #[serde(default)] - pub encoding: Option, - #[serde(default)] - pub httpseeds: Option>, - #[serde(default)] - #[serde(rename = "announce-list")] - pub announce_list: Option>>, - #[serde(default)] - #[serde(rename = "creation date")] - pub creation_date: Option, - #[serde(default)] - pub comment: Option, - #[serde(default)] - #[serde(rename = "created by")] - pub created_by: Option, -} - -impl Torrent { - /// It builds a `Torrent` from a `NewTorrentInfoRequest`. + /// It hydrates a `Torrent` struct from the database data. /// /// # Panics /// - /// This function will panic if the `torrent_info.pieces` is not a valid hex string. + /// This function will panic if the `torrent_info.pieces` is not a valid + /// hex string. #[must_use] - pub fn from_new_torrent_info_request(torrent_info: NewTorrentInfoRequest) -> Self { - // the info part of the torrent file - let mut info = TorrentInfo { - name: torrent_info.name.to_string(), + pub fn from_database( + db_torrent: DbTorrent, + torrent_files: Vec, + torrent_announce_urls: Vec>, + ) -> Self { + let mut info_dict = TorrentInfoDictionary { + name: db_torrent.name, pieces: None, - piece_length: torrent_info.piece_length, + piece_length: db_torrent.piece_length, md5sum: None, length: None, files: None, - private: torrent_info.private, + private: db_torrent.private, path: None, root_hash: None, source: None, }; // a torrent file has a root hash or a pieces key, but not both. - if torrent_info.root_hash > 0 { - info.root_hash = Some(torrent_info.pieces); + if db_torrent.root_hash > 0 { + info_dict.root_hash = Some(db_torrent.pieces); } else { - let pieces = into_bytes(&torrent_info.pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); - info.pieces = Some(ByteBuf::from(pieces)); + let buffer = into_bytes(&db_torrent.pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); + info_dict.pieces = Some(ByteBuf::from(buffer)); } // either set the single file or the multiple files information - if torrent_info.files.len() == 1 { - let torrent_file = torrent_info - .files + if torrent_files.len() == 1 { + let torrent_file = torrent_files .first() .expect("vector `torrent_files` should have at least one element"); - info.md5sum = torrent_file.md5sum.clone(); + info_dict.md5sum = torrent_file.md5sum.clone(); - info.length = Some(torrent_file.length); + info_dict.length = Some(torrent_file.length); let path = if torrent_file .path @@ -158,44 +145,24 @@ impl Torrent { Some(torrent_file.path.clone()) }; - info.path = path; + info_dict.path = path; } else { - info.files = Some(torrent_info.files); + info_dict.files = Some(torrent_files); } Self { - info, + info: info_dict, announce: None, nodes: None, encoding: None, httpseeds: None, - announce_list: Some(torrent_info.announce_urls), + announce_list: Some(torrent_announce_urls), creation_date: None, - comment: torrent_info.comment, + comment: db_torrent.comment.clone(), created_by: None, } } - /// It hydrates a `Torrent` struct from the database data. - #[must_use] - pub fn from_db_info_files_and_announce_urls( - torrent_info: DbTorrentInfo, - torrent_files: Vec, - torrent_announce_urls: Vec>, - ) -> Self { - let torrent_info_request = NewTorrentInfoRequest { - name: torrent_info.name, - pieces: torrent_info.pieces, - piece_length: torrent_info.piece_length, - private: torrent_info.private, - root_hash: torrent_info.root_hash, - files: torrent_files, - announce_urls: torrent_announce_urls, - comment: torrent_info.comment, - }; - Torrent::from_new_torrent_info_request(torrent_info_request) - } - /// Sets the announce url to the tracker url and removes all other trackers /// if the torrent is private. pub async fn set_announce_urls(&mut self, cfg: &Configuration) { @@ -278,17 +245,46 @@ impl Torrent { } } -#[allow(clippy::module_name_repetitions)] -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] -pub struct DbTorrentFile { - pub path: Option, - pub length: i64, - #[serde(default)] - pub md5sum: Option, +impl TorrentInfoDictionary { + /// torrent file can only hold a pieces key or a root hash key: + /// [BEP 39](http://www.bittorrent.org/beps/bep_0030.html) + #[must_use] + pub fn get_pieces_as_string(&self) -> String { + match &self.pieces { + None => String::new(), + Some(byte_buf) => from_bytes(byte_buf.as_ref()), + } + } + + /// It returns the root hash as a `i64` value. + /// + /// # Panics + /// + /// This function will panic if the root hash cannot be converted into a + /// `i64` value. + #[must_use] + pub fn get_root_hash_as_i64(&self) -> i64 { + match &self.root_hash { + None => 0i64, + Some(root_hash) => root_hash + .parse::() + .expect("variable `root_hash` cannot be converted into a `i64`"), + } + } + + #[must_use] + pub fn is_a_single_file_torrent(&self) -> bool { + self.length.is_some() + } + + #[must_use] + pub fn is_a_multiple_file_torrent(&self) -> bool { + self.files.is_some() + } } #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] -pub struct DbTorrentInfo { +pub struct DbTorrent { pub torrent_id: i64, pub info_hash: String, pub name: String, @@ -300,6 +296,15 @@ pub struct DbTorrentInfo { pub comment: Option, } +#[allow(clippy::module_name_repetitions)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct DbTorrentFile { + pub path: Option, + pub length: i64, + #[serde(default)] + pub md5sum: Option, +} + #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct DbTorrentAnnounceUrl { pub tracker_url: String, @@ -312,7 +317,7 @@ mod tests { use serde_bytes::ByteBuf; - use crate::models::torrent_file::{Torrent, TorrentInfo}; + use crate::models::torrent_file::{Torrent, TorrentInfoDictionary}; #[test] fn the_parsed_torrent_file_should_calculated_the_torrent_info_hash() { @@ -349,7 +354,7 @@ mod tests { let sample_data_in_txt_file = "mandelbrot\n"; - let info = TorrentInfo { + let info = TorrentInfoDictionary { name: "sample.txt".to_string(), pieces: Some(ByteBuf::from(vec![ // D4 91 58 7F 1C 42 DF F0 CB 0F F5 C2 B8 CE FE 22 B3 AD 31 0A // hex @@ -384,13 +389,13 @@ mod tests { use serde_bytes::ByteBuf; - use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfo}; + use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfoDictionary}; #[test] fn a_simple_single_file_torrent() { let sample_data_in_txt_file = "mandelbrot\n"; - let info = TorrentInfo { + let info = TorrentInfoDictionary { name: "sample.txt".to_string(), pieces: Some(ByteBuf::from(vec![ // D4 91 58 7F 1C 42 DF F0 CB 0F F5 C2 B8 CE FE 22 B3 AD 31 0A // hex @@ -425,7 +430,7 @@ mod tests { fn a_simple_multi_file_torrent() { let sample_data_in_txt_file = "mandelbrot\n"; - let info = TorrentInfo { + let info = TorrentInfoDictionary { name: "sample".to_string(), pieces: Some(ByteBuf::from(vec![ // D4 91 58 7F 1C 42 DF F0 CB 0F F5 C2 B8 CE FE 22 B3 AD 31 0A // hex @@ -464,7 +469,7 @@ mod tests { fn a_simple_single_file_torrent_with_a_source() { let sample_data_in_txt_file = "mandelbrot\n"; - let info = TorrentInfo { + let info = TorrentInfoDictionary { name: "sample.txt".to_string(), pieces: Some(ByteBuf::from(vec![ // D4 91 58 7F 1C 42 DF F0 CB 0F F5 C2 B8 CE FE 22 B3 AD 31 0A // hex @@ -499,7 +504,7 @@ mod tests { fn a_simple_single_file_private_torrent() { let sample_data_in_txt_file = "mandelbrot\n"; - let info = TorrentInfo { + let info = TorrentInfoDictionary { name: "sample.txt".to_string(), pieces: Some(ByteBuf::from(vec![ // D4 91 58 7F 1C 42 DF F0 CB 0F F5 C2 B8 CE FE 22 B3 AD 31 0A // hex diff --git a/src/services/torrent.rs b/src/services/torrent.rs index 7dce0db1..71c8fb48 100644 --- a/src/services/torrent.rs +++ b/src/services/torrent.rs @@ -13,7 +13,7 @@ use crate::models::category::CategoryId; use crate::models::info_hash::InfoHash; use crate::models::response::{DeletedTorrentResponse, TorrentResponse, TorrentsResponse}; use crate::models::torrent::{Metadata, TorrentId, TorrentListing}; -use crate::models::torrent_file::{DbTorrentInfo, Torrent, TorrentFile}; +use crate::models::torrent_file::{DbTorrent, Torrent, TorrentFile}; use crate::models::torrent_tag::{TagId, TorrentTag}; use crate::models::user::UserId; use crate::tracker::statistics_importer::StatisticsImporter; @@ -649,7 +649,7 @@ impl DbTorrentInfoRepository { /// # Errors /// /// This function will return an error there is a database error. - pub async fn get_by_info_hash(&self, info_hash: &InfoHash) -> Result { + pub async fn get_by_info_hash(&self, info_hash: &InfoHash) -> Result { self.database.get_torrent_info_from_info_hash(info_hash).await } diff --git a/src/services/torrent_file.rs b/src/services/torrent_file.rs index dbfa72f5..3b180ab2 100644 --- a/src/services/torrent_file.rs +++ b/src/services/torrent_file.rs @@ -1,14 +1,16 @@ //! This module contains the services related to torrent file management. +use serde_bytes::ByteBuf; use uuid::Uuid; -use crate::models::torrent_file::{Torrent, TorrentFile}; +use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfoDictionary}; use crate::services::hasher::sha1; +use crate::utils::hex::into_bytes; /// It contains the information required to create a new torrent file. /// /// It's not the full in-memory representation of a torrent file. The full /// in-memory representation is the `Torrent` struct. -pub struct NewTorrentInfoRequest { +pub struct CreateTorrentRequest { // The `info` dictionary fields pub name: String, pub pieces: String, @@ -21,6 +23,67 @@ pub struct NewTorrentInfoRequest { pub comment: Option, } +impl CreateTorrentRequest { + /// It builds a `TorrentInfoDictionary` from the current torrent request. + /// + /// # Panics + /// + /// This function will panic if the `pieces` field is not a valid hex string. + #[must_use] + pub fn build_info_dictionary(&self) -> TorrentInfoDictionary { + let mut info_dict = TorrentInfoDictionary { + name: self.name.to_string(), + pieces: None, + piece_length: self.piece_length, + md5sum: None, + length: None, + files: None, + private: self.private, + path: None, + root_hash: None, + source: None, + }; + + // a torrent file has a root hash or a pieces key, but not both. + if self.root_hash > 0 { + info_dict.root_hash = Some(self.pieces.clone()); + } else { + let buffer = into_bytes(&self.pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); + info_dict.pieces = Some(ByteBuf::from(buffer)); + } + + // either set the single file or the multiple files information + if self.files.len() == 1 { + let torrent_file = self + .files + .first() + .expect("vector `torrent_files` should have at least one element"); + + info_dict.md5sum = torrent_file.md5sum.clone(); + + info_dict.length = Some(torrent_file.length); + + let path = if torrent_file + .path + .first() + .as_ref() + .expect("the vector for the `path` should have at least one element") + .is_empty() + { + None + } else { + Some(torrent_file.path.clone()) + }; + + info_dict.path = path; + } else { + info_dict.files = Some(self.files.clone()); + } + + info_dict + } +} + /// It generates a random single-file torrent for testing purposes. /// /// The torrent will contain a single text file with the UUID as its content. @@ -43,7 +106,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent { let torrent_announce_urls: Vec> = vec![]; - let torrent_info_request = NewTorrentInfoRequest { + let torrent_info_request = CreateTorrentRequest { name: format!("file-{id}.txt"), pieces: sha1(&file_contents), piece_length: 16384, @@ -54,7 +117,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent { comment: None, }; - Torrent::from_new_torrent_info_request(torrent_info_request) + Torrent::from_request(torrent_info_request) } #[cfg(test)] @@ -62,7 +125,7 @@ mod tests { use serde_bytes::ByteBuf; use uuid::Uuid; - use crate::models::torrent_file::{Torrent, TorrentInfo}; + use crate::models::torrent_file::{Torrent, TorrentInfoDictionary}; use crate::services::torrent_file::generate_random_torrent; #[test] @@ -72,7 +135,7 @@ mod tests { let torrent = generate_random_torrent(uuid); let expected_torrent = Torrent { - info: TorrentInfo { + info: TorrentInfoDictionary { name: "file-d6170378-2c14-4ccc-870d-2a8e15195e23.txt".to_string(), pieces: Some(ByteBuf::from(vec![ 62, 231, 243, 51, 234, 165, 204, 209, 51, 132, 163, 133, 249, 50, 107, 46, 24, 15, 251, 32, diff --git a/src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs b/src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs index f0315ff2..f5a0204c 100644 --- a/src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs +++ b/src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs @@ -7,7 +7,7 @@ use sqlx::{query, query_as, SqlitePool}; use super::sqlite_v1_0_0::{TorrentRecordV1, UserRecordV1}; use crate::databases::database::{self, TABLES_TO_TRUNCATE}; -use crate::models::torrent_file::{TorrentFile, TorrentInfo}; +use crate::models::torrent_file::{TorrentFile, TorrentInfoDictionary}; #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct CategoryRecordV2 { @@ -32,7 +32,7 @@ pub struct TorrentRecordV2 { impl TorrentRecordV2 { #[must_use] - pub fn from_v1_data(torrent: &TorrentRecordV1, torrent_info: &TorrentInfo, uploader: &UserRecordV1) -> Self { + pub fn from_v1_data(torrent: &TorrentRecordV1, torrent_info: &TorrentInfoDictionary, uploader: &UserRecordV1) -> Self { Self { torrent_id: torrent.torrent_id, uploader_id: uploader.user_id, From b6fe36b86c386168d42f7e711f034b7baaae9158 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 19 Sep 2023 10:43:06 +0100 Subject: [PATCH 4/7] refactor: [#296] extract duplicate code --- src/databases/database.rs | 4 +- src/models/torrent_file.rs | 127 +++++++++++++++++++++-------------- src/services/torrent_file.rs | 60 +++-------------- 3 files changed, 88 insertions(+), 103 deletions(-) diff --git a/src/databases/database.rs b/src/databases/database.rs index e5778649..e947090a 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -209,7 +209,7 @@ pub trait Database: Sync + Send { let torrent_announce_urls = self.get_torrent_announce_urls_from_id(torrent_info.torrent_id).await?; - Ok(Torrent::from_database(torrent_info, torrent_files, torrent_announce_urls)) + Ok(Torrent::from_database(&torrent_info, &torrent_files, torrent_announce_urls)) } /// Get `Torrent` from `torrent_id`. @@ -220,7 +220,7 @@ pub trait Database: Sync + Send { let torrent_announce_urls = self.get_torrent_announce_urls_from_id(torrent_id).await?; - Ok(Torrent::from_database(torrent_info, torrent_files, torrent_announce_urls)) + Ok(Torrent::from_database(&torrent_info, &torrent_files, torrent_announce_urls)) } /// It returns the list of all infohashes producing the same canonical diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index ace9f9fa..4a7ade58 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -98,57 +98,18 @@ impl Torrent { /// hex string. #[must_use] pub fn from_database( - db_torrent: DbTorrent, - torrent_files: Vec, + db_torrent: &DbTorrent, + torrent_files: &Vec, torrent_announce_urls: Vec>, ) -> Self { - let mut info_dict = TorrentInfoDictionary { - name: db_torrent.name, - pieces: None, - piece_length: db_torrent.piece_length, - md5sum: None, - length: None, - files: None, - private: db_torrent.private, - path: None, - root_hash: None, - source: None, - }; - - // a torrent file has a root hash or a pieces key, but not both. - if db_torrent.root_hash > 0 { - info_dict.root_hash = Some(db_torrent.pieces); - } else { - let buffer = into_bytes(&db_torrent.pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); - info_dict.pieces = Some(ByteBuf::from(buffer)); - } - - // either set the single file or the multiple files information - if torrent_files.len() == 1 { - let torrent_file = torrent_files - .first() - .expect("vector `torrent_files` should have at least one element"); - - info_dict.md5sum = torrent_file.md5sum.clone(); - - info_dict.length = Some(torrent_file.length); - - let path = if torrent_file - .path - .first() - .as_ref() - .expect("the vector for the `path` should have at least one element") - .is_empty() - { - None - } else { - Some(torrent_file.path.clone()) - }; - - info_dict.path = path; - } else { - info_dict.files = Some(torrent_files); - } + let info_dict = TorrentInfoDictionary::with( + &db_torrent.name, + db_torrent.piece_length, + db_torrent.private, + db_torrent.root_hash, + &db_torrent.pieces, + torrent_files, + ); Self { info: info_dict, @@ -246,6 +207,74 @@ impl Torrent { } impl TorrentInfoDictionary { + /// Constructor. + /// + /// # Panics + /// + /// This function will panic if: + /// + /// - The `pieces` field is not a valid hex string. + /// - For single files torrents the `TorrentFile` path is empty. + #[must_use] + pub fn with( + name: &str, + piece_length: i64, + private: Option, + root_hash: i64, + pieces: &str, + files: &Vec, + ) -> Self { + let mut info_dict = Self { + name: name.to_string(), + pieces: None, + piece_length, + md5sum: None, + length: None, + files: None, + private, + path: None, + root_hash: None, + source: None, + }; + + // a torrent file has a root hash or a pieces key, but not both. + if root_hash > 0 { + info_dict.root_hash = Some(pieces.to_owned()); + } else { + let buffer = into_bytes(pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); + info_dict.pieces = Some(ByteBuf::from(buffer)); + } + + // either set the single file or the multiple files information + if files.len() == 1 { + let torrent_file = files + .first() + .expect("vector `torrent_files` should have at least one element"); + + info_dict.md5sum = torrent_file.md5sum.clone(); + + info_dict.length = Some(torrent_file.length); + + let path = if torrent_file + .path + .first() + .as_ref() + .expect("the vector for the `path` should have at least one element") + .is_empty() + { + None + } else { + Some(torrent_file.path.clone()) + }; + + info_dict.path = path; + } else { + info_dict.files = Some(files.clone()); + } + + info_dict + } + /// torrent file can only hold a pieces key or a root hash key: /// [BEP 39](http://www.bittorrent.org/beps/bep_0030.html) #[must_use] diff --git a/src/services/torrent_file.rs b/src/services/torrent_file.rs index 3b180ab2..15e414b4 100644 --- a/src/services/torrent_file.rs +++ b/src/services/torrent_file.rs @@ -1,10 +1,8 @@ //! This module contains the services related to torrent file management. -use serde_bytes::ByteBuf; use uuid::Uuid; use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfoDictionary}; use crate::services::hasher::sha1; -use crate::utils::hex::into_bytes; /// It contains the information required to create a new torrent file. /// @@ -31,56 +29,14 @@ impl CreateTorrentRequest { /// This function will panic if the `pieces` field is not a valid hex string. #[must_use] pub fn build_info_dictionary(&self) -> TorrentInfoDictionary { - let mut info_dict = TorrentInfoDictionary { - name: self.name.to_string(), - pieces: None, - piece_length: self.piece_length, - md5sum: None, - length: None, - files: None, - private: self.private, - path: None, - root_hash: None, - source: None, - }; - - // a torrent file has a root hash or a pieces key, but not both. - if self.root_hash > 0 { - info_dict.root_hash = Some(self.pieces.clone()); - } else { - let buffer = into_bytes(&self.pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); - info_dict.pieces = Some(ByteBuf::from(buffer)); - } - - // either set the single file or the multiple files information - if self.files.len() == 1 { - let torrent_file = self - .files - .first() - .expect("vector `torrent_files` should have at least one element"); - - info_dict.md5sum = torrent_file.md5sum.clone(); - - info_dict.length = Some(torrent_file.length); - - let path = if torrent_file - .path - .first() - .as_ref() - .expect("the vector for the `path` should have at least one element") - .is_empty() - { - None - } else { - Some(torrent_file.path.clone()) - }; - - info_dict.path = path; - } else { - info_dict.files = Some(self.files.clone()); - } - - info_dict + TorrentInfoDictionary::with( + &self.name, + self.piece_length, + self.private, + self.root_hash, + &self.pieces, + &self.files, + ) } } From 1660fd5a5180c5e9f64be12eaa08919ef152bc1e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 19 Sep 2023 10:44:44 +0100 Subject: [PATCH 5/7] refactor: [#296] rename vars to follow type name. --- src/databases/database.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/databases/database.rs b/src/databases/database.rs index e947090a..9205d843 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -203,24 +203,24 @@ pub trait Database: Sync + Send { /// Get `Torrent` from `InfoHash`. async fn get_torrent_from_info_hash(&self, info_hash: &InfoHash) -> Result { - let torrent_info = self.get_torrent_info_from_info_hash(info_hash).await?; + let db_torrent = self.get_torrent_info_from_info_hash(info_hash).await?; - let torrent_files = self.get_torrent_files_from_id(torrent_info.torrent_id).await?; + let torrent_files = self.get_torrent_files_from_id(db_torrent.torrent_id).await?; - let torrent_announce_urls = self.get_torrent_announce_urls_from_id(torrent_info.torrent_id).await?; + let torrent_announce_urls = self.get_torrent_announce_urls_from_id(db_torrent.torrent_id).await?; - Ok(Torrent::from_database(&torrent_info, &torrent_files, torrent_announce_urls)) + Ok(Torrent::from_database(&db_torrent, &torrent_files, torrent_announce_urls)) } /// Get `Torrent` from `torrent_id`. async fn get_torrent_from_id(&self, torrent_id: i64) -> Result { - let torrent_info = self.get_torrent_info_from_id(torrent_id).await?; + let db_torrent = self.get_torrent_info_from_id(torrent_id).await?; let torrent_files = self.get_torrent_files_from_id(torrent_id).await?; let torrent_announce_urls = self.get_torrent_announce_urls_from_id(torrent_id).await?; - Ok(Torrent::from_database(&torrent_info, &torrent_files, torrent_announce_urls)) + Ok(Torrent::from_database(&db_torrent, &torrent_files, torrent_announce_urls)) } /// It returns the list of all infohashes producing the same canonical From dfdac195c8a6160db8256e97894ce687954e678d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 19 Sep 2023 10:54:33 +0100 Subject: [PATCH 6/7] refator: [#296] move logic to service layer The service should know the model but the model should not know the service. The dependency should be only on one way. --- src/models/torrent_file.rs | 23 ----------------------- src/services/torrent_file.rs | 28 +++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index 4a7ade58..a7e98bb4 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -5,7 +5,6 @@ use sha1::{Digest, Sha1}; use super::info_hash::InfoHash; use crate::config::Configuration; -use crate::services::torrent_file::CreateTorrentRequest; use crate::utils::hex::{from_bytes, into_bytes}; #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] @@ -68,28 +67,6 @@ pub struct TorrentFile { } impl Torrent { - /// It builds a `Torrent` from a request. - /// - /// # Panics - /// - /// This function will panic if the `torrent_info.pieces` is not a valid hex string. - #[must_use] - pub fn from_request(create_torrent_req: CreateTorrentRequest) -> Self { - let info_dict = create_torrent_req.build_info_dictionary(); - - Self { - info: info_dict, - announce: None, - nodes: None, - encoding: None, - httpseeds: None, - announce_list: Some(create_torrent_req.announce_urls), - creation_date: None, - comment: create_torrent_req.comment, - created_by: None, - } - } - /// It hydrates a `Torrent` struct from the database data. /// /// # Panics diff --git a/src/services/torrent_file.rs b/src/services/torrent_file.rs index 15e414b4..1a1dd933 100644 --- a/src/services/torrent_file.rs +++ b/src/services/torrent_file.rs @@ -22,13 +22,35 @@ pub struct CreateTorrentRequest { } impl CreateTorrentRequest { + /// It builds a `Torrent` from a request. + /// + /// # Panics + /// + /// This function will panic if the `torrent_info.pieces` is not a valid hex string. + #[must_use] + pub fn build_torrent(&self) -> Torrent { + let info_dict = self.build_info_dictionary(); + + Torrent { + info: info_dict, + announce: None, + nodes: None, + encoding: None, + httpseeds: None, + announce_list: Some(self.announce_urls.clone()), + creation_date: None, + comment: self.comment.clone(), + created_by: None, + } + } + /// It builds a `TorrentInfoDictionary` from the current torrent request. /// /// # Panics /// /// This function will panic if the `pieces` field is not a valid hex string. #[must_use] - pub fn build_info_dictionary(&self) -> TorrentInfoDictionary { + fn build_info_dictionary(&self) -> TorrentInfoDictionary { TorrentInfoDictionary::with( &self.name, self.piece_length, @@ -62,7 +84,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent { let torrent_announce_urls: Vec> = vec![]; - let torrent_info_request = CreateTorrentRequest { + let create_torrent_req = CreateTorrentRequest { name: format!("file-{id}.txt"), pieces: sha1(&file_contents), piece_length: 16384, @@ -73,7 +95,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent { comment: None, }; - Torrent::from_request(torrent_info_request) + create_torrent_req.build_torrent() } #[cfg(test)] From 88213461ede54446879486a888cc54c583c997e3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 19 Sep 2023 12:33:25 +0100 Subject: [PATCH 7/7] doc: add some comments for BEP 30 implementation --- src/models/torrent_file.rs | 1 + src/services/torrent_file.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index a7e98bb4..aba633c5 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -216,6 +216,7 @@ impl TorrentInfoDictionary { // a torrent file has a root hash or a pieces key, but not both. if root_hash > 0 { + // If `root_hash` is true the `pieces` field contains the `root hash` info_dict.root_hash = Some(pieces.to_owned()); } else { let buffer = into_bytes(pieces).expect("variable `torrent_info.pieces` is not a valid hex string"); diff --git a/src/services/torrent_file.rs b/src/services/torrent_file.rs index 1a1dd933..338ba6e6 100644 --- a/src/services/torrent_file.rs +++ b/src/services/torrent_file.rs @@ -14,7 +14,7 @@ pub struct CreateTorrentRequest { pub pieces: String, pub piece_length: i64, pub private: Option, - pub root_hash: i64, + pub root_hash: i64, // True (1) if it's a BEP 30 torrent. pub files: Vec, // Other fields of the root level metainfo dictionary pub announce_urls: Vec>,