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/database.rs b/src/databases/database.rs index 8fc10d79..9205d843 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}; @@ -203,32 +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_db_info_files_and_announce_urls( - 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_db_info_files_and_announce_urls( - 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 @@ -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 8a044fb4..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}; @@ -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 @@ -649,24 +676,20 @@ 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 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) + 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 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) + 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 + .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..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}; @@ -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 @@ -639,24 +666,20 @@ 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 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) + 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 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) + 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 + .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..aba633c5 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -5,22 +5,37 @@ use sha1::{Digest, Sha1}; use super::info_hash::InfoHash; use crate::config::Configuration; -use crate::services::torrent_file::NewTorrentInfoRequest; 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,158 +58,49 @@ 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()), - } - } - - /// 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, 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(rename = "comment")] - pub comment: Option, +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct TorrentFile { + pub path: Vec, + pub length: i64, #[serde(default)] - #[serde(rename = "created by")] - pub created_by: Option, + pub md5sum: 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(), - pieces: None, - piece_length: torrent_info.piece_length, - md5sum: None, - length: None, - files: None, - private: torrent_info.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); - } 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)); - } - - // either set the single file or the multiple files information - if torrent_info.files.len() == 1 { - let torrent_file = torrent_info - .files - .first() - .expect("vector `torrent_files` should have at least one element"); - - info.md5sum = torrent_file.md5sum.clone(); - - info.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.path = path; - } else { - info.files = Some(torrent_info.files); - } + pub fn from_database( + db_torrent: &DbTorrent, + torrent_files: &Vec, + torrent_announce_urls: Vec>, + ) -> Self { + 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: 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: None, + 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, - }; - 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) { @@ -277,17 +183,115 @@ 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 { + /// 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 { + // 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"); + 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] + 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, @@ -296,6 +300,16 @@ pub struct DbTorrentInfo { #[serde(default)] pub private: Option, pub root_hash: i64, + 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)] @@ -310,7 +324,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() { @@ -347,7 +361,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 @@ -382,13 +396,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 @@ -423,7 +437,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 @@ -462,7 +476,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 @@ -497,7 +511,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 dfa72dbd..338ba6e6 100644 --- a/src/services/torrent_file.rs +++ b/src/services/torrent_file.rs @@ -1,21 +1,65 @@ //! This module contains the services related to torrent file management. use uuid::Uuid; -use crate::models::torrent_file::{Torrent, TorrentFile}; +use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfoDictionary}; use crate::services::hasher::sha1; /// 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, 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>, + pub comment: Option, +} + +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] + fn build_info_dictionary(&self) -> TorrentInfoDictionary { + TorrentInfoDictionary::with( + &self.name, + self.piece_length, + self.private, + self.root_hash, + &self.pieces, + &self.files, + ) + } } /// It generates a random single-file torrent for testing purposes. @@ -40,7 +84,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent { let torrent_announce_urls: Vec> = vec![]; - let torrent_info_request = NewTorrentInfoRequest { + let create_torrent_req = CreateTorrentRequest { name: format!("file-{id}.txt"), pieces: sha1(&file_contents), piece_length: 16384, @@ -48,9 +92,10 @@ 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) + create_torrent_req.build_torrent() } #[cfg(test)] @@ -58,7 +103,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] @@ -68,7 +113,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, 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 { 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);