Skip to content

Commit e766b4c

Browse files
committed
Merge #162: 76 add tags to torrents
4730afd chore: clippy errors (Warm Beer) a6cf184 chore: cargo fmt (Warm Beer) 4286ba9 feat: added filtering torrents on tags (Warm Beer) a1bd92f fix: sql queries (Warm Beer) 7ce3d5e feat: torrent tags (Warm Beer) Pull request description:
2 parents 9e40be6 + 4730afd commit e766b4c

18 files changed

+568
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE TABLE IF NOT EXISTS torrust_torrent_tags (
2+
tag_id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
3+
name VARCHAR(255) NOT NULL,
4+
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
5+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE IF NOT EXISTS torrust_torrent_tag_links (
2+
torrent_id INTEGER NOT NULL,
3+
tag_id INTEGER NOT NULL,
4+
FOREIGN KEY (torrent_id) REFERENCES torrust_torrents(torrent_id) ON DELETE CASCADE,
5+
FOREIGN KEY (tag_id) REFERENCES torrust_torrent_tags(tag_id) ON DELETE CASCADE,
6+
PRIMARY KEY (torrent_id, tag_id)
7+
);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE TABLE IF NOT EXISTS torrust_torrent_tags (
2+
tag_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
3+
name VARCHAR(255) NOT NULL,
4+
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
5+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE IF NOT EXISTS torrust_torrent_tag_links (
2+
torrent_id INTEGER NOT NULL,
3+
tag_id INTEGER NOT NULL,
4+
FOREIGN KEY (torrent_id) REFERENCES torrust_torrents(torrent_id) ON DELETE CASCADE,
5+
FOREIGN KEY (tag_id) REFERENCES torrust_torrent_tags(tag_id) ON DELETE CASCADE,
6+
PRIMARY KEY (torrent_id, tag_id)
7+
);

src/app.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebTok
1313
use crate::services::category::{self, DbCategoryRepository};
1414
use crate::services::torrent::{
1515
DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator,
16-
DbTorrentRepository,
16+
DbTorrentRepository, DbTorrentTagRepository,
1717
};
1818
use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
1919
use crate::services::{proxy, settings, torrent};
@@ -65,6 +65,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati
6565
let torrent_info_repository = Arc::new(DbTorrentInfoRepository::new(database.clone()));
6666
let torrent_file_repository = Arc::new(DbTorrentFileRepository::new(database.clone()));
6767
let torrent_announce_url_repository = Arc::new(DbTorrentAnnounceUrlRepository::new(database.clone()));
68+
let torrent_tag_repository = Arc::new(DbTorrentTagRepository::new(database.clone()));
6869
let torrent_listing_generator = Arc::new(DbTorrentListingGenerator::new(database.clone()));
6970
let banned_user_list = Arc::new(DbBannedUserList::new(database.clone()));
7071

@@ -87,6 +88,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati
8788
torrent_info_repository.clone(),
8889
torrent_file_repository.clone(),
8990
torrent_announce_url_repository.clone(),
91+
torrent_tag_repository.clone(),
9092
torrent_listing_generator.clone(),
9193
));
9294
let registration_service = Arc::new(user::RegistrationService::new(
@@ -128,6 +130,7 @@ pub async fn run(configuration: Configuration, api_implementation: &Implementati
128130
torrent_info_repository,
129131
torrent_file_repository,
130132
torrent_announce_url_repository,
133+
torrent_tag_repository,
131134
torrent_listing_generator,
132135
banned_user_list,
133136
category_service,

src/common.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebTok
88
use crate::services::category::{self, DbCategoryRepository};
99
use crate::services::torrent::{
1010
DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator,
11-
DbTorrentRepository,
11+
DbTorrentRepository, DbTorrentTagRepository,
1212
};
1313
use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
1414
use crate::services::{proxy, settings, torrent};
@@ -37,6 +37,7 @@ pub struct AppData {
3737
pub torrent_info_repository: Arc<DbTorrentInfoRepository>,
3838
pub torrent_file_repository: Arc<DbTorrentFileRepository>,
3939
pub torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
40+
pub torrent_tag_repository: Arc<DbTorrentTagRepository>,
4041
pub torrent_listing_generator: Arc<DbTorrentListingGenerator>,
4142
pub banned_user_list: Arc<DbBannedUserList>,
4243
// Services
@@ -69,6 +70,7 @@ impl AppData {
6970
torrent_info_repository: Arc<DbTorrentInfoRepository>,
7071
torrent_file_repository: Arc<DbTorrentFileRepository>,
7172
torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
73+
torrent_tag_repository: Arc<DbTorrentTagRepository>,
7274
torrent_listing_generator: Arc<DbTorrentListingGenerator>,
7375
banned_user_list: Arc<DbBannedUserList>,
7476
// Services
@@ -98,6 +100,7 @@ impl AppData {
98100
torrent_info_repository,
99101
torrent_file_repository,
100102
torrent_announce_url_repository,
103+
torrent_tag_repository,
101104
torrent_listing_generator,
102105
banned_user_list,
103106
// Services

src/databases/database.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::models::info_hash::InfoHash;
88
use crate::models::response::TorrentsResponse;
99
use crate::models::torrent::TorrentListing;
1010
use crate::models::torrent_file::{DbTorrentInfo, Torrent, TorrentFile};
11+
use crate::models::torrent_tag::{TagId, TorrentTag};
1112
use crate::models::tracker_key::TrackerKey;
1213
use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
1314

@@ -52,12 +53,15 @@ pub enum Sorting {
5253
#[derive(Debug)]
5354
pub enum Error {
5455
Error,
56+
ErrorWithText(String),
5557
UnrecognizedDatabaseDriver, // when the db path does not start with sqlite or mysql
5658
UsernameTaken,
5759
EmailTaken,
5860
UserNotFound,
5961
CategoryAlreadyExists,
6062
CategoryNotFound,
63+
TagAlreadyExists,
64+
TagNotFound,
6165
TorrentNotFound,
6266
TorrentAlreadyExists, // when uploading an already uploaded info_hash
6367
TorrentTitleAlreadyExists,
@@ -156,6 +160,7 @@ pub trait Database: Sync + Send {
156160
&self,
157161
search: &Option<String>,
158162
categories: &Option<Vec<String>>,
163+
tags: &Option<Vec<String>>,
159164
sort: &Sorting,
160165
offset: u64,
161166
page_size: u8,
@@ -228,6 +233,33 @@ pub trait Database: Sync + Send {
228233
/// Update a torrent's description with `torrent_id` and `description`.
229234
async fn update_torrent_description(&self, torrent_id: i64, description: &str) -> Result<(), Error>;
230235

236+
/// Add a new tag.
237+
async fn add_tag(&self, name: &str) -> Result<(), Error>;
238+
239+
/// Delete a tag.
240+
async fn delete_tag(&self, tag_id: TagId) -> Result<(), Error>;
241+
242+
/// Add a tag to torrent.
243+
async fn add_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), Error>;
244+
245+
/// Add multiple tags to a torrent at once.
246+
async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec<TagId>) -> Result<(), Error>;
247+
248+
/// Remove a tag from torrent.
249+
async fn delete_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), Error>;
250+
251+
/// Remove all tags from torrent.
252+
async fn delete_all_torrent_tag_links(&self, torrent_id: i64) -> Result<(), Error>;
253+
254+
/// Get tag from name.
255+
async fn get_tag_from_name(&self, name: &str) -> Result<TorrentTag, Error>;
256+
257+
/// Get all tags as `Vec<TorrentTag>`.
258+
async fn get_tags(&self) -> Result<Vec<TorrentTag>, Error>;
259+
260+
/// Get tags for `torrent_id`.
261+
async fn get_tags_for_torrent_id(&self, torrent_id: i64) -> Result<Vec<TorrentTag>, Error>;
262+
231263
/// Update the seeders and leechers info for a torrent with `torrent_id`, `tracker_url`, `seeders` and `leechers`.
232264
async fn update_tracker_info(&self, torrent_id: i64, tracker_url: &str, seeders: i64, leechers: i64) -> Result<(), Error>;
233265

src/databases/mysql.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::models::info_hash::InfoHash;
1212
use crate::models::response::TorrentsResponse;
1313
use crate::models::torrent::TorrentListing;
1414
use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrentInfo, Torrent, TorrentFile};
15+
use crate::models::torrent_tag::{TagId, TorrentTag};
1516
use crate::models::tracker_key::TrackerKey;
1617
use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
1718
use crate::utils::clock;
@@ -300,6 +301,7 @@ impl Database for Mysql {
300301
&self,
301302
search: &Option<String>,
302303
categories: &Option<Vec<String>>,
304+
tags: &Option<Vec<String>>,
303305
sort: &Sorting,
304306
offset: u64,
305307
limit: u8,
@@ -345,11 +347,36 @@ impl Database for Mysql {
345347
String::new()
346348
};
347349

350+
let tag_filter_query = if let Some(t) = tags {
351+
let mut i = 0;
352+
let mut tag_filters = String::new();
353+
for tag in t.iter() {
354+
// don't take user input in the db query
355+
if let Ok(sanitized_tag) = self.get_tag_from_name(tag).await {
356+
let mut str = format!("tl.tag_id = '{}'", sanitized_tag.tag_id);
357+
if i > 0 {
358+
str = format!(" OR {str}");
359+
}
360+
tag_filters.push_str(&str);
361+
i += 1;
362+
}
363+
}
364+
if tag_filters.is_empty() {
365+
String::new()
366+
} else {
367+
format!("INNER JOIN torrust_torrent_tag_links tl ON tt.torrent_id = tl.torrent_id AND ({tag_filters}) ")
368+
}
369+
} else {
370+
String::new()
371+
};
372+
348373
let mut query_string = format!(
349374
"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,
350375
CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders,
351376
CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers
352-
FROM torrust_torrents tt {category_filter_query}
377+
FROM torrust_torrents tt
378+
{category_filter_query}
379+
{tag_filter_query}
353380
INNER JOIN torrust_user_profiles tp ON tt.uploader_id = tp.user_id
354381
INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id
355382
LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id
@@ -677,6 +704,103 @@ impl Database for Mysql {
677704
})
678705
}
679706

707+
async fn add_tag(&self, name: &str) -> Result<(), database::Error> {
708+
query("INSERT INTO torrust_torrent_tags (name) VALUES (?)")
709+
.bind(name)
710+
.execute(&self.pool)
711+
.await
712+
.map(|_| ())
713+
.map_err(|err| database::Error::ErrorWithText(err.to_string()))
714+
}
715+
716+
async fn delete_tag(&self, tag_id: TagId) -> Result<(), database::Error> {
717+
query("DELETE FROM torrust_torrent_tags WHERE tag_id = ?")
718+
.bind(tag_id)
719+
.execute(&self.pool)
720+
.await
721+
.map(|_| ())
722+
.map_err(|err| database::Error::ErrorWithText(err.to_string()))
723+
}
724+
725+
async fn add_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), database::Error> {
726+
query("INSERT INTO torrust_torrent_tag_links (torrent_id, tag_id) VALUES (?, ?)")
727+
.bind(torrent_id)
728+
.bind(tag_id)
729+
.execute(&self.pool)
730+
.await
731+
.map(|_| ())
732+
.map_err(|_| database::Error::Error)
733+
}
734+
735+
async fn add_torrent_tag_links(&self, torrent_id: i64, tag_ids: &Vec<TagId>) -> Result<(), database::Error> {
736+
let mut transaction = self
737+
.pool
738+
.begin()
739+
.await
740+
.map_err(|err| database::Error::ErrorWithText(err.to_string()))?;
741+
742+
for tag_id in tag_ids {
743+
query("INSERT INTO torrust_torrent_tag_links (torrent_id, tag_id) VALUES (?, ?)")
744+
.bind(torrent_id)
745+
.bind(tag_id)
746+
.execute(&mut transaction)
747+
.await
748+
.map_err(|err| database::Error::ErrorWithText(err.to_string()))?;
749+
}
750+
751+
transaction
752+
.commit()
753+
.await
754+
.map_err(|err| database::Error::ErrorWithText(err.to_string()))
755+
}
756+
757+
async fn delete_torrent_tag_link(&self, torrent_id: i64, tag_id: TagId) -> Result<(), database::Error> {
758+
query("DELETE FROM torrust_torrent_tag_links WHERE torrent_id = ? AND tag_id = ?")
759+
.bind(torrent_id)
760+
.bind(tag_id)
761+
.execute(&self.pool)
762+
.await
763+
.map(|_| ())
764+
.map_err(|_| database::Error::Error)
765+
}
766+
767+
async fn delete_all_torrent_tag_links(&self, torrent_id: i64) -> Result<(), database::Error> {
768+
query("DELETE FROM torrust_torrent_tag_links WHERE torrent_id = ?")
769+
.bind(torrent_id)
770+
.execute(&self.pool)
771+
.await
772+
.map(|_| ())
773+
.map_err(|err| database::Error::ErrorWithText(err.to_string()))
774+
}
775+
776+
async fn get_tag_from_name(&self, name: &str) -> Result<TorrentTag, database::Error> {
777+
query_as::<_, TorrentTag>("SELECT tag_id, name FROM torrust_torrent_tags WHERE name = ?")
778+
.bind(name)
779+
.fetch_one(&self.pool)
780+
.await
781+
.map_err(|err| database::Error::TagNotFound)
782+
}
783+
784+
async fn get_tags(&self) -> Result<Vec<TorrentTag>, database::Error> {
785+
query_as::<_, TorrentTag>("SELECT tag_id, name FROM torrust_torrent_tags")
786+
.fetch_all(&self.pool)
787+
.await
788+
.map_err(|_| database::Error::Error)
789+
}
790+
791+
async fn get_tags_for_torrent_id(&self, torrent_id: i64) -> Result<Vec<TorrentTag>, database::Error> {
792+
query_as::<_, TorrentTag>(
793+
"SELECT torrust_torrent_tags.tag_id, torrust_torrent_tags.name
794+
FROM torrust_torrent_tags
795+
JOIN torrust_torrent_tag_links ON torrust_torrent_tags.tag_id = torrust_torrent_tag_links.tag_id
796+
WHERE torrust_torrent_tag_links.torrent_id = ?",
797+
)
798+
.bind(torrent_id)
799+
.fetch_all(&self.pool)
800+
.await
801+
.map_err(|_| database::Error::Error)
802+
}
803+
680804
async fn update_tracker_info(
681805
&self,
682806
torrent_id: i64,

0 commit comments

Comments
 (0)