diff --git a/.git-blame-ignore b/.git-blame-ignore new file mode 100644 index 00000000..749a0f1e --- /dev/null +++ b/.git-blame-ignore @@ -0,0 +1,4 @@ +# https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt + +# Format the world! +9ddc079b00fc5d6ecd80199edc078d6793fb0a9c \ No newline at end of file diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 00000000..f9301a6d --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,20 @@ +name: Development Checks + +on: [pull_request] + +jobs: + format: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - name: Verify Formatting + uses: ClementTsang/cargo-action@main + with: + command: fmt + args: --all --check \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..3e878b27 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +max_width = 130 +imports_granularity = "Module" +group_imports = "StdExternalCrate" + diff --git a/src/auth.rs b/src/auth.rs index c13e5da0..2b38c14e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,23 +1,22 @@ -use actix_web::HttpRequest; -use crate::models::user::{UserClaims, UserCompact}; -use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm, encode, Header, EncodingKey}; -use crate::utils::time::current_time; -use crate::errors::ServiceError; use std::sync::Arc; + +use actix_web::HttpRequest; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; + use crate::config::Configuration; use crate::databases::database::Database; +use crate::errors::ServiceError; +use crate::models::user::{UserClaims, UserCompact}; +use crate::utils::time::current_time; pub struct AuthorizationService { cfg: Arc, - database: Arc> + database: Arc>, } impl AuthorizationService { pub fn new(cfg: Arc, database: Arc>) -> AuthorizationService { - AuthorizationService { - cfg, - database - } + AuthorizationService { cfg, database } } pub async fn sign_jwt(&self, user: UserCompact) -> String { @@ -28,17 +27,9 @@ impl AuthorizationService { // TODO: create config option for setting the token validity in seconds let exp_date = current_time() + 1_209_600; // two weeks from now - let claims = UserClaims { - user, - exp: exp_date, - }; + let claims = UserClaims { user, exp: exp_date }; - let token = encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(key), - ) - .unwrap(); + let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).unwrap(); token } @@ -53,11 +44,11 @@ impl AuthorizationService { ) { Ok(token_data) => { if token_data.claims.exp < current_time() { - return Err(ServiceError::TokenExpired) + return Err(ServiceError::TokenExpired); } Ok(token_data.claims) - }, - Err(_) => Err(ServiceError::TokenInvalid) + } + Err(_) => Err(ServiceError::TokenInvalid), } } @@ -73,14 +64,15 @@ impl AuthorizationService { Err(e) => Err(e), } } - None => Err(ServiceError::TokenNotFound) + None => Err(ServiceError::TokenNotFound), } } pub async fn get_user_compact_from_request(&self, req: &HttpRequest) -> Result { let claims = self.get_claims_from_request(req).await?; - self.database.get_user_compact_from_id(claims.user.user_id) + self.database + .get_user_compact_from_id(claims.user.user_id) .await .map_err(|_| ServiceError::UserNotFound) } diff --git a/src/common.rs b/src/common.rs index 2f11f6ec..9bd43dd9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,9 +1,10 @@ use std::sync::Arc; -use crate::config::Configuration; + use crate::auth::AuthorizationService; +use crate::config::Configuration; use crate::databases::database::Database; -use crate::tracker::TrackerService; use crate::mailer::MailerService; +use crate::tracker::TrackerService; pub type Username = String; @@ -14,11 +15,17 @@ pub struct AppData { pub database: Arc>, pub auth: Arc, pub tracker: Arc, - pub mailer: Arc + pub mailer: Arc, } impl AppData { - pub fn new(cfg: Arc, database: Arc>, auth: Arc, tracker: Arc, mailer: Arc) -> AppData { + pub fn new( + cfg: Arc, + database: Arc>, + auth: Arc, + tracker: Arc, + mailer: Arc, + ) -> AppData { AppData { cfg, database, diff --git a/src/config.rs b/src/config.rs index cb3b5546..00d390dc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,8 @@ use std::fs; -use config::{ConfigError, Config, File}; use std::path::Path; -use serde::{Serialize, Deserialize}; + +use config::{Config, ConfigError, File}; +use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -14,7 +15,7 @@ pub enum TrackerMode { Public, Private, Whitelisted, - PrivateWhitelisted + PrivateWhitelisted, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -36,7 +37,7 @@ pub struct Network { pub enum EmailOnSignup { Required, Optional, - None + None, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -76,35 +77,35 @@ pub struct TorrustConfig { #[derive(Debug)] pub struct Configuration { - pub settings: RwLock + pub settings: RwLock, } impl Configuration { pub fn default() -> Configuration { let torrust_config = TorrustConfig { website: Website { - name: "Torrust".to_string() + name: "Torrust".to_string(), }, tracker: Tracker { url: "udp://localhost:6969".to_string(), mode: TrackerMode::Public, api_url: "http://localhost:1212".to_string(), token: "MyAccessToken".to_string(), - token_valid_seconds: 7257600 + token_valid_seconds: 7257600, }, net: Network { port: 3000, - base_url: None + base_url: None, }, auth: Auth { email_on_signup: EmailOnSignup::Optional, min_password_length: 6, max_password_length: 64, - secret_key: "MaxVerstappenWC2021".to_string() + secret_key: "MaxVerstappenWC2021".to_string(), }, database: Database { connect_url: "sqlite://data.db?mode=rwc".to_string(), - torrent_info_update_interval: 3600 + torrent_info_update_interval: 3600, }, mail: Mail { email_verification_enabled: false, @@ -113,12 +114,12 @@ impl Configuration { username: "".to_string(), password: "".to_string(), server: "".to_string(), - port: 25 - } + port: 25, + }, }; Configuration { - settings: RwLock::new(torrust_config) + settings: RwLock::new(torrust_config), } } @@ -134,7 +135,9 @@ impl Configuration { eprintln!("Creating config file.."); let config = Configuration::default(); let _ = config.save_to_file().await; - return Err(ConfigError::Message(format!("Please edit the config.TOML in the root folder and restart the tracker."))) + return Err(ConfigError::Message(format!( + "Please edit the config.TOML in the root folder and restart the tracker." + ))); } let torrust_config: TorrustConfig = match config.try_into() { @@ -143,11 +146,11 @@ impl Configuration { }?; Ok(Configuration { - settings: RwLock::new(torrust_config) + settings: RwLock::new(torrust_config), }) } - pub async fn save_to_file(&self) -> Result<(), ()>{ + pub async fn save_to_file(&self) -> Result<(), ()> { let settings = self.settings.read().await; let toml_string = toml::to_string(&*settings).expect("Could not encode TOML value"); @@ -178,7 +181,7 @@ impl Configuration { website_name: settings_lock.website.name.clone(), tracker_url: settings_lock.tracker.url.clone(), tracker_mode: settings_lock.tracker.mode.clone(), - email_on_signup: settings_lock.auth.email_on_signup.clone() + email_on_signup: settings_lock.auth.email_on_signup.clone(), } } } @@ -188,5 +191,5 @@ pub struct ConfigurationPublic { website_name: String, tracker_url: String, tracker_mode: TrackerMode, - email_on_signup: EmailOnSignup + email_on_signup: EmailOnSignup, } diff --git a/src/databases/database.rs b/src/databases/database.rs index 856092d5..c4dad043 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -1,10 +1,10 @@ use async_trait::async_trait; -use chrono::{NaiveDateTime}; -use serde::{Serialize, Deserialize}; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; use crate::databases::mysql::MysqlDatabase; use crate::databases::sqlite::SqliteDatabase; -use crate::models::response::{TorrentsResponse}; +use crate::models::response::TorrentsResponse; use crate::models::torrent::TorrentListing; use crate::models::torrent_file::{DbTorrentInfo, Torrent, TorrentFile}; use crate::models::tracker_key::TrackerKey; @@ -14,7 +14,7 @@ use crate::models::user::{User, UserAuthentication, UserCompact, UserProfile}; #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub enum DatabaseDriver { Sqlite3, - Mysql + Mysql, } /// Compact representation of torrent. @@ -29,7 +29,7 @@ pub struct TorrentCompact { pub struct Category { pub category_id: i64, pub name: String, - pub num_torrents: i64 + pub num_torrents: i64, } /// Sorting options for torrents. @@ -73,9 +73,7 @@ pub async fn connect_database(db_path: &str) -> Result, Databa let db = MysqlDatabase::new(db_path).await; Ok(Box::new(db)) } - _ => { - Err(DatabaseError::UnrecognizedDatabaseDriver) - } + _ => Err(DatabaseError::UnrecognizedDatabaseDriver), } } @@ -137,10 +135,24 @@ pub trait Database: Sync + Send { async fn delete_category(&self, category_name: &str) -> Result<(), DatabaseError>; /// Get results of a torrent search in a paginated and sorted form as `TorrentsResponse` from `search`, `categories`, `sort`, `offset` and `page_size`. - async fn get_torrents_search_sorted_paginated(&self, search: &Option, categories: &Option>, sort: &Sorting, offset: u64, page_size: u8) -> Result; + async fn get_torrents_search_sorted_paginated( + &self, + search: &Option, + categories: &Option>, + sort: &Sorting, + offset: u64, + page_size: u8, + ) -> Result; /// Add new torrent and return the newly inserted `torrent_id` with `torrent`, `uploader_id`, `category_id`, `title` and `description`. - async fn insert_torrent_and_get_id(&self, torrent: &Torrent, uploader_id: i64, category_id: i64, title: &str, description: &str) -> Result; + async fn insert_torrent_and_get_id( + &self, + torrent: &Torrent, + uploader_id: i64, + category_id: i64, + title: &str, + description: &str, + ) -> Result; /// Get `Torrent` from `torrent_id`. async fn get_torrent_from_id(&self, torrent_id: i64) -> Result; @@ -167,7 +179,13 @@ pub trait Database: Sync + Send { async fn update_torrent_description(&self, torrent_id: i64, description: &str) -> Result<(), DatabaseError>; /// 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<(), DatabaseError>; + async fn update_tracker_info( + &self, + torrent_id: i64, + tracker_url: &str, + seeders: i64, + leechers: i64, + ) -> Result<(), DatabaseError>; /// Delete a torrent with `torrent_id`. async fn delete_torrent(&self, torrent_id: i64) -> Result<(), DatabaseError>; diff --git a/src/databases/mod.rs b/src/databases/mod.rs index 9340e821..169d99f4 100644 --- a/src/databases/mod.rs +++ b/src/databases/mod.rs @@ -1,3 +1,3 @@ pub mod database; -pub mod sqlite; pub mod mysql; +pub mod sqlite; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index 679bd35e..75afe1c8 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -1,19 +1,19 @@ -use sqlx::{Acquire, MySqlPool, query, query_as}; use async_trait::async_trait; -use chrono::{NaiveDateTime}; +use chrono::NaiveDateTime; use sqlx::mysql::MySqlPoolOptions; +use sqlx::{query, query_as, Acquire, MySqlPool}; -use crate::models::user::{User, UserAuthentication, UserCompact, UserProfile}; -use crate::models::torrent::TorrentListing; -use crate::utils::time::current_time; -use crate::models::tracker_key::{TrackerKey}; use crate::databases::database::{Category, Database, DatabaseDriver, DatabaseError, Sorting, TorrentCompact}; -use crate::models::response::{TorrentsResponse}; -use crate::models::torrent_file::{DbTorrentInfo, Torrent, DbTorrentFile, DbTorrentAnnounceUrl, TorrentFile}; +use crate::models::response::TorrentsResponse; +use crate::models::torrent::TorrentListing; +use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrentInfo, Torrent, TorrentFile}; +use crate::models::tracker_key::TrackerKey; +use crate::models::user::{User, UserAuthentication, UserCompact, UserProfile}; use crate::utils::hex::bytes_to_hex; +use crate::utils::time::current_time; pub struct MysqlDatabase { - pub pool: MySqlPool + pub pool: MySqlPool, } impl MysqlDatabase { @@ -28,9 +28,7 @@ impl MysqlDatabase { .await .expect("Could not run database migrations."); - Self { - pool: db - } + Self { pool: db } } } @@ -41,16 +39,11 @@ impl Database for MysqlDatabase { } async fn insert_user_and_get_id(&self, username: &str, email: &str, password_hash: &str) -> Result { - // open pool connection - let mut conn = self.pool.acquire() - .await - .map_err(|_| DatabaseError::Error)?; + let mut conn = self.pool.acquire().await.map_err(|_| DatabaseError::Error)?; // start db transaction - let mut tx = conn.begin() - .await - .map_err(|_| DatabaseError::Error)?; + let mut tx = conn.begin().await.map_err(|_| DatabaseError::Error)?; // create the user account and get the user id let user_id = query("INSERT INTO torrust_users (date_registered) VALUES (UTC_TIMESTAMP())") @@ -70,7 +63,7 @@ impl Database for MysqlDatabase { // rollback transaction on error if let Err(e) = insert_user_auth_result { let _ = tx.rollback().await; - return Err(e) + return Err(e); } // add account profile details @@ -181,10 +174,12 @@ impl Database for MysqlDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::UserNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::UserNotFound) + } }) } @@ -194,10 +189,12 @@ impl Database for MysqlDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::UserNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::UserNotFound) + } }) } @@ -220,10 +217,12 @@ impl Database for MysqlDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::UserNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::UserNotFound) + } }) } @@ -240,8 +239,8 @@ impl Database for MysqlDatabase { } else { DatabaseError::Error } - }, - _ => DatabaseError::Error + } + _ => DatabaseError::Error, }) } @@ -274,18 +273,27 @@ impl Database for MysqlDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::CategoryNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::CategoryNotFound) + } }) } // TODO: refactor this - async fn get_torrents_search_sorted_paginated(&self, search: &Option, categories: &Option>, sort: &Sorting, offset: u64, page_size: u8) -> Result { + async fn get_torrents_search_sorted_paginated( + &self, + search: &Option, + categories: &Option>, + sort: &Sorting, + offset: u64, + page_size: u8, + ) -> Result { let title = match search { None => "%".to_string(), - Some(v) => format!("%{}%", v) + Some(v) => format!("%{}%", v), }; let sort_query: String = match sort { @@ -308,13 +316,18 @@ impl Database for MysqlDatabase { // don't take user input in the db query if let Ok(sanitized_category) = self.get_category_from_name(category).await { let mut str = format!("tc.name = '{}'", sanitized_category.name); - if i > 0 { str = format!(" OR {}", str); } + if i > 0 { + str = format!(" OR {}", str); + } category_filters.push_str(&str); i += 1; } } if category_filters.len() > 0 { - format!("INNER JOIN torrust_categories tc ON tt.category_id = tc.category_id AND ({}) ", category_filters) + format!( + "INNER JOIN torrust_categories tc ON tt.category_id = tc.category_id AND ({}) ", + category_filters + ) } else { String::new() } @@ -358,22 +371,25 @@ impl Database for MysqlDatabase { Ok(TorrentsResponse { total: count as u32, - results: res + results: res, }) } - async fn insert_torrent_and_get_id(&self, torrent: &Torrent, uploader_id: i64, category_id: i64, title: &str, description: &str) -> Result { + async fn insert_torrent_and_get_id( + &self, + torrent: &Torrent, + uploader_id: i64, + category_id: i64, + title: &str, + description: &str, + ) -> Result { let info_hash = torrent.info_hash(); // open pool connection - let mut conn = self.pool.acquire() - .await - .map_err(|_| DatabaseError::Error)?; + let mut conn = self.pool.acquire().await.map_err(|_| DatabaseError::Error)?; // start db transaction - let mut tx = conn.begin() - .await - .map_err(|_| DatabaseError::Error)?; + let mut tx = conn.begin().await.map_err(|_| DatabaseError::Error)?; // torrent file can only hold a pieces key or a root hash key: http://www.bittorrent.org/beps/bep_0030.html let (pieces, root_hash): (String, bool) = if let Some(pieces) = &torrent.info.pieces { @@ -443,7 +459,7 @@ impl Database for MysqlDatabase { // rollback transaction on error if let Err(e) = insert_torrent_files_result { let _ = tx.rollback().await; - return Err(e) + return Err(e); } let insert_torrent_announce_urls_result: Result<(), DatabaseError> = if let Some(announce_urls) = &torrent.announce_list { @@ -476,27 +492,28 @@ impl Database for MysqlDatabase { // rollback transaction on error if let Err(e) = insert_torrent_announce_urls_result { let _ = tx.rollback().await; - return Err(e) + return Err(e); } - let insert_torrent_info_result = query(r#"INSERT INTO torrust_torrent_info (torrent_id, title, description) VALUES (?, ?, NULLIF(?, ""))"#) - .bind(torrent_id) - .bind(title) - .bind(description) - .execute(&mut tx) - .await - .map_err(|e| match e { - sqlx::Error::Database(err) => { - if err.message().contains("info_hash") { - DatabaseError::TorrentAlreadyExists - } else if err.message().contains("title") { - DatabaseError::TorrentTitleAlreadyExists - } else { - DatabaseError::Error + let insert_torrent_info_result = + query(r#"INSERT INTO torrust_torrent_info (torrent_id, title, description) VALUES (?, ?, NULLIF(?, ""))"#) + .bind(torrent_id) + .bind(title) + .bind(description) + .execute(&mut tx) + .await + .map_err(|e| match e { + sqlx::Error::Database(err) => { + if err.message().contains("info_hash") { + DatabaseError::TorrentAlreadyExists + } else if err.message().contains("title") { + DatabaseError::TorrentTitleAlreadyExists + } else { + DatabaseError::Error + } } - } - _ => DatabaseError::Error - }); + _ => DatabaseError::Error, + }); // commit or rollback transaction and return user_id on success match insert_torrent_info_result { @@ -518,43 +535,45 @@ impl Database for MysqlDatabase { 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_db_info_files_and_announce_urls( + torrent_info, + torrent_files, + torrent_announce_urls, + )) } async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { query_as::<_, DbTorrentInfo>( - "SELECT name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?" + "SELECT name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?", ) - .bind(torrent_id) - .fetch_one(&self.pool) - .await - .map_err(|_| DatabaseError::TorrentNotFound) + .bind(torrent_id) + .fetch_one(&self.pool) + .await + .map_err(|_| DatabaseError::TorrentNotFound) } async fn get_torrent_files_from_id(&self, torrent_id: i64) -> Result, DatabaseError> { - let db_torrent_files = query_as::<_, DbTorrentFile>( - "SELECT md5sum, length, path FROM torrust_torrent_files WHERE torrent_id = ?" - ) - .bind(torrent_id) - .fetch_all(&self.pool) - .await - .map_err(|_| DatabaseError::TorrentNotFound)?; + let db_torrent_files = + query_as::<_, DbTorrentFile>("SELECT md5sum, length, path FROM torrust_torrent_files WHERE torrent_id = ?") + .bind(torrent_id) + .fetch_all(&self.pool) + .await + .map_err(|_| DatabaseError::TorrentNotFound)?; - let torrent_files: Vec = db_torrent_files.into_iter().map(|tf| { - TorrentFile { + let torrent_files: Vec = db_torrent_files + .into_iter() + .map(|tf| TorrentFile { path: tf.path.unwrap_or("".to_string()).split('/').map(|v| v.to_string()).collect(), length: tf.length, - md5sum: tf.md5sum - } - }).collect(); + md5sum: tf.md5sum, + }) + .collect(); Ok(torrent_files) } async fn get_torrent_announce_urls_from_id(&self, torrent_id: i64) -> Result>, DatabaseError> { - query_as::<_, DbTorrentAnnounceUrl>( - "SELECT tracker_url FROM torrust_torrent_announce_urls WHERE torrent_id = ?" - ) + query_as::<_, DbTorrentAnnounceUrl>("SELECT tracker_url FROM torrust_torrent_announce_urls WHERE torrent_id = ?") .bind(torrent_id) .fetch_all(&self.pool) .await @@ -601,12 +620,14 @@ impl Database for MysqlDatabase { DatabaseError::Error } } - _ => DatabaseError::Error + _ => DatabaseError::Error, }) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::TorrentNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::TorrentNotFound) + } }) } @@ -617,14 +638,22 @@ impl Database for MysqlDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::TorrentNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::TorrentNotFound) + } }) } - async fn update_tracker_info(&self, torrent_id: i64, tracker_url: &str, seeders: i64, leechers: i64) -> Result<(), DatabaseError> { + async fn update_tracker_info( + &self, + torrent_id: i64, + tracker_url: &str, + seeders: i64, + leechers: i64, + ) -> Result<(), DatabaseError> { query("REPLACE INTO torrust_torrent_tracker_stats (torrent_id, tracker_url, seeders, leechers) VALUES (?, ?, ?, ?)") .bind(torrent_id) .bind(tracker_url) @@ -642,10 +671,12 @@ impl Database for MysqlDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::TorrentNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::TorrentNotFound) + } }) } diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 6b65eeb9..44e297a2 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -1,19 +1,19 @@ -use sqlx::{Acquire, query, query_as, SqlitePool}; -use sqlx::sqlite::SqlitePoolOptions; use async_trait::async_trait; -use chrono::{NaiveDateTime}; +use chrono::NaiveDateTime; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::{query, query_as, Acquire, SqlitePool}; -use crate::models::torrent::TorrentListing; -use crate::utils::time::current_time; -use crate::models::tracker_key::{TrackerKey}; use crate::databases::database::{Category, Database, DatabaseDriver, DatabaseError, Sorting, TorrentCompact}; -use crate::models::response::{TorrentsResponse}; +use crate::models::response::TorrentsResponse; +use crate::models::torrent::TorrentListing; use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrentInfo, Torrent, TorrentFile}; +use crate::models::tracker_key::TrackerKey; use crate::models::user::{User, UserAuthentication, UserCompact, UserProfile}; use crate::utils::hex::bytes_to_hex; +use crate::utils::time::current_time; pub struct SqliteDatabase { - pub pool: SqlitePool + pub pool: SqlitePool, } impl SqliteDatabase { @@ -28,9 +28,7 @@ impl SqliteDatabase { .await .expect("Could not run database migrations."); - Self { - pool: db - } + Self { pool: db } } } @@ -41,23 +39,19 @@ impl Database for SqliteDatabase { } async fn insert_user_and_get_id(&self, username: &str, email: &str, password_hash: &str) -> Result { - // open pool connection - let mut conn = self.pool.acquire() - .await - .map_err(|_| DatabaseError::Error)?; + let mut conn = self.pool.acquire().await.map_err(|_| DatabaseError::Error)?; // start db transaction - let mut tx = conn.begin() - .await - .map_err(|_| DatabaseError::Error)?; + let mut tx = conn.begin().await.map_err(|_| DatabaseError::Error)?; // create the user account and get the user id - let user_id = query("INSERT INTO torrust_users (date_registered) VALUES (strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')))") - .execute(&mut tx) - .await - .map(|v| v.last_insert_rowid()) - .map_err(|_| DatabaseError::Error)?; + let user_id = + query("INSERT INTO torrust_users (date_registered) VALUES (strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')))") + .execute(&mut tx) + .await + .map(|v| v.last_insert_rowid()) + .map_err(|_| DatabaseError::Error)?; // add password hash for account let insert_user_auth_result = query("INSERT INTO torrust_user_authentication (user_id, password_hash) VALUES (?, ?)") @@ -70,7 +64,7 @@ impl Database for SqliteDatabase { // rollback transaction on error if let Err(e) = insert_user_auth_result { let _ = tx.rollback().await; - return Err(e) + return Err(e); } // add account profile details @@ -181,10 +175,12 @@ impl Database for SqliteDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::UserNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::UserNotFound) + } }) } @@ -216,10 +212,12 @@ impl Database for SqliteDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::UserNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::UserNotFound) + } }) } @@ -236,8 +234,8 @@ impl Database for SqliteDatabase { } else { DatabaseError::Error } - }, - _ => DatabaseError::Error + } + _ => DatabaseError::Error, }) } @@ -270,18 +268,27 @@ impl Database for SqliteDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::CategoryNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::CategoryNotFound) + } }) } // TODO: refactor this - async fn get_torrents_search_sorted_paginated(&self, search: &Option, categories: &Option>, sort: &Sorting, offset: u64, page_size: u8) -> Result { + async fn get_torrents_search_sorted_paginated( + &self, + search: &Option, + categories: &Option>, + sort: &Sorting, + offset: u64, + page_size: u8, + ) -> Result { let title = match search { None => "%".to_string(), - Some(v) => format!("%{}%", v) + Some(v) => format!("%{}%", v), }; let sort_query: String = match sort { @@ -304,13 +311,18 @@ impl Database for SqliteDatabase { // don't take user input in the db query if let Ok(sanitized_category) = self.get_category_from_name(category).await { let mut str = format!("tc.name = '{}'", sanitized_category.name); - if i > 0 { str = format!(" OR {}", str); } + if i > 0 { + str = format!(" OR {}", str); + } category_filters.push_str(&str); i += 1; } } if category_filters.len() > 0 { - format!("INNER JOIN torrust_categories tc ON tt.category_id = tc.category_id AND ({}) ", category_filters) + format!( + "INNER JOIN torrust_categories tc ON tt.category_id = tc.category_id AND ({}) ", + category_filters + ) } else { String::new() } @@ -354,22 +366,25 @@ impl Database for SqliteDatabase { Ok(TorrentsResponse { total: count as u32, - results: res + results: res, }) } - async fn insert_torrent_and_get_id(&self, torrent: &Torrent, uploader_id: i64, category_id: i64, title: &str, description: &str) -> Result { + async fn insert_torrent_and_get_id( + &self, + torrent: &Torrent, + uploader_id: i64, + category_id: i64, + title: &str, + description: &str, + ) -> Result { let info_hash = torrent.info_hash(); // open pool connection - let mut conn = self.pool.acquire() - .await - .map_err(|_| DatabaseError::Error)?; + let mut conn = self.pool.acquire().await.map_err(|_| DatabaseError::Error)?; // start db transaction - let mut tx = conn.begin() - .await - .map_err(|_| DatabaseError::Error)?; + let mut tx = conn.begin().await.map_err(|_| DatabaseError::Error)?; // torrent file can only hold a pieces key or a root hash key: http://www.bittorrent.org/beps/bep_0030.html let (pieces, root_hash): (String, bool) = if let Some(pieces) = &torrent.info.pieces { @@ -439,7 +454,7 @@ impl Database for SqliteDatabase { // rollback transaction on error if let Err(e) = insert_torrent_files_result { let _ = tx.rollback().await; - return Err(e) + return Err(e); } let insert_torrent_announce_urls_result: Result<(), DatabaseError> = if let Some(announce_urls) = &torrent.announce_list { @@ -472,27 +487,28 @@ impl Database for SqliteDatabase { // rollback transaction on error if let Err(e) = insert_torrent_announce_urls_result { let _ = tx.rollback().await; - return Err(e) + return Err(e); } - let insert_torrent_info_result = query(r#"INSERT INTO torrust_torrent_info (torrent_id, title, description) VALUES (?, ?, NULLIF(?, ""))"#) - .bind(torrent_id) - .bind(title) - .bind(description) - .execute(&mut tx) - .await - .map_err(|e| match e { - sqlx::Error::Database(err) => { - if err.message().contains("info_hash") { - DatabaseError::TorrentAlreadyExists - } else if err.message().contains("title") { - DatabaseError::TorrentTitleAlreadyExists - } else { - DatabaseError::Error + let insert_torrent_info_result = + query(r#"INSERT INTO torrust_torrent_info (torrent_id, title, description) VALUES (?, ?, NULLIF(?, ""))"#) + .bind(torrent_id) + .bind(title) + .bind(description) + .execute(&mut tx) + .await + .map_err(|e| match e { + sqlx::Error::Database(err) => { + if err.message().contains("info_hash") { + DatabaseError::TorrentAlreadyExists + } else if err.message().contains("title") { + DatabaseError::TorrentTitleAlreadyExists + } else { + DatabaseError::Error + } } - } - _ => DatabaseError::Error - }); + _ => DatabaseError::Error, + }); // commit or rollback transaction and return user_id on success match insert_torrent_info_result { @@ -514,43 +530,45 @@ impl Database for SqliteDatabase { 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_db_info_files_and_announce_urls( + torrent_info, + torrent_files, + torrent_announce_urls, + )) } async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result { query_as::<_, DbTorrentInfo>( - "SELECT name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?" + "SELECT name, pieces, piece_length, private, root_hash FROM torrust_torrents WHERE torrent_id = ?", ) - .bind(torrent_id) - .fetch_one(&self.pool) - .await - .map_err(|_| DatabaseError::TorrentNotFound) + .bind(torrent_id) + .fetch_one(&self.pool) + .await + .map_err(|_| DatabaseError::TorrentNotFound) } async fn get_torrent_files_from_id(&self, torrent_id: i64) -> Result, DatabaseError> { - let db_torrent_files = query_as::<_, DbTorrentFile>( - "SELECT md5sum, length, path FROM torrust_torrent_files WHERE torrent_id = ?" - ) - .bind(torrent_id) - .fetch_all(&self.pool) - .await - .map_err(|_| DatabaseError::TorrentNotFound)?; + let db_torrent_files = + query_as::<_, DbTorrentFile>("SELECT md5sum, length, path FROM torrust_torrent_files WHERE torrent_id = ?") + .bind(torrent_id) + .fetch_all(&self.pool) + .await + .map_err(|_| DatabaseError::TorrentNotFound)?; - let torrent_files: Vec = db_torrent_files.into_iter().map(|tf| { - TorrentFile { + let torrent_files: Vec = db_torrent_files + .into_iter() + .map(|tf| TorrentFile { path: tf.path.unwrap_or("".to_string()).split('/').map(|v| v.to_string()).collect(), length: tf.length, - md5sum: tf.md5sum - } - }).collect(); + md5sum: tf.md5sum, + }) + .collect(); Ok(torrent_files) } async fn get_torrent_announce_urls_from_id(&self, torrent_id: i64) -> Result>, DatabaseError> { - query_as::<_, DbTorrentAnnounceUrl>( - "SELECT tracker_url FROM torrust_torrent_announce_urls WHERE torrent_id = ?" - ) + query_as::<_, DbTorrentAnnounceUrl>("SELECT tracker_url FROM torrust_torrent_announce_urls WHERE torrent_id = ?") .bind(torrent_id) .fetch_all(&self.pool) .await @@ -597,12 +615,14 @@ impl Database for SqliteDatabase { DatabaseError::Error } } - _ => DatabaseError::Error + _ => DatabaseError::Error, }) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::TorrentNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::TorrentNotFound) + } }) } @@ -613,14 +633,22 @@ impl Database for SqliteDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::TorrentNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::TorrentNotFound) + } }) } - async fn update_tracker_info(&self, torrent_id: i64, tracker_url: &str, seeders: i64, leechers: i64) -> Result<(), DatabaseError> { + async fn update_tracker_info( + &self, + torrent_id: i64, + tracker_url: &str, + seeders: i64, + leechers: i64, + ) -> Result<(), DatabaseError> { query("REPLACE INTO torrust_torrent_tracker_stats (torrent_id, tracker_url, seeders, leechers) VALUES ($1, $2, $3, $4)") .bind(torrent_id) .bind(tracker_url) @@ -638,10 +666,12 @@ impl Database for SqliteDatabase { .execute(&self.pool) .await .map_err(|_| DatabaseError::Error) - .and_then(|v| if v.rows_affected() > 0 { - Ok(()) - } else { - Err(DatabaseError::TorrentNotFound) + .and_then(|v| { + if v.rows_affected() > 0 { + Ok(()) + } else { + Err(DatabaseError::TorrentNotFound) + } }) } diff --git a/src/errors.rs b/src/errors.rs index 675ca05f..6f5ee0a2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,9 +1,11 @@ use std::borrow::Cow; -use derive_more::{Display, Error}; -use actix_web::{ResponseError, HttpResponse, HttpResponseBuilder}; +use std::error; + use actix_web::http::{header, StatusCode}; +use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError}; +use derive_more::{Display, Error}; use serde::{Deserialize, Serialize}; -use std::error; + use crate::databases::database::DatabaseError; pub type ServiceResult = Result; @@ -14,9 +16,7 @@ pub enum ServiceError { #[display(fmt = "internal server error")] InternalServerError, - #[display( - fmt = "This server is is closed for registration. Contact admin if this is unexpected" - )] + #[display(fmt = "This server is is closed for registration. Contact admin if this is unexpected")] ClosedForRegistration, #[display(fmt = "Email is required")] //405j @@ -174,19 +174,14 @@ impl ResponseError for ServiceError { ServiceError::CategoryExists => StatusCode::BAD_REQUEST, - _ => StatusCode::INTERNAL_SERVER_ERROR + _ => StatusCode::INTERNAL_SERVER_ERROR, } } fn error_response(&self) -> HttpResponse { HttpResponseBuilder::new(self.status_code()) .append_header((header::CONTENT_TYPE, "application/json; charset=UTF-8")) - .body( - serde_json::to_string(&ErrorToResponse { - error: self.to_string(), - }) - .unwrap(), - ) + .body(serde_json::to_string(&ErrorToResponse { error: self.to_string() }).unwrap()) .into() } } @@ -204,7 +199,7 @@ impl From for ServiceError { } } else { ServiceError::TorrentNotFound - } + }; } ServiceError::InternalServerError diff --git a/src/lib.rs b/src/lib.rs index a4f34c34..5a0100c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,27 @@ -pub mod routes; -pub mod models; -pub mod utils; +pub mod auth; +pub mod common; pub mod config; +pub mod databases; pub mod errors; -pub mod common; -pub mod auth; -pub mod tracker; pub mod mailer; -pub mod databases; +pub mod models; +pub mod routes; +pub mod tracker; +pub mod utils; trait AsCSV { fn as_csv(&self) -> Result>, ()> - where - T: std::str::FromStr; + where + T: std::str::FromStr; } impl AsCSV for Option - where - S: AsRef, +where + S: AsRef, { fn as_csv(&self) -> Result>, ()> - where - T: std::str::FromStr, + where + T: std::str::FromStr, { match self { Some(ref s) if !s.as_ref().trim().is_empty() => { diff --git a/src/mailer.rs b/src/mailer.rs index e71f213c..94dd8325 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -1,17 +1,19 @@ -use crate::config::Configuration; use std::sync::Arc; -use crate::errors::ServiceError; -use serde::{Serialize, Deserialize}; -use lettre::{AsyncSmtpTransport, Tokio1Executor, Message, AsyncTransport}; -use lettre::transport::smtp::authentication::{Credentials, Mechanism}; + +use jsonwebtoken::{encode, EncodingKey, Header}; use lettre::message::{MessageBuilder, MultiPart, SinglePart}; -use jsonwebtoken::{encode, Header, EncodingKey}; +use lettre::transport::smtp::authentication::{Credentials, Mechanism}; +use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; use sailfish::TemplateOnce; +use serde::{Deserialize, Serialize}; + +use crate::config::Configuration; +use crate::errors::ServiceError; use crate::utils::time::current_time; pub struct MailerService { cfg: Arc, - mailer: Arc + mailer: Arc, } #[derive(Debug, Serialize, Deserialize)] @@ -28,15 +30,11 @@ struct VerifyTemplate { verification_url: String, } - impl MailerService { pub async fn new(cfg: Arc) -> MailerService { let mailer = Arc::new(Self::get_mailer(&cfg).await); - Self { - cfg, - mailer, - } + Self { cfg, mailer } } async fn get_mailer(cfg: &Configuration) -> Mailer { @@ -47,15 +45,17 @@ impl MailerService { AsyncSmtpTransport::::builder_dangerous(&settings.mail.server) .port(settings.mail.port) .credentials(creds) - .authentication(vec![ - Mechanism::Login, - Mechanism::Xoauth2, - Mechanism::Plain, - ]) + .authentication(vec![Mechanism::Login, Mechanism::Xoauth2, Mechanism::Plain]) .build() } - pub async fn send_verification_mail(&self, to: &str, username: &str, user_id: i64, base_url: &str) -> Result<(), ServiceError> { + pub async fn send_verification_mail( + &self, + to: &str, + username: &str, + user_id: i64, + base_url: &str, + ) -> Result<(), ServiceError> { let builder = self.get_builder(to).await; let verification_url = self.get_verification_url(user_id, base_url).await; @@ -68,8 +68,7 @@ impl MailerService { If this account wasn't made by you, you can ignore this email. "#, - username, - verification_url + username, verification_url ); let ctx = VerifyTemplate { @@ -84,13 +83,13 @@ impl MailerService { .singlepart( SinglePart::builder() .header(lettre::message::header::ContentType::TEXT_PLAIN) - .body(mail_body) + .body(mail_body), ) .singlepart( SinglePart::builder() .header(lettre::message::header::ContentType::TEXT_HTML) - .body(ctx.render_once().unwrap()) - ) + .body(ctx.render_once().unwrap()), + ), ) .unwrap(); @@ -99,7 +98,7 @@ impl MailerService { Err(e) => { eprintln!("Failed to send email: {}", e); Err(ServiceError::FailedToSendVerificationEmail) - }, + } } } @@ -122,15 +121,10 @@ impl MailerService { let claims = VerifyClaims { iss: String::from("email-verification"), sub: user_id, - exp: current_time() + 315_569_260 // 10 years from now + exp: current_time() + 315_569_260, // 10 years from now }; - let token = encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(key), - ) - .unwrap(); + let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(key)).unwrap(); let mut base_url = base_url.clone(); if let Some(cfg_base_url) = &settings.net.base_url { diff --git a/src/main.rs b/src/main.rs index 7fc5d0f6..ce7bf581 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ use std::sync::Arc; -use actix_web::{App, HttpServer, middleware, web}; + use actix_cors::Cors; -use torrust_index_backend::{routes}; -use torrust_index_backend::config::{Configuration}; -use torrust_index_backend::common::AppData; +use actix_web::{middleware, web, App, HttpServer}; use torrust_index_backend::auth::AuthorizationService; +use torrust_index_backend::common::AppData; +use torrust_index_backend::config::Configuration; use torrust_index_backend::databases::database::connect_database; -use torrust_index_backend::tracker::TrackerService; use torrust_index_backend::mailer::MailerService; +use torrust_index_backend::routes; +use torrust_index_backend::tracker::TrackerService; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -20,23 +21,22 @@ async fn main() -> std::io::Result<()> { let settings = cfg.settings.read().await; - let database = Arc::new(connect_database(&settings.database.connect_url) + let database = Arc::new( + connect_database(&settings.database.connect_url) .await - .expect("Database error.") + .expect("Database error."), ); let auth = Arc::new(AuthorizationService::new(cfg.clone(), database.clone())); let tracker_service = Arc::new(TrackerService::new(cfg.clone(), database.clone())); let mailer_service = Arc::new(MailerService::new(cfg.clone()).await); - let app_data = Arc::new( - AppData::new( - cfg.clone(), - database.clone(), - auth.clone(), - tracker_service.clone(), - mailer_service.clone(), - ) - ); + let app_data = Arc::new(AppData::new( + cfg.clone(), + database.clone(), + auth.clone(), + tracker_service.clone(), + mailer_service.clone(), + )); let interval = settings.database.torrent_info_update_interval; let weak_tracker_service = Arc::downgrade(&tracker_service); @@ -69,7 +69,7 @@ async fn main() -> std::io::Result<()> { .wrap(middleware::Logger::default()) .configure(routes::init_routes) }) - .bind(("0.0.0.0", port))? - .run() - .await + .bind(("0.0.0.0", port))? + .run() + .await } diff --git a/src/models/mod.rs b/src/models/mod.rs index cb31379a..acfccf77 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,5 @@ -pub mod user; +pub mod response; pub mod torrent; pub mod torrent_file; -pub mod response; pub mod tracker_key; +pub mod user; diff --git a/src/models/response.rs b/src/models/response.rs index ac34a34b..059e1c3b 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -1,20 +1,21 @@ use serde::{Deserialize, Serialize}; + use crate::databases::database::Category; use crate::models::torrent::TorrentListing; use crate::models::torrent_file::TorrentFile; pub enum OkResponses { - TokenResponse(TokenResponse) + TokenResponse(TokenResponse), } #[derive(Serialize, Deserialize, Debug)] pub struct OkResponse { - pub data: T + pub data: T, } #[derive(Serialize, Deserialize, Debug)] pub struct ErrorResponse { - pub errors: Vec + pub errors: Vec, } #[derive(Serialize, Deserialize, Debug)] @@ -54,7 +55,11 @@ impl TorrentResponse { info_hash: torrent_listing.info_hash, title: torrent_listing.title, description: torrent_listing.description, - category: Category { category_id: 0, name: "".to_string(), num_torrents: 0 }, + category: Category { + category_id: 0, + name: "".to_string(), + num_torrents: 0, + }, upload_date: torrent_listing.date_uploaded, file_size: torrent_listing.file_size, seeders: torrent_listing.seeders, diff --git a/src/models/torrent.rs b/src/models/torrent.rs index 0395b985..4adab162 100644 --- a/src/models/torrent.rs +++ b/src/models/torrent.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; + use crate::models::torrent_file::Torrent; use crate::routes::torrent::CreateTorrent; diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index c659b67f..98cd4c00 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -1,8 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::config::Configuration; use serde_bencode::ser; use serde_bytes::ByteBuf; use sha1::{Digest, Sha1}; + +use crate::config::Configuration; use crate::utils::hex::{bytes_to_hex, hex_to_bytes}; #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] @@ -21,7 +22,7 @@ pub struct TorrentInfo { pub name: String, #[serde(default)] pub pieces: Option, - #[serde(rename="piece length")] + #[serde(rename = "piece length")] pub piece_length: i64, #[serde(default)] pub md5sum: Option, @@ -34,7 +35,7 @@ pub struct TorrentInfo { #[serde(default)] pub path: Option>, #[serde(default)] - #[serde(rename="root hash")] + #[serde(rename = "root hash")] pub root_hash: Option, } @@ -50,20 +51,24 @@ pub struct Torrent { #[serde(default)] pub httpseeds: Option>, #[serde(default)] - #[serde(rename="announce-list")] + #[serde(rename = "announce-list")] pub announce_list: Option>>, #[serde(default)] - #[serde(rename="creation date")] + #[serde(rename = "creation date")] pub creation_date: Option, - #[serde(rename="comment")] + #[serde(rename = "comment")] pub comment: Option, #[serde(default)] - #[serde(rename="created by")] + #[serde(rename = "created by")] pub created_by: Option, } impl Torrent { - pub fn from_db_info_files_and_announce_urls(torrent_info: DbTorrentInfo, torrent_files: Vec, torrent_announce_urls: Vec>) -> Self { + pub fn from_db_info_files_and_announce_urls( + torrent_info: DbTorrentInfo, + torrent_files: Vec, + torrent_announce_urls: Vec>, + ) -> Self { let private = if let Some(private_i64) = torrent_info.private { // must fit in a byte let private = if (0..256).contains(&private_i64) { private_i64 } else { 0 }; @@ -82,7 +87,7 @@ impl Torrent { files: None, private, path: None, - root_hash: None + root_hash: None, }; // a torrent file has a root hash or a pieces key, but not both. @@ -122,7 +127,7 @@ impl Torrent { announce_list: Some(torrent_announce_urls), creation_date: None, comment: None, - created_by: None + created_by: None, } } @@ -155,7 +160,7 @@ impl Torrent { pub fn file_size(&self) -> i64 { if self.info.length.is_some() { - return self.info.length.unwrap() + return self.info.length.unwrap(); } else { match &self.info.files { None => 0, diff --git a/src/models/tracker_key.rs b/src/models/tracker_key.rs index d130cd6d..71bf51c3 100644 --- a/src/models/tracker_key.rs +++ b/src/models/tracker_key.rs @@ -1,4 +1,4 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use sqlx::FromRow; #[derive(Debug, Serialize, Deserialize, FromRow)] diff --git a/src/models/user.rs b/src/models/user.rs index 3885aaa2..53d0a4e0 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,4 +1,4 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct User { diff --git a/src/routes/about.rs b/src/routes/about.rs index 9f23c4d1..2a632c85 100644 --- a/src/routes/about.rs +++ b/src/routes/about.rs @@ -1,17 +1,13 @@ -use actix_web::{Responder, web, HttpResponse}; use actix_web::http::StatusCode; +use actix_web::{web, HttpResponse, Responder}; use crate::errors::ServiceResult; pub fn init_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/about") - .service(web::resource("") - .route(web::get().to(get_about)) - ) - .service(web::resource("/license") - .route(web::get().to(get_license)) - ) + .service(web::resource("").route(web::get().to(get_about))) + .service(web::resource("/license").route(web::get().to(get_license))), ); } @@ -36,8 +32,7 @@ const ABOUT: &str = r#" pub async fn get_about() -> ServiceResult { Ok(HttpResponse::build(StatusCode::OK) .content_type("text/html; charset=utf-8") - .body(ABOUT) - ) + .body(ABOUT)) } const LICENSE: &str = r#" @@ -71,6 +66,5 @@ const LICENSE: &str = r#" pub async fn get_license() -> ServiceResult { Ok(HttpResponse::build(StatusCode::OK) .content_type("text/html; charset=utf-8") - .body(LICENSE) - ) + .body(LICENSE)) } diff --git a/src/routes/category.rs b/src/routes/category.rs index 34db369d..8bcd0348 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -1,33 +1,31 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, web}; -use serde::{Serialize, Deserialize}; +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::response::OkResponse; pub fn init_routes(cfg: &mut web::ServiceConfig) { cfg.service( - web::scope("/category") - .service(web::resource("") + web::scope("/category").service( + web::resource("") .route(web::get().to(get_categories)) .route(web::post().to(add_category)) - .route(web::delete().to(delete_category)) - ) + .route(web::delete().to(delete_category)), + ), ); } pub async fn get_categories(app_data: WebAppData) -> ServiceResult { let categories = app_data.database.get_categories().await?; - Ok(HttpResponse::Ok().json(OkResponse { - data: categories - })) + Ok(HttpResponse::Ok().json(OkResponse { data: categories })) } #[derive(Debug, Serialize, Deserialize)] pub struct Category { pub name: String, - pub icon: Option + pub icon: Option, } pub async fn add_category(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { @@ -35,25 +33,33 @@ pub async fn add_category(req: HttpRequest, payload: web::Json, app_da let user = app_data.auth.get_user_compact_from_request(&req).await?; // check if user is administrator - if !user.administrator { return Err(ServiceError::Unauthorized) } + if !user.administrator { + return Err(ServiceError::Unauthorized); + } let _ = app_data.database.insert_category_and_get_id(&payload.name).await?; Ok(HttpResponse::Ok().json(OkResponse { - data: payload.name.clone() + data: payload.name.clone(), })) } -pub async fn delete_category(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { +pub async fn delete_category( + req: HttpRequest, + payload: web::Json, + app_data: WebAppData, +) -> ServiceResult { // check for user let user = app_data.auth.get_user_compact_from_request(&req).await?; // check if user is administrator - if !user.administrator { return Err(ServiceError::Unauthorized) } + if !user.administrator { + return Err(ServiceError::Unauthorized); + } let _ = app_data.database.delete_category(&payload.name).await?; Ok(HttpResponse::Ok().json(OkResponse { - data: payload.name.clone() + data: payload.name.clone(), })) } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index dbbcc31a..5761390a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,11 +1,11 @@ use actix_web::web; -pub mod user; -pub mod torrent; -pub mod category; -pub mod settings; pub mod about; +pub mod category; pub mod root; +pub mod settings; +pub mod torrent; +pub mod user; pub fn init_routes(cfg: &mut web::ServiceConfig) { user::init_routes(cfg); diff --git a/src/routes/root.rs b/src/routes/root.rs index 9ae00e4e..69f11fd6 100644 --- a/src/routes/root.rs +++ b/src/routes/root.rs @@ -1,11 +1,7 @@ use actix_web::web; + use crate::routes::about; pub fn init_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/") - .service(web::resource("") - .route(web::get().to(about::get_about)) - ) - ); + cfg.service(web::scope("/").service(web::resource("").route(web::get().to(about::get_about)))); } diff --git a/src/routes/settings.rs b/src/routes/settings.rs index fc48a7e1..6ba5d2aa 100644 --- a/src/routes/settings.rs +++ b/src/routes/settings.rs @@ -1,22 +1,20 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use actix_web::{web, HttpRequest, HttpResponse, Responder}; + use crate::common::WebAppData; -use crate::config::{TorrustConfig}; +use crate::config::TorrustConfig; use crate::errors::{ServiceError, ServiceResult}; -use crate::models::response::{OkResponse}; +use crate::models::response::OkResponse; pub fn init_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/settings") - .service(web::resource("") - .route(web::get().to(get_settings)) - .route(web::post().to(update_settings)) - ) - .service(web::resource("/name") - .route(web::get().to(get_site_name)) - ) - .service(web::resource("/public") - .route(web::get().to(get_public_settings)) + .service( + web::resource("") + .route(web::get().to(get_settings)) + .route(web::post().to(update_settings)), ) + .service(web::resource("/name").route(web::get().to(get_site_name))) + .service(web::resource("/public").route(web::get().to(get_public_settings))), ); } @@ -25,43 +23,45 @@ pub async fn get_settings(req: HttpRequest, app_data: WebAppData) -> ServiceResu let user = app_data.auth.get_user_compact_from_request(&req).await?; // check if user is administrator - if !user.administrator { return Err(ServiceError::Unauthorized) } + if !user.administrator { + return Err(ServiceError::Unauthorized); + } let settings = app_data.cfg.settings.read().await; - Ok(HttpResponse::Ok().json(OkResponse { - data: &*settings - })) + Ok(HttpResponse::Ok().json(OkResponse { data: &*settings })) } pub async fn get_public_settings(app_data: WebAppData) -> ServiceResult { let public_settings = app_data.cfg.get_public().await; - Ok(HttpResponse::Ok().json(OkResponse { - data: public_settings - })) + Ok(HttpResponse::Ok().json(OkResponse { data: public_settings })) } pub async fn get_site_name(app_data: WebAppData) -> ServiceResult { let settings = app_data.cfg.settings.read().await; Ok(HttpResponse::Ok().json(OkResponse { - data: &settings.website.name + data: &settings.website.name, })) } -pub async fn update_settings(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { +pub async fn update_settings( + req: HttpRequest, + payload: web::Json, + app_data: WebAppData, +) -> ServiceResult { // check for user let user = app_data.auth.get_user_compact_from_request(&req).await?; // check if user is administrator - if !user.administrator { return Err(ServiceError::Unauthorized) } + if !user.administrator { + return Err(ServiceError::Unauthorized); + } let _ = app_data.cfg.update_settings(payload.into_inner()).await; let settings = app_data.cfg.settings.read().await; - Ok(HttpResponse::Ok().json(OkResponse { - data: &*settings - })) + Ok(HttpResponse::Ok().json(OkResponse { data: &*settings })) } diff --git a/src/routes/torrent.rs b/src/routes/torrent.rs index ea43b3d9..5d87e8b2 100644 --- a/src/routes/torrent.rs +++ b/src/routes/torrent.rs @@ -1,37 +1,33 @@ +use std::io::{Cursor, Write}; + use actix_multipart::Multipart; -use actix_web::{HttpRequest, HttpResponse, Responder, web}; -use actix_web::web::{Query}; +use actix_web::web::Query; +use actix_web::{web, HttpRequest, HttpResponse, Responder}; use futures::{StreamExt, TryStreamExt}; -use serde::{Deserialize}; -use std::io::Cursor; -use std::io::{Write}; -use sqlx::{FromRow}; +use serde::Deserialize; +use sqlx::FromRow; -use crate::AsCSV; +use crate::common::WebAppData; use crate::databases::database::Sorting; use crate::errors::{ServiceError, ServiceResult}; use crate::models::response::{NewTorrentResponse, OkResponse, TorrentResponse}; use crate::models::torrent::TorrentRequest; use crate::utils::parse_torrent; -use crate::common::{WebAppData}; +use crate::AsCSV; pub fn init_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/torrent") - .service(web::resource("/upload") - .route(web::post().to(upload_torrent))) - .service(web::resource("/download/{id}") - .route(web::get().to(download_torrent))) - .service(web::resource("/{id}") - .route(web::get().to(get_torrent)) - .route(web::put().to(update_torrent)) - .route(web::delete().to(delete_torrent))) - ); - cfg.service( - web::scope("/torrents") - .service(web::resource("") - .route(web::get().to(get_torrents))) + .service(web::resource("/upload").route(web::post().to(upload_torrent))) + .service(web::resource("/download/{id}").route(web::get().to(download_torrent))) + .service( + web::resource("/{id}") + .route(web::get().to(get_torrent)) + .route(web::put().to(update_torrent)) + .route(web::delete().to(delete_torrent)), + ), ); + cfg.service(web::scope("/torrents").service(web::resource("").route(web::get().to(get_torrents)))); } #[derive(FromRow)] @@ -47,9 +43,9 @@ pub struct CreateTorrent { } impl CreateTorrent { - pub fn verify(&self) -> Result<(), ServiceError>{ + pub fn verify(&self) -> Result<(), ServiceError> { if !self.title.is_empty() && !self.category.is_empty() { - return Ok(()) + return Ok(()); } Err(ServiceError::BadRequest) @@ -69,7 +65,7 @@ pub struct TorrentSearch { #[derive(Debug, Deserialize)] pub struct TorrentUpdate { title: Option, - description: Option + description: Option, } pub async fn upload_torrent(req: HttpRequest, payload: Multipart, app_data: WebAppData) -> ServiceResult { @@ -82,20 +78,30 @@ pub async fn upload_torrent(req: HttpRequest, payload: Multipart, app_data: WebA torrent_request.torrent.set_torrust_config(&app_data.cfg).await; // get the correct category name from database - let category = app_data.database.get_category_from_name(&torrent_request.fields.category).await + let category = app_data + .database + .get_category_from_name(&torrent_request.fields.category) + .await .map_err(|_| ServiceError::InvalidCategory)?; // insert entire torrent in database - let torrent_id = app_data.database.insert_torrent_and_get_id( - &torrent_request.torrent, - user.user_id, - category.category_id, - &torrent_request.fields.title, - &torrent_request.fields.description - ).await?; + let torrent_id = app_data + .database + .insert_torrent_and_get_id( + &torrent_request.torrent, + user.user_id, + category.category_id, + &torrent_request.fields.title, + &torrent_request.fields.description, + ) + .await?; // whitelist info hash on tracker - if let Err(e) = app_data.tracker.whitelist_info_hash(torrent_request.torrent.info_hash()).await { + if let Err(e) = app_data + .tracker + .whitelist_info_hash(torrent_request.torrent.info_hash()) + .await + { // if the torrent can't be whitelisted somehow, remove the torrent from database let _ = app_data.database.delete_torrent(torrent_id).await; return Err(e); @@ -103,9 +109,7 @@ pub async fn upload_torrent(req: HttpRequest, payload: Multipart, app_data: WebA // respond with the newly uploaded torrent id Ok(HttpResponse::Ok().json(OkResponse { - data: NewTorrentResponse { - torrent_id - } + data: NewTorrentResponse { torrent_id }, })) } @@ -126,7 +130,11 @@ pub async fn download_torrent(req: HttpRequest, app_data: WebAppData) -> Service // add personal tracker url or default tracker url match user { Ok(user) => { - let personal_announce_url = app_data.tracker.get_personal_announce_url(user.user_id).await.unwrap_or(tracker_url); + let personal_announce_url = app_data + .tracker + .get_personal_announce_url(user.user_id) + .await + .unwrap_or(tracker_url); torrent.announce = Some(personal_announce_url.clone()); if let Some(list) = &mut torrent.announce_list { let vec = vec![personal_announce_url]; @@ -140,10 +148,7 @@ pub async fn download_torrent(req: HttpRequest, app_data: WebAppData) -> Service let buffer = parse_torrent::encode_torrent(&torrent).map_err(|_| ServiceError::InternalServerError)?; - Ok(HttpResponse::Ok() - .content_type("application/x-bittorrent") - .body(buffer) - ) + Ok(HttpResponse::Ok().content_type("application/x-bittorrent").body(buffer)) } pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResult { @@ -171,10 +176,15 @@ pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResul if torrent_response.files.len() == 1 { let torrent_info = app_data.database.get_torrent_info_from_id(torrent_id).await?; - torrent_response.files.iter_mut().for_each(|v| v.path = vec![torrent_info.name.to_string()]); + torrent_response + .files + .iter_mut() + .for_each(|v| v.path = vec![torrent_info.name.to_string()]); } - torrent_response.trackers = app_data.database.get_torrent_announce_urls_from_id(torrent_id) + torrent_response.trackers = app_data + .database + .get_torrent_announce_urls_from_id(torrent_id) .await .map(|v| v.into_iter().flatten().collect())?; @@ -182,17 +192,25 @@ pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResul match user { Ok(user) => { // if no user owned tracker key can be found, use default tracker url - let personal_announce_url = app_data.tracker.get_personal_announce_url(user.user_id).await.unwrap_or(tracker_url); + let personal_announce_url = app_data + .tracker + .get_personal_announce_url(user.user_id) + .await + .unwrap_or(tracker_url); // add personal tracker url to front of vec torrent_response.trackers.insert(0, personal_announce_url); - }, + } Err(_) => { torrent_response.trackers.insert(0, tracker_url); } } // add magnet link - let mut magnet = format!("magnet:?xt=urn:btih:{}&dn={}", torrent_response.info_hash, urlencoding::encode(&torrent_response.title)); + let mut magnet = format!( + "magnet:?xt=urn:btih:{}&dn={}", + torrent_response.info_hash, + urlencoding::encode(&torrent_response.title) + ); // add trackers from torrent file to magnet link for tracker in &torrent_response.trackers { @@ -202,17 +220,23 @@ pub async fn get_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResul torrent_response.magnet_link = magnet; // get realtime seeders and leechers - if let Ok(torrent_info) = app_data.tracker.get_torrent_info(torrent_response.torrent_id, &torrent_response.info_hash).await { + if let Ok(torrent_info) = app_data + .tracker + .get_torrent_info(torrent_response.torrent_id, &torrent_response.info_hash) + .await + { torrent_response.seeders = torrent_info.seeders; torrent_response.leechers = torrent_info.leechers; } - Ok(HttpResponse::Ok().json(OkResponse { - data: torrent_response - })) + Ok(HttpResponse::Ok().json(OkResponse { data: torrent_response })) } -pub async fn update_torrent(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { +pub async fn update_torrent( + req: HttpRequest, + payload: web::Json, + app_data: WebAppData, +) -> ServiceResult { let user = app_data.auth.get_user_compact_from_request(&req).await?; let torrent_id = get_torrent_id_from_request(&req)?; @@ -220,7 +244,9 @@ pub async fn update_torrent(req: HttpRequest, payload: web::Json, let torrent_listing = app_data.database.get_torrent_listing_from_id(torrent_id).await?; // check if user is owner or administrator - if torrent_listing.uploader != user.username && !user.administrator { return Err(ServiceError::Unauthorized) } + if torrent_listing.uploader != user.username && !user.administrator { + return Err(ServiceError::Unauthorized); + } // update torrent title if let Some(title) = &payload.title { @@ -236,16 +262,16 @@ pub async fn update_torrent(req: HttpRequest, payload: web::Json, let torrent_response = TorrentResponse::from_listing(torrent_listing); - Ok(HttpResponse::Ok().json(OkResponse { - data: torrent_response - })) + Ok(HttpResponse::Ok().json(OkResponse { data: torrent_response })) } pub async fn delete_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceResult { let user = app_data.auth.get_user_compact_from_request(&req).await?; // check if user is administrator - if !user.administrator { return Err(ServiceError::Unauthorized) } + if !user.administrator { + return Err(ServiceError::Unauthorized); + } let torrent_id = get_torrent_id_from_request(&req)?; @@ -255,12 +281,13 @@ pub async fn delete_torrent(req: HttpRequest, app_data: WebAppData) -> ServiceRe let _res = app_data.database.delete_torrent(torrent_id).await?; // remove info_hash from tracker whitelist - let _ = app_data.tracker.remove_info_hash_from_whitelist(torrent_listing.info_hash).await; + let _ = app_data + .tracker + .remove_info_hash_from_whitelist(torrent_listing.info_hash) + .await; Ok(HttpResponse::Ok().json(OkResponse { - data: NewTorrentResponse { - torrent_id - } + data: NewTorrentResponse { torrent_id }, })) } @@ -272,30 +299,29 @@ pub async fn get_torrents(params: Query, app_data: WebAppData) -> // make sure the min page size = 10 let page_size = match params.page_size.unwrap_or(30) { - 0 ..= 9 => 10, - v => v + 0..=9 => 10, + v => v, }; let offset = (page * page_size as u32) as u64; let categories = params.categories.as_csv::().unwrap_or(None); - let torrents_response = app_data.database.get_torrents_search_sorted_paginated(¶ms.search, &categories, &sort, offset, page_size as u8).await?; + let torrents_response = app_data + .database + .get_torrents_search_sorted_paginated(¶ms.search, &categories, &sort, offset, page_size as u8) + .await?; - Ok(HttpResponse::Ok().json(OkResponse { - data: torrents_response - })) + Ok(HttpResponse::Ok().json(OkResponse { data: torrents_response })) } fn get_torrent_id_from_request(req: &HttpRequest) -> Result { 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) - } - } + Some(torrent_id) => match torrent_id.parse() { + Err(_) => Err(ServiceError::BadRequest), + Ok(v) => Ok(v), + }, } } @@ -314,20 +340,22 @@ async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result { let data = field.next().await; - if data.is_none() { continue } + if data.is_none() { + continue; + } let wrapped_data = &data.unwrap().unwrap(); let parsed_data = std::str::from_utf8(&wrapped_data).unwrap(); match name { - "title" => { title = parsed_data.to_string() } - "description" => { description = parsed_data.to_string() } - "category" => { category = parsed_data.to_string() } + "title" => title = parsed_data.to_string(), + "description" => description = parsed_data.to_string(), + "category" => category = parsed_data.to_string(), _ => {} } } "torrent" => { if *field.content_type() != "application/x-bittorrent" { - return Err(ServiceError::InvalidFileType) + return Err(ServiceError::InvalidFileType); } while let Some(chunk) = field.next().await { @@ -354,11 +382,10 @@ async fn get_torrent_request_from_payload(mut payload: Multipart) -> Result, app_da match settings.auth.email_on_signup { EmailOnSignup::Required => { - if payload.email.is_none() { return Err(ServiceError::EmailMissing) } - } - EmailOnSignup::None => { - payload.email = None + if payload.email.is_none() { + return Err(ServiceError::EmailMissing); + } } + EmailOnSignup::None => payload.email = None, _ => {} } if let Some(email) = &payload.email { // check if email address is valid if !validate_email_address(email) { - return Err(ServiceError::EmailInvalid) + return Err(ServiceError::EmailInvalid); } } if payload.password != payload.confirm_password { - return Err(ServiceError::PasswordsDontMatch) + return Err(ServiceError::PasswordsDontMatch); } let password_length = payload.password.len(); if password_length <= settings.auth.min_password_length { - return Err(ServiceError::PasswordTooShort) + return Err(ServiceError::PasswordTooShort); } if password_length >= settings.auth.max_password_length { - return Err(ServiceError::PasswordTooLong) + return Err(ServiceError::PasswordTooLong); } let salt = SaltString::generate(&mut OsRng); @@ -94,12 +87,15 @@ pub async fn register(req: HttpRequest, mut payload: web::Json, app_da let password_hash = argon2.hash_password(payload.password.as_bytes(), &salt)?.to_string(); if payload.username.contains('@') { - return Err(ServiceError::UsernameInvalid) + return Err(ServiceError::UsernameInvalid); } let email = payload.email.as_ref().unwrap_or(&"".to_string()).to_string(); - let user_id = app_data.database.insert_user_and_get_id(&payload.username, &email, &password_hash).await?; + let user_id = app_data + .database + .insert_user_and_get_id(&payload.username, &email, &password_hash) + .await?; // if this is the first created account, give administrator rights if user_id == 1 { @@ -109,17 +105,19 @@ pub async fn register(req: HttpRequest, mut payload: web::Json, app_da let conn_info = req.connection_info(); if settings.mail.email_verification_enabled && payload.email.is_some() { - let mail_res = app_data.mailer.send_verification_mail( - payload.email.as_ref().unwrap(), - &payload.username, - user_id, - format!("{}://{}", conn_info.scheme(), conn_info.host()).as_str() - ) + let mail_res = app_data + .mailer + .send_verification_mail( + payload.email.as_ref().unwrap(), + &payload.username, + user_id, + format!("{}://{}", conn_info.scheme(), conn_info.host()).as_str(), + ) .await; if mail_res.is_err() { let _ = app_data.database.delete_user(user_id).await; - return Err(ServiceError::FailedToSendVerificationEmail) + return Err(ServiceError::FailedToSendVerificationEmail); } } @@ -128,12 +126,16 @@ pub async fn register(req: HttpRequest, mut payload: web::Json, app_da pub async fn login(payload: web::Json, app_data: WebAppData) -> ServiceResult { // get the user profile from database - let user_profile = app_data.database.get_user_profile_from_username(&payload.login) + let user_profile = app_data + .database + .get_user_profile_from_username(&payload.login) .await .map_err(|_| ServiceError::WrongPasswordOrUsername)?; // should not be able to fail if user_profile succeeded - let user_authentication = app_data.database.get_user_authentication_from_id(user_profile.user_id) + let user_authentication = app_data + .database + .get_user_authentication_from_id(user_profile.user_id) .await .map_err(|_| ServiceError::InternalServerError)?; @@ -141,15 +143,18 @@ pub async fn login(payload: web::Json, app_data: WebAppData) -> ServiceRe let parsed_hash = PasswordHash::new(&user_authentication.password_hash)?; // verify if the user supplied and the database supplied passwords match - if Argon2::default().verify_password(payload.password.as_bytes(), &parsed_hash).is_err() { - return Err(ServiceError::WrongPasswordOrUsername) + if Argon2::default() + .verify_password(payload.password.as_bytes(), &parsed_hash) + .is_err() + { + return Err(ServiceError::WrongPasswordOrUsername); } let settings = app_data.cfg.settings.read().await; // fail login if email verification is required and this email is not verified if settings.mail.email_verification_enabled && !user_profile.email_verified { - return Err(ServiceError::EmailNotVerified) + return Err(ServiceError::EmailNotVerified); } // drop read lock on settings @@ -160,13 +165,12 @@ pub async fn login(payload: web::Json, app_data: WebAppData) -> ServiceRe // sign jwt with compact user details as payload let token = app_data.auth.sign_jwt(user_compact.clone()).await; - Ok(HttpResponse::Ok().json(OkResponse { data: TokenResponse { token, username: user_compact.username, - admin: user_compact.administrator - } + admin: user_compact.administrator, + }, })) } @@ -175,7 +179,7 @@ pub async fn verify_token(payload: web::Json, app_data: WebAppData) -> Se let _claims = app_data.auth.verify_jwt(&payload.token).await?; Ok(HttpResponse::Ok().json(OkResponse { - data: format!("Token is valid.") + data: format!("Token is valid."), })) } @@ -190,15 +194,15 @@ pub async fn renew_token(payload: web::Json, app_data: WebAppData) -> Ser // renew token if it is valid for less than one week let token = match claims.exp - current_time() { x if x < ONE_WEEK_IN_SECONDS => app_data.auth.sign_jwt(user_compact.clone()).await, - _ => payload.token.clone() + _ => payload.token.clone(), }; Ok(HttpResponse::Ok().json(OkResponse { data: TokenResponse { token, username: user_compact.username, - admin: user_compact.administrator - } + admin: user_compact.administrator, + }, })) } @@ -213,18 +217,18 @@ pub async fn verify_email(req: HttpRequest, app_data: WebAppData) -> String { ) { Ok(token_data) => { if !token_data.claims.iss.eq("email-verification") { - return ServiceError::TokenInvalid.to_string() + return ServiceError::TokenInvalid.to_string(); } token_data.claims - }, - Err(_) => return ServiceError::TokenInvalid.to_string() + } + Err(_) => return ServiceError::TokenInvalid.to_string(), }; drop(settings); if app_data.database.verify_email(token_data.sub).await.is_err() { - return ServiceError::InternalServerError.to_string() + return ServiceError::InternalServerError.to_string(); }; String::from("Email verified, you can close this page.") @@ -235,20 +239,26 @@ pub async fn ban_user(req: HttpRequest, app_data: WebAppData) -> ServiceResult, - pub client: Option + pub client: Option, } pub struct TrackerService { cfg: Arc, - database: Arc> + database: Arc>, } impl TrackerService { pub fn new(cfg: Arc, database: Arc>) -> TrackerService { - TrackerService { - cfg, - database - } + TrackerService { cfg, database } } pub async fn whitelist_info_hash(&self, info_hash: String) -> Result<(), ServiceError> { let settings = self.cfg.settings.read().await; - let request_url = format!("{}/api/whitelist/{}?token={}", settings.tracker.api_url, info_hash, settings.tracker.token); + let request_url = format!( + "{}/api/whitelist/{}?token={}", + settings.tracker.api_url, info_hash, settings.tracker.token + ); drop(settings); let client = reqwest::Client::new(); - let response = client.post(request_url).send().await.map_err(|_| ServiceError::TrackerOffline)?; + let response = client + .post(request_url) + .send() + .await + .map_err(|_| ServiceError::TrackerOffline)?; if response.status().is_success() { Ok(()) @@ -67,8 +71,10 @@ impl TrackerService { pub async fn remove_info_hash_from_whitelist(&self, info_hash: String) -> Result<(), ServiceError> { let settings = self.cfg.settings.read().await; - let request_url = - format!("{}/api/whitelist/{}?token={}", settings.tracker.api_url, info_hash, settings.tracker.token); + let request_url = format!( + "{}/api/whitelist/{}?token={}", + settings.tracker.api_url, info_hash, settings.tracker.token + ); drop(settings); @@ -76,11 +82,11 @@ impl TrackerService { let response = match client.delete(request_url).send().await { Ok(v) => Ok(v), - Err(_) => Err(ServiceError::InternalServerError) + Err(_) => Err(ServiceError::InternalServerError), }?; if response.status().is_success() { - return Ok(()) + return Ok(()); } Err(ServiceError::InternalServerError) @@ -95,13 +101,11 @@ impl TrackerService { let tracker_key = self.database.get_user_tracker_key(user_id).await; match tracker_key { - Some(v) => { Ok(format!("{}/{}", settings.tracker.url, v.key)) } - None => { - match self.retrieve_new_tracker_key(user_id).await { - Ok(v) => { Ok(format!("{}/{}", settings.tracker.url, v.key)) }, - Err(_) => { Err(ServiceError::TrackerOffline) } - } - } + Some(v) => Ok(format!("{}/{}", settings.tracker.url, v.key)), + None => match self.retrieve_new_tracker_key(user_id).await { + Ok(v) => Ok(format!("{}/{}", settings.tracker.url, v.key)), + Err(_) => Err(ServiceError::TrackerOffline), + }, } } @@ -109,17 +113,27 @@ impl TrackerService { pub async fn retrieve_new_tracker_key(&self, user_id: i64) -> Result { let settings = self.cfg.settings.read().await; - let request_url = format!("{}/api/key/{}?token={}", settings.tracker.api_url, settings.tracker.token_valid_seconds, settings.tracker.token); + let request_url = format!( + "{}/api/key/{}?token={}", + settings.tracker.api_url, settings.tracker.token_valid_seconds, settings.tracker.token + ); drop(settings); let client = reqwest::Client::new(); // issue new tracker key - let response = client.post(request_url).send().await.map_err(|_| ServiceError::InternalServerError)?; + let response = client + .post(request_url) + .send() + .await + .map_err(|_| ServiceError::InternalServerError)?; // get tracker key from response - let tracker_key = response.json::().await.map_err(|_| ServiceError::InternalServerError)?; + let tracker_key = response + .json::() + .await + .map_err(|_| ServiceError::InternalServerError)?; // add tracker key to database (tied to a user) self.database.add_tracker_key(user_id, &tracker_key).await?; @@ -134,24 +148,27 @@ impl TrackerService { let tracker_url = settings.tracker.url.clone(); - let request_url = - format!("{}/api/torrent/{}?token={}", settings.tracker.api_url, info_hash, settings.tracker.token); + let request_url = format!( + "{}/api/torrent/{}?token={}", + settings.tracker.api_url, info_hash, settings.tracker.token + ); drop(settings); let client = reqwest::Client::new(); - let response = match client.get(request_url) - .send() - .await { + let response = match client.get(request_url).send().await { Ok(v) => Ok(v), - Err(_) => Err(ServiceError::InternalServerError) + Err(_) => Err(ServiceError::InternalServerError), }?; let torrent_info = match response.json::().await { Ok(torrent_info) => { - let _ = self.database.update_tracker_info(torrent_id, &tracker_url, torrent_info.seeders, torrent_info.leechers).await; + let _ = self + .database + .update_tracker_info(torrent_id, &tracker_url, torrent_info.seeders, torrent_info.leechers) + .await; Ok(torrent_info) - }, + } Err(_) => { let _ = self.database.update_tracker_info(torrent_id, &tracker_url, 0, 0).await; Err(ServiceError::TorrentNotFound) diff --git a/src/utils/hex.rs b/src/utils/hex.rs index 1432a474..7903c741 100644 --- a/src/utils/hex.rs +++ b/src/utils/hex.rs @@ -6,7 +6,7 @@ pub fn bytes_to_hex(bytes: &[u8]) -> String { for byte in bytes { write!(s, "{:02X}", byte).unwrap(); - }; + } s } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7226920a..53ec37a3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,4 @@ -pub mod parse_torrent; -pub mod time; pub mod hex; +pub mod parse_torrent; pub mod regex; +pub mod time; diff --git a/src/utils/parse_torrent.rs b/src/utils/parse_torrent.rs index 3b4df6f4..e272ede8 100644 --- a/src/utils/parse_torrent.rs +++ b/src/utils/parse_torrent.rs @@ -1,5 +1,7 @@ -use std::{error}; +use std::error; + use serde_bencode::{de, Error}; + use crate::models::torrent_file::Torrent; pub fn decode_torrent(bytes: &[u8]) -> Result> { diff --git a/tests/databases/mod.rs b/tests/databases/mod.rs index adf8bc52..c5a03519 100644 --- a/tests/databases/mod.rs +++ b/tests/databases/mod.rs @@ -1,15 +1,16 @@ use std::future::Future; + use torrust_index_backend::databases::database::{connect_database, Database}; mod mysql; -mod tests; mod sqlite; +mod tests; // used to run tests with a clean database async fn run_test<'a, T, F>(db_fn: T, db: &'a Box) - where - T: FnOnce(&'a Box) -> F + 'a, - F: Future +where + T: FnOnce(&'a Box) -> F + 'a, + F: Future, { // cleanup database before testing assert!(db.delete_all_database_rows().await.is_ok()); @@ -30,4 +31,3 @@ pub async fn run_tests(db_path: &str) { run_test(tests::it_can_add_a_torrent_category, &db).await; run_test(tests::it_can_add_a_torrent_and_tracker_stats_to_that_torrent, &db).await; } - diff --git a/tests/databases/mysql.rs b/tests/databases/mysql.rs index c0f78429..28aa92a3 100644 --- a/tests/databases/mysql.rs +++ b/tests/databases/mysql.rs @@ -1,4 +1,4 @@ -use crate::databases::{run_tests}; +use crate::databases::run_tests; const DATABASE_URL: &str = "mysql://root:password@localhost:3306/torrust-index_test"; @@ -6,5 +6,3 @@ const DATABASE_URL: &str = "mysql://root:password@localhost:3306/torrust-index_t async fn run_mysql_tests() { run_tests(DATABASE_URL).await; } - - diff --git a/tests/databases/sqlite.rs b/tests/databases/sqlite.rs index 940d7e6b..37d89a97 100644 --- a/tests/databases/sqlite.rs +++ b/tests/databases/sqlite.rs @@ -1,4 +1,4 @@ -use crate::databases::{run_tests}; +use crate::databases::run_tests; const DATABASE_URL: &str = "sqlite::memory:"; @@ -6,5 +6,3 @@ const DATABASE_URL: &str = "sqlite::memory:"; async fn run_sqlite_tests() { run_tests(DATABASE_URL).await; } - - diff --git a/tests/databases/tests.rs b/tests/databases/tests.rs index 0c33cba1..a38219e1 100644 --- a/tests/databases/tests.rs +++ b/tests/databases/tests.rs @@ -1,7 +1,7 @@ use serde_bytes::ByteBuf; use torrust_index_backend::databases::database::{Database, DatabaseError}; use torrust_index_backend::models::torrent::TorrentListing; -use torrust_index_backend::models::torrent_file::{TorrentInfo, Torrent}; +use torrust_index_backend::models::torrent_file::{Torrent, TorrentInfo}; use torrust_index_backend::models::user::UserProfile; // test user options @@ -20,7 +20,8 @@ const TEST_TORRENT_SEEDERS: i64 = 437; const TEST_TORRENT_LEECHERS: i64 = 1289; async fn add_test_user(db: &Box) -> Result { - db.insert_user_and_get_id(TEST_USER_USERNAME, TEST_USER_EMAIL, TEST_USER_PASSWORD).await + db.insert_user_and_get_id(TEST_USER_USERNAME, TEST_USER_EMAIL, TEST_USER_PASSWORD) + .await } async fn add_test_torrent_category(db: &Box) -> Result { @@ -42,14 +43,17 @@ pub async fn it_can_add_a_user(db: &Box) { let returned_user_profile = get_user_profile_from_username_result.unwrap(); // verify that the profile data is as we expect it to be - assert_eq!(returned_user_profile, UserProfile { - user_id: inserted_user_id, - username: TEST_USER_USERNAME.to_string(), - email: TEST_USER_EMAIL.to_string(), - email_verified: returned_user_profile.email_verified.clone(), - bio: returned_user_profile.bio.clone(), - avatar: returned_user_profile.avatar.clone() - }); + assert_eq!( + returned_user_profile, + UserProfile { + user_id: inserted_user_id, + username: TEST_USER_USERNAME.to_string(), + email: TEST_USER_EMAIL.to_string(), + email_verified: returned_user_profile.email_verified.clone(), + bio: returned_user_profile.bio.clone(), + avatar: returned_user_profile.avatar.clone() + } + ); } pub async fn it_can_add_a_torrent_category(db: &Box) { @@ -69,7 +73,9 @@ pub async fn it_can_add_a_torrent_category(db: &Box) { pub async fn it_can_add_a_torrent_and_tracker_stats_to_that_torrent(db: &Box) { // set pre-conditions let user_id = add_test_user(&db).await.expect("add_test_user failed."); - let torrent_category_id = add_test_torrent_category(&db).await.expect("add_test_torrent_category failed."); + let torrent_category_id = add_test_torrent_category(&db) + .await + .expect("add_test_torrent_category failed."); let torrent = Torrent { info: TorrentInfo { @@ -81,7 +87,7 @@ pub async fn it_can_add_a_torrent_and_tracker_stats_to_that_torrent(db: &Box