Skip to content

Commit

Permalink
feat!: [torrust#115] change endpoints /torrent/{id} to /torrent({info…
Browse files Browse the repository at this point in the history
…hash}

BREAKING CHANGE: you cannot use the old endpoints anymore:

- `GET /torrent/{id}`.
- `PUT /torrent/{id}`.
- `DELETE /torrent/{id}`.

New endpoints:

- `GET /torrent/{infohashi`.
- `PUT /torrent/{infohash}`.
- `DELETE /torrent/{infohash}`.
  • Loading branch information
josecelano committed May 8, 2023
1 parent e9762ff commit 7298238
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 72 deletions.
9 changes: 6 additions & 3 deletions src/databases/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ pub trait Database: Sync + Send {
) -> Result<i64, DatabaseError>;

/// Get `Torrent` from `InfoHash`.
async fn get_torrent_from_info_hash(&self, info_hash: &InfoHash) -> Result<Torrent, DatabaseError> {
let torrent_info = self.get_torrent_info_from_infohash(*info_hash).await?;
async fn get_torrent_from_infohash(&self, infohash: &InfoHash) -> Result<Torrent, DatabaseError> {
let torrent_info = self.get_torrent_info_from_infohash(infohash).await?;

let torrent_files = self.get_torrent_files_from_id(torrent_info.torrent_id).await?;

Expand Down Expand Up @@ -189,7 +189,7 @@ pub trait Database: Sync + Send {
async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result<DbTorrentInfo, DatabaseError>;

/// Get torrent's info as `DbTorrentInfo` from torrent `InfoHash`.
async fn get_torrent_info_from_infohash(&self, info_hash: InfoHash) -> Result<DbTorrentInfo, DatabaseError>;
async fn get_torrent_info_from_infohash(&self, info_hash: &InfoHash) -> Result<DbTorrentInfo, DatabaseError>;

/// Get all torrent's files as `Vec<TorrentFile>` from `torrent_id`.
async fn get_torrent_files_from_id(&self, torrent_id: i64) -> Result<Vec<TorrentFile>, DatabaseError>;
Expand All @@ -200,6 +200,9 @@ pub trait Database: Sync + Send {
/// Get `TorrentListing` from `torrent_id`.
async fn get_torrent_listing_from_id(&self, torrent_id: i64) -> Result<TorrentListing, DatabaseError>;

/// Get `TorrentListing` from `InfoHash`.
async fn get_torrent_listing_from_infohash(&self, infohash: &InfoHash) -> Result<TorrentListing, DatabaseError>;

/// Get all torrents as `Vec<TorrentCompact>`.
async fn get_all_torrents_compact(&self) -> Result<Vec<TorrentCompact>, DatabaseError>;

Expand Down
26 changes: 22 additions & 4 deletions src/databases/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl Database for MysqlDatabase {
let torrent_id = query("INSERT INTO torrust_torrents (uploader_id, category_id, info_hash, size, name, pieces, piece_length, private, root_hash, date_uploaded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())")
.bind(uploader_id)
.bind(category_id)
.bind(info_hash)
.bind(info_hash.to_uppercase())
.bind(torrent.file_size())
.bind(torrent.info.name.to_string())
.bind(pieces)
Expand Down Expand Up @@ -539,11 +539,11 @@ impl Database for MysqlDatabase {
.map_err(|_| DatabaseError::TorrentNotFound)
}

async fn get_torrent_info_from_infohash(&self, info_hash: InfoHash) -> Result<DbTorrentInfo, DatabaseError> {
async fn get_torrent_info_from_infohash(&self, infohash: &InfoHash) -> Result<DbTorrentInfo, DatabaseError> {
query_as::<_, DbTorrentInfo>(
"SELECT torrent_id, info_hash, name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?",
"SELECT torrent_id, info_hash, name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE info_hash = ?",
)
.bind(info_hash.to_string())
.bind(infohash.to_hex_string().to_uppercase()) // `info_hash` is stored as uppercase hex string
.fetch_one(&self.pool)
.await
.map_err(|_| DatabaseError::TorrentNotFound)
Expand Down Expand Up @@ -596,6 +596,24 @@ impl Database for MysqlDatabase {
.map_err(|_| DatabaseError::TorrentNotFound)
}

async fn get_torrent_listing_from_infohash(&self, infohash: &InfoHash) -> Result<TorrentListing, DatabaseError> {
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,
CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders,
CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers
FROM torrust_torrents tt
INNER JOIN torrust_user_profiles tp ON tt.uploader_id = tp.user_id
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"
)
.bind(infohash.to_hex_string().to_uppercase()) // `info_hash` is stored as uppercase hex string
.fetch_one(&self.pool)
.await
.map_err(|_| DatabaseError::TorrentNotFound)
}

async fn get_all_torrents_compact(&self) -> Result<Vec<TorrentCompact>, DatabaseError> {
query_as::<_, TorrentCompact>("SELECT torrent_id, info_hash FROM torrust_torrents")
.fetch_all(&self.pool)
Expand Down
24 changes: 21 additions & 3 deletions src/databases/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ impl Database for SqliteDatabase {
let torrent_id = query("INSERT INTO torrust_torrents (uploader_id, category_id, info_hash, size, name, pieces, piece_length, private, root_hash, date_uploaded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')))")
.bind(uploader_id)
.bind(category_id)
.bind(info_hash)
.bind(info_hash.to_uppercase())
.bind(torrent.file_size())
.bind(torrent.info.name.to_string())
.bind(pieces)
Expand Down Expand Up @@ -534,11 +534,11 @@ impl Database for SqliteDatabase {
.map_err(|_| DatabaseError::TorrentNotFound)
}

async fn get_torrent_info_from_infohash(&self, info_hash: InfoHash) -> Result<DbTorrentInfo, DatabaseError> {
async fn get_torrent_info_from_infohash(&self, infohash: &InfoHash) -> Result<DbTorrentInfo, DatabaseError> {
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_string().to_uppercase()) // info_hash is stored as uppercase
.bind(infohash.to_hex_string().to_uppercase()) // `info_hash` is stored as uppercase hex string
.fetch_one(&self.pool)
.await
.map_err(|_| DatabaseError::TorrentNotFound)
Expand Down Expand Up @@ -591,6 +591,24 @@ impl Database for SqliteDatabase {
.map_err(|_| DatabaseError::TorrentNotFound)
}

async fn get_torrent_listing_from_infohash(&self, infohash: &InfoHash) -> Result<TorrentListing, DatabaseError> {
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,
CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders,
CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers
FROM torrust_torrents tt
INNER JOIN torrust_user_profiles tp ON tt.uploader_id = tp.user_id
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"
)
.bind(infohash.to_string().to_uppercase()) // `info_hash` is stored as uppercase
.fetch_one(&self.pool)
.await
.map_err(|_| DatabaseError::TorrentNotFound)
}

async fn get_all_torrents_compact(&self) -> Result<Vec<TorrentCompact>, DatabaseError> {
query_as::<_, TorrentCompact>("SELECT torrent_id, info_hash FROM torrust_torrents")
.fetch_all(&self.pool)
Expand Down
79 changes: 47 additions & 32 deletions src/routes/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ pub fn init_routes(cfg: &mut web::ServiceConfig) {
.service(web::resource("/upload").route(web::post().to(upload_torrent)))
.service(web::resource("/download/{info_hash}").route(web::get().to(download_torrent_handler)))
.service(
web::resource("/{id}")
.route(web::get().to(get_torrent))
.route(web::put().to(update_torrent))
.route(web::delete().to(delete_torrent)),
web::resource("/{info_hash}")
.route(web::get().to(get_torrent_handler))
.route(web::put().to(update_torrent_handler))
.route(web::delete().to(delete_torrent_handler)),
),
);
cfg.service(web::scope("/torrents").service(web::resource("").route(web::get().to(get_torrents))));
Expand Down Expand Up @@ -129,12 +129,12 @@ pub async fn upload_torrent(req: HttpRequest, payload: Multipart, app_data: WebA
///
/// Returns `ServiceError::BadRequest` if the torrent infohash is invalid.
pub async fn download_torrent_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let info_hash = get_torrent_info_hash_from_request(&req)?;
let info_hash = get_torrent_infohash_from_request(&req)?;

// optional
let user = app_data.auth.get_user_compact_from_request(&req).await;

let mut torrent = app_data.database.get_torrent_from_info_hash(&info_hash).await?;
let mut torrent = app_data.database.get_torrent_from_infohash(&info_hash).await?;

let settings = app_data.cfg.settings.read().await;

Expand Down Expand Up @@ -166,18 +166,26 @@ pub async fn download_torrent_handler(req: HttpRequest, app_data: WebAppData) ->
Ok(HttpResponse::Ok().content_type("application/x-bittorrent").body(buffer))
}

pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
pub async fn get_torrent_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
// optional
let user = app_data.auth.get_user_compact_from_request(&req).await;

let settings = app_data.cfg.settings.read().await;

let torrent_id = get_torrent_id_from_request(&req)?;
let infohash = get_torrent_infohash_from_request(&req)?;

let torrent_listing = app_data.database.get_torrent_listing_from_id(torrent_id).await?;
println!("infohash: {}", infohash);

let torrent_listing = app_data.database.get_torrent_listing_from_infohash(&infohash).await?;

let torrent_id = torrent_listing.torrent_id;

println!("torrent_listing: {:#?}", torrent_listing);

let category = app_data.database.get_category_from_id(torrent_listing.category_id).await?;

println!("category: {:#?}", category);

let mut torrent_response = TorrentResponse::from_listing(torrent_listing);

torrent_response.category = category;
Expand All @@ -188,8 +196,12 @@ pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResul

torrent_response.files = app_data.database.get_torrent_files_from_id(torrent_id).await?;

println!("torrent_response.files: {:#?}", torrent_response.files);

if torrent_response.files.len() == 1 {
let torrent_info = app_data.database.get_torrent_info_from_id(torrent_id).await?;
let torrent_info = app_data.database.get_torrent_info_from_infohash(&infohash).await?;

println!("torrent_info: {:#?}", torrent_info);

torrent_response
.files
Expand All @@ -203,6 +215,8 @@ pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResul
.await
.map(|v| v.into_iter().flatten().collect())?;

println!("trackers: {:#?}", torrent_response.trackers);

// add tracker url
match user {
Ok(user) => {
Expand Down Expand Up @@ -249,16 +263,16 @@ pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResul
Ok(HttpResponse::Ok().json(OkResponse { data: torrent_response }))
}

pub async fn update_torrent(
pub async fn update_torrent_handler(
req: HttpRequest,
payload: web::Json<TorrentUpdate>,
app_data: WebAppData,
) -> ServiceResult<impl Responder> {
let user = app_data.auth.get_user_compact_from_request(&req).await?;

let torrent_id = get_torrent_id_from_request(&req)?;
let infohash = get_torrent_infohash_from_request(&req)?;

let torrent_listing = app_data.database.get_torrent_listing_from_id(torrent_id).await?;
let torrent_listing = app_data.database.get_torrent_listing_from_infohash(&infohash).await?;

// check if user is owner or administrator
if torrent_listing.uploader != user.username && !user.administrator {
Expand All @@ -267,35 +281,44 @@ pub async fn update_torrent(

// update torrent title
if let Some(title) = &payload.title {
app_data.database.update_torrent_title(torrent_id, title).await?;
app_data
.database
.update_torrent_title(torrent_listing.torrent_id, title)
.await?;
}

// update torrent description
if let Some(description) = &payload.description {
app_data.database.update_torrent_description(torrent_id, description).await?;
app_data
.database
.update_torrent_description(torrent_listing.torrent_id, description)
.await?;
}

let torrent_listing = app_data.database.get_torrent_listing_from_id(torrent_id).await?;
let torrent_listing = app_data
.database
.get_torrent_listing_from_id(torrent_listing.torrent_id)
.await?;

let torrent_response = TorrentResponse::from_listing(torrent_listing);

Ok(HttpResponse::Ok().json(OkResponse { data: torrent_response }))
}

pub async fn delete_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
pub async fn delete_torrent_handler(req: HttpRequest, app_data: WebAppData) -> ServiceResult<impl Responder> {
let user = app_data.auth.get_user_compact_from_request(&req).await?;

// check if user is administrator
if !user.administrator {
return Err(ServiceError::Unauthorized);
}

let torrent_id = get_torrent_id_from_request(&req)?;
let infohash = get_torrent_infohash_from_request(&req)?;

// needed later for removing torrent from tracker whitelist
let torrent_listing = app_data.database.get_torrent_listing_from_id(torrent_id).await?;
let torrent_listing = app_data.database.get_torrent_listing_from_infohash(&infohash).await?;

app_data.database.delete_torrent(torrent_id).await?;
app_data.database.delete_torrent(torrent_listing.torrent_id).await?;

// remove info_hash from tracker whitelist
let _ = app_data
Expand All @@ -304,7 +327,9 @@ pub async fn delete_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceRe
.await;

Ok(HttpResponse::Ok().json(OkResponse {
data: NewTorrentResponse { torrent_id },
data: NewTorrentResponse {
torrent_id: torrent_listing.torrent_id,
},
}))
}

Expand Down Expand Up @@ -332,17 +357,7 @@ pub async fn get_torrents(params: Query<TorrentSearch>, app_data: WebAppData) ->
Ok(HttpResponse::Ok().json(OkResponse { data: torrents_response }))
}

fn get_torrent_id_from_request(req: &HttpRequest) -> Result<i64, ServiceError> {
match req.match_info().get("id") {
None => Err(ServiceError::BadRequest),
Some(torrent_id) => match torrent_id.parse() {
Err(_) => Err(ServiceError::BadRequest),
Ok(v) => Ok(v),
},
}
}

fn get_torrent_info_hash_from_request(req: &HttpRequest) -> Result<InfoHash, ServiceError> {
fn get_torrent_infohash_from_request(req: &HttpRequest) -> Result<InfoHash, ServiceError> {
match req.match_info().get("info_hash") {
None => Err(ServiceError::BadRequest),
Some(info_hash) => match InfoHash::from_str(info_hash) {
Expand Down
18 changes: 10 additions & 8 deletions tests/common/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::connection_info::ConnectionInfo;
use super::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm};
use super::contexts::settings::form::UpdateSettings;
use super::contexts::torrent::forms::UpdateTorrentFrom;
use super::contexts::torrent::requests::{InfoHash, TorrentId};
use super::contexts::torrent::requests::InfoHash;
use super::contexts::user::forms::{LoginForm, RegistrationForm, TokenRenewalForm, TokenVerificationForm, Username};
use super::http::{Query, ReqwestQuery};
use super::responses::{self, BinaryResponse, TextResponse};
Expand Down Expand Up @@ -93,23 +93,25 @@ impl Client {
self.http_client.get("torrents", Query::empty()).await
}

pub async fn get_torrent(&self, id: TorrentId) -> TextResponse {
self.http_client.get(&format!("torrent/{id}"), Query::empty()).await
pub async fn get_torrent(&self, infohash: &InfoHash) -> TextResponse {
self.http_client.get(&format!("torrent/{infohash}"), Query::empty()).await
}

pub async fn delete_torrent(&self, id: TorrentId) -> TextResponse {
self.http_client.delete(&format!("torrent/{id}")).await
pub async fn delete_torrent(&self, infohash: &InfoHash) -> TextResponse {
self.http_client.delete(&format!("torrent/{infohash}")).await
}

pub async fn update_torrent(&self, id: TorrentId, update_torrent_form: UpdateTorrentFrom) -> TextResponse {
self.http_client.put(&format!("torrent/{id}"), &update_torrent_form).await
pub async fn update_torrent(&self, infohash: &InfoHash, update_torrent_form: UpdateTorrentFrom) -> TextResponse {
self.http_client
.put(&format!("torrent/{infohash}"), &update_torrent_form)
.await
}

pub async fn upload_torrent(&self, form: multipart::Form) -> TextResponse {
self.http_client.post_multipart("torrent/upload", form).await
}

pub async fn download_torrent(&self, info_hash: InfoHash) -> responses::BinaryResponse {
pub async fn download_torrent(&self, info_hash: &InfoHash) -> responses::BinaryResponse {
self.http_client
.get_binary(&format!("torrent/download/{info_hash}"), Query::empty())
.await
Expand Down
3 changes: 2 additions & 1 deletion tests/common/contexts/torrent/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use uuid::Uuid;

use super::file::{create_torrent, parse_torrent, TorrentFileInfo};
use super::forms::{BinaryFile, UploadTorrentMultipartForm};
use super::requests::InfoHash;
use super::responses::Id;
use crate::common::contexts::category::fixtures::software_predefined_category_name;

Expand Down Expand Up @@ -91,7 +92,7 @@ impl TestTorrent {
}
}

pub fn info_hash(&self) -> String {
pub fn infohash(&self) -> InfoHash {
self.file_info.info_hash.clone()
}
}
Expand Down
1 change: 0 additions & 1 deletion tests/common/contexts/torrent/requests.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
pub type TorrentId = i64;
pub type InfoHash = String;
Loading

0 comments on commit 7298238

Please sign in to comment.