Skip to content

Commit

Permalink
feat: [torrust#278] redirect to URL with canonical infohash
Browse files Browse the repository at this point in the history
For endpoints using a GET param with an infohash:

- GET /v1/torrent/CANONICAL_INFO_HASH
- GET /v1/torrent/download/CANONICAL_INFO_HASH

Suppose the URL contains an info-hash, which is not canonical but belongs to a canonical info-hash group. In that case, the response will be a redirection (307) to the same URL but using the canonical info-hash.
  • Loading branch information
josecelano committed Sep 13, 2023
1 parent 48202ec commit 83d31f2
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 69 deletions.
8 changes: 4 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebTok
use crate::services::category::{self, DbCategoryRepository};
use crate::services::tag::{self, DbTagRepository};
use crate::services::torrent::{
DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoHashRepository, DbTorrentInfoRepository,
DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository,
DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository,
};
use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
Expand Down Expand Up @@ -68,7 +68,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
let user_authentication_repository = Arc::new(DbUserAuthenticationRepository::new(database.clone()));
let user_profile_repository = Arc::new(DbUserProfileRepository::new(database.clone()));
let torrent_repository = Arc::new(DbTorrentRepository::new(database.clone()));
let torrent_info_hash_repository = Arc::new(DbTorrentInfoHashRepository::new(database.clone()));
let canonical_info_hash_group_repository = Arc::new(DbCanonicalInfoHashGroupRepository::new(database.clone()));
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()));
Expand All @@ -93,7 +93,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
user_repository.clone(),
category_repository.clone(),
torrent_repository.clone(),
torrent_info_hash_repository.clone(),
canonical_info_hash_group_repository.clone(),
torrent_info_repository.clone(),
torrent_file_repository.clone(),
torrent_announce_url_repository.clone(),
Expand Down Expand Up @@ -137,7 +137,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
user_authentication_repository,
user_profile_repository,
torrent_repository,
torrent_info_hash_repository,
canonical_info_hash_group_repository,
torrent_info_repository,
torrent_file_repository,
torrent_announce_url_repository,
Expand Down
6 changes: 3 additions & 3 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebTok
use crate::services::category::{self, DbCategoryRepository};
use crate::services::tag::{self, DbTagRepository};
use crate::services::torrent::{
DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoHashRepository, DbTorrentInfoRepository,
DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository,
DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository,
};
use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
Expand All @@ -34,7 +34,7 @@ pub struct AppData {
pub user_authentication_repository: Arc<DbUserAuthenticationRepository>,
pub user_profile_repository: Arc<DbUserProfileRepository>,
pub torrent_repository: Arc<DbTorrentRepository>,
pub torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
pub torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
pub torrent_info_repository: Arc<DbTorrentInfoRepository>,
pub torrent_file_repository: Arc<DbTorrentFileRepository>,
pub torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
Expand Down Expand Up @@ -70,7 +70,7 @@ impl AppData {
user_authentication_repository: Arc<DbUserAuthenticationRepository>,
user_profile_repository: Arc<DbUserProfileRepository>,
torrent_repository: Arc<DbTorrentRepository>,
torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
torrent_info_repository: Arc<DbTorrentInfoRepository>,
torrent_file_repository: Arc<DbTorrentFileRepository>,
torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
Expand Down
31 changes: 21 additions & 10 deletions src/databases/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ 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};
use crate::services::torrent::OriginalInfoHashes;
use crate::services::torrent::CanonicalInfoHashGroup;

/// Database tables to be truncated when upgrading from v1.0.0 to v2.0.0.
/// They must be in the correct order to avoid foreign key errors.
Expand Down Expand Up @@ -231,19 +231,30 @@ pub trait Database: Sync + Send {
))
}

/// Returns the list of all infohashes producing the same canonical infohash.
///
/// When you upload a torrent the infohash migth change because the Index
/// remove the non-standard fields in the `info` dictionary. That makes the
/// infohash change. The canonical infohash is the resulting infohash.
/// This function returns the original infohashes of a canonical infohash.
/// It returns the list of all infohashes producing the same canonical
/// infohash.
///
/// If the original infohash was unknown, it returns the canonical infohash.
///
/// The relationship is 1 canonical infohash -> N original infohashes.
async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<OriginalInfoHashes, Error>;
/// # Errors
///
/// Returns an error is there was a problem with the database.
async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<CanonicalInfoHashGroup, Error>;

async fn insert_torrent_info_hash(&self, original: &InfoHash, canonical: &InfoHash) -> Result<(), Error>;
/// It returns the [`CanonicalInfoHashGroup`] the info-hash belongs to, if
/// the info-hash belongs to a group. Otherwise, returns `None`.
///
/// # Errors
///
/// Returns an error is there was a problem with the database.
async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, Error>;

