From e9476fcc5ef3937273925d2367309aefcd9ccea2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 18 Sep 2023 15:33:45 +0100 Subject: [PATCH] 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);