diff --git a/migrations/mysql/20230321122049_torrust_torrent_tags.sql b/migrations/mysql/20230321122049_torrust_torrent_tags.sql new file mode 100644 index 00000000..6205d59a --- /dev/null +++ b/migrations/mysql/20230321122049_torrust_torrent_tags.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS torrust_torrent_tags ( + tag_id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/mysql/20230321122825_torrust_torrent_tag_links.sql b/migrations/mysql/20230321122825_torrust_torrent_tag_links.sql new file mode 100644 index 00000000..f23cf89c --- /dev/null +++ b/migrations/mysql/20230321122825_torrust_torrent_tag_links.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS torrust_torrent_tag_links ( + torrent_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY (torrent_id) REFERENCES torrust_torrents(torrent_id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES torrust_torrent_tags(tag_id) ON DELETE CASCADE, + PRIMARY KEY (torrent_id, tag_id) +); diff --git a/migrations/sqlite3/20230321122049_torrust_torrent_tags.sql b/migrations/sqlite3/20230321122049_torrust_torrent_tags.sql new file mode 100644 index 00000000..0f71de15 --- /dev/null +++ b/migrations/sqlite3/20230321122049_torrust_torrent_tags.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS torrust_torrent_tags ( + tag_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/sqlite3/20230321122825_torrust_torrent_tag_links.sql b/migrations/sqlite3/20230321122825_torrust_torrent_tag_links.sql new file mode 100644 index 00000000..f23cf89c --- /dev/null +++ b/migrations/sqlite3/20230321122825_torrust_torrent_tag_links.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS torrust_torrent_tag_links ( + torrent_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY (torrent_id) REFERENCES torrust_torrents(torrent_id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES torrust_torrent_tags(tag_id) ON DELETE CASCADE, + PRIMARY KEY (torrent_id, tag_id) +); diff --git a/src/app.rs b/src/app.rs index 3aa8e29e..005616aa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,10 +14,7 @@ use crate::config::Configuration; use crate::databases::database; use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; use crate::services::category::{self, DbCategoryRepository}; -use crate::services::torrent::{ - DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, - DbTorrentRepository, -}; +use crate::services::torrent::{DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository}; use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; @@ -63,6 +60,7 @@ pub async fn run(configuration: Configuration) -> Running { let torrent_info_repository = Arc::new(DbTorrentInfoRepository::new(database.clone())); let torrent_file_repository = Arc::new(DbTorrentFileRepository::new(database.clone())); let torrent_announce_url_repository = Arc::new(DbTorrentAnnounceUrlRepository::new(database.clone())); + let torrent_tag_repository = Arc::new(DbTorrentTagRepository::new(database.clone())); let torrent_listing_generator = Arc::new(DbTorrentListingGenerator::new(database.clone())); let banned_user_list = Arc::new(DbBannedUserList::new(database.clone())); @@ -85,6 +83,7 @@ pub async fn run(configuration: Configuration) -> Running { torrent_info_repository.clone(), torrent_file_repository.clone(), torrent_announce_url_repository.clone(), + torrent_tag_repository.clone(), torrent_listing_generator.clone(), )); let registration_service = Arc::new(user::RegistrationService::new( @@ -126,6 +125,7 @@ pub async fn run(configuration: Configuration) -> Running { torrent_info_repository, torrent_file_repository, torrent_announce_url_repository, + torrent_tag_repository, torrent_listing_generator, banned_user_list, category_service, diff --git a/src/common.rs b/src/common.rs index 4faa4cae..9c13a40b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,10 +6,7 @@ use crate::config::Configuration; use crate::databases::database::Database; use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; use crate::services::category::{self, DbCategoryRepository}; -use crate::services::torrent::{ - DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, - DbTorrentRepository, -}; +use crate::services::torrent::{DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository}; use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; @@ -37,6 +34,7 @@ pub struct AppData { pub torrent_info_repository: Arc, pub torrent_file_repository: Arc, pub torrent_announce_url_repository: Arc, + pub torrent_tag_repository: Arc, pub torrent_listing_generator: Arc, pub banned_user_list: Arc, // Services @@ -69,6 +67,7 @@ impl AppData { torrent_info_repository: Arc, torrent_file_repository: Arc, torrent_announce_url_repository: Arc, + torrent_tag_repository: Arc, torrent_listing_generator: Arc, banned_user_list: Arc, // Services @@ -98,6 +97,7 @@ impl AppData { torrent_info_repository, torrent_file_repository, torrent_announce_url_repository, + torrent_tag_repository, torrent_listing_generator, banned_user_list, // Services diff --git a/src/databases/database.rs b/src/databases/database.rs index 8ac719c6..93cb3204 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -8,6 +8,7 @@ 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_tag::{TagId, TorrentTag}; use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile}; @@ -52,6 +53,7 @@ pub enum Sorting { #[derive(Debug)] pub enum Error { Error, + ErrorWithText(String), UnrecognizedDatabaseDriver, // when the db path does not start with sqlite or mysql UsernameTaken, EmailTaken, @@ -228,6 +230,30 @@ pub trait Database: Sync + Send { /// Update a torrent's description with `torrent_id` and `description`. async fn update_torrent_description(&self, torrent_id: i64, description: &str) -> Result<(), Error>; + /// Add a new tag. + async fn add_tag(&self, name: &str) -> Result; + + /// Delete a tag. + async fn delete_tag(&self, tag_id: TagId) -> Result; + + /// Add a tag to torrent. + async fn add_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), Error>; + + /// Add multiple tags to a torrent at once. + async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec) -> Result<(), Error>; + + /// Remove a tag from torrent. + async fn delete_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), Error>; + + /// Remove all tags from torrent. + async fn delete_all_torrent_tag_links(&self, torrent_id: i64) -> Result<(), Error>; + + /// Get all tags as `Vec`. + async fn get_tags(&self) -> Result, Error>; + + /// Get tags for `torrent_id`. + async fn get_tags_for_torrent_id(&self, torrent_id: i64) -> Result, Error>; + /// Update the seeders and leechers info for a torrent with `torrent_id`, `tracker_url`, `seeders` and `leechers`. async fn update_tracker_info(&self, torrent_id: i64, tracker_url: &str, seeders: i64, leechers: i64) -> Result<(), Error>; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index 5e3206db..c9f45daa 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -9,6 +9,7 @@ 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_tag::{TagId, TorrentTag}; use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile}; use crate::utils::clock; @@ -669,6 +670,96 @@ impl Database for Mysql { }) } + async fn add_tag(&self, name: &str) -> Result { + println!("inserting tag: {}", name); + + query_as("INSERT INTO torrust_torrent_tags (name) VALUES (?) RETURNING id, name") + .bind(name) + .fetch_one(&self.pool) + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn delete_tag(&self, tag_id: TagId) -> Result { + println!("deleting tag: {}", tag_id); + + query_as("DELETE FROM torrust_torrent_tags WHERE tag_id = ? RETURNING id, name") + .bind(tag_id) + .fetch_one(&self.pool) + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn add_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), database::Error> { + query("INSERT INTO torrust_torrent_tag_links (torrent_id, tag_id) VALUES (?, ?)") + .bind(torrent_id) + .bind(tag_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|_| database::Error::Error) + } + + async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec) -> Result<(), database::Error> { + let mut transaction = self.pool.begin() + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string()))?; + + for tag_id in tag_ids { + query("INSERT INTO torrust_torrent_tag_links (torrent_id, tag_id) VALUES (?, ?)") + .bind(torrent_id) + .bind(tag_id) + .execute(&mut transaction) + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string()))?; + } + + transaction.commit() + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn delete_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), database::Error> { + query("DELETE FROM torrust_torrent_tag_links WHERE torrent_id = ? AND tag_id = ?") + .bind(torrent_id) + .bind(tag_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|_| database::Error::Error) + } + + async fn delete_all_torrent_tag_links(&self, torrent_id: i64) -> Result<(), database::Error> { + query("DELETE FROM torrust_torrent_tag_links WHERE torrent_id = ?") + .bind(torrent_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn get_tags(&self) -> Result, database::Error> { + query_as::<_, TorrentTag>( + "SELECT tag_id, name FROM torrust_torrent_tags" + ) + .fetch_all(&self.pool) + .await + .map_err(|_| database::Error::Error) + } + + async fn get_tags_for_torrent_id(&self, torrent_id: i64) -> Result, database::Error> { + query_as::<_, TorrentTag>( + "SELECT torrust_torrent_tags.tag_id, torrust_torrent_tags.name + FROM torrust_torrent_tags + JOIN torrust_torrent_tag_links ON torrust_torrent_tags.tag_id = torrust_torrent_tag_links.tag_id + WHERE torrust_torrent_tag_links.torrent_id = ?" + ) + .bind(torrent_id) + .fetch_all(&self.pool) + .await + .map_err(|_| database::Error::Error) + } + async fn update_tracker_info( &self, torrent_id: i64, diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 31bec6a2..042654b6 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -9,6 +9,7 @@ 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_tag::{TagId, TorrentTag}; use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile}; use crate::utils::clock; @@ -659,6 +660,92 @@ impl Database for Sqlite { }) } + async fn add_tag(&self, name: &str) -> Result { + query_as("INSERT INTO torrust_torrent_tags (name) VALUES (?) RETURNING id, name") + .bind(name) + .fetch_one(&self.pool) + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn delete_tag(&self, tag_id: TagId) -> Result { + query_as("DELETE FROM torrust_torrent_tags WHERE tag_id = ? RETURNING id, name") + .bind(tag_id) + .fetch_one(&self.pool) + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn add_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), database::Error> { + query("INSERT INTO torrust_torrent_tag_links (torrent_id, tag_id) VALUES (?, ?)") + .bind(torrent_id) + .bind(tag_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|_| database::Error::Error) + } + + async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec) -> Result<(), database::Error> { + let mut transaction = self.pool.begin() + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string()))?; + + for tag_id in tag_ids { + query("INSERT INTO torrust_torrent_tag_links (torrent_id, tag_id) VALUES (?, ?)") + .bind(torrent_id) + .bind(tag_id) + .execute(&mut transaction) + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string()))?; + } + + transaction.commit() + .await + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn delete_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), database::Error> { + query("DELETE FROM torrust_torrent_tag_links WHERE torrent_id = ? AND tag_id = ?") + .bind(torrent_id) + .bind(tag_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|_| database::Error::Error) + } + + async fn delete_all_torrent_tag_links(&self, torrent_id: i64) -> Result<(), database::Error> { + query("DELETE FROM torrust_torrent_tag_links WHERE torrent_id = ?") + .bind(torrent_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|err| database::Error::ErrorWithText(err.to_string())) + } + + async fn get_tags(&self) -> Result, database::Error> { + query_as::<_, TorrentTag>( + "SELECT tag_id, name FROM torrust_torrent_tags" + ) + .fetch_all(&self.pool) + .await + .map_err(|_| database::Error::Error) + } + + async fn get_tags_for_torrent_id(&self, torrent_id: i64) -> Result, database::Error> { + query_as::<_, TorrentTag>( + "SELECT torrust_torrent_tags.tag_id, torrust_torrent_tags.name + FROM torrust_torrent_tags + JOIN torrust_torrent_tag_links ON torrust_torrent_tags.tag_id = torrust_torrent_tag_links.tag_id + WHERE torrust_torrent_tag_links.torrent_id = ?" + ) + .bind(torrent_id) + .fetch_all(&self.pool) + .await + .map_err(|_| database::Error::Error) + } + async fn update_tracker_info( &self, torrent_id: i64, diff --git a/src/errors.rs b/src/errors.rs index 0d3d4067..528e7bc0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -211,6 +211,7 @@ impl From for ServiceError { #[allow(clippy::match_same_arms)] match e { database::Error::Error => ServiceError::InternalServerError, + database::Error::ErrorWithText(_) => ServiceError::InternalServerError, database::Error::UsernameTaken => ServiceError::UsernameTaken, database::Error::EmailTaken => ServiceError::EmailTaken, database::Error::UserNotFound => ServiceError::UserNotFound, diff --git a/src/models/mod.rs b/src/models/mod.rs index 5e54368f..5fff6a4a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -5,3 +5,4 @@ pub mod torrent; pub mod torrent_file; pub mod tracker_key; pub mod user; +pub mod torrent_tag; diff --git a/src/models/response.rs b/src/models/response.rs index 8d9a2d90..09476be3 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -4,6 +4,7 @@ use super::torrent::TorrentId; use crate::databases::database::Category; use crate::models::torrent::TorrentListing; use crate::models::torrent_file::TorrentFile; +use crate::models::torrent_tag::TorrentTag; pub enum OkResponses { TokenResponse(TokenResponse), @@ -59,6 +60,7 @@ pub struct TorrentResponse { pub files: Vec, pub trackers: Vec, pub magnet_link: String, + pub tags: Vec, } impl TorrentResponse { @@ -78,6 +80,7 @@ impl TorrentResponse { files: vec![], trackers: vec![], magnet_link: String::new(), + tags: vec![], } } } diff --git a/src/models/torrent_tag.rs b/src/models/torrent_tag.rs new file mode 100644 index 00000000..2da97303 --- /dev/null +++ b/src/models/torrent_tag.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +pub type TagId = i64; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, FromRow)] +pub struct TorrentTag { + pub tag_id: TagId, + pub name: String, +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index f9b0f2cd..fc76c52a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -7,6 +7,7 @@ pub mod root; pub mod settings; pub mod torrent; pub mod user; +pub mod tag; pub const API_VERSION: &str = "v1"; @@ -17,5 +18,6 @@ pub fn init(cfg: &mut web::ServiceConfig) { settings::init(cfg); about::init(cfg); proxy::init(cfg); + tag::init(cfg); root::init(cfg); } diff --git a/src/routes/tag.rs b/src/routes/tag.rs new file mode 100644 index 00000000..b35fb622 --- /dev/null +++ b/src/routes/tag.rs @@ -0,0 +1,80 @@ +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use crate::common::WebAppData; +use crate::errors::{ServiceError, ServiceResult}; +use crate::models::response::OkResponse; +use crate::models::torrent_tag::TagId; +use crate::routes::API_VERSION; + +pub fn init(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope(&format!("/{API_VERSION}/tag")) + .service( + web::resource("") + .route(web::post().to(add_tag)) + .route(web::delete().to(delete_tag)), + ) + ); + cfg.service( + web::scope(&format!("/{API_VERSION}/tags")) + .service( + web::resource("") + .route(web::get().to(get_tags)) + ) + ); +} + +pub async fn get_tags(app_data: WebAppData) -> ServiceResult { + let tags = app_data.torrent_tag_repository.get_tags().await?; + + Ok(HttpResponse::Ok().json(OkResponse { data: tags })) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddTag { + pub name: String, +} + +pub async fn add_tag(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { + let user_id = app_data.auth.get_user_id_from_request(&req).await?; + + let user = app_data.user_repository.get_compact(&user_id).await?; + + // check if user is administrator + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + let tag = app_data.torrent_tag_repository.add_tag(&payload.name).await?; + + Ok(HttpResponse::Ok().json(OkResponse { + data: tag, + })) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteTag { + pub tag_id: TagId, +} + +pub async fn delete_tag( + req: HttpRequest, + payload: web::Json, + app_data: WebAppData, +) -> ServiceResult { + let user_id = app_data.auth.get_user_id_from_request(&req).await?; + + let user = app_data.user_repository.get_compact(&user_id).await?; + + // check if user is administrator + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + let tag = app_data.torrent_tag_repository.delete_tag(&payload.tag_id).await?; + + Ok(HttpResponse::Ok().json(OkResponse { + data: tag, + })) +} diff --git a/src/routes/torrent.rs b/src/routes/torrent.rs index 7327ff80..4b857f4b 100644 --- a/src/routes/torrent.rs +++ b/src/routes/torrent.rs @@ -13,6 +13,7 @@ use crate::errors::{ServiceError, ServiceResult}; use crate::models::info_hash::InfoHash; use crate::models::response::{NewTorrentResponse, OkResponse}; use crate::models::torrent::TorrentRequest; +use crate::models::torrent_tag::TagId; use crate::routes::API_VERSION; use crate::services::torrent::ListingRequest; use crate::utils::parse_torrent; @@ -44,6 +45,7 @@ pub struct Create { pub title: String, pub description: String, pub category: String, + pub tags: Vec } impl Create { @@ -65,6 +67,7 @@ impl Create { pub struct Update { title: Option, description: Option, + tags: Option>, } /// Upload a Torrent to the Index @@ -138,7 +141,7 @@ pub async fn update_torrent_info_handler( let torrent_response = app_data .torrent_service - .update_torrent_info(&info_hash, &payload.title, &payload.description, &user_id) + .update_torrent_info(&info_hash, &payload.title, &payload.description, &payload.tags, &user_id) .await?; Ok(HttpResponse::Ok().json(OkResponse { data: torrent_response })) @@ -193,21 +196,25 @@ async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result = vec![]; while let Ok(Some(mut field)) = payload.try_next().await { match field.content_disposition().get_name().unwrap() { - "title" | "description" | "category" => { + "title" | "description" | "category" | "tags" => { let data = field.next().await; + if data.is_none() { continue; } - let wrapped_data = &data.unwrap().unwrap(); - let parsed_data = std::str::from_utf8(wrapped_data).unwrap(); + + let wrapped_data = &data.unwrap().map_err(|_| ServiceError::BadRequest)?; + let parsed_data = std::str::from_utf8(wrapped_data).map_err(|_| ServiceError::BadRequest)?; match field.content_disposition().get_name().unwrap() { "title" => title = parsed_data.to_string(), "description" => description = parsed_data.to_string(), "category" => category = parsed_data.to_string(), + "tags" => tags = serde_json::from_str(parsed_data).map_err(|_| ServiceError::BadRequest)?, _ => {} } } @@ -229,6 +236,7 @@ async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result, @@ -25,6 +26,7 @@ pub struct Index { torrent_info_repository: Arc, torrent_file_repository: Arc, torrent_announce_url_repository: Arc, + torrent_tag_repository: Arc, torrent_listing_generator: Arc, } @@ -62,6 +64,7 @@ impl Index { torrent_info_repository: Arc, torrent_file_repository: Arc, torrent_announce_url_repository: Arc, + torrent_tag_repository: Arc, torrent_listing_repository: Arc, ) -> Self { Self { @@ -74,6 +77,7 @@ impl Index { torrent_info_repository, torrent_file_repository, torrent_announce_url_repository, + torrent_tag_repository, torrent_listing_generator: torrent_listing_repository, } } @@ -117,6 +121,8 @@ impl Index { return Err(e); } + let _ = self.torrent_tag_repository.link_torrent_to_tags(&torrent_id, &torrent_request.fields.tags).await?; + Ok(torrent_id) } @@ -274,6 +280,8 @@ impl Index { torrent_response.leechers = torrent_info.leechers; } + torrent_response.tags = self.torrent_tag_repository.get_tags_for_torrent(&torrent_id).await?; + Ok(torrent_response) } @@ -341,6 +349,7 @@ impl Index { info_hash: &InfoHash, title: &Option, description: &Option, + tags: &Option>, user_id: &UserId, ) -> Result { let updater = self.user_repository.get_compact(user_id).await?; @@ -354,7 +363,7 @@ impl Index { } self.torrent_info_repository - .update(&torrent_listing.torrent_id, title, description) + .update(&torrent_listing.torrent_id, title, description, tags) .await?; let torrent_listing = self @@ -450,6 +459,7 @@ impl DbTorrentInfoRepository { torrent_id: &TorrentId, opt_title: &Option, opt_description: &Option, + opt_tags: &Option>, ) -> Result<(), Error> { if let Some(title) = &opt_title { self.database.update_torrent_title(*torrent_id, title).await?; @@ -459,6 +469,24 @@ impl DbTorrentInfoRepository { self.database.update_torrent_description(*torrent_id, description).await?; } + if let Some(tags) = opt_tags { + let mut current_tags: Vec = self.database.get_tags_for_torrent_id(*torrent_id) + .await? + .iter() + .map(|tag| tag.tag_id) + .collect(); + + let mut new_tags = tags.clone(); + + current_tags.sort(); + new_tags.sort(); + + if new_tags != current_tags { + self.database.delete_all_torrent_tag_links(*torrent_id).await?; + self.database.add_torrent_tag_links(*torrent_id, tags).await?; + } + } + Ok(()) } } @@ -506,6 +534,89 @@ impl DbTorrentAnnounceUrlRepository { } } +pub struct DbTorrentTagRepository { + database: Arc>, +} + +impl DbTorrentTagRepository { + #[must_use] + pub fn new(database: Arc>) -> Self { + Self { database } + } + + /// It adds a new tag and returns the newly created tag. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn add_tag(&self, tag_name: &str) -> Result { + self.database.add_tag(tag_name).await + } + + /// It adds a new torrent tag link. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn link_torrent_to_tag(&self, torrent_id: &TorrentId, tag_id: &TagId) -> Result<(), Error> { + self.database.add_torrent_tag_link(*torrent_id, *tag_id).await + } + + /// It adds multiple torrent tag links at once. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn link_torrent_to_tags(&self, torrent_id: &TorrentId, tag_ids: &Vec) -> Result<(), Error> { + self.database.add_torrent_tag_links(*torrent_id, tag_ids).await + } + + /// It returns all the tags. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn get_tags(&self) -> Result, Error> { + self.database.get_tags().await + } + + /// It returns all the tags linked to a certain torrent ID. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn get_tags_for_torrent(&self, torrent_id: &TorrentId) -> Result, Error> { + self.database.get_tags_for_torrent_id(*torrent_id).await + } + + /// It removes a tag and returns it. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn delete_tag(&self, tag_id: &TagId) -> Result { + self.database.delete_tag(*tag_id).await + } + + /// It removes a torrent tag link. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn unlink_torrent_from_tag(&self, torrent_id: &TorrentId, tag_id: &TagId) -> Result<(), Error> { + self.database.delete_torrent_tag_link(*torrent_id, *tag_id).await + } + + /// It removes all tags for a certain torrent. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn unlink_all_tags_for_torrent(&self, torrent_id: &TorrentId) -> Result<(), Error> { + self.database.delete_all_torrent_tag_links(*torrent_id).await + } +} + pub struct DbTorrentListingGenerator { database: Arc>, }