/// It adds a new info-hash to the canonical info-hash group.
///
/// # Errors
///
/// Returns an error is there was a problem with the database.
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<DbTorrentInfo, Error>;
Expand Down
33 changes: 29 additions & 4 deletions src/databases/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrent
use crate::models::torrent_tag::{TagId, TorrentTag};
use crate::models::tracker_key::TrackerKey;
use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
use crate::services::torrent::{DbTorrentInfoHash, OriginalInfoHashes};
use crate::services::torrent::{CanonicalInfoHashGroup, DbTorrentInfoHash};
use crate::utils::clock;
use crate::utils::hex::from_bytes;

Expand Down Expand Up @@ -590,7 +590,10 @@ impl Database for Mysql {
}
}

async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<OriginalInfoHashes, database::Error> {
async fn get_torrent_canonical_info_hash_group(
&self,
canonical: &InfoHash,
) -> Result<CanonicalInfoHashGroup, database::Error> {
let db_info_hashes = query_as::<_, DbTorrentInfoHash>(
"SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE canonical_info_hash = ?",
)
Expand All @@ -607,13 +610,35 @@ impl Database for Mysql {
})
.collect();

Ok(OriginalInfoHashes {
Ok(CanonicalInfoHashGroup {
canonical_info_hash: *canonical,
original_info_hashes: info_hashes,
})
}

async fn insert_torrent_info_hash(&self, info_hash: &InfoHash, canonical: &InfoHash) -> Result<(), database::Error> {
async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, database::Error> {
let maybe_db_torrent_info_hash = query_as::<_, DbTorrentInfoHash>(
"SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE info_hash = ?",
)
.bind(info_hash.to_hex_string())
.fetch_optional(&self.pool)
.await
.map_err(|err| database::Error::ErrorWithText(err.to_string()))?;

match maybe_db_torrent_info_hash {
Some(db_torrent_info_hash) => Ok(Some(
InfoHash::from_str(&db_torrent_info_hash.canonical_info_hash)
.unwrap_or_else(|_| panic!("Invalid info-hash in database: {}", db_torrent_info_hash.canonical_info_hash)),
)),
None => Ok(None),
}
}

async fn add_info_hash_to_canonical_info_hash_group(
&self,
info_hash: &InfoHash,
canonical: &InfoHash,
) -> Result<(), database::Error> {
query("INSERT INTO torrust_torrent_info_hashes (info_hash, canonical_info_hash, original_is_known) VALUES (?, ?, ?)")
.bind(info_hash.to_hex_string())
.bind(canonical.to_hex_string())
Expand Down
33 changes: 29 additions & 4 deletions src/databases/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrent
use crate::models::torrent_tag::{TagId, TorrentTag};
use crate::models::tracker_key::TrackerKey;
use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
use crate::services::torrent::{DbTorrentInfoHash, OriginalInfoHashes};
use crate::services::torrent::{CanonicalInfoHashGroup, DbTorrentInfoHash};
use crate::utils::clock;
use crate::utils::hex::from_bytes;

Expand Down Expand Up @@ -580,7 +580,10 @@ impl Database for Sqlite {
}
}

async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<OriginalInfoHashes, database::Error> {
async fn get_torrent_canonical_info_hash_group(
&self,
canonical: &InfoHash,
) -> Result<CanonicalInfoHashGroup, database::Error> {
let db_info_hashes = query_as::<_, DbTorrentInfoHash>(
"SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE canonical_info_hash = ?",
)
Expand All @@ -597,13 +600,35 @@ impl Database for Sqlite {
})
.collect();

Ok(OriginalInfoHashes {
Ok(CanonicalInfoHashGroup {
canonical_info_hash: *canonical,
original_info_hashes: info_hashes,
})
}

async fn insert_torrent_info_hash(&self, original: &InfoHash, canonical: &InfoHash) -> Result<(), database::Error> {
async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, database::Error> {
let maybe_db_torrent_info_hash = query_as::<_, DbTorrentInfoHash>(
"SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE info_hash = ?",
)
.bind(info_hash.to_hex_string())
.fetch_optional(&self.pool)
.await
.map_err(|err| database::Error::ErrorWithText(err.to_string()))?;

match maybe_db_torrent_info_hash {
Some(db_torrent_info_hash) => Ok(Some(
InfoHash::from_str(&db_torrent_info_hash.canonical_info_hash)
.unwrap_or_else(|_| panic!("Invalid info-hash in database: {}", db_torrent_info_hash.canonical_info_hash)),
)),
None => Ok(None),
}
}

async fn add_info_hash_to_canonical_info_hash_group(
&self,
original: &InfoHash,
canonical: &InfoHash,
) -> Result<(), database::Error> {
query("INSERT INTO torrust_torrent_info_hashes (info_hash, canonical_info_hash, original_is_known) VALUES (?, ?, ?)")
.bind(original.to_hex_string())
.bind(canonical.to_hex_string())
Expand Down
67 changes: 43 additions & 24 deletions src/services/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub struct Index {
user_repository: Arc<DbUserRepository>,
category_repository: Arc<DbCategoryRepository>,
torrent_repository: Arc<DbTorrentRepository>,
torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
torrent_info_repository: Arc<DbTorrentInfoRepository>,
torrent_file_repository: Arc<DbTorrentFileRepository>,
torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
Expand Down Expand Up @@ -85,7 +85,7 @@ impl Index {
user_repository: Arc<DbUserRepository>,
category_repository: Arc<DbCategoryRepository>,
torrent_repository: Arc<DbTorrentRepository>,
torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
torrent_info_repository: Arc<DbTorrentInfoRepository>,
torrent_file_repository: Arc<DbTorrentFileRepository>,
torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
Expand Down Expand Up @@ -189,7 +189,7 @@ impl Index {

// Add the new associated original infohash to the canonical one.
self.torrent_info_hash_repository
.add(&original_info_hash, &canonical_info_hash)
.add_info_hash_to_canonical_info_hash_group(&original_info_hash, &canonical_info_hash)
.await?;
return Err(ServiceError::CanonicalInfoHashAlreadyExists);
}
Expand Down Expand Up @@ -555,16 +555,24 @@ pub struct DbTorrentInfoHash {
pub original_is_known: bool,
}

pub struct DbTorrentInfoHashRepository {
database: Arc<Box<dyn Database>>,
}

pub struct OriginalInfoHashes {
/// All the infohashes associated to a canonical one.
///
/// When you upload a torrent the info-hash migth change because the Index
/// remove the non-standard fields in the `info` dictionary. That makes the
/// infohash change. The canonical infohash is the resulting infohash.
/// This function returns the original infohashes of a canonical infohash.
///
/// The relationship is 1 canonical infohash -> N original infohashes.
pub struct CanonicalInfoHashGroup {
pub canonical_info_hash: InfoHash,
/// The list of original infohashes associated to the canonical one.
pub original_info_hashes: Vec<InfoHash>,
}
pub struct DbCanonicalInfoHashGroupRepository {
database: Arc<Box<dyn Database>>,
}

impl OriginalInfoHashes {
impl CanonicalInfoHashGroup {
#[must_use]
pub fn is_empty(&self) -> bool {
self.original_info_hashes.is_empty()
Expand All @@ -576,7 +584,7 @@ impl OriginalInfoHashes {
}
}

impl DbTorrentInfoHashRepository {
impl DbCanonicalInfoHashGroupRepository {
#[must_use]
pub fn new(database: Arc<Box<dyn Database>>) -> Self {
Self { database }
Expand All @@ -587,31 +595,42 @@ impl DbTorrentInfoHashRepository {
/// # Errors
///
/// This function will return an error there is a database error.
pub async fn get_canonical_info_hash_group(&self, info_hash: &InfoHash) -> Result<OriginalInfoHashes, Error> {
///
/// # Errors
///
/// Returns an error is there was a problem with the database.
pub async fn get_canonical_info_hash_group(&self, info_hash: &InfoHash) -> Result<CanonicalInfoHashGroup, Error> {
self.database.get_torrent_canonical_info_hash_group(info_hash).await
}

/// Inserts a new infohash for the torrent. Torrents can be associated to
/// different infohashes because the Index might change the original infohash.
/// The index track the final infohash used (canonical) and all the original
/// ones.
/// It returns the list of all infohashes producing the same canonical
/// infohash.
///
/// If the original infohash was unknown, it returns the canonical infohash.
///
/// # Errors
///
/// This function will return an error there is a database error.
pub async fn add(&self, original_info_hash: &InfoHash, canonical_info_hash: &InfoHash) -> Result<(), Error> {
self.database
.insert_torrent_info_hash(original_info_hash, canonical_info_hash)
.await
/// Returns an error is there was a problem with the database.
pub async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, Error> {
self.database.find_canonical_info_hash_for(info_hash).await
}

/// Deletes the entire torrent in the database.
/// It returns the list of all infohashes producing the same canonical
/// infohash.
///
/// If the original infohash was unknown, it returns the canonical infohash.
///
/// # Errors
///
/// This function will return an error there is a database error.
pub async fn delete(&self, torrent_id: &TorrentId) -> Result<(), Error> {
self.database.delete_torrent(*torrent_id).await
/// Returns an error is there was a problem with the database.
pub async fn add_info_hash_to_canonical_info_hash_group(
&self,
original_info_hash: &InfoHash,
canonical_info_hash: &InfoHash,
) -> Result<(), Error> {
self.database
.add_info_hash_to_canonical_info_hash_group(original_info_hash, canonical_info_hash)
.await
}
}

Expand Down
Loading

0 comments on commit 83d31f2

Please sign in to comment.