diff --git a/Cargo.lock b/Cargo.lock index 90e50a75..a1e872cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,60 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + [[package]] name = "argon2" version = "0.5.2" @@ -359,7 +413,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -372,6 +426,52 @@ dependencies = [ "stacker", ] +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "colored" version = "2.0.4" @@ -380,7 +480,7 @@ checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ "is-terminal", "lazy_static", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -619,7 +719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -630,7 +730,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", "home", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -980,7 +1080,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1155,7 +1255,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1397,7 +1497,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1604,7 +1704,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1983,7 +2083,7 @@ dependencies = [ "libc", "spin 0.9.8", "untrusted", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2061,7 +2161,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2147,7 +2247,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2393,7 +2493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2656,6 +2756,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -2730,7 +2836,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2879,7 +2985,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2985,12 +3091,14 @@ dependencies = [ name = "torrust-index" version = "3.0.0-alpha.3-develop" dependencies = [ + "anyhow", "argon2", "async-trait", "axum", "binascii", "bytes", "chrono", + "clap", "config", "derive_more", "email_address", @@ -3315,6 +3423,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.5.0" @@ -3453,7 +3567,7 @@ dependencies = [ "home", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3499,7 +3613,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -3508,7 +3622,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -3517,13 +3640,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -3532,42 +3670,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.19" @@ -3584,7 +3764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cc884b39..e2be04f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,12 +34,14 @@ version = "3.0.0-alpha.3-develop" opt-level = 3 [dependencies] +anyhow = "1.0.79" argon2 = "0" async-trait = "0" axum = { version = "0", features = ["multipart"] } binascii = "0" bytes = "1" chrono = { version = "0", default-features = false, features = ["clock"] } +clap = { version = "4.4.18", features = ["derive", "env"]} config = "0" derive_more = "0" email_address = "0" @@ -53,6 +55,7 @@ lazy_static = "1.4.0" lettre = { version = "0", features = ["builder", "smtp-transport", "tokio1", "tokio1-native-tls", "tokio1-rustls-tls"] } log = "0" pbkdf2 = { version = "0", features = ["simple"] } +rand = "0" rand_core = { version = "0", features = ["std"] } regex = "1" reqwest = { version = "0", features = ["json", "multipart"] } @@ -76,7 +79,6 @@ urlencoding = "2" uuid = { version = "1", features = ["v4"] } [dev-dependencies] -rand = "0" tempfile = "3" uuid = { version = "1", features = ["v4"] } which = "5" diff --git a/src/app.rs b/src/app.rs index 22ffd4f7..bb71d5dc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ use crate::services::torrent::{ use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; -use crate::web::api::v1::auth::Authentication; +use crate::web::api::server::v1::auth::Authentication; use crate::web::api::Version; use crate::{console, mailer, tracker, web}; @@ -159,7 +159,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running // Start cronjob to import tracker torrent data and updating // seeders and leechers info. - let tracker_statistics_importer_handle = console::tracker_statistics_importer::start( + let tracker_statistics_importer_handle = console::cronjobs::tracker_statistics_importer::start( importer_port, importer_torrent_info_update_interval, &tracker_statistics_importer, diff --git a/src/bin/import_tracker_statistics.rs b/src/bin/import_tracker_statistics.rs index a405248b..07a0a0c6 100644 --- a/src/bin/import_tracker_statistics.rs +++ b/src/bin/import_tracker_statistics.rs @@ -1,11 +1,11 @@ //! Import Tracker Statistics command. //! -//! It imports the number of seeders and leechers for all torrent from the linked tracker. +//! It imports the number of seeders and leechers for all torrents from the linked tracker. //! //! You can execute it with: `cargo run --bin import_tracker_statistics` -use torrust_index::console::commands::import_tracker_statistics::run_importer; +use torrust_index::console::commands::tracker_statistics_importer::app::run; #[tokio::main] async fn main() { - run_importer().await; + run().await; } diff --git a/src/bin/seeder.rs b/src/bin/seeder.rs new file mode 100644 index 00000000..ae512041 --- /dev/null +++ b/src/bin/seeder.rs @@ -0,0 +1,7 @@ +//! Program to upload random torrents to a live Index API. +use torrust_index::console::commands::seeder::app; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + app::run().await +} diff --git a/src/common.rs b/src/common.rs index bf16889a..755a775b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -13,7 +13,7 @@ use crate::services::torrent::{ use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; -use crate::web::api::v1::auth::Authentication; +use crate::web::api::server::v1::auth::Authentication; use crate::{mailer, tracker}; pub type Username = String; diff --git a/src/console/commands/mod.rs b/src/console/commands/mod.rs index 6dad4966..38506b77 100644 --- a/src/console/commands/mod.rs +++ b/src/console/commands/mod.rs @@ -1 +1,2 @@ -pub mod import_tracker_statistics; +pub mod seeder; +pub mod tracker_statistics_importer; diff --git a/src/console/commands/seeder/api.rs b/src/console/commands/seeder/api.rs new file mode 100644 index 00000000..2236082f --- /dev/null +++ b/src/console/commands/seeder/api.rs @@ -0,0 +1,108 @@ +use log::debug; +use thiserror::Error; + +use crate::web::api::client::v1::client::Client; +use crate::web::api::client::v1::contexts::category::forms::AddCategoryForm; +use crate::web::api::client::v1::contexts::category::responses::{ListItem, ListResponse}; +use crate::web::api::client::v1::contexts::torrent::forms::UploadTorrentMultipartForm; +use crate::web::api::client::v1::contexts::torrent::responses::{UploadedTorrent, UploadedTorrentResponse}; +use crate::web::api::client::v1::contexts::user::forms::LoginForm; +use crate::web::api::client::v1::contexts::user::responses::{LoggedInUserData, SuccessfulLoginResponse}; +use crate::web::api::client::v1::responses::TextResponse; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Torrent with the same info-hash already exist in the database")] + TorrentInfoHashAlreadyExists, + #[error("Torrent with the same title already exist in the database")] + TorrentTitleAlreadyExists, +} + +/// It uploads a torrent file to the Torrust Index. +/// +/// # Errors +/// +/// It returns an error if the torrent already exists in the database. +/// +/// # Panics +/// +/// Panics if the response body is not a valid JSON. +pub async fn upload_torrent(client: &Client, upload_torrent_form: UploadTorrentMultipartForm) -> Result { + let categories = get_categories(client).await; + + if !contains_category_with_name(&categories, &upload_torrent_form.category) { + add_category(client, &upload_torrent_form.category).await; + } + + let response = client.upload_torrent(upload_torrent_form.into()).await; + + debug!(target:"seeder", "response: {}", response.status); + + if response.status == 400 { + if response.body.contains("This torrent already exists in our database") { + return Err(Error::TorrentInfoHashAlreadyExists); + } + + if response.body.contains("This torrent title has already been used") { + return Err(Error::TorrentTitleAlreadyExists); + } + } + + assert!(response.is_json_and_ok(), "Error uploading torrent: {}", response.body); + + let uploaded_torrent_response: UploadedTorrentResponse = + serde_json::from_str(&response.body).expect("a valid JSON response should be returned from the Torrust Index API"); + + Ok(uploaded_torrent_response.data) +} + +/// It logs in the user and returns the user data. +/// +/// # Panics +/// +/// Panics if the response body is not a valid JSON. +pub async fn login(client: &Client, username: &str, password: &str) -> LoggedInUserData { + let response = client + .login_user(LoginForm { + login: username.to_owned(), + password: password.to_owned(), + }) + .await; + + let res: SuccessfulLoginResponse = serde_json::from_str(&response.body).unwrap_or_else(|_| { + panic!( + "a valid JSON response should be returned after login. Received: {}", + response.body + ) + }); + + res.data +} + +/// It returns all the index categories. +/// +/// # Panics +/// +/// Panics if the response body is not a valid JSON. +pub async fn get_categories(client: &Client) -> Vec { + let response = client.get_categories().await; + + let res: ListResponse = serde_json::from_str(&response.body).unwrap(); + + res.data +} + +/// It adds a new category. +pub async fn add_category(client: &Client, name: &str) -> TextResponse { + client + .add_category(AddCategoryForm { + name: name.to_owned(), + icon: None, + }) + .await +} + +/// It checks if the category list contains the given category. +fn contains_category_with_name(items: &[ListItem], category_name: &str) -> bool { + items.iter().any(|item| item.name == category_name) +} diff --git a/src/console/commands/seeder/app.rs b/src/console/commands/seeder/app.rs new file mode 100644 index 00000000..ac84dc83 --- /dev/null +++ b/src/console/commands/seeder/app.rs @@ -0,0 +1,132 @@ +//! Program to upload random torrent to a live Index API. +//! +//! Run with: +//! +//! ```text +//! cargo run --bin seeder -- --api-base-url --number-of-torrents --user --password --interval +//! ``` +//! +//! For example: +//! +//! ```text +//! cargo run --bin seeder -- --api-base-url "localhost:3001" --number-of-torrents 1000 --user admin --password 12345678 --interval 0 +//! ``` +//! +//! That command would upload 1000 random torrents to the Index using the user +//! account admin with password 123456 and waiting 1 second between uploads. +use std::thread::sleep; +use std::time::Duration; + +use anyhow::Context; +use clap::Parser; +use log::{debug, info, LevelFilter}; +use text_colorizer::Colorize; +use uuid::Uuid; + +use super::api::Error; +use crate::console::commands::seeder::api::{login, upload_torrent}; +use crate::console::commands::seeder::logging; +use crate::services::torrent_file::generate_random_torrent; +use crate::utils::parse_torrent; +use crate::web::api::client::v1::client::Client; +use crate::web::api::client::v1::contexts::torrent::forms::{BinaryFile, UploadTorrentMultipartForm}; +use crate::web::api::client::v1::contexts::torrent::responses::UploadedTorrent; +use crate::web::api::client::v1::contexts::user::responses::LoggedInUserData; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[arg(short, long)] + api_base_url: String, + + #[arg(short, long)] + number_of_torrents: i32, + + #[arg(short, long)] + user: String, + + #[arg(short, long)] + password: String, + + #[arg(short, long)] + interval: u64, +} + +/// # Errors +/// +/// Will not return any errors for the time being. +pub async fn run() -> anyhow::Result<()> { + logging::setup(LevelFilter::Info); + + let args = Args::parse(); + + let api_user = login_index_api(&args.api_base_url, &args.user, &args.password).await; + + let api_client = Client::authenticated(&args.api_base_url, &api_user.token); + + info!(target:"seeder", "Uploading { } random torrents to the Torrust Index with a { } seconds interval...", args.number_of_torrents.to_string().yellow(), args.interval.to_string().yellow()); + + for i in 1..=args.number_of_torrents { + info!(target:"seeder", "Uploading torrent #{} ...", i.to_string().yellow()); + + match upload_random_torrent(&api_client).await { + Ok(uploaded_torrent) => { + debug!(target:"seeder", "Uploaded torrent {uploaded_torrent:?}"); + + let json = serde_json::to_string(&uploaded_torrent).context("failed to serialize upload response into JSON")?; + + info!(target:"seeder", "Uploaded torrent: {}", json.yellow()); + } + Err(err) => print!("Error uploading torrent {err:?}"), + }; + + if i != args.number_of_torrents { + sleep(Duration::from_secs(args.interval)); + } + } + + Ok(()) +} + +/// It logs in a user in the Index API. +pub async fn login_index_api(api_url: &str, username: &str, password: &str) -> LoggedInUserData { + let unauthenticated_client = Client::unauthenticated(api_url); + + info!(target:"seeder", "Trying to login with username: {} ...", username.yellow()); + + let user: LoggedInUserData = login(&unauthenticated_client, username, password).await; + + if user.admin { + info!(target:"seeder", "Logged as admin with account: {} ", username.yellow()); + } else { + info!(target:"seeder", "Logged as {} ", username.yellow()); + } + + user +} + +async fn upload_random_torrent(api_client: &Client) -> Result { + let uuid = Uuid::new_v4(); + + info!(target:"seeder", "Uploading torrent with uuid: {} ...", uuid.to_string().yellow()); + + let torrent_file = generate_random_torrent_file(uuid); + + let upload_form = UploadTorrentMultipartForm { + title: format!("title-{uuid}"), + description: format!("description-{uuid}"), + category: "test".to_string(), + torrent_file, + }; + + upload_torrent(api_client, upload_form).await +} + +/// It returns the bencoded binary data of the torrent meta file. +fn generate_random_torrent_file(uuid: Uuid) -> BinaryFile { + let torrent = generate_random_torrent(uuid); + + let bytes = parse_torrent::encode_torrent(&torrent).expect("msg:the torrent should be bencoded"); + + BinaryFile::from_bytes(torrent.info.name, bytes) +} diff --git a/src/console/commands/seeder/logging.rs b/src/console/commands/seeder/logging.rs new file mode 100644 index 00000000..13d9c745 --- /dev/null +++ b/src/console/commands/seeder/logging.rs @@ -0,0 +1,25 @@ +use log::{debug, LevelFilter}; + +/// # Panics +/// +/// +pub fn setup(level: LevelFilter) { + if let Err(_err) = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} [{}][{}] {}", + chrono::Local::now().format("%+"), + record.target(), + record.level(), + message + )); + }) + .level(level) + .chain(std::io::stdout()) + .apply() + { + panic!("Failed to initialize logging.") + } + + debug!("logging initialized."); +} diff --git a/src/console/commands/seeder/mod.rs b/src/console/commands/seeder/mod.rs new file mode 100644 index 00000000..c29812b4 --- /dev/null +++ b/src/console/commands/seeder/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod app; +pub mod logging; diff --git a/src/console/commands/import_tracker_statistics.rs b/src/console/commands/tracker_statistics_importer/app.rs similarity index 99% rename from src/console/commands/import_tracker_statistics.rs rename to src/console/commands/tracker_statistics_importer/app.rs index 08acbb31..115b7bd8 100644 --- a/src/console/commands/import_tracker_statistics.rs +++ b/src/console/commands/tracker_statistics_importer/app.rs @@ -75,7 +75,7 @@ fn print_usage() { /// # Panics /// /// Panics if arguments cannot be parsed. -pub async fn run_importer() { +pub async fn run() { parse_args().expect("unable to parse command arguments"); import().await; } diff --git a/src/console/commands/tracker_statistics_importer/mod.rs b/src/console/commands/tracker_statistics_importer/mod.rs new file mode 100644 index 00000000..309be628 --- /dev/null +++ b/src/console/commands/tracker_statistics_importer/mod.rs @@ -0,0 +1 @@ +pub mod app; diff --git a/src/console/cronjobs/mod.rs b/src/console/cronjobs/mod.rs new file mode 100644 index 00000000..43be8073 --- /dev/null +++ b/src/console/cronjobs/mod.rs @@ -0,0 +1 @@ +pub mod tracker_statistics_importer; diff --git a/src/console/tracker_statistics_importer.rs b/src/console/cronjobs/tracker_statistics_importer.rs similarity index 95% rename from src/console/tracker_statistics_importer.rs rename to src/console/cronjobs/tracker_statistics_importer.rs index c0787ad1..4c65c5a0 100644 --- a/src/console/tracker_statistics_importer.rs +++ b/src/console/cronjobs/tracker_statistics_importer.rs @@ -33,6 +33,10 @@ struct ImporterState { pub torrent_info_update_interval: u64, } +/// # Panics +/// +/// Will panic if it can't start the tracker statistics importer API +#[must_use] pub fn start( importer_port: u16, torrent_info_update_interval: u64, @@ -60,7 +64,7 @@ pub fn start( let addr = format!("{IMPORTER_API_IP}:{importer_port}"); - info!("Tracker statistics importer API server listening on http://{}", addr); + info!("Tracker statistics importer API server listening on http://{}", addr); // # DevSkim: ignore DS137138 axum::Server::bind(&addr.parse().unwrap()) .serve(app.into_make_service()) @@ -122,7 +126,7 @@ async fn heartbeat_handler(State(state): State>) -> Json Result<(), reqwest::Error> { let client = reqwest::Client::new(); - let url = format!("http://{IMPORTER_API_IP}:{importer_port}/heartbeat"); + let url = format!("http://{IMPORTER_API_IP}:{importer_port}/heartbeat"); // # DevSkim: ignore DS137138 client.post(url).send().await?; diff --git a/src/console/mod.rs b/src/console/mod.rs index 889c06dc..80eff453 100644 --- a/src/console/mod.rs +++ b/src/console/mod.rs @@ -1,2 +1,2 @@ pub mod commands; -pub(crate) mod tracker_statistics_importer; +pub mod cronjobs; diff --git a/src/lib.rs b/src/lib.rs index ba89003c..45e6ad24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! used with by the [Torrust Tracker Index Gui](https://github.com/torrust/torrust-index-gui). //! //! If you are looking for information on how to use the API, please see the -//! [API v1](crate::web::api::v1) section of the documentation. +//! [API v1](crate::web::api::server::v1) section of the documentation. //! //! # Table of contents //! @@ -37,7 +37,7 @@ //! //! From the end-user perspective the Torrust Tracker exposes three different services. //! -//! - A REST [API](crate::web::api::v1) +//! - A REST [API](crate::web::api::server::v1) //! //! From the administrator perspective, the Torrust Index exposes: //! diff --git a/src/mailer.rs b/src/mailer.rs index 0c48acd6..39c1d0d4 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -13,7 +13,7 @@ use tera::{try_get_value, Context, Tera}; use crate::config::Configuration; use crate::errors::ServiceError; use crate::utils::clock; -use crate::web::api::v1::routes::API_VERSION_URL_PREFIX; +use crate::web::api::server::v1::routes::API_VERSION_URL_PREFIX; lazy_static! { pub static ref TEMPLATES: Tera = { diff --git a/src/services/about.rs b/src/services/about.rs index 82175bf6..28eedc02 100644 --- a/src/services/about.rs +++ b/src/services/about.rs @@ -1,5 +1,5 @@ //! Templates for "about" static pages. -use crate::web::api::v1::routes::API_VERSION_URL_PREFIX; +use crate::web::api::server::v1::routes::API_VERSION_URL_PREFIX; #[must_use] pub fn index_page() -> String { diff --git a/src/services/user.rs b/src/services/user.rs index 358e7431..cd4e5937 100644 --- a/src/services/user.rs +++ b/src/services/user.rs @@ -14,7 +14,7 @@ use crate::mailer; use crate::mailer::VerifyClaims; use crate::models::user::{UserCompact, UserId, UserProfile}; use crate::utils::validation::validate_email_address; -use crate::web::api::v1::contexts::user::forms::RegistrationForm; +use crate::web::api::server::v1::contexts::user::forms::RegistrationForm; /// Since user email could be optional, we need a way to represent "no email" /// in the database. This function returns the string that should be used for diff --git a/src/web/api/client/mod.rs b/src/web/api/client/mod.rs new file mode 100644 index 00000000..a3a6d96c --- /dev/null +++ b/src/web/api/client/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/src/web/api/client/v1/client.rs b/src/web/api/client/v1/client.rs new file mode 100644 index 00000000..59176203 --- /dev/null +++ b/src/web/api/client/v1/client.rs @@ -0,0 +1,333 @@ +use reqwest::multipart; +use serde::Serialize; + +use super::connection_info::ConnectionInfo; +use super::contexts::category::forms::{AddCategoryForm, DeleteCategoryForm}; +use super::contexts::tag::forms::{AddTagForm, DeleteTagForm}; +use super::contexts::torrent::forms::UpdateTorrentForm; +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}; + +/// API Client +pub struct Client { + http_client: Http, +} + +impl Client { + // todo: forms in POST requests can be passed by reference. + + fn base_path() -> String { + "/v1".to_string() + } + + #[must_use] + pub fn unauthenticated(bind_address: &str) -> Self { + Self::new(ConnectionInfo::anonymous(bind_address, &Self::base_path())) + } + + #[must_use] + pub fn authenticated(bind_address: &str, token: &str) -> Self { + Self::new(ConnectionInfo::new(bind_address, &Self::base_path(), token)) + } + + #[must_use] + pub fn new(connection_info: ConnectionInfo) -> Self { + Self { + http_client: Http::new(connection_info), + } + } + + /// It checks if the server is running. + pub async fn server_is_running(&self) -> bool { + let response = self.http_client.inner_get("").await; + response.is_ok() + } + + // Context: about + + pub async fn about(&self) -> TextResponse { + self.http_client.get("/about", Query::empty()).await + } + + pub async fn license(&self) -> TextResponse { + self.http_client.get("/about/license", Query::empty()).await + } + + // Context: category + + pub async fn get_categories(&self) -> TextResponse { + self.http_client.get("/category", Query::empty()).await + } + + pub async fn add_category(&self, add_category_form: AddCategoryForm) -> TextResponse { + self.http_client.post("/category", &add_category_form).await + } + + pub async fn delete_category(&self, delete_category_form: DeleteCategoryForm) -> TextResponse { + self.http_client.delete_with_body("/category", &delete_category_form).await + } + + // Context: tag + + pub async fn get_tags(&self) -> TextResponse { + // code-review: some endpoint are using plural + // (for instance, `get_categories`) and some singular. + self.http_client.get("/tags", Query::empty()).await + } + + pub async fn add_tag(&self, add_tag_form: AddTagForm) -> TextResponse { + self.http_client.post("/tag", &add_tag_form).await + } + + pub async fn delete_tag(&self, delete_tag_form: DeleteTagForm) -> TextResponse { + self.http_client.delete_with_body("/tag", &delete_tag_form).await + } + + // Context: root + + pub async fn root(&self) -> TextResponse { + self.http_client.get("", Query::empty()).await + } + + // Context: settings + + pub async fn get_public_settings(&self) -> TextResponse { + self.http_client.get("/settings/public", Query::empty()).await + } + + pub async fn get_site_name(&self) -> TextResponse { + self.http_client.get("/settings/name", Query::empty()).await + } + + pub async fn get_settings(&self) -> TextResponse { + self.http_client.get("/settings", Query::empty()).await + } + + // Context: torrent + + pub async fn get_torrents(&self, params: Query) -> TextResponse { + self.http_client.get("/torrents", params).await + } + + pub async fn get_torrent(&self, info_hash: &InfoHash) -> TextResponse { + self.http_client.get(&format!("/torrent/{info_hash}"), Query::empty()).await + } + + pub async fn delete_torrent(&self, info_hash: &InfoHash) -> TextResponse { + self.http_client.delete(&format!("/torrent/{info_hash}")).await + } + + pub async fn update_torrent(&self, info_hash: &InfoHash, update_torrent_form: UpdateTorrentForm) -> TextResponse { + self.http_client + .put(&format!("/torrent/{info_hash}"), &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 { + self.http_client + .get_binary(&format!("/torrent/download/{info_hash}"), Query::empty()) + .await + } + + // Context: user + + pub async fn register_user(&self, registration_form: RegistrationForm) -> TextResponse { + self.http_client.post("/user/register", ®istration_form).await + } + + pub async fn login_user(&self, registration_form: LoginForm) -> TextResponse { + self.http_client.post("/user/login", ®istration_form).await + } + + pub async fn verify_token(&self, token_verification_form: TokenVerificationForm) -> TextResponse { + self.http_client.post("/user/token/verify", &token_verification_form).await + } + + pub async fn renew_token(&self, token_verification_form: TokenRenewalForm) -> TextResponse { + self.http_client.post("/user/token/renew", &token_verification_form).await + } + + pub async fn ban_user(&self, username: Username) -> TextResponse { + self.http_client.delete(&format!("/user/ban/{}", &username.value)).await + } +} + +/// Generic HTTP Client +struct Http { + connection_info: ConnectionInfo, +} + +impl Http { + pub fn new(connection_info: ConnectionInfo) -> Self { + Self { connection_info } + } + + pub async fn get(&self, path: &str, params: Query) -> TextResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::builder() + .build() + .unwrap() + .get(self.base_url(path).clone()) + .query(&ReqwestQuery::from(params)) + .bearer_auth(token) + .send() + .await + .unwrap(), + None => reqwest::Client::builder() + .build() + .unwrap() + .get(self.base_url(path).clone()) + .query(&ReqwestQuery::from(params)) + .send() + .await + .unwrap(), + }; + TextResponse::from(response).await + } + + pub async fn get_binary(&self, path: &str, params: Query) -> BinaryResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::builder() + .build() + .unwrap() + .get(self.base_url(path).clone()) + .query(&ReqwestQuery::from(params)) + .bearer_auth(token) + .send() + .await + .unwrap(), + None => reqwest::Client::builder() + .build() + .unwrap() + .get(self.base_url(path).clone()) + .query(&ReqwestQuery::from(params)) + .send() + .await + .unwrap(), + }; + // todo: If the response is a JSON, it returns the JSON body in a byte + // array. This is not the expected behavior. + // - Rename BinaryResponse to BinaryTorrentResponse + // - Return an error if the response is not a bittorrent file + BinaryResponse::from(response).await + } + + pub async fn inner_get(&self, path: &str) -> Result { + reqwest::Client::builder() + .build() + .unwrap() + .get(self.base_url(path).clone()) + .send() + .await + } + + pub async fn post(&self, path: &str, form: &T) -> TextResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::new() + .post(self.base_url(path).clone()) + .bearer_auth(token) + .json(&form) + .send() + .await + .unwrap(), + None => reqwest::Client::new() + .post(self.base_url(path).clone()) + .json(&form) + .send() + .await + .unwrap(), + }; + TextResponse::from(response).await + } + + pub async fn post_multipart(&self, path: &str, form: multipart::Form) -> TextResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::builder() + .build() + .unwrap() + .post(self.base_url(path).clone()) + .multipart(form) + .bearer_auth(token) + .send() + .await + .expect("failed to send multipart request with token"), + None => reqwest::Client::builder() + .build() + .unwrap() + .post(self.base_url(path).clone()) + .multipart(form) + .send() + .await + .expect("failed to send multipart request without token"), + }; + TextResponse::from(response).await + } + + pub async fn put(&self, path: &str, form: &T) -> TextResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::new() + .put(self.base_url(path).clone()) + .bearer_auth(token) + .json(&form) + .send() + .await + .unwrap(), + None => reqwest::Client::new() + .put(self.base_url(path).clone()) + .json(&form) + .send() + .await + .unwrap(), + }; + TextResponse::from(response).await + } + + async fn delete(&self, path: &str) -> TextResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::new() + .delete(self.base_url(path).clone()) + .bearer_auth(token) + .send() + .await + .unwrap(), + None => reqwest::Client::new() + .delete(self.base_url(path).clone()) + .send() + .await + .unwrap(), + }; + TextResponse::from(response).await + } + + async fn delete_with_body(&self, path: &str, form: &T) -> TextResponse { + let response = match &self.connection_info.token { + Some(token) => reqwest::Client::new() + .delete(self.base_url(path).clone()) + .bearer_auth(token) + .json(&form) + .send() + .await + .unwrap(), + None => reqwest::Client::new() + .delete(self.base_url(path).clone()) + .json(&form) + .send() + .await + .unwrap(), + }; + TextResponse::from(response).await + } + + fn base_url(&self, path: &str) -> String { + format!( + "http://{}{}{path}", + &self.connection_info.bind_address, &self.connection_info.base_path + ) + } +} diff --git a/src/web/api/client/v1/connection_info.rs b/src/web/api/client/v1/connection_info.rs new file mode 100644 index 00000000..2183f4b9 --- /dev/null +++ b/src/web/api/client/v1/connection_info.rs @@ -0,0 +1,26 @@ +#[derive(Clone)] +pub struct ConnectionInfo { + pub bind_address: String, + pub base_path: String, + pub token: Option, +} + +impl ConnectionInfo { + #[must_use] + pub fn new(bind_address: &str, base_path: &str, token: &str) -> Self { + Self { + bind_address: bind_address.to_string(), + base_path: base_path.to_string(), + token: Some(token.to_string()), + } + } + + #[must_use] + pub fn anonymous(bind_address: &str, base_path: &str) -> Self { + Self { + bind_address: bind_address.to_string(), + base_path: base_path.to_string(), + token: None, + } + } +} diff --git a/src/web/api/client/v1/contexts/category/forms.rs b/src/web/api/client/v1/contexts/category/forms.rs new file mode 100644 index 00000000..ea9cf429 --- /dev/null +++ b/src/web/api/client/v1/contexts/category/forms.rs @@ -0,0 +1,9 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct AddCategoryForm { + pub name: String, + pub icon: Option, +} + +pub type DeleteCategoryForm = AddCategoryForm; diff --git a/src/web/api/client/v1/contexts/category/mod.rs b/src/web/api/client/v1/contexts/category/mod.rs new file mode 100644 index 00000000..ea737db8 --- /dev/null +++ b/src/web/api/client/v1/contexts/category/mod.rs @@ -0,0 +1,2 @@ +pub mod forms; +pub mod responses; diff --git a/src/web/api/client/v1/contexts/category/responses.rs b/src/web/api/client/v1/contexts/category/responses.rs new file mode 100644 index 00000000..cbadb631 --- /dev/null +++ b/src/web/api/client/v1/contexts/category/responses.rs @@ -0,0 +1,23 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct AddedCategoryResponse { + pub data: String, +} + +#[derive(Deserialize)] +pub struct DeletedCategoryResponse { + pub data: String, +} + +#[derive(Deserialize, Debug)] +pub struct ListResponse { + pub data: Vec, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct ListItem { + pub category_id: i64, + pub name: String, + pub num_torrents: i64, +} diff --git a/src/web/api/client/v1/contexts/mod.rs b/src/web/api/client/v1/contexts/mod.rs new file mode 100644 index 00000000..44f74414 --- /dev/null +++ b/src/web/api/client/v1/contexts/mod.rs @@ -0,0 +1,5 @@ +pub mod category; +pub mod settings; +pub mod tag; +pub mod torrent; +pub mod user; diff --git a/src/web/api/client/v1/contexts/settings/mod.rs b/src/web/api/client/v1/contexts/settings/mod.rs new file mode 100644 index 00000000..e08a4103 --- /dev/null +++ b/src/web/api/client/v1/contexts/settings/mod.rs @@ -0,0 +1,193 @@ +pub mod responses; + +use serde::{Deserialize, Serialize}; + +use crate::config::{ + Api as DomainApi, Auth as DomainAuth, Database as DomainDatabase, ImageCache as DomainImageCache, Mail as DomainMail, + Network as DomainNetwork, TorrustIndex as DomainSettings, Tracker as DomainTracker, + TrackerStatisticsImporter as DomainTrackerStatisticsImporter, Website as DomainWebsite, +}; + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Settings { + pub website: Website, + pub tracker: Tracker, + pub net: Network, + pub auth: Auth, + pub database: Database, + pub mail: Mail, + pub image_cache: ImageCache, + pub api: Api, + pub tracker_statistics_importer: TrackerStatisticsImporter, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Website { + pub name: String, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Tracker { + pub url: String, + pub mode: String, + pub api_url: String, + pub token: String, + pub token_valid_seconds: u64, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Network { + pub port: u16, + pub base_url: Option, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Auth { + pub email_on_signup: String, + pub min_password_length: usize, + pub max_password_length: usize, + pub secret_key: String, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Database { + pub connect_url: String, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Mail { + pub email_verification_enabled: bool, + pub from: String, + pub reply_to: String, + pub username: String, + pub password: String, + pub server: String, + pub port: u16, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct ImageCache { + pub max_request_timeout_ms: u64, + pub capacity: usize, + pub entry_size_limit: usize, + pub user_quota_period_seconds: u64, + pub user_quota_bytes: usize, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct Api { + pub default_torrent_page_size: u8, + pub max_torrent_page_size: u8, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] +pub struct TrackerStatisticsImporter { + pub torrent_info_update_interval: u64, + port: u16, +} + +impl From for Settings { + fn from(settings: DomainSettings) -> Self { + Settings { + website: Website::from(settings.website), + tracker: Tracker::from(settings.tracker), + net: Network::from(settings.net), + auth: Auth::from(settings.auth), + database: Database::from(settings.database), + mail: Mail::from(settings.mail), + image_cache: ImageCache::from(settings.image_cache), + api: Api::from(settings.api), + tracker_statistics_importer: TrackerStatisticsImporter::from(settings.tracker_statistics_importer), + } + } +} + +impl From for Website { + fn from(website: DomainWebsite) -> Self { + Self { name: website.name } + } +} + +impl From for Tracker { + fn from(tracker: DomainTracker) -> Self { + Self { + url: tracker.url, + mode: format!("{:?}", tracker.mode), + api_url: tracker.api_url, + token: tracker.token, + token_valid_seconds: tracker.token_valid_seconds, + } + } +} + +impl From for Network { + fn from(net: DomainNetwork) -> Self { + Self { + port: net.port, + base_url: net.base_url, + } + } +} + +impl From for Auth { + fn from(auth: DomainAuth) -> Self { + Self { + email_on_signup: format!("{:?}", auth.email_on_signup), + min_password_length: auth.min_password_length, + max_password_length: auth.max_password_length, + secret_key: auth.secret_key, + } + } +} + +impl From for Database { + fn from(database: DomainDatabase) -> Self { + Self { + connect_url: database.connect_url, + } + } +} + +impl From for Mail { + fn from(mail: DomainMail) -> Self { + Self { + email_verification_enabled: mail.email_verification_enabled, + from: mail.from, + reply_to: mail.reply_to, + username: mail.username, + password: mail.password, + server: mail.server, + port: mail.port, + } + } +} + +impl From for ImageCache { + fn from(image_cache: DomainImageCache) -> Self { + Self { + max_request_timeout_ms: image_cache.max_request_timeout_ms, + capacity: image_cache.capacity, + entry_size_limit: image_cache.entry_size_limit, + user_quota_period_seconds: image_cache.user_quota_period_seconds, + user_quota_bytes: image_cache.user_quota_bytes, + } + } +} + +impl From for Api { + fn from(api: DomainApi) -> Self { + Self { + default_torrent_page_size: api.default_torrent_page_size, + max_torrent_page_size: api.max_torrent_page_size, + } + } +} + +impl From for TrackerStatisticsImporter { + fn from(tracker_statistics_importer: DomainTrackerStatisticsImporter) -> Self { + Self { + torrent_info_update_interval: tracker_statistics_importer.torrent_info_update_interval, + port: tracker_statistics_importer.port, + } + } +} diff --git a/src/web/api/client/v1/contexts/settings/responses.rs b/src/web/api/client/v1/contexts/settings/responses.rs new file mode 100644 index 00000000..096ef1f4 --- /dev/null +++ b/src/web/api/client/v1/contexts/settings/responses.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; + +use super::Settings; + +#[derive(Deserialize)] +pub struct AllSettingsResponse { + pub data: Settings, +} + +#[derive(Deserialize)] +pub struct PublicSettingsResponse { + pub data: Public, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct Public { + pub website_name: String, + pub tracker_url: String, + pub tracker_mode: String, + pub email_on_signup: String, +} + +#[derive(Deserialize)] +pub struct SiteNameResponse { + pub data: String, +} diff --git a/src/web/api/client/v1/contexts/tag/forms.rs b/src/web/api/client/v1/contexts/tag/forms.rs new file mode 100644 index 00000000..26d1395d --- /dev/null +++ b/src/web/api/client/v1/contexts/tag/forms.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct AddTagForm { + pub name: String, +} + +#[derive(Serialize)] +pub struct DeleteTagForm { + pub tag_id: i64, +} diff --git a/src/web/api/client/v1/contexts/tag/mod.rs b/src/web/api/client/v1/contexts/tag/mod.rs new file mode 100644 index 00000000..ea737db8 --- /dev/null +++ b/src/web/api/client/v1/contexts/tag/mod.rs @@ -0,0 +1,2 @@ +pub mod forms; +pub mod responses; diff --git a/src/web/api/client/v1/contexts/tag/responses.rs b/src/web/api/client/v1/contexts/tag/responses.rs new file mode 100644 index 00000000..a08cdf55 --- /dev/null +++ b/src/web/api/client/v1/contexts/tag/responses.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +// code-review: we should always include a API resource in the `data`attribute. +// +// ``` +// pub struct DeletedTagResponse { +// pub data: DeletedTag, +// } +// +// pub struct DeletedTag { +// pub tag_id: i64, +// } +// ``` +// +// This way the API client knows what's the meaning of the `data` attribute. + +#[derive(Deserialize)] +pub struct AddedTagResponse { + pub data: String, +} + +#[derive(Deserialize)] +pub struct DeletedTagResponse { + pub data: i64, +} + +#[derive(Deserialize, Debug)] +pub struct ListResponse { + pub data: Vec, +} + +impl ListResponse { + /// # Panics + /// + /// Will panic if it can't fin the tag in the response. + #[must_use] + pub fn find_tag_id(&self, tag_name: &str) -> i64 { + self.data.iter().find(|tag| tag.name == tag_name).unwrap().tag_id + } +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct ListItem { + pub tag_id: i64, + pub name: String, +} diff --git a/src/web/api/client/v1/contexts/torrent/forms.rs b/src/web/api/client/v1/contexts/torrent/forms.rs new file mode 100644 index 00000000..64a0360d --- /dev/null +++ b/src/web/api/client/v1/contexts/torrent/forms.rs @@ -0,0 +1,66 @@ +use std::fs; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct UpdateTorrentForm { + pub title: Option, + pub description: Option, + pub category: Option, + pub tags: Option>, +} + +use reqwest::multipart::Form; + +pub struct UploadTorrentMultipartForm { + pub title: String, + pub description: String, + pub category: String, + pub torrent_file: BinaryFile, +} + +#[derive(Clone)] +pub struct BinaryFile { + pub name: String, + pub contents: Vec, +} + +impl BinaryFile { + /// # Panics + /// + /// Will panic if: + /// + /// - The path is not a file. + /// - The path can't be converted into string. + /// - The file can't be read. + #[must_use] + pub fn from_file_at_path(path: &Path) -> Self { + BinaryFile { + name: path.file_name().unwrap().to_owned().into_string().unwrap(), + contents: fs::read(path).unwrap(), + } + } + + /// Build the binary file directly from the binary data provided. + #[must_use] + pub fn from_bytes(name: String, contents: Vec) -> Self { + BinaryFile { name, contents } + } +} + +impl From for Form { + fn from(form: UploadTorrentMultipartForm) -> Self { + Form::new() + .text("title", form.title) + .text("description", form.description) + .text("category", form.category) + .part( + "torrent", + reqwest::multipart::Part::bytes(form.torrent_file.contents) + .file_name(form.torrent_file.name) + .mime_str("application/x-bittorrent") + .unwrap(), + ) + } +} diff --git a/src/web/api/client/v1/contexts/torrent/mod.rs b/src/web/api/client/v1/contexts/torrent/mod.rs new file mode 100644 index 00000000..a3bb0936 --- /dev/null +++ b/src/web/api/client/v1/contexts/torrent/mod.rs @@ -0,0 +1,3 @@ +pub mod forms; +pub mod requests; +pub mod responses; diff --git a/src/web/api/client/v1/contexts/torrent/requests.rs b/src/web/api/client/v1/contexts/torrent/requests.rs new file mode 100644 index 00000000..1d4ac583 --- /dev/null +++ b/src/web/api/client/v1/contexts/torrent/requests.rs @@ -0,0 +1 @@ +pub type InfoHash = String; diff --git a/src/web/api/client/v1/contexts/torrent/responses.rs b/src/web/api/client/v1/contexts/torrent/responses.rs new file mode 100644 index 00000000..07355e99 --- /dev/null +++ b/src/web/api/client/v1/contexts/torrent/responses.rs @@ -0,0 +1,126 @@ +use serde::{Deserialize, Serialize}; + +pub type Id = i64; +pub type CategoryId = i64; +pub type TagId = i64; +pub type UtcDateTime = String; // %Y-%m-%d %H:%M:%S + +#[derive(Deserialize, PartialEq, Debug)] +pub struct ErrorResponse { + pub error: String, +} + +#[derive(Deserialize)] +pub struct TorrentListResponse { + pub data: TorrentList, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct TorrentList { + pub total: u32, + pub results: Vec, +} + +impl TorrentList { + #[must_use] + pub fn _contains(&self, torrent_id: Id) -> bool { + self.results.iter().any(|item| item.torrent_id == torrent_id) + } +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct ListItem { + pub torrent_id: i64, + pub uploader: String, + pub info_hash: String, + pub title: String, + pub description: Option, + pub category_id: i64, + pub date_uploaded: String, + pub file_size: i64, + pub seeders: i64, + pub leechers: i64, + pub name: String, + pub comment: Option, + pub creation_date: Option, + pub created_by: Option, + pub encoding: Option, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct TorrentDetailsResponse { + pub data: TorrentDetails, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct TorrentDetails { + pub torrent_id: Id, + pub uploader: String, + pub info_hash: String, + pub title: String, + pub description: String, + pub category: Category, + pub upload_date: UtcDateTime, + pub file_size: u64, + pub seeders: u64, + pub leechers: u64, + pub files: Vec, + pub trackers: Vec, + pub magnet_link: String, + pub tags: Vec, + pub name: String, + pub comment: Option, + pub creation_date: Option, + pub created_by: Option, + pub encoding: Option, +} + +#[allow(unknown_lints)] +#[allow(clippy::struct_field_names)] +#[derive(Deserialize, PartialEq, Debug)] +pub struct Category { + pub category_id: CategoryId, // todo: rename to `id` + pub name: String, + pub num_torrents: u64, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct Tag { + pub tag_id: TagId, + pub name: String, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct File { + pub path: Vec, + pub length: u64, + pub md5sum: Option, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct UploadedTorrentResponse { + pub data: UploadedTorrent, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug)] +pub struct UploadedTorrent { + pub torrent_id: Id, + pub info_hash: String, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct DeletedTorrentResponse { + pub data: DeletedTorrent, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct DeletedTorrent { + pub torrent_id: Id, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct UpdatedTorrentResponse { + pub data: UpdatedTorrent, +} + +pub type UpdatedTorrent = TorrentDetails; diff --git a/src/web/api/client/v1/contexts/user/forms.rs b/src/web/api/client/v1/contexts/user/forms.rs new file mode 100644 index 00000000..18d08f9e --- /dev/null +++ b/src/web/api/client/v1/contexts/user/forms.rs @@ -0,0 +1,38 @@ +use serde::Serialize; + +#[derive(Clone, Serialize)] +pub struct RegistrationForm { + pub username: String, + pub email: Option, + pub password: String, + pub confirm_password: String, +} + +pub type RegisteredUser = RegistrationForm; + +#[derive(Serialize)] +pub struct LoginForm { + pub login: String, + pub password: String, +} + +#[derive(Serialize)] +pub struct TokenVerificationForm { + pub token: String, +} + +#[derive(Serialize)] +pub struct TokenRenewalForm { + pub token: String, +} + +pub struct Username { + pub value: String, +} + +impl Username { + #[must_use] + pub fn new(value: String) -> Self { + Self { value } + } +} diff --git a/src/web/api/client/v1/contexts/user/mod.rs b/src/web/api/client/v1/contexts/user/mod.rs new file mode 100644 index 00000000..ea737db8 --- /dev/null +++ b/src/web/api/client/v1/contexts/user/mod.rs @@ -0,0 +1,2 @@ +pub mod forms; +pub mod responses; diff --git a/src/web/api/client/v1/contexts/user/responses.rs b/src/web/api/client/v1/contexts/user/responses.rs new file mode 100644 index 00000000..1a9a3837 --- /dev/null +++ b/src/web/api/client/v1/contexts/user/responses.rs @@ -0,0 +1,45 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct AddedUserResponse { + pub data: NewUserData, +} + +#[derive(Deserialize, Debug)] +pub struct NewUserData { + pub user_id: i64, +} + +#[derive(Deserialize, Debug)] +pub struct SuccessfulLoginResponse { + pub data: LoggedInUserData, +} + +#[derive(Deserialize, Debug)] +pub struct LoggedInUserData { + pub token: String, + pub username: String, + pub admin: bool, +} + +#[derive(Deserialize)] +pub struct TokenVerifiedResponse { + pub data: String, +} + +#[derive(Deserialize)] +pub struct BannedUserResponse { + pub data: String, +} + +#[derive(Deserialize)] +pub struct TokenRenewalResponse { + pub data: TokenRenewalData, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct TokenRenewalData { + pub token: String, + pub username: String, + pub admin: bool, +} diff --git a/src/web/api/client/v1/http.rs b/src/web/api/client/v1/http.rs new file mode 100644 index 00000000..a3b172dc --- /dev/null +++ b/src/web/api/client/v1/http.rs @@ -0,0 +1,57 @@ +pub type ReqwestQuery = Vec; +pub type ReqwestQueryParam = (String, String); + +/// URL Query component +#[derive(Default, Debug)] +pub struct Query { + params: Vec, +} + +impl Query { + #[must_use] + pub fn empty() -> Self { + Self { params: vec![] } + } + + #[must_use] + pub fn with_params(params: Vec) -> Self { + Self { params } + } + + pub fn add_param(&mut self, param: QueryParam) { + self.params.push(param); + } +} + +impl From for ReqwestQuery { + fn from(url_search_params: Query) -> Self { + url_search_params + .params + .iter() + .map(|param| ReqwestQueryParam::from((*param).clone())) + .collect() + } +} + +/// URL query param +#[derive(Clone, Debug)] +pub struct QueryParam { + name: String, + value: String, +} + +impl QueryParam { + #[must_use] + pub fn new(name: &str, value: &str) -> Self { + Self { + name: name.to_string(), + value: value.to_string(), + } + } +} + +impl From for ReqwestQueryParam { + fn from(param: QueryParam) -> Self { + (param.name, param.value) + } +} diff --git a/src/web/api/client/v1/mod.rs b/src/web/api/client/v1/mod.rs new file mode 100644 index 00000000..5d0fbf2f --- /dev/null +++ b/src/web/api/client/v1/mod.rs @@ -0,0 +1,6 @@ +pub mod client; +pub mod connection_info; +pub mod contexts; +pub mod http; +pub mod random; +pub mod responses; diff --git a/src/web/api/client/v1/random.rs b/src/web/api/client/v1/random.rs new file mode 100644 index 00000000..2133dcd2 --- /dev/null +++ b/src/web/api/client/v1/random.rs @@ -0,0 +1,10 @@ +//! Random data generators for testing. +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +/// Returns a random alphanumeric string of a certain size. +/// +/// It is useful for generating random names, IDs, etc for testing. +pub fn string(size: usize) -> String { + thread_rng().sample_iter(&Alphanumeric).take(size).map(char::from).collect() +} diff --git a/src/web/api/client/v1/responses.rs b/src/web/api/client/v1/responses.rs new file mode 100644 index 00000000..aae5a338 --- /dev/null +++ b/src/web/api/client/v1/responses.rs @@ -0,0 +1,89 @@ +use reqwest::Response as ReqwestResponse; + +#[derive(Debug)] +pub struct TextResponse { + pub status: u16, + pub content_type: Option, + pub body: String, +} + +impl TextResponse { + /// # Panics + /// + /// Will panic if: + /// + /// - It can't map the content type in the response header to string. + /// - It can't get the response bytes. + pub async fn from(response: ReqwestResponse) -> Self { + Self { + status: response.status().as_u16(), + content_type: response + .headers() + .get("content-type") + .map(|content_type| content_type.to_str().unwrap().to_owned()), + body: response.text().await.unwrap(), + } + } + + #[must_use] + pub fn is_json_and_ok(&self) -> bool { + self.is_ok() && self.is_json() + } + + #[must_use] + pub fn is_json(&self) -> bool { + if let Some(content_type) = &self.content_type { + return content_type == "application/json"; + } + false + } + + #[must_use] + pub fn is_ok(&self) -> bool { + self.status == 200 + } +} + +#[derive(Debug)] +pub struct BinaryResponse { + pub status: u16, + pub content_type: Option, + pub bytes: Vec, +} + +impl BinaryResponse { + /// # Panics + /// + /// Will panic if: + /// + /// - It can't map the content type in the response header to string. + /// - It can't get the response bytes. + pub async fn from(response: ReqwestResponse) -> Self { + Self { + status: response.status().as_u16(), + content_type: response + .headers() + .get("content-type") + .map(|content_type| content_type.to_str().unwrap().to_owned()), + bytes: response.bytes().await.unwrap().to_vec(), + } + } + + #[must_use] + pub fn is_a_bit_torrent_file(&self) -> bool { + self.is_ok() && self.is_bittorrent_content_type() + } + + #[must_use] + pub fn is_bittorrent_content_type(&self) -> bool { + if let Some(content_type) = &self.content_type { + return content_type == "application/x-bittorrent"; + } + false + } + + #[must_use] + pub fn is_ok(&self) -> bool { + self.status == 200 + } +} diff --git a/src/web/api/mod.rs b/src/web/api/mod.rs index 749008f1..8bfdacd9 100644 --- a/src/web/api/mod.rs +++ b/src/web/api/mod.rs @@ -3,8 +3,8 @@ //! Currently, the API has only one version: `v1`. //! //! Refer to the [`v1`]) module for more information. +pub mod client; pub mod server; -pub mod v1; use std::net::SocketAddr; use std::sync::Arc; diff --git a/src/web/api/server.rs b/src/web/api/server/mod.rs similarity index 93% rename from src/web/api/server.rs rename to src/web/api/server/mod.rs index 8fa1e704..9edda873 100644 --- a/src/web/api/server.rs +++ b/src/web/api/server/mod.rs @@ -1,11 +1,13 @@ +pub mod v1; + use std::net::SocketAddr; use std::sync::Arc; use futures::Future; use log::info; use tokio::sync::oneshot::{self, Sender}; +use v1::routes::router; -use super::v1::routes::router; use super::{Running, ServerStartedMessage}; use crate::common::AppData; @@ -57,7 +59,7 @@ fn start_server( .local_addr() .expect("tcp listener to be bound to a socket address."); - info!("API server listening on http://{}", bound_addr); + info!("API server listening on http://{}", bound_addr); // # DevSkim: ignore DS137138 let app = router(app_data); @@ -70,6 +72,6 @@ fn start_server( server.with_graceful_shutdown(async move { tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - info!("Stopping API server on http://{} ...", bound_addr); + info!("Stopping API server on http://{} ...", bound_addr); // # DevSkim: ignore DS137138 }) } diff --git a/src/web/api/v1/auth.rs b/src/web/api/server/v1/auth.rs similarity index 98% rename from src/web/api/v1/auth.rs rename to src/web/api/server/v1/auth.rs index e52542cc..3e355b30 100644 --- a/src/web/api/v1/auth.rs +++ b/src/web/api/server/v1/auth.rs @@ -86,7 +86,7 @@ use crate::common::AppData; use crate::errors::ServiceError; use crate::models::user::{UserClaims, UserCompact, UserId}; use crate::services::authentication::JsonWebToken; -use crate::web::api::v1::extractors::bearer_token::BearerToken; +use crate::web::api::server::v1::extractors::bearer_token::BearerToken; pub struct Authentication { json_web_token: Arc, diff --git a/src/web/api/v1/contexts/about/handlers.rs b/src/web/api/server/v1/contexts/about/handlers.rs similarity index 89% rename from src/web/api/v1/contexts/about/handlers.rs rename to src/web/api/server/v1/contexts/about/handlers.rs index 07d5977b..76004133 100644 --- a/src/web/api/v1/contexts/about/handlers.rs +++ b/src/web/api/server/v1/contexts/about/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the the [`about`](crate::web::api::v1::contexts::about) API +//! API handlers for the the [`about`](crate::web::api::server::v1::contexts::about) API //! context. use std::sync::Arc; diff --git a/src/web/api/v1/contexts/about/mod.rs b/src/web/api/server/v1/contexts/about/mod.rs similarity index 100% rename from src/web/api/v1/contexts/about/mod.rs rename to src/web/api/server/v1/contexts/about/mod.rs diff --git a/src/web/api/v1/contexts/about/routes.rs b/src/web/api/server/v1/contexts/about/routes.rs similarity index 58% rename from src/web/api/v1/contexts/about/routes.rs rename to src/web/api/server/v1/contexts/about/routes.rs index d3877a3b..8a9ccef7 100644 --- a/src/web/api/v1/contexts/about/routes.rs +++ b/src/web/api/server/v1/contexts/about/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`about`](crate::web::api::v1::contexts::about) API context. +//! API routes for the [`about`](crate::web::api::server::v1::contexts::about) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::about). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::about). use std::sync::Arc; use axum::routing::get; @@ -9,7 +9,7 @@ use axum::Router; use super::handlers::{about_page_handler, license_page_handler}; use crate::common::AppData; -/// Routes for the [`about`](crate::web::api::v1::contexts::about) API context. +/// Routes for the [`about`](crate::web::api::server::v1::contexts::about) API context. pub fn router(app_data: Arc) -> Router { Router::new() .route("/", get(about_page_handler).with_state(app_data.clone())) diff --git a/src/web/api/v1/contexts/category/forms.rs b/src/web/api/server/v1/contexts/category/forms.rs similarity index 100% rename from src/web/api/v1/contexts/category/forms.rs rename to src/web/api/server/v1/contexts/category/forms.rs diff --git a/src/web/api/v1/contexts/category/handlers.rs b/src/web/api/server/v1/contexts/category/handlers.rs similarity index 90% rename from src/web/api/v1/contexts/category/handlers.rs rename to src/web/api/server/v1/contexts/category/handlers.rs index da0c1209..b73b15c5 100644 --- a/src/web/api/v1/contexts/category/handlers.rs +++ b/src/web/api/server/v1/contexts/category/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the the [`category`](crate::web::api::v1::contexts::category) API +//! API handlers for the the [`category`](crate::web::api::server::v1::contexts::category) API //! context. use std::sync::Arc; @@ -8,8 +8,8 @@ use axum::response::{IntoResponse, Json, Response}; use super::forms::{AddCategoryForm, DeleteCategoryForm}; use super::responses::{added_category, deleted_category}; use crate::common::AppData; -use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::{self}; +use crate::web::api::server::v1::extractors::bearer_token::Extract; +use crate::web::api::server::v1::responses::{self}; /// It handles the request to get all the categories. /// @@ -18,7 +18,7 @@ use crate::web::api::v1::responses::{self}; /// - `200` response with a json containing the category list [`Vec`](crate::databases::database::Category). /// - Other error status codes if there is a database error. /// -/// Refer to the [API endpoint documentation](crate::web::api::v1::contexts::category) +/// Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::category) /// for more information about this endpoint. /// /// # Errors diff --git a/src/web/api/v1/contexts/category/mod.rs b/src/web/api/server/v1/contexts/category/mod.rs similarity index 100% rename from src/web/api/v1/contexts/category/mod.rs rename to src/web/api/server/v1/contexts/category/mod.rs diff --git a/src/web/api/v1/contexts/category/responses.rs b/src/web/api/server/v1/contexts/category/responses.rs similarity index 74% rename from src/web/api/v1/contexts/category/responses.rs rename to src/web/api/server/v1/contexts/category/responses.rs index b1e20d19..2f761d2c 100644 --- a/src/web/api/v1/contexts/category/responses.rs +++ b/src/web/api/server/v1/contexts/category/responses.rs @@ -1,8 +1,8 @@ -//! API responses for the the [`category`](crate::web::api::v1::contexts::category) API +//! API responses for the the [`category`](crate::web::api::server::v1::contexts::category) API //! context. use axum::Json; -use crate::web::api::v1::responses::OkResponseData; +use crate::web::api::server::v1::responses::OkResponseData; /// Response after successfully creating a new category. pub fn added_category(category_name: &str) -> Json> { diff --git a/src/web/api/v1/contexts/category/routes.rs b/src/web/api/server/v1/contexts/category/routes.rs similarity index 61% rename from src/web/api/v1/contexts/category/routes.rs rename to src/web/api/server/v1/contexts/category/routes.rs index 2d762c47..df8e728d 100644 --- a/src/web/api/v1/contexts/category/routes.rs +++ b/src/web/api/server/v1/contexts/category/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`category`](crate::web::api::v1::contexts::category) API context. +//! API routes for the [`category`](crate::web::api::server::v1::contexts::category) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::category). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::category). use std::sync::Arc; use axum::routing::{delete, get, post}; @@ -9,7 +9,7 @@ use axum::Router; use super::handlers::{add_handler, delete_handler, get_all_handler}; use crate::common::AppData; -/// Routes for the [`category`](crate::web::api::v1::contexts::category) API context. +/// Routes for the [`category`](crate::web::api::server::v1::contexts::category) API context. pub fn router(app_data: Arc) -> Router { Router::new() .route("/", get(get_all_handler).with_state(app_data.clone())) diff --git a/src/web/api/server/v1/contexts/mod.rs b/src/web/api/server/v1/contexts/mod.rs new file mode 100644 index 00000000..a39cbf4e --- /dev/null +++ b/src/web/api/server/v1/contexts/mod.rs @@ -0,0 +1,19 @@ +//! The API is organized in the following contexts: +//! +//! Context | Description | Version +//! ---|---|--- +//! `About` | Metadata about the API | [`v1`](crate::web::api::server::v1::contexts::about) +//! `Category` | Torrent categories | [`v1`](crate::web::api::server::v1::contexts::category) +//! `Proxy` | Image proxy cache | [`v1`](crate::web::api::server::v1::contexts::proxy) +//! `Settings` | Index settings | [`v1`](crate::web::api::server::v1::contexts::settings) +//! `Tag` | Torrent tags | [`v1`](crate::web::api::server::v1::contexts::tag) +//! `Torrent` | Indexed torrents | [`v1`](crate::web::api::server::v1::contexts::torrent) +//! `User` | Users | [`v1`](crate::web::api::server::v1::contexts::user) +//! +pub mod about; +pub mod category; +pub mod proxy; +pub mod settings; +pub mod tag; +pub mod torrent; +pub mod user; diff --git a/src/web/api/v1/contexts/proxy/handlers.rs b/src/web/api/server/v1/contexts/proxy/handlers.rs similarity index 91% rename from src/web/api/v1/contexts/proxy/handlers.rs rename to src/web/api/server/v1/contexts/proxy/handlers.rs index 1e5105ee..7c04e50f 100644 --- a/src/web/api/v1/contexts/proxy/handlers.rs +++ b/src/web/api/server/v1/contexts/proxy/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the the [`proxy`](crate::web::api::v1::contexts::proxy) API +//! API handlers for the the [`proxy`](crate::web::api::server::v1::contexts::proxy) API //! context. use std::sync::Arc; @@ -9,7 +9,7 @@ use super::responses::png_image; use crate::cache::image::manager::Error; use crate::common::AppData; use crate::ui::proxy::map_error_to_image; -use crate::web::api::v1::extractors::bearer_token::Extract; +use crate::web::api::server::v1::extractors::bearer_token::Extract; /// Get the remote image. It uses the cached image if available. #[allow(clippy::unused_async)] diff --git a/src/web/api/v1/contexts/proxy/mod.rs b/src/web/api/server/v1/contexts/proxy/mod.rs similarity index 100% rename from src/web/api/v1/contexts/proxy/mod.rs rename to src/web/api/server/v1/contexts/proxy/mod.rs diff --git a/src/web/api/v1/contexts/proxy/responses.rs b/src/web/api/server/v1/contexts/proxy/responses.rs similarity index 100% rename from src/web/api/v1/contexts/proxy/responses.rs rename to src/web/api/server/v1/contexts/proxy/responses.rs diff --git a/src/web/api/v1/contexts/proxy/routes.rs b/src/web/api/server/v1/contexts/proxy/routes.rs similarity index 51% rename from src/web/api/v1/contexts/proxy/routes.rs rename to src/web/api/server/v1/contexts/proxy/routes.rs index e6bd7bef..b12ff0ee 100644 --- a/src/web/api/v1/contexts/proxy/routes.rs +++ b/src/web/api/server/v1/contexts/proxy/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`proxy`](crate::web::api::v1::contexts::proxy) API context. +//! API routes for the [`proxy`](crate::web::api::server::v1::contexts::proxy) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::proxy). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::proxy). use std::sync::Arc; use axum::routing::get; @@ -9,7 +9,7 @@ use axum::Router; use super::handlers::get_proxy_image_handler; use crate::common::AppData; -/// Routes for the [`about`](crate::web::api::v1::contexts::about) API context. +/// Routes for the [`about`](crate::web::api::server::v1::contexts::about) API context. pub fn router(app_data: Arc) -> Router { Router::new().route("/image/:url", get(get_proxy_image_handler).with_state(app_data)) } diff --git a/src/web/api/v1/contexts/settings/handlers.rs b/src/web/api/server/v1/contexts/settings/handlers.rs similarity index 87% rename from src/web/api/v1/contexts/settings/handlers.rs rename to src/web/api/server/v1/contexts/settings/handlers.rs index f4d94541..48c0cb91 100644 --- a/src/web/api/v1/contexts/settings/handlers.rs +++ b/src/web/api/server/v1/contexts/settings/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the the [`category`](crate::web::api::v1::contexts::category) API +//! API handlers for the the [`category`](crate::web::api::server::v1::contexts::category) API //! context. use std::sync::Arc; @@ -6,8 +6,8 @@ use axum::extract::State; use axum::response::{IntoResponse, Json, Response}; use crate::common::AppData; -use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses; +use crate::web::api::server::v1::extractors::bearer_token::Extract; +use crate::web::api::server::v1::responses; /// Get all settings. /// diff --git a/src/web/api/v1/contexts/settings/mod.rs b/src/web/api/server/v1/contexts/settings/mod.rs similarity index 100% rename from src/web/api/v1/contexts/settings/mod.rs rename to src/web/api/server/v1/contexts/settings/mod.rs diff --git a/src/web/api/v1/contexts/settings/routes.rs b/src/web/api/server/v1/contexts/settings/routes.rs similarity index 62% rename from src/web/api/v1/contexts/settings/routes.rs rename to src/web/api/server/v1/contexts/settings/routes.rs index e0990f52..ebca3bb6 100644 --- a/src/web/api/v1/contexts/settings/routes.rs +++ b/src/web/api/server/v1/contexts/settings/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`settings`](crate::web::api::v1::contexts::settings) API context. +//! API routes for the [`settings`](crate::web::api::server::v1::contexts::settings) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::settings). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::settings). use std::sync::Arc; use axum::routing::get; @@ -9,7 +9,7 @@ use axum::Router; use super::handlers::{get_all_handler, get_public_handler, get_site_name_handler}; use crate::common::AppData; -/// Routes for the [`category`](crate::web::api::v1::contexts::category) API context. +/// Routes for the [`category`](crate::web::api::server::v1::contexts::category) API context. pub fn router(app_data: Arc) -> Router { Router::new() .route("/", get(get_all_handler).with_state(app_data.clone())) diff --git a/src/web/api/v1/contexts/tag/forms.rs b/src/web/api/server/v1/contexts/tag/forms.rs similarity index 76% rename from src/web/api/v1/contexts/tag/forms.rs rename to src/web/api/server/v1/contexts/tag/forms.rs index 12c751ad..d254b324 100644 --- a/src/web/api/v1/contexts/tag/forms.rs +++ b/src/web/api/server/v1/contexts/tag/forms.rs @@ -1,4 +1,4 @@ -//! API forms for the the [`tag`](crate::web::api::v1::contexts::tag) API +//! API forms for the the [`tag`](crate::web::api::server::v1::contexts::tag) API //! context. use serde::{Deserialize, Serialize}; diff --git a/src/web/api/v1/contexts/tag/handlers.rs b/src/web/api/server/v1/contexts/tag/handlers.rs similarity index 89% rename from src/web/api/v1/contexts/tag/handlers.rs rename to src/web/api/server/v1/contexts/tag/handlers.rs index f750c385..5ba38249 100644 --- a/src/web/api/v1/contexts/tag/handlers.rs +++ b/src/web/api/server/v1/contexts/tag/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the [`tag`](crate::web::api::v1::contexts::tag) API +//! API handlers for the [`tag`](crate::web::api::server::v1::contexts::tag) API //! context. use std::sync::Arc; @@ -8,8 +8,8 @@ use axum::response::{IntoResponse, Json, Response}; use super::forms::{AddTagForm, DeleteTagForm}; use super::responses::{added_tag, deleted_tag}; use crate::common::AppData; -use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::{self}; +use crate::web::api::server::v1::extractors::bearer_token::Extract; +use crate::web::api::server::v1::responses::{self}; /// It handles the request to get all the tags. /// @@ -18,7 +18,7 @@ use crate::web::api::v1::responses::{self}; /// - `200` response with a json containing the tag list [`Vec`](crate::models::torrent_tag::TorrentTag). /// - Other error status codes if there is a database error. /// -/// Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag) +/// Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::tag) /// for more information about this endpoint. /// /// # Errors diff --git a/src/web/api/v1/contexts/tag/mod.rs b/src/web/api/server/v1/contexts/tag/mod.rs similarity index 100% rename from src/web/api/v1/contexts/tag/mod.rs rename to src/web/api/server/v1/contexts/tag/mod.rs diff --git a/src/web/api/v1/contexts/tag/responses.rs b/src/web/api/server/v1/contexts/tag/responses.rs similarity index 74% rename from src/web/api/v1/contexts/tag/responses.rs rename to src/web/api/server/v1/contexts/tag/responses.rs index a1645994..ac1e2c75 100644 --- a/src/web/api/v1/contexts/tag/responses.rs +++ b/src/web/api/server/v1/contexts/tag/responses.rs @@ -1,9 +1,9 @@ -//! API responses for the [`tag`](crate::web::api::v1::contexts::tag) API +//! API responses for the [`tag`](crate::web::api::server::v1::contexts::tag) API //! context. use axum::Json; use crate::models::torrent_tag::TagId; -use crate::web::api::v1::responses::OkResponseData; +use crate::web::api::server::v1::responses::OkResponseData; /// Response after successfully creating a new tag. pub fn added_tag(tag_name: &str) -> Json> { diff --git a/src/web/api/v1/contexts/tag/routes.rs b/src/web/api/server/v1/contexts/tag/routes.rs similarity index 64% rename from src/web/api/v1/contexts/tag/routes.rs rename to src/web/api/server/v1/contexts/tag/routes.rs index 4d72970a..0ec554e4 100644 --- a/src/web/api/v1/contexts/tag/routes.rs +++ b/src/web/api/server/v1/contexts/tag/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`tag`](crate::web::api::v1::contexts::tag) API context. +//! API routes for the [`tag`](crate::web::api::server::v1::contexts::tag) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::tag). use std::sync::Arc; use axum::routing::{delete, get, post}; @@ -11,14 +11,14 @@ use crate::common::AppData; // code-review: should we use `tags` also for single resources? -/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context. +/// Routes for the [`tag`](crate::web::api::server::v1::contexts::tag) API context. pub fn router_for_single_resources(app_data: Arc) -> Router { Router::new() .route("/", post(add_handler).with_state(app_data.clone())) .route("/", delete(delete_handler).with_state(app_data)) } -/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context. +/// Routes for the [`tag`](crate::web::api::server::v1::contexts::tag) API context. pub fn router_for_multiple_resources(app_data: Arc) -> Router { Router::new().route("/", get(get_all_handler).with_state(app_data)) } diff --git a/src/web/api/v1/contexts/torrent/errors.rs b/src/web/api/server/v1/contexts/torrent/errors.rs similarity index 96% rename from src/web/api/v1/contexts/torrent/errors.rs rename to src/web/api/server/v1/contexts/torrent/errors.rs index 9bf24d48..87166419 100644 --- a/src/web/api/v1/contexts/torrent/errors.rs +++ b/src/web/api/server/v1/contexts/torrent/errors.rs @@ -2,7 +2,7 @@ use axum::response::{IntoResponse, Response}; use derive_more::{Display, Error}; use hyper::StatusCode; -use crate::web::api::v1::responses::{json_error_response, ErrorResponseData}; +use crate::web::api::server::v1::responses::{json_error_response, ErrorResponseData}; #[derive(Debug, Display, PartialEq, Eq, Error)] pub enum Request { diff --git a/src/web/api/v1/contexts/torrent/forms.rs b/src/web/api/server/v1/contexts/torrent/forms.rs similarity index 100% rename from src/web/api/v1/contexts/torrent/forms.rs rename to src/web/api/server/v1/contexts/torrent/forms.rs diff --git a/src/web/api/v1/contexts/torrent/handlers.rs b/src/web/api/server/v1/contexts/torrent/handlers.rs similarity index 97% rename from src/web/api/v1/contexts/torrent/handlers.rs rename to src/web/api/server/v1/contexts/torrent/handlers.rs index bab51443..745a81df 100644 --- a/src/web/api/v1/contexts/torrent/handlers.rs +++ b/src/web/api/server/v1/contexts/torrent/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the [`torrent`](crate::web::api::v1::contexts::torrent) API +//! API handlers for the [`torrent`](crate::web::api::server::v1::contexts::torrent) API //! context. use std::io::{Cursor, Write}; use std::str::FromStr; @@ -21,10 +21,10 @@ use crate::models::torrent_tag::TagId; use crate::services::torrent::{AddTorrentRequest, ListingRequest}; use crate::services::torrent_file::generate_random_torrent; use crate::utils::parse_torrent; -use crate::web::api::v1::auth::get_optional_logged_in_user; -use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::OkResponseData; -use crate::web::api::v1::routes::API_VERSION_URL_PREFIX; +use crate::web::api::server::v1::auth::get_optional_logged_in_user; +use crate::web::api::server::v1::extractors::bearer_token::Extract; +use crate::web::api::server::v1::responses::OkResponseData; +use crate::web::api::server::v1::routes::API_VERSION_URL_PREFIX; /// Upload a new torrent file to the Index /// diff --git a/src/web/api/v1/contexts/torrent/mod.rs b/src/web/api/server/v1/contexts/torrent/mod.rs similarity index 99% rename from src/web/api/v1/contexts/torrent/mod.rs rename to src/web/api/server/v1/contexts/torrent/mod.rs index 6b047940..3f915f76 100644 --- a/src/web/api/v1/contexts/torrent/mod.rs +++ b/src/web/api/server/v1/contexts/torrent/mod.rs @@ -271,7 +271,7 @@ //! `tags` | `Option>` | The tag Id list | No | `[1,2,3]` //! //! -//! Refer to the [`UpdateTorrentInfoForm`](crate::web::api::v1::contexts::torrent::forms::UpdateTorrentInfoForm) +//! Refer to the [`UpdateTorrentInfoForm`](crate::web::api::server::v1::contexts::torrent::forms::UpdateTorrentInfoForm) //! struct for more information about the request attributes. //! //! **Example request** diff --git a/src/web/api/v1/contexts/torrent/responses.rs b/src/web/api/server/v1/contexts/torrent/responses.rs similarity index 96% rename from src/web/api/v1/contexts/torrent/responses.rs rename to src/web/api/server/v1/contexts/torrent/responses.rs index 9873b420..d928f675 100644 --- a/src/web/api/v1/contexts/torrent/responses.rs +++ b/src/web/api/server/v1/contexts/torrent/responses.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::models::torrent::TorrentId; use crate::services::torrent::AddTorrentResponse; -use crate::web::api::v1::responses::OkResponseData; +use crate::web::api::server::v1::responses::OkResponseData; #[allow(clippy::module_name_repetitions)] #[derive(Serialize, Deserialize, Debug)] diff --git a/src/web/api/v1/contexts/torrent/routes.rs b/src/web/api/server/v1/contexts/torrent/routes.rs similarity index 75% rename from src/web/api/v1/contexts/torrent/routes.rs rename to src/web/api/server/v1/contexts/torrent/routes.rs index 1c529599..952ff396 100644 --- a/src/web/api/v1/contexts/torrent/routes.rs +++ b/src/web/api/server/v1/contexts/torrent/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`torrent`](crate::web::api::v1::contexts::torrent) API context. +//! API routes for the [`torrent`](crate::web::api::server::v1::contexts::torrent) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::torrent). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::torrent). use std::sync::Arc; use axum::routing::{delete, get, post, put}; @@ -12,7 +12,7 @@ use super::handlers::{ }; use crate::common::AppData; -/// Routes for the [`torrent`](crate::web::api::v1::contexts::torrent) API context for single resources. +/// Routes for the [`torrent`](crate::web::api::server::v1::contexts::torrent) API context for single resources. pub fn router_for_single_resources(app_data: Arc) -> Router { let torrent_info_routes = Router::new() .route("/", get(get_torrent_info_handler).with_state(app_data.clone())) @@ -32,7 +32,7 @@ pub fn router_for_single_resources(app_data: Arc) -> Router { .nest("/:info_hash", torrent_info_routes) } -/// Routes for the [`torrent`](crate::web::api::v1::contexts::torrent) API context for multiple resources. +/// Routes for the [`torrent`](crate::web::api::server::v1::contexts::torrent) API context for multiple resources. pub fn router_for_multiple_resources(app_data: Arc) -> Router { Router::new().route("/", get(get_torrents_handler).with_state(app_data)) } diff --git a/src/web/api/v1/contexts/user/forms.rs b/src/web/api/server/v1/contexts/user/forms.rs similarity index 100% rename from src/web/api/v1/contexts/user/forms.rs rename to src/web/api/server/v1/contexts/user/forms.rs diff --git a/src/web/api/v1/contexts/user/handlers.rs b/src/web/api/server/v1/contexts/user/handlers.rs similarity index 95% rename from src/web/api/v1/contexts/user/handlers.rs rename to src/web/api/server/v1/contexts/user/handlers.rs index 170bd073..ee33d2e0 100644 --- a/src/web/api/v1/contexts/user/handlers.rs +++ b/src/web/api/server/v1/contexts/user/handlers.rs @@ -1,4 +1,4 @@ -//! API handlers for the the [`user`](crate::web::api::v1::contexts::user) API +//! API handlers for the the [`user`](crate::web::api::server::v1::contexts::user) API //! context. use std::sync::Arc; @@ -10,8 +10,8 @@ use serde::Deserialize; use super::forms::{JsonWebToken, LoginForm, RegistrationForm}; use super::responses::{self}; use crate::common::AppData; -use crate::web::api::v1::extractors::bearer_token::Extract; -use crate::web::api::v1::responses::OkResponseData; +use crate::web::api::server::v1::extractors::bearer_token::Extract; +use crate::web::api::server::v1::responses::OkResponseData; // Registration diff --git a/src/web/api/v1/contexts/user/mod.rs b/src/web/api/server/v1/contexts/user/mod.rs similarity index 93% rename from src/web/api/v1/contexts/user/mod.rs rename to src/web/api/server/v1/contexts/user/mod.rs index 4f4682e0..a13c2bb8 100644 --- a/src/web/api/v1/contexts/user/mod.rs +++ b/src/web/api/server/v1/contexts/user/mod.rs @@ -6,7 +6,7 @@ //! - User authentication //! - User ban //! -//! For more information about the API authentication, refer to the [`auth`](crate::web::api::v1::auth) +//! For more information about the API authentication, refer to the [`auth`](crate::web::api::server::v1::auth) //! module. //! //! # Endpoints @@ -50,7 +50,7 @@ //! max_password_length = 64 //! ``` //! -//! Refer to the [`RegistrationForm`](crate::web::api::v1::contexts::user::forms::RegistrationForm) +//! Refer to the [`RegistrationForm`](crate::web::api::server::v1::contexts::user::forms::RegistrationForm) //! struct for more information about the registration form. //! //! **Example request** @@ -63,7 +63,7 @@ //! http://127.0.0.1:3001/v1/user/register //! ``` //! -//! For more information about the registration process, refer to the [`auth`](crate::web::api::v1::auth) +//! For more information about the registration process, refer to the [`auth`](crate::web::api::server::v1::auth) //! module. //! //! # Email verification @@ -97,7 +97,7 @@ //! `login` | `String` | The password | Yes | `indexadmin` //! `password` | `String` | The password | Yes | `BenoitMandelbrot1924` //! -//! Refer to the [`LoginForm`](crate::web::api::v1::contexts::user::forms::LoginForm) +//! Refer to the [`LoginForm`](crate::web::api::server::v1::contexts::user::forms::LoginForm) //! struct for more information about the login form. //! //! **Example request** @@ -110,7 +110,7 @@ //! http://127.0.0.1:3001/v1/user/login //! ``` //! -//! For more information about the login process, refer to the [`auth`](crate::web::api::v1::auth) +//! For more information about the login process, refer to the [`auth`](crate::web::api::server::v1::auth) //! module. //! //! # Token verification @@ -125,7 +125,7 @@ //! ---|---|---|---|--- //! `token` | `String` | The token you want to verify | Yes | `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI` //! -//! Refer to the [`JsonWebToken`](crate::web::api::v1::contexts::user::forms::JsonWebToken) +//! Refer to the [`JsonWebToken`](crate::web::api::server::v1::contexts::user::forms::JsonWebToken) //! struct for more information about the token. //! //! **Example request** @@ -171,7 +171,7 @@ //! ---|---|---|---|--- //! `token` | `String` | The current valid token | Yes | `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI` //! -//! Refer to the [`JsonWebToken`](crate::web::api::v1::contexts::user::forms::JsonWebToken) +//! Refer to the [`JsonWebToken`](crate::web::api::server::v1::contexts::user::forms::JsonWebToken) //! struct for more information about the token. //! //! **Example request** diff --git a/src/web/api/v1/contexts/user/responses.rs b/src/web/api/server/v1/contexts/user/responses.rs similarity index 95% rename from src/web/api/v1/contexts/user/responses.rs rename to src/web/api/server/v1/contexts/user/responses.rs index 17a06bdf..fde7c78b 100644 --- a/src/web/api/v1/contexts/user/responses.rs +++ b/src/web/api/server/v1/contexts/user/responses.rs @@ -2,7 +2,7 @@ use axum::Json; use serde::{Deserialize, Serialize}; use crate::models::user::{UserCompact, UserId}; -use crate::web::api::v1::responses::OkResponseData; +use crate::web::api::server::v1::responses::OkResponseData; // Registration diff --git a/src/web/api/v1/contexts/user/routes.rs b/src/web/api/server/v1/contexts/user/routes.rs similarity index 83% rename from src/web/api/v1/contexts/user/routes.rs rename to src/web/api/server/v1/contexts/user/routes.rs index b2a21624..04ae9980 100644 --- a/src/web/api/v1/contexts/user/routes.rs +++ b/src/web/api/server/v1/contexts/user/routes.rs @@ -1,6 +1,6 @@ -//! API routes for the [`user`](crate::web::api::v1::contexts::user) API context. +//! API routes for the [`user`](crate::web::api::server::v1::contexts::user) API context. //! -//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::user). +//! Refer to the [API endpoint documentation](crate::web::api::server::v1::contexts::user). use std::sync::Arc; use axum::routing::{delete, get, post}; @@ -11,7 +11,7 @@ use super::handlers::{ }; use crate::common::AppData; -/// Routes for the [`user`](crate::web::api::v1::contexts::user) API context. +/// Routes for the [`user`](crate::web::api::server::v1::contexts::user) API context. pub fn router(app_data: Arc) -> Router { Router::new() // Registration diff --git a/src/web/api/v1/extractors/bearer_token.rs b/src/web/api/server/v1/extractors/bearer_token.rs similarity index 93% rename from src/web/api/v1/extractors/bearer_token.rs rename to src/web/api/server/v1/extractors/bearer_token.rs index 1c9b5be9..7a166503 100644 --- a/src/web/api/v1/extractors/bearer_token.rs +++ b/src/web/api/server/v1/extractors/bearer_token.rs @@ -4,7 +4,7 @@ use axum::http::request::Parts; use axum::response::Response; use serde::Deserialize; -use crate::web::api::v1::auth::parse_token; +use crate::web::api::server::v1::auth::parse_token; pub struct Extract(pub Option); diff --git a/src/web/api/v1/extractors/mod.rs b/src/web/api/server/v1/extractors/mod.rs similarity index 100% rename from src/web/api/v1/extractors/mod.rs rename to src/web/api/server/v1/extractors/mod.rs diff --git a/src/web/api/v1/mod.rs b/src/web/api/server/v1/mod.rs similarity index 100% rename from src/web/api/v1/mod.rs rename to src/web/api/server/v1/mod.rs diff --git a/src/web/api/v1/responses.rs b/src/web/api/server/v1/responses.rs similarity index 100% rename from src/web/api/v1/responses.rs rename to src/web/api/server/v1/responses.rs diff --git a/src/web/api/v1/routes.rs b/src/web/api/server/v1/routes.rs similarity index 100% rename from src/web/api/v1/routes.rs rename to src/web/api/server/v1/routes.rs diff --git a/src/web/api/v1/contexts/mod.rs b/src/web/api/v1/contexts/mod.rs deleted file mode 100644 index f6ef4069..00000000 --- a/src/web/api/v1/contexts/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! The API is organized in the following contexts: -//! -//! Context | Description | Version -//! ---|---|--- -//! `About` | Metadata about the API | [`v1`](crate::web::api::v1::contexts::about) -//! `Category` | Torrent categories | [`v1`](crate::web::api::v1::contexts::category) -//! `Proxy` | Image proxy cache | [`v1`](crate::web::api::v1::contexts::proxy) -//! `Settings` | Index settings | [`v1`](crate::web::api::v1::contexts::settings) -//! `Tag` | Torrent tags | [`v1`](crate::web::api::v1::contexts::tag) -//! `Torrent` | Indexed torrents | [`v1`](crate::web::api::v1::contexts::torrent) -//! `User` | Users | [`v1`](crate::web::api::v1::contexts::user) -//! -pub mod about; -pub mod category; -pub mod proxy; -pub mod settings; -pub mod tag; -pub mod torrent; -pub mod user; diff --git a/src/web/mod.rs b/src/web/mod.rs index 9007e88f..d51f4b78 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -2,5 +2,5 @@ //! //! Currently, the API has only one version: `v1`. //! -//! Refer to the [`v1`](crate::web::api::v1) module for more information. +//! Refer to the [`v1`](crate::web::api::server::v1) module for more information. pub mod api; diff --git a/tests/common/asserts.rs b/tests/common/asserts.rs index 50a60e26..0760d1cd 100644 --- a/tests/common/asserts.rs +++ b/tests/common/asserts.rs @@ -1,6 +1,6 @@ // Text responses -use torrust_index::web::api::v1::responses::ErrorResponseData; +use torrust_index::web::api::server::v1::responses::ErrorResponseData; use super::responses::TextResponse; diff --git a/tests/e2e/web/api/v1/contexts/torrent/steps.rs b/tests/e2e/web/api/v1/contexts/torrent/steps.rs index 56c7a648..3782f307 100644 --- a/tests/e2e/web/api/v1/contexts/torrent/steps.rs +++ b/tests/e2e/web/api/v1/contexts/torrent/steps.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use torrust_index::models::info_hash::InfoHash; -use torrust_index::web::api::v1::responses::ErrorResponseData; +use torrust_index::web::api::server::v1::responses::ErrorResponseData; use crate::common::client::Client; use crate::common::contexts::torrent::fixtures::{random_torrent, TestTorrent, TorrentIndexInfo, TorrentListedInIndex};