From 9a93803a1ec8c2b6039d69c81e8194a1cb76b015 Mon Sep 17 00:00:00 2001 From: angrynode Date: Fri, 29 May 2026 10:52:36 +0200 Subject: [PATCH 1/4] feat: Allow submitting magnet/torrent in a folder --- Cargo.lock | 117 ++++++++- Cargo.toml | 6 +- src/database/content_folder/model.rs | 3 + src/database/mod.rs | 1 + src/database/operation.rs | 3 + src/database/operator.rs | 5 + src/database/torrent/error.rs | 25 ++ src/database/torrent/mod.rs | 8 + src/database/torrent/model.rs | 26 ++ src/database/torrent/operation.rs | 33 +++ src/database/torrent/operator.rs | 238 ++++++++++++++++++ src/lib.rs | 9 + src/migration/m20260528_090000_add_torrent.rs | 51 ++++ src/migration/mod.rs | 6 +- src/routes/content_folder.rs | 2 +- src/routes/mod.rs | 1 + src/routes/torrent.rs | 110 ++++++++ src/state/error.rs | 3 + .../content_folders/dropdown_actions.html | 76 +++++- templates/torrent/list.html | 36 +++ 20 files changed, 748 insertions(+), 11 deletions(-) create mode 100644 src/database/torrent/error.rs create mode 100644 src/database/torrent/mod.rs create mode 100644 src/database/torrent/model.rs create mode 100644 src/database/torrent/operation.rs create mode 100644 src/database/torrent/operator.rs create mode 100644 src/migration/m20260528_090000_add_torrent.rs create mode 100644 src/routes/torrent.rs create mode 100644 templates/torrent/list.html diff --git a/Cargo.lock b/Cargo.lock index f3eaea2..2b65cdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayvec" version = "0.7.6" @@ -438,6 +444,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde_core", @@ -504,6 +511,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "axum_typed_multipart" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b119aa21c8511fd05757a37f521d20b65bc85cfc41415b2281a01b8460c39764" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "axum_typed_multipart_macros", + "bytes", + "futures-core", + "futures-util", + "thiserror", +] + +[[package]] +name = "axum_typed_multipart_macros" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d752f8e44098682e56c28027836ada161798a90ec0f3430f7f079fc6f16554a3" +dependencies = [ + "darling 0.23.0", + "heck 0.5.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", + "ubyte", +] + [[package]] name = "base64" version = "0.22.1" @@ -965,6 +1003,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -992,6 +1040,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -1014,6 +1075,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + [[package]] name = "der" version = "0.7.10" @@ -1128,6 +1200,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "1.0.0" @@ -1488,13 +1569,14 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hightorrent" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224af163ca2cc8a7e931071877cd38dd8af1da9a4a3efd647b728364f4916339" +checksum = "47427359411eab613e38d102e22c335560af3b539c7b14dec9a1007a7cdbc2f8" dependencies = [ "bt_bencode", "fluent-uri", "rustc-hex", + "sea-orm", "serde", "sha1", "sha256", @@ -1502,8 +1584,9 @@ dependencies = [ [[package]] name = "hightorrent_api" -version = "0.2.1" -source = "git+https://github.com/angrynode/hightorrent_api#2288d5325d5ad4130e80cb8f714a130c54a60397" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f739f85e5f16c06b90ad2cc42aa61bd555b94a76e682b6c7be8bb1c085727910" dependencies = [ "async-trait", "hightorrent", @@ -1635,7 +1718,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -2137,6 +2220,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "nix" version = "0.26.4" @@ -3908,6 +4008,7 @@ dependencies = [ "async-tempfile", "axum", "axum-extra", + "axum_typed_multipart", "camino", "chrono", "clap", @@ -4043,6 +4144,12 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" + [[package]] name = "unic-langid" version = "0.9.6" diff --git a/Cargo.toml b/Cargo.toml index 52d3b18..88799aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ askama = "0.16" askama_web = { version = "0.16", features = ["axum-0.8"] } axum = { version = "0.8.9", features = ["macros"] } axum-extra = { version = "0.12.6", features = ["cookie"] } +# For torrent uploads +axum_typed_multipart = { version = "0.16.5", default-features = false } # UTF-8 paths for easier String/PathBuf interop camino = { version = "1.2.2", features = ["serde1"] } # Date/time management @@ -36,8 +38,8 @@ env_logger = "0.11.10" # Interactions with the torrent client # Comment/uncomment below for development version # hightorrent_api = { path = "../hightorrent_api" } -hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api" } -# hightorrent_api = "0.2" +# hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api", branch = "feat-sea-orm", features = [ "sea_orm" ] } +hightorrent_api = { version = "0.2.2", features = [ "sea_orm" ] } log = "0.4.29" # SQLite ORM sea-orm = { version = "2.0.0-rc.38", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] } diff --git a/src/database/content_folder/model.rs b/src/database/content_folder/model.rs index 215a1b2..38632da 100644 --- a/src/database/content_folder/model.rs +++ b/src/database/content_folder/model.rs @@ -1,6 +1,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use sea_orm::entity::prelude::*; +use crate::database::torrent; use crate::extractors::normalized_path::NormalizedPathComponent; /// A content folder to store associated files. @@ -18,6 +19,8 @@ pub struct Model { pub parent_id: Option, #[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")] pub parent: HasOne, + #[sea_orm(has_many)] + pub torrents: HasMany, } #[async_trait::async_trait] diff --git a/src/database/mod.rs b/src/database/mod.rs index 4fd7b9e..b5223a3 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -2,3 +2,4 @@ pub mod content_folder; pub mod operation; pub mod operator; +pub mod torrent; diff --git a/src/database/operation.rs b/src/database/operation.rs index ab52b2e..78edfbd 100644 --- a/src/database/operation.rs +++ b/src/database/operation.rs @@ -3,6 +3,7 @@ use derive_more::Display; use serde::{Deserialize, Serialize}; use crate::database::content_folder; +use crate::database::torrent; use crate::extractors::user::User; /// Type of operation applied to the database. @@ -16,6 +17,7 @@ pub enum OperationType { #[derive(Clone, Debug, Display, Serialize, Deserialize)] pub enum Table { ContentFolder, + Torrent, } /// Operation applied to the database. @@ -25,6 +27,7 @@ pub enum Table { #[serde(untagged)] pub enum Operation { ContentFolder(content_folder::ContentFolderOperation), + Torrent(torrent::TorrentOperation), } impl std::fmt::Display for Operation { diff --git a/src/database/operator.rs b/src/database/operator.rs index 8bce469..975db18 100644 --- a/src/database/operator.rs +++ b/src/database/operator.rs @@ -4,6 +4,7 @@ use std::ops::Deref; use crate::database::content_folder::ContentFolderOperator; use crate::database::operation::{Operation, OperationLog, OperationType, Table}; +use crate::database::torrent::TorrentOperator; use crate::extractors::user::User; use crate::state::AppState; use crate::state::logger::LoggerError; @@ -64,4 +65,8 @@ impl DatabaseOperator { pub fn content_folder<'a>(&'a self) -> ContentFolderOperator<'a> { ContentFolderOperator { db: self } } + + pub fn torrent<'a>(&'a self) -> TorrentOperator<'a> { + TorrentOperator { db: self } + } } diff --git a/src/database/torrent/error.rs b/src/database/torrent/error.rs new file mode 100644 index 0000000..870cee3 --- /dev/null +++ b/src/database/torrent/error.rs @@ -0,0 +1,25 @@ +use hightorrent_api::hightorrent::{MagnetLinkError, TorrentFileError, TorrentID}; +use snafu::prelude::*; + +use crate::state::logger::LoggerError; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum TorrentError { + #[snafu(display("Submitted torrent file is invalid"))] + InvalidFile { source: TorrentFileError }, + #[snafu(display("The magnet is invalid"))] + InvalidLink { source: MagnetLinkError }, + #[snafu(display("Database error"))] + DB { source: sea_orm::DbErr }, + #[snafu(display("The torrent (ID: {id}) does not exist"))] + NotFound { id: i32 }, + #[snafu(display("The torrent (TorrentID: {id}) does not exist"))] + NotFoundTorrentID { id: TorrentID }, + #[snafu(display("Failed to save the operation log"))] + Logger { source: LoggerError }, + #[snafu(display("Requested content folder not found"))] + NoSuchContentFolder { id: i32 }, + #[snafu(display("The torrent ID {torrent_id} is already imported"))] + Duplicate { torrent_id: TorrentID }, +} diff --git a/src/database/torrent/mod.rs b/src/database/torrent/mod.rs new file mode 100644 index 0000000..77fdf0d --- /dev/null +++ b/src/database/torrent/mod.rs @@ -0,0 +1,8 @@ +mod error; +pub use error::*; +mod model; +pub use model::*; +mod operation; +pub use operation::*; +mod operator; +pub use operator::*; diff --git a/src/database/torrent/model.rs b/src/database/torrent/model.rs new file mode 100644 index 0000000..e5a070f --- /dev/null +++ b/src/database/torrent/model.rs @@ -0,0 +1,26 @@ +use hightorrent_api::hightorrent::{MagnetLink, TorrentFile, TorrentID}; +use sea_orm::entity::prelude::*; + +use crate::database::content_folder; + +/// A category to store associated files. +/// +/// Each category has a name and an associated path on disk, where +/// symlinks to the content will be created. +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "torrent")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: TorrentID, + pub torrent_file: Option, + pub magnet_link: MagnetLink, + pub name: String, + pub content_folder_id: i32, + #[sea_orm(belongs_to, from = "content_folder_id", to = "id")] + pub content_folder: HasOne, +} + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/database/torrent/operation.rs b/src/database/torrent/operation.rs new file mode 100644 index 0000000..5c1f388 --- /dev/null +++ b/src/database/torrent/operation.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +use crate::database::operation::Operation; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum TorrentOperation { + ImportTorrent { + id: i32, + name: String, + folder: (i32, String), + }, + ImportMagnet { + id: i32, + name: String, + folder: (i32, String), + }, + ResolveMagnet { + id: i32, + name: String, + }, + MoveTorrent { + id: i32, + name: String, + previous_folder: (i32, String), + new_folder: (i32, String), + }, +} + +impl From for Operation { + fn from(o: TorrentOperation) -> Self { + Self::Torrent(o) + } +} diff --git a/src/database/torrent/operator.rs b/src/database/torrent/operator.rs new file mode 100644 index 0000000..2961a86 --- /dev/null +++ b/src/database/torrent/operator.rs @@ -0,0 +1,238 @@ +use hightorrent_api::hightorrent::{MagnetLink, TorrentFile, TorrentID}; +use sea_orm::*; +use snafu::prelude::*; + +use std::ops::Deref; + +use crate::database::content_folder; +use crate::database::operation::Table; +use crate::database::operator::{DatabaseOperator, TableOperator}; + +use super::*; + +#[derive(Clone, Debug)] +pub struct TorrentOperator<'a> { + pub db: &'a DatabaseOperator, +} + +impl Deref for TorrentOperator<'_> { + type Target = DatabaseOperator; + + fn deref(&self) -> &DatabaseOperator { + self.db + } +} + +impl TableOperator for TorrentOperator<'_> { + fn table(&self) -> Table { + Table::Torrent + } +} + +impl TorrentOperator<'_> { + /// List torrents with related content_folder + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn list_with_related(&self) -> Result, TorrentError> { + Entity::load() + .with(content_folder::Entity) + .all(&self.state.database) + .await + .context(DBSnafu) + } + + /// List torrents + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn list(&self) -> Result, TorrentError> { + Entity::find() + .all(&self.state.database) + .await + .context(DBSnafu) + } + + /// Count torrents + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn count(&self) -> Result { + // TODO: there may be a faster sea_orm operation for this + Ok(self.list().await?.len()) + } + + pub async fn get(&self, id: i32) -> Result { + let db = &self.state.database; + + Entity::find_by_id(id) + .one(db) + .await + .context(DBSnafu)? + .ok_or(TorrentError::NotFound { id }) + } + + pub async fn get_by_torrent_id(&self, id: &TorrentID) -> Result { + let db = &self.state.database; + + Entity::find() + .filter(Column::TorrentId.eq(id.clone())) + .one(db) + .await + .context(DBSnafu)? + .ok_or(TorrentError::NotFoundTorrentID { id: id.clone() }) + } + + /// Import a magnet link + /// + /// Fails if: + /// + /// - the torrent is already imported + /// - the magnet file is invalid + pub async fn import_magnet( + &self, + folder: content_folder::Model, + magnet: String, + ) -> Result { + let magnet = MagnetLink::new(&magnet).context(InvalidLinkSnafu)?; + let torrent_id = magnet.id(); + + // Check duplicates + let list = self.list().await?; + + if list.iter().find(|x| x.torrent_id == torrent_id).is_some() { + return Err(TorrentError::Duplicate { torrent_id }); + } + + let model = ActiveModel { + torrent_id: Set(torrent_id.clone()), + torrent_file: Set(None), + magnet_link: Set(magnet.clone()), + name: Set(magnet.name().to_string()), + content_folder_id: Set(folder.id), + ..Default::default() + } + .save(&self.state.database) + .await + .context(DBSnafu)?; + + // Should not fail + let model = model.try_into_model().unwrap(); + + self.log_create(TorrentOperation::ImportMagnet { + id: model.id, + name: magnet.name().to_string(), + folder: (folder.id, folder.name.to_string()), + }) + .await + .context(LoggerSnafu)?; + + Ok(model) + } + + /// Import a torrent file. If there was previously an unresolved magnet + /// with the same torrent ID, the previous folder will be overriden. + /// + /// Fails if: + /// + /// - the torrent is already imported (and resolved if it was a magnet link) + /// - the torrent file is invalid + pub async fn import_torrent( + &self, + folder: content_folder::Model, + file: axum::body::Bytes, + ) -> Result { + let torrent = TorrentFile::from_slice(&file).context(InvalidFileSnafu)?; + let torrent_id = torrent.id(); + + // Check duplicates + let list = self.list().await?; + + if let Some(already_torrent) = list.iter().find(|x| x.torrent_id == torrent_id) { + return self + .update_from_torrent_file(already_torrent.clone(), folder, torrent) + .await; + } + + let model = ActiveModel { + torrent_id: Set(torrent.id()), + torrent_file: Set(Some(torrent.clone())), + magnet_link: Set(torrent.magnet_link().unwrap()), + name: Set(torrent.name().to_string()), + content_folder_id: Set(folder.id), + ..Default::default() + } + .save(&self.state.database) + .await + .context(DBSnafu)?; + + // Should not fail + let model = model.try_into_model().unwrap(); + + self.log_create(TorrentOperation::ImportTorrent { + id: model.id, + name: torrent.name, + folder: (folder.id, folder.name.to_string()), + }) + .await + .context(LoggerSnafu)?; + + Ok(model) + } + + /// Internal method used by `import_torrent`. + /// + /// Resolves a magnet to a torrent, or if it's already resolved, errors because of duplicate. + /// Also updates the associated content folder. + async fn update_from_torrent_file( + &self, + torrent: Model, + folder: content_folder::Model, + file: TorrentFile, + ) -> Result { + if torrent.torrent_file.is_some() { + // TODO: merge trackers found in magnet/torrent? + return Err(TorrentError::Duplicate { + torrent_id: torrent.torrent_id, + }); + } + + let previous_folder = self + .content_folder() + .find_by_id(torrent.content_folder_id) + .await + .unwrap(); + + // TODO: cancel magnet resolution + let mut active_model: ActiveModel = torrent.clone().into(); + active_model.torrent_file = Set(Some(file)); + + let mut folder_operation_log = None; + + // Override previous folder if needed + if previous_folder.id != folder.id { + active_model.content_folder_id = Set(folder.id); + folder_operation_log = Some(TorrentOperation::MoveTorrent { + id: torrent.id, + name: torrent.name.clone(), + previous_folder: (previous_folder.id, previous_folder.name.to_string()), + new_folder: (folder.id, folder.name.to_string()), + }); + } + + let torrent = active_model + .update(&self.state.database) + .await + .context(DBSnafu)?; + + self.log_update(TorrentOperation::ResolveMagnet { + id: torrent.id, + name: torrent.name.to_string(), + }) + .await + .context(LoggerSnafu)?; + + if let Some(operation) = folder_operation_log { + self.log_update(operation).await.context(LoggerSnafu)?; + } + + Ok(torrent) + } +} diff --git a/src/lib.rs b/src/lib.rs index 4e25348..32dd07e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,15 @@ pub fn router(state: state::AppState) -> Router { "/folders/{id}", post(routes::content_folder::create_subfolder), ) + .route( + "/folders/{id}/upload_torrent", + post(routes::torrent::upload_torrent), + ) + .route( + "/folders/{id}/upload_magnet", + post(routes::torrent::upload_magnet), + ) + .route("/torrent", get(routes::torrent::list)) .route("/logs", get(routes::logs::index)) // Register static assets routes .nest("/assets", static_router()) diff --git a/src/migration/m20260528_090000_add_torrent.rs b/src/migration/m20260528_090000_add_torrent.rs new file mode 100644 index 0000000..f9fdfc0 --- /dev/null +++ b/src/migration/m20260528_090000_add_torrent.rs @@ -0,0 +1,51 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +use crate::migration::m20251113_203047_add_content_folder::ContentFolder; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Torrent::Table) + .if_not_exists() + .col(pk_auto(Torrent::Id)) + .col(string(Torrent::TorrentID).unique_key()) + .col(var_binary(Torrent::TorrentFile, 0).null()) + .col(string(Torrent::MagnetLink)) + .col(string(Torrent::Name)) + .col(ColumnDef::new(Torrent::ContentFolderId).integer().null()) + .foreign_key( + ForeignKey::create() + .name("fk-magnet-content_folder_id") + .from(Torrent::Table, Torrent::ContentFolderId) + .to(ContentFolder::Table, ContentFolder::Id), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Torrent::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum Torrent { + Table, + Id, + #[allow(clippy::enum_variant_names)] + TorrentID, + #[allow(clippy::enum_variant_names)] + TorrentFile, + MagnetLink, + Name, + ContentFolderId, +} diff --git a/src/migration/mod.rs b/src/migration/mod.rs index 9384250..5d45406 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20251113_203047_add_content_folder; +mod m20260528_090000_add_torrent; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20251113_203047_add_content_folder::Migration)] + vec![ + Box::new(m20251113_203047_add_content_folder::Migration), + Box::new(m20260528_090000_add_torrent::Migration), + ] } } diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 4d5c031..2633af7 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -34,7 +34,7 @@ pub struct ContentFolderShowTemplate { } impl ContentFolderShowTemplate { - fn new(context: AppStateContext, folder: content_folder::FolderView) -> Self { + pub fn new(context: AppStateContext, folder: content_folder::FolderView) -> Self { let content_folder::FolderView { ancestors, children, diff --git a/src/routes/mod.rs b/src/routes/mod.rs index cb276de..5838fb3 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,3 +1,4 @@ pub mod content_folder; pub mod logs; pub mod progress; +pub mod torrent; diff --git a/src/routes/torrent.rs b/src/routes/torrent.rs new file mode 100644 index 0000000..d1a6fc6 --- /dev/null +++ b/src/routes/torrent.rs @@ -0,0 +1,110 @@ +use askama::Template; +use askama_web::WebTemplate; +use axum::extract::{Form, Path}; +use axum::response::{IntoResponse, Response}; +use axum_extra::extract::CookieJar; +use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; +use serde::Deserialize; +use snafu::prelude::*; + +use crate::database::content_folder::FolderView; +use crate::database::torrent; +use crate::routes::content_folder::ContentFolderShowTemplate; +use crate::state::flash_message::{FallibleTemplate, OperationStatus, StatusCookie}; +use crate::state::{AppStateContext, error::*}; + +/// POST multipart form submitted to /folders/ID/upload_torrent. +/// +/// At this point, we don't want to do all the sanity checks, +/// simply ensure the data is well-formed to further process +/// the request. +#[derive(Clone, Debug, TryFromMultipart)] +pub struct TorrentForm { + // axum_typed_multipart doesn't work with Vec with non-UTF8 + // content, see https://github.com/murar8/axum_typed_multipart/issues/88 + pub file: axum::body::Bytes, +} + +/// POST form submitted to /folders/ID/upload_magnet +#[derive(Clone, Debug, Deserialize)] +pub struct MagnetForm { + pub magnet: String, +} + +pub async fn upload_torrent( + context: AppStateContext, + Path(id): Path, + jar: CookieJar, + TypedMultipart(form): TypedMultipart, +) -> Result { + let view = FolderView::from_id(&context.db.content_folder(), id).await?; + + if let Err(e) = context + .db + .torrent() + .import_torrent(view.folder.clone().unwrap(), form.file) + .await + .context(TorrentSnafu) + { + let mut template = ContentFolderShowTemplate::new(context, view); + template.with_optional_flash(Some(OperationStatus::error(e))); + return Ok(template.into_response()); + } + + let status = StatusCookie::success( + jar, + "The torrent has been uploaded and is now awaiting confirmation".to_string(), + ); + Ok(status.redirect("/torrent").into_response()) +} + +pub async fn upload_magnet( + context: AppStateContext, + Path(id): Path, + jar: CookieJar, + Form(form): Form, +) -> Result { + let view = FolderView::from_id(&context.db.content_folder(), id).await?; + + if let Err(e) = context + .db + .torrent() + .import_magnet(view.folder.clone().unwrap(), form.magnet) + .await + .context(TorrentSnafu) + { + let mut template = ContentFolderShowTemplate::new(context, view); + template.with_optional_flash(Some(OperationStatus::error(e))); + return Ok(template.into_response()); + } + + let status = StatusCookie::success( + jar, + "The magnet has been uploaded and is now resolving".to_string(), + ); + Ok(status.redirect("/torrent").into_response()) +} + +#[derive(Template, WebTemplate)] +#[template(path = "torrent/list.html")] +pub struct TorrentListTemplate { + /// Global application state (errors/warnings) + pub state: AppStateContext, + /// Torrents stored in database + pub torrents: Vec, +} + +pub async fn list(context: AppStateContext) -> Result { + let torrents = context + .db + .torrent() + .list_with_related() + .await + .boxed() + .context(OtherSnafu)?; + + Ok(TorrentListTemplate { + state: context, + torrents, + }) +} diff --git a/src/state/error.rs b/src/state/error.rs index e9ad45f..b70894d 100644 --- a/src/state/error.rs +++ b/src/state/error.rs @@ -6,6 +6,7 @@ use snafu::ErrorCompat; use snafu::prelude::*; use crate::database::content_folder::ContentFolderError; +use crate::database::torrent::TorrentError; use crate::migration::DbErr as MigrationError; use crate::state::free_space::FreeSpaceError; use crate::state::logger::LoggerError; @@ -31,6 +32,8 @@ pub enum AppStateError { Migration { source: MigrationError }, #[snafu(display("Content folder error"))] ContentFolder { source: ContentFolderError }, + #[snafu(display("Torrent error"))] + Torrent { source: TorrentError }, #[snafu(display("IO error"))] IO { source: std::io::Error }, #[snafu(display("{reason}"))] diff --git a/templates/content_folders/dropdown_actions.html b/templates/content_folders/dropdown_actions.html index f7db052..9f5ea07 100644 --- a/templates/content_folders/dropdown_actions.html +++ b/templates/content_folders/dropdown_actions.html @@ -4,7 +4,79 @@ + +{% if let Some(folder) = folder %} + + + +{% endif %} diff --git a/templates/torrent/list.html b/templates/torrent/list.html new file mode 100644 index 0000000..07c4674 --- /dev/null +++ b/templates/torrent/list.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block main %} +
+

List of uploaded torrents

+ +
+
+
+ + + + + + + + + {% for torrent in torrents %} + + + + + + {% endfor %} +
StatusNameActions
+ {% if torrent.torrent_file.is_none() %} + + {% else %} + + {% endif %} + {{ torrent.name }}Aller dans le dossier
+
+
+
+
+{% endblock main %} From c7dee48a9f37e977a999689a8323e7992474a213 Mon Sep 17 00:00:00 2001 From: angrynode Date: Fri, 29 May 2026 11:59:04 +0200 Subject: [PATCH 2/4] feat: Resolve magnets using librqbit --- Cargo.lock | 1400 +++++++++++++++++++++++++++++- Cargo.toml | 2 + src/config.rs | 11 + src/database/torrent/operator.rs | 15 +- src/lib.rs | 14 +- src/resolver.rs | 215 +++++ src/state/mod.rs | 11 +- 7 files changed, 1628 insertions(+), 40 deletions(-) create mode 100644 src/resolver.rs diff --git a/Cargo.lock b/Cargo.lock index 2b65cdd..6aaed40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -170,7 +179,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.16.1", - "num-complex", + "num-complex 0.4.6", "num-integer", "num-traits", ] @@ -361,6 +370,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "assert_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e2651f366b7ee3f97729fded1441539b49d5f39eeb05b842689e11e84501b2" +dependencies = [ + "const_panic", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -478,6 +508,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "serde_html_form", + "serde_path_to_error", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-extra" version = "0.12.6" @@ -524,7 +579,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -542,6 +597,17 @@ dependencies = [ "ubyte", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "base64" version = "0.22.1" @@ -577,6 +643,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -610,7 +696,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -745,6 +831,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] + [[package]] name = "chrono" version = "0.4.44" @@ -816,6 +913,41 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "commoncrypto" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d056a8586ba25a1e4d61cb090900e495952c7886786fc55f909ab2f819b69007" +dependencies = [ + "commoncrypto-sys", +] + +[[package]] +name = "commoncrypto-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fed34f46747aa73dfaa578069fd8279d2818ade2b55f38f22a9401c7f4083e2" +dependencies = [ + "libc", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -857,6 +989,15 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -895,6 +1036,16 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -970,7 +1121,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -983,6 +1134,18 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "crypto-hash" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a77162240fd97248d19a564a565eb563a3f592b386e4136fb300909e67dddca" +dependencies = [ + "commoncrypto", + "hex 0.3.2", + "openssl", + "winapi", +] + [[package]] name = "darling" version = "0.20.11" @@ -1086,6 +1249,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "der" version = "0.7.10" @@ -1153,6 +1337,27 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + [[package]] name = "display_full_error" version = "1.1.0" @@ -1179,6 +1384,18 @@ dependencies = [ "litrs", ] +[[package]] +name = "dontfrag" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117949f5a8b25ba471b4dfea927a07a2f8730c585532a2eb4294e18f5155397" +dependencies = [ + "cfg-if", + "libc", + "tokio", + "windows-sys 0.52.0", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1270,6 +1487,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1328,7 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1365,6 +1588,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1380,6 +1624,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1447,12 +1706,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1463,6 +1729,15 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1489,11 +1764,26 @@ name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", + "wasip3", ] [[package]] @@ -1502,6 +1792,41 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "governor" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.4", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "half" version = "2.7.1" @@ -1523,6 +1848,12 @@ dependencies = [ "ahash 0.7.8", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1531,7 +1862,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1539,6 +1870,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -1561,6 +1897,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" + [[package]] name = "hex" version = "0.4.3" @@ -1701,6 +2043,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1830,8 +2188,14 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" @@ -1899,6 +2263,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "intervaltree" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270bc34e57047cab801a8c871c124d9dc7132f6473c6401f645524f4e6edd111" +dependencies = [ + "smallvec", +] + [[package]] name = "intl-memoizer" version = "0.5.3" @@ -2008,6 +2381,23 @@ dependencies = [ "spin", ] +[[package]] +name = "leaky-bucket" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a396bb213c2d09ed6c5495fd082c991b6ab39c9daf4fff59e6727f85c73e4c5" +dependencies = [ + "parking_lot", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lexical-core" version = "1.0.6" @@ -2088,6 +2478,302 @@ dependencies = [ "redox_syscall 0.7.1", ] +[[package]] +name = "librqbit" +version = "9.0.0-beta.2" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "arc-swap", + "async-compression", + "async-stream", + "async-trait", + "axum-extra 0.10.3", + "backon", + "base64", + "bincode", + "bitvec", + "byteorder", + "bytes", + "dashmap", + "futures", + "governor", + "hex 0.4.3", + "http", + "intervaltree", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "librqbit-dht", + "librqbit-dualstack-sockets", + "librqbit-lsd", + "librqbit-peer-protocol", + "librqbit-sha1-wrapper", + "librqbit-tracker-comms", + "librqbit-upnp", + "librqbit-utp", + "memmap2", + "mime_guess", + "nix 0.30.1", + "parking_lot", + "rand 0.9.4", + "regex", + "reqwest", + "rlimit", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "serde_with", + "size_format", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tokio-socks", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "uuid", + "walkdir", + "windows", +] + +[[package]] +name = "librqbit-bencode" +version = "3.1.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "arrayvec", + "atoi", + "bytes", + "librqbit-buffers", + "librqbit-clone-to-owned", + "serde", + "serde_derive", + "thiserror 2.0.18", +] + +[[package]] +name = "librqbit-buffers" +version = "4.2.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "bytes", + "librqbit-clone-to-owned", + "serde", + "serde_derive", +] + +[[package]] +name = "librqbit-clone-to-owned" +version = "3.0.1" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "bytes", +] + +[[package]] +name = "librqbit-core" +version = "5.0.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "bytes", + "chardetng", + "data-encoding", + "directories", + "encoding_rs", + "hex 0.4.3", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-sha1-wrapper", + "memchr", + "parking_lot", + "rand 0.9.4", + "serde", + "serde_derive", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "librqbit-dht" +version = "5.3.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "backon", + "bytes", + "chrono", + "dashmap", + "futures", + "indexmap 2.13.0", + "leaky-bucket", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "librqbit-dualstack-sockets", + "parking_lot", + "rand 0.9.4", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "librqbit-dualstack-sockets" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7189f1ff66ca75055137a38b062ba7df9691d071b7172e5090997c62eb4b4de" +dependencies = [ + "axum", + "backon", + "futures", + "libc", + "network-interface", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "librqbit-lsd" +version = "0.1.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "atoi", + "bstr", + "futures", + "httparse", + "librqbit-core", + "librqbit-dualstack-sockets", + "librqbit-sha1-wrapper", + "parking_lot", + "rand 0.9.4", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "librqbit-peer-protocol" +version = "4.3.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "bitvec", + "byteorder", + "bytes", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "serde", + "serde_derive", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "librqbit-sha1-wrapper" +version = "4.1.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "assert_cfg", + "crypto-hash", +] + +[[package]] +name = "librqbit-tracker-comms" +version = "3.0.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "async-stream", + "backon", + "byteorder", + "futures", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-core", + "librqbit-dualstack-sockets", + "parking_lot", + "rand 0.9.4", + "reqwest", + "serde", + "serde_derive", + "serde_with", + "tokio", + "tokio-util", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "librqbit-upnp" +version = "1.0.0" +source = "git+https://github.com/ikatson/rqbit#61e189a9b37ab3ba678efa66586c2c1aabeed584" +dependencies = [ + "anyhow", + "bstr", + "futures", + "httparse", + "librqbit-dualstack-sockets", + "network-interface", + "quick-xml", + "reqwest", + "serde", + "serde_derive", + "socket2 0.6.3", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "librqbit-utp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3bfdc73944bc76cab24d5690a98816770040a654c449edf5ff2b9ba22626aa" +dependencies = [ + "bitvec", + "dontfrag", + "lazy_static", + "libc", + "librqbit-dualstack-sockets", + "metrics", + "parking_lot", + "rand 0.9.4", + "ringbuf", + "rustc-hash", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -2174,6 +2860,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2183,6 +2878,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metrics" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" +dependencies = [ + "portable-atomic", + "rapidhash", +] + [[package]] name = "mime" version = "0.3.17" @@ -2237,6 +2942,35 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "network-interface" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +dependencies = [ + "cc", + "libc", + "thiserror 2.0.18", + "winapi", +] + [[package]] name = "nix" version = "0.26.4" @@ -2261,6 +2995,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nix" version = "0.31.3" @@ -2273,6 +3019,25 @@ dependencies = [ "libc", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2294,11 +3059,21 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2334,6 +3109,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2356,6 +3142,55 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "4.6.0" @@ -2559,6 +3394,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2648,6 +3493,31 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.45" @@ -2663,6 +3533,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2676,8 +3552,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2687,7 +3573,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2699,6 +3595,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "range-requests" version = "0.2.0" @@ -2708,7 +3613,25 @@ dependencies = [ "axum-core", "bytes", "http", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -2729,6 +3652,17 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -2803,17 +3737,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-tls", "hyper-util", "js-sys", "log", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -2825,6 +3763,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -2854,6 +3803,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + [[package]] name = "rsa" version = "0.9.10" @@ -2867,7 +3825,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -2884,7 +3842,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -2924,6 +3882,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2936,6 +3903,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -3006,7 +3991,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -3021,7 +4006,7 @@ checksum = "5c2eee8405f16c1f337fe3a83389361caea83c928d14dbd666a480407072c365" dependencies = [ "arrow", "sea-query", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -3100,7 +4085,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -3144,6 +4129,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.2.2" @@ -3186,6 +4194,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap 2.13.0", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -3239,7 +4260,7 @@ checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64", "chrono", - "hex", + "hex 0.4.3", "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", @@ -3303,7 +4324,7 @@ checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" dependencies = [ "async-trait", "bytes", - "hex", + "hex 0.4.3", "sha2 0.10.9", "tokio", ] @@ -3330,7 +4351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3345,6 +4366,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "size_format" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed5f6ab2122c6dec69dca18c72fa4590a27e581ad20d44960fe74c032a0b23b" +dependencies = [ + "generic-array 0.12.4", + "num", +] + [[package]] name = "slab" version = "0.4.12" @@ -3431,6 +4462,15 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -3483,7 +4523,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -3514,7 +4554,7 @@ dependencies = [ "dotenvy", "either", "heck 0.5.0", - "hex", + "hex 0.4.3", "once_cell", "proc-macro2", "quote", @@ -3550,8 +4590,8 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array", - "hex", + "generic-array 0.14.7", + "hex 0.4.3", "hkdf", "hmac", "itoa", @@ -3560,7 +4600,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "rust_decimal", "serde", @@ -3569,7 +4609,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -3593,7 +4633,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "hex", + "hex 0.4.3", "hkdf", "hmac", "home", @@ -3602,7 +4642,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "rust_decimal", "serde", "serde_json", @@ -3610,7 +4650,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -3637,7 +4677,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.18", "time", "tracing", "url", @@ -3676,7 +4716,7 @@ dependencies = [ "quote", "sha2 0.11.0", "syn 2.0.117", - "thiserror", + "thiserror 2.0.18", "zstd", ] @@ -3763,6 +4803,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -3773,13 +4826,33 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -3915,6 +4988,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -3924,6 +5019,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -4007,7 +5103,7 @@ dependencies = [ "askama_web", "async-tempfile", "axum", - "axum-extra", + "axum-extra 0.12.6", "axum_typed_multipart", "camino", "chrono", @@ -4016,6 +5112,7 @@ dependencies = [ "derive_more", "env_logger", "hightorrent_api", + "librqbit", "log", "sea-orm", "sea-orm-migration", @@ -4144,6 +5241,12 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + [[package]] name = "ubyte" version = "0.10.4" @@ -4219,6 +5322,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -4229,8 +5338,15 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4259,7 +5375,7 @@ dependencies = [ "os_display", "rustc-hash", "rustix", - "thiserror", + "thiserror 2.0.18", "unic-langid", "uucore_procs", "wild", @@ -4282,6 +5398,7 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -4299,6 +5416,22 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4323,6 +5456,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -4388,6 +5530,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -4401,6 +5565,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.88" @@ -4411,6 +5587,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.6.1" @@ -4446,12 +5632,42 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4465,6 +5681,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4493,6 +5720,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4595,6 +5832,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4756,6 +6002,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml index 88799aa..71994e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,8 @@ env_logger = "0.11.10" # hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api", branch = "feat-sea-orm", features = [ "sea_orm" ] } hightorrent_api = { version = "0.2.2", features = [ "sea_orm" ] } log = "0.4.29" +# rqbit torrent client to resolve magnets +librqbit = { git = "https://github.com/ikatson/rqbit" } # SQLite ORM sea-orm = { version = "2.0.0-rc.38", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] } # SQLite migrations diff --git a/src/config.rs b/src/config.rs index c6c095c..9f63d3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -88,6 +88,10 @@ pub struct AppConfig { #[serde(default = "AppConfig::default_log_path")] pub log_path: Utf8PathBuf, + + /// Where rqbit saves its internal state. + #[serde(default = "AppConfig::default_rqbit_path")] + pub rqbit_path: Utf8PathBuf, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -127,6 +131,13 @@ impl AppConfig { Self::config_dir().join("operations.log") } + pub fn default_rqbit_path() -> Utf8PathBuf { + // TODO: it looks like rqbit actually saves stuff in + // ~/.cache/com.rqbit.dht/dht.json and nothing in this + // folder we provide. + Self::config_dir().join("rqbit") + } + pub async fn load_from_xdg() -> Result { let config_dir = Self::config_dir(); create_dir_all(&config_dir) diff --git a/src/database/torrent/operator.rs b/src/database/torrent/operator.rs index 2961a86..4204149 100644 --- a/src/database/torrent/operator.rs +++ b/src/database/torrent/operator.rs @@ -7,6 +7,7 @@ use std::ops::Deref; use crate::database::content_folder; use crate::database::operation::Table; use crate::database::operator::{DatabaseOperator, TableOperator}; +use crate::resolver::ResolverAction; use super::*; @@ -124,6 +125,13 @@ impl TorrentOperator<'_> { .await .context(LoggerSnafu)?; + // Now that the magnet has been summoned into the DB, + // we should let the resolver know about it. + self.state + .resolver + .send(ResolverAction::Resolve(magnet)) + .expect("resolver sender channel has been closed"); + Ok(model) } @@ -200,7 +208,6 @@ impl TorrentOperator<'_> { .await .unwrap(); - // TODO: cancel magnet resolution let mut active_model: ActiveModel = torrent.clone().into(); active_model.torrent_file = Set(Some(file)); @@ -222,6 +229,12 @@ impl TorrentOperator<'_> { .await .context(DBSnafu)?; + // Cancel any pending resolution operation + self.state + .resolver + .send(ResolverAction::Cancel(torrent.torrent_id.clone())) + .expect("resolver sender channel has been closed"); + self.log_update(TorrentOperation::ResolveMagnet { id: torrent.id, name: torrent.name.to_string(), diff --git a/src/lib.rs b/src/lib.rs index 32dd07e..36df5a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod database; pub mod extractors; pub mod middleware; pub mod migration; +pub mod resolver; pub mod routes; pub mod state; @@ -54,8 +55,17 @@ where L: Listener, L::Addr: std::fmt::Debug, { - let state = state::AppState::new(config).await?; - let app = router(state); + // Create a channel so the webapp can let the resolver know + // to resolve new magnets. + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + let state = state::AppState::new(config, sender).await?; + let app = router(state.clone()); + + tokio::task::spawn(async move { + // Spawn the background task to resolve magnets + resolver::Resolver::new(state, receiver).await.serve().await + }); axum::serve(listener, app.into_make_service()) .await diff --git a/src/resolver.rs b/src/resolver.rs new file mode 100644 index 0000000..3a1f164 --- /dev/null +++ b/src/resolver.rs @@ -0,0 +1,215 @@ +use hightorrent_api::hightorrent::{MagnetLink, TorrentFile, TorrentID}; +use librqbit::*; +use sea_orm::*; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::task::JoinHandle; +use tokio::time::{Duration, sleep}; + +use std::collections::HashMap; +use std::sync::Arc; + +use crate::database::operator::{DatabaseOperator, TableOperator}; +use crate::database::torrent::{ + ActiveModel as TorrentActiveModel, Model as TorrentModel, TorrentOperation, +}; +use crate::state::AppState; + +#[derive(Clone, Debug)] +pub enum ResolverAction { + /// Resolve the magnet link, then save the torrent file. + Resolve(MagnetLink), + /// Stop resolving the magnet link, because the corresponding + /// torrent file was uploaded manually. + Cancel(TorrentID), +} + +/// A magnet link resolver +/// +/// The resolver keeps track of background tasks (JoinHandle) and +/// listens to new messages for new tasks or cancelations. +pub struct Resolver { + operator: DatabaseOperator, + // Channel to receive new magnets to resolve + receiver: UnboundedReceiver, + /// The rqbit session + session: Arc, + // Keep track of background tasks resolving torrent files from magnets + // so then can be polled/canceled. + tasks: HashMap>, +} + +impl Resolver { + /// Initialize rqbit in the background + pub async fn new(state: AppState, receiver: UnboundedReceiver) -> Self { + let session = Session::new_with_opts( + state.config.rqbit_path.clone().into(), + SessionOptions { + listen: Some(ListenerOptions::default()), + ..Default::default() + }, + ) + // TODO: catch errors here? Why would it fail though? + .await + .unwrap(); + + Self { + operator: DatabaseOperator::new(state, None), + receiver, + session, + tasks: HashMap::new(), + } + } + + /// Starts a background task to resolve the magnet + /// + /// If the magnet is already being resolved, this is a no-op. + pub fn resolve(&mut self, magnet: MagnetLink) { + if self.tasks.contains_key(&magnet.id()) { + log::warn!( + "Magnet {} is already resolving. This is a logic bug.", + magnet.id() + ); + return; + } + + log::info!("Starting to resolve magnet {}", magnet.id()); + let session = self.session.clone(); + let magnet_str = magnet.to_string(); + let handle = tokio::task::spawn(async move { + let resp = session + .add_torrent( + AddTorrent::from_url(magnet_str), + Some(AddTorrentOptions { + list_only: true, + ..Default::default() + }), + ) + .await + .unwrap(); + + match resp { + AddTorrentResponse::ListOnly(resp) => { + log::info!("Found metainfo for torrent {:?}", resp.info_hash); + TorrentFile::from_slice(&resp.torrent_bytes).unwrap() + // TODO: here we should process the resolved torrent + } + _ => { + panic!("RQBIT BUG!"); + } + } + }); + + self.tasks.insert(magnet.id(), handle); + } + + /// Periodically check if new magnets have been added. If so, + /// start a new task to resolve the magnet. + /// + /// When the magnet is resolved, save the new value in the DB. + pub async fn serve(&mut self) { + log::info!("Starting the magnet resolver. Checking for saved unresolved magnets."); + let unresolved_torrents: Vec = self + .operator + .torrent() + .list() + .await + .unwrap() + .into_iter() + .filter(|x| x.torrent_file.is_none()) + .collect(); + for torrent in unresolved_torrents { + // Start a new resolving task + self.resolve(torrent.magnet_link); + } + + // Now keep looking for new magnets to resolve + loop { + // First, let's check if we have successfully resolved some magnets + // to further process them (TODO). + self.save_resolved().await; + + // We apply a one-second timeout so we can go back to save_resolved + // if there are no new submitted magnets. UnboundedReceiver::recv is + // cancel-safe so no message will be dropped here. But just to avoid + // shenanigans we use a proper sleep and not a timeout of the + // actual receiver. + tokio::select! { + _ = sleep(Duration::from_secs(1)) => {}, + recv = self.receiver.recv() => { + match recv.expect("magnetlink resolver sender has been closed") { + ResolverAction::Resolve(magnet_link) => self.resolve(magnet_link), + ResolverAction::Cancel(torrent_id) => self.cancel(&torrent_id), + } + } + }; + } + } + + /// Returns the list of TorrentIDs whose resolution tasks have finished + fn finished_ids(&self) -> Vec { + self.tasks + .iter() + .filter_map(|(k, v)| { + if v.is_finished() { + Some(k.clone()) + } else { + None + } + }) + .collect() + } + + /// Cancels resolution for a given TorrentID. + /// + /// Does not produce an error if there was no resolution ongoing. + fn cancel(&mut self, id: &TorrentID) { + if self.tasks.remove(id).is_some() { + log::info!("Canceled resolution for magnet {id}"); + } + } + + /// Check if some tasks have finished resolving, and save result in the DB. + pub async fn save_resolved(&mut self) { + for finished_id in self.finished_ids() { + log::info!("Magnet {finished_id} has finished resolving. Saving to DB."); + + // Get the raw task handle, removing it from the active tasks + let handle = self.tasks.remove(&finished_id).unwrap(); + let torrent_file = handle.await.unwrap(); + + // If the torrent was deleted for some reason, this is a no-op + if let Ok(torrent) = self + .operator + .torrent() + .get_by_torrent_id(&finished_id) + .await + { + if torrent.torrent_file.is_some() { + log::info!( + "Magnet {finished_id} already has a torrent file in DB. Maybe it was uploaded manually and this is a race condition with task cancelation? Otherwise, there may be a logic bug somewhere." + ); + continue; + } + + let mut active_torrent: TorrentActiveModel = torrent.into(); + active_torrent.torrent_file = Set(Some(torrent_file)); + let torrent = active_torrent + .update(&self.operator.state.database) + .await + .unwrap(); + + // TODO: log errors instead of failing + self.operator + .torrent() + .log_update(TorrentOperation::ResolveMagnet { + id: torrent.id, + name: torrent.name.to_string(), + }) + .await + .unwrap(); + + log::info!("Torrent file for magnet {finished_id} has been saved to DB"); + } + } + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index d5090b4..f3549c9 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -2,10 +2,12 @@ use hightorrent_api::hightorrent::{SingleTarget, TorrentContent, TorrentList}; use hightorrent_api::{Api, QBittorrentClient}; use sea_orm::*; use snafu::prelude::*; +use tokio::sync::mpsc::UnboundedSender; use crate::config::AppConfig; use crate::extractors::user::User; use crate::migration::{Migrator, MigratorTrait}; +use crate::resolver::ResolverAction; mod context; pub use context::AppStateContext; @@ -35,10 +37,16 @@ pub struct AppState { // TODO: multiple torrent backends pub torrent_client: QBittorrentClient, + + /// MagnetLink resolver. + pub resolver: UnboundedSender, } impl AppState { - pub async fn new(config: AppConfig) -> Result { + pub async fn new( + config: AppConfig, + resolver: UnboundedSender, + ) -> Result { // TODO: config for torrent backend let torrent_client = QBittorrentClient::new_not_logged_in( @@ -66,6 +74,7 @@ impl AppState { database, logger, torrent_client, + resolver, }) } From 5e086a2503bec316da90bf521a521898cb943c03 Mon Sep 17 00:00:00 2001 From: angrynode Date: Fri, 29 May 2026 12:43:52 +0200 Subject: [PATCH 3/4] feat: Display pending torrents in content folders --- src/database/content_folder/folder_view.rs | 7 ++++++ src/database/torrent/operator.rs | 16 ++++++++++++ src/routes/content_folder.rs | 6 ++++- templates/content_folders/show.html | 29 ++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/database/content_folder/folder_view.rs b/src/database/content_folder/folder_view.rs index 6889bc7..5736dda 100644 --- a/src/database/content_folder/folder_view.rs +++ b/src/database/content_folder/folder_view.rs @@ -1,3 +1,5 @@ +use crate::database::torrent; + use super::*; /// A loaded folder, with all surrounding entities loaded as well: @@ -11,6 +13,7 @@ pub struct FolderView { pub ancestors: Vec, pub children: Vec, pub folder: Option, + pub torrents: Vec, } impl FolderView { @@ -27,6 +30,7 @@ impl FolderView { ancestors: vec![], folder: None, children, + torrents: vec![], }) } @@ -43,6 +47,8 @@ impl FolderView { let list = operator.list().await?; if let Some(folder) = list.iter().find(|x| x.id == id) { + let torrents = operator.torrent().list_for_folder(folder).await.unwrap(); + Ok(Self { ancestors: folder .ancestors_from_list(&list) @@ -55,6 +61,7 @@ impl FolderView { .cloned() .collect(), folder: Some(folder.clone()), + torrents, }) } else { Err(ContentFolderError::NotFound { id }) diff --git a/src/database/torrent/operator.rs b/src/database/torrent/operator.rs index 4204149..78d2f9a 100644 --- a/src/database/torrent/operator.rs +++ b/src/database/torrent/operator.rs @@ -52,6 +52,22 @@ impl TorrentOperator<'_> { .context(DBSnafu) } + /// List torrents for a given content folder + /// + /// Should not fail, unless SQLite was corrupted for some reason. + pub async fn list_for_folder( + &self, + folder: &content_folder::Model, + ) -> Result, TorrentError> { + // TODO: optimization + Ok(self + .list() + .await? + .into_iter() + .filter(|x| x.content_folder_id == folder.id) + .collect()) + } + /// Count torrents /// /// Should not fail, unless SQLite was corrupted for some reason. diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 2633af7..a20458a 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -5,7 +5,7 @@ use axum::extract::Path; use axum_extra::extract::CookieJar; use serde::{Deserialize, Serialize}; -use crate::database::content_folder; +use crate::database::{content_folder, torrent}; use crate::state::AppStateContext; use crate::state::error::AppStateError; use crate::state::flash_message::{ @@ -24,6 +24,8 @@ pub struct ContentFolderShowTemplate { pub state: AppStateContext, /// Current folder, unless we're on the index page pub folder: Option, + /// Torrents associated with the content folder (empty on index page) + pub torrents: Vec, /// Folders with parent_id set to current folder // TODO: order by alphanumeric pub children: Vec, @@ -39,12 +41,14 @@ impl ContentFolderShowTemplate { ancestors, children, folder, + torrents, } = folder; Self { children, flash: None, folder, + torrents, ancestors, state: context, } diff --git a/templates/content_folders/show.html b/templates/content_folders/show.html index ff392b8..d2f97cd 100644 --- a/templates/content_folders/show.html +++ b/templates/content_folders/show.html @@ -45,6 +45,35 @@

{% block folder_title %}{% if let Some(folder) = folder %}{{ fo + {% if torrents.len() > 0 %} +
+
+
+
+

{{ torrents.len() }} torrents waiting for confirmation in this folder

+
+
+
    + {% for torrent in torrents %} + {% if let Some(torrent_file) = torrent.torrent_file %} +
  • + {{ torrent.name }} will create the following folders/files in this folder: + {# TODO: file tree in hightorrent #} +
      + {% for file in torrent_file.decoded.files().unwrap() %} +
    • ({{ file.size | filesizeformat }}) {{ file.path.to_str().unwrap() }}
    • + {% endfor %} +
    +
  • + {% else %} +
  • {{ torrent.name }} is still resolving so we can't know the files yet
  • + {% endif %} + {% endfor %} +
+
+
+ {% endif %} +
    {% block system_list %} {% if children.is_empty() %} From b061ed1160a7d607e42534b605b48c9c52dfc6d1 Mon Sep 17 00:00:00 2001 From: angrynode Date: Fri, 29 May 2026 13:38:35 +0200 Subject: [PATCH 4/4] feat: Allow moving (resolved) torrents between folders --- Cargo.lock | 64 +++++++--------------- Cargo.toml | 1 + src/database/content_folder/error.rs | 5 ++ src/database/content_folder/folder_view.rs | 18 ++++++ src/database/torrent/operator.rs | 34 ++++++++++++ src/routes/content_folder.rs | 59 ++++++++++++++++++-- src/routes/torrent.rs | 4 +- src/state/flash_message.rs | 8 +++ templates/content_folders/show.html | 26 ++++++++- 9 files changed, 166 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6aaed40..ca3c80a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -1156,16 +1165,6 @@ dependencies = [ "darling_macro 0.20.11", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" @@ -1189,20 +1188,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - [[package]] name = "darling_core" version = "0.23.0" @@ -1227,17 +1212,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" @@ -1355,7 +1329,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4254,11 +4228,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64", + "bs58", "chrono", "hex 0.4.3", "indexmap 1.9.3", @@ -4273,11 +4248,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -4810,10 +4785,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5118,6 +5093,7 @@ dependencies = [ "sea-orm-migration", "serde", "serde_json", + "serde_with", "snafu 0.9.0", "static-serve", "tokio", @@ -5638,7 +5614,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 71994e0..1f615a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ toml = { version = "1", features = ["preserve_order"] } uucore = { version = "0.8.0", features = ["fsext"] } # Finding XDG standard directories (such as ~/.config/torrentmanager/config.toml) xdg = "3.0.0" +serde_with = "3.20.0" [dev-dependencies] async-tempfile = "0.7" diff --git a/src/database/content_folder/error.rs b/src/database/content_folder/error.rs index 633f139..990c78e 100644 --- a/src/database/content_folder/error.rs +++ b/src/database/content_folder/error.rs @@ -22,6 +22,11 @@ pub enum ContentFolderError { Logger { source: LoggerError }, #[snafu(display("Failed to create the folder on disk"))] IO { source: std::io::Error }, + #[snafu(display("Failed to load the torrent {id} requested to be moved"))] + MovingTorrent { + id: i32, + source: crate::database::torrent::TorrentError, + }, } impl From for AppStateError { diff --git a/src/database/content_folder/folder_view.rs b/src/database/content_folder/folder_view.rs index 5736dda..c8ddd9d 100644 --- a/src/database/content_folder/folder_view.rs +++ b/src/database/content_folder/folder_view.rs @@ -1,3 +1,5 @@ +use snafu::prelude::*; + use crate::database::torrent; use super::*; @@ -14,6 +16,7 @@ pub struct FolderView { pub children: Vec, pub folder: Option, pub torrents: Vec, + pub moving_torrent: Option, } impl FolderView { @@ -31,6 +34,7 @@ impl FolderView { folder: None, children, torrents: vec![], + moving_torrent: None, }) } @@ -43,12 +47,25 @@ impl FolderView { pub async fn from_id( operator: &ContentFolderOperator<'_>, id: i32, + moving_id: Option, ) -> Result { let list = operator.list().await?; if let Some(folder) = list.iter().find(|x| x.id == id) { let torrents = operator.torrent().list_for_folder(folder).await.unwrap(); + let moving_torrent = if let Some(moving_id) = moving_id { + Some( + operator + .torrent() + .get(moving_id) + .await + .context(MovingTorrentSnafu { id: moving_id })?, + ) + } else { + None + }; + Ok(Self { ancestors: folder .ancestors_from_list(&list) @@ -62,6 +79,7 @@ impl FolderView { .collect(), folder: Some(folder.clone()), torrents, + moving_torrent, }) } else { Err(ContentFolderError::NotFound { id }) diff --git a/src/database/torrent/operator.rs b/src/database/torrent/operator.rs index 78d2f9a..dfe5339 100644 --- a/src/database/torrent/operator.rs +++ b/src/database/torrent/operator.rs @@ -201,6 +201,40 @@ impl TorrentOperator<'_> { Ok(model) } + /// Moves a torrent to a given content folder + /// + /// TODO: This should error if there are conflicting files over there, + /// by checking torrent content. + pub async fn move_folder( + &self, + torrent: Model, + folder: &content_folder::Model, + ) -> Result { + let previous_folder = self + .content_folder() + .find_by_id(torrent.content_folder_id) + .await + .unwrap(); + + let mut active_model: ActiveModel = torrent.into(); + active_model.content_folder_id = Set(folder.id); + let torrent = active_model + .update(&self.state.database) + .await + .context(DBSnafu)?; + + self.log_update(TorrentOperation::MoveTorrent { + id: torrent.id, + name: torrent.name.to_string(), + previous_folder: (previous_folder.id, previous_folder.name.to_string()), + new_folder: (folder.id, folder.name.to_string()), + }) + .await + .context(LoggerSnafu)?; + + Ok(torrent) + } + /// Internal method used by `import_torrent`. /// /// Resolves a magnet to a torrent, or if it's already resolved, errors because of duplicate. diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index a20458a..b60d50f 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -2,8 +2,11 @@ use askama::Template; use askama_web::WebTemplate; use axum::extract::Form; use axum::extract::Path; +use axum::extract::Query; +use axum::response::{IntoResponse, Response}; use axum_extra::extract::CookieJar; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use crate::database::{content_folder, torrent}; use crate::state::AppStateContext; @@ -17,6 +20,19 @@ pub struct ContentFolderForm { pub name: String, } +/// A request to move a torrent around in the categories/folders. +/// +/// When validate is set, the requested folder is set to the database. +#[derive(Clone, Debug, Deserialize)] +#[serde_as] +pub struct MoveTorrentQuery { + #[serde(default)] + #[serde_as(as = "DeserializeFromStr")] + pub moving_id: Option, + #[serde(default)] + pub moving_validate: bool, +} + #[derive(Template, WebTemplate)] #[template(path = "content_folders/show.html")] pub struct ContentFolderShowTemplate { @@ -33,6 +49,8 @@ pub struct ContentFolderShowTemplate { pub ancestors: Vec, /// Operation status for UI confirmation (Cookie) pub flash: Option, + /// Torrent being moved (if any) + pub moving_torrent: Option, } impl ContentFolderShowTemplate { @@ -42,6 +60,7 @@ impl ContentFolderShowTemplate { children, folder, torrents, + moving_torrent, } = folder; Self { @@ -51,6 +70,7 @@ impl ContentFolderShowTemplate { torrents, ancestors, state: context, + moving_torrent, } } } @@ -65,10 +85,40 @@ pub async fn show( context: AppStateContext, Path(id): Path, status: StatusCookie, -) -> Result, AppStateError> { + Query(moving): Query, +) -> Result { // 404 if requested ID does not exist - let view = content_folder::FolderView::from_id(&context.db.content_folder(), id).await?; - Ok(status.with_template(ContentFolderShowTemplate::new(context, view))) + let view = + content_folder::FolderView::from_id(&context.db.content_folder(), id, moving.moving_id) + .await?; + + if view.moving_torrent.is_some() && moving.moving_validate { + // Request to effectively move the torrent to this folder + // Once done, redirect to the same page + if let Err(e) = context + .db + .torrent() + .move_folder( + view.moving_torrent.clone().unwrap(), + view.folder.as_ref().unwrap(), + ) + .await + { + return Ok(OperationStatus::error(e) + .with_template(ContentFolderShowTemplate::new(context, view)) + .into_response()); + } + + // Success! Perform a redirection + return Ok(status + .with_success("Torrent successfully moved".to_string()) + .redirect(&format!("/folders/{}", view.folder.as_ref().unwrap().id)) + .into_response()); + } + + Ok(status + .with_template(ContentFolderShowTemplate::new(context, view)) + .into_response()) } pub async fn index( @@ -112,14 +162,13 @@ pub async fn create_folder( } } -// TODO: create top-level pub async fn create_subfolder( context: AppStateContext, jar: CookieJar, Path(id): Path, Form(form): Form, ) -> Result, AppStateError> { - let view = content_folder::FolderView::from_id(&context.db.content_folder(), id).await?; + let view = content_folder::FolderView::from_id(&context.db.content_folder(), id, None).await?; match context .db .content_folder() diff --git a/src/routes/torrent.rs b/src/routes/torrent.rs index d1a6fc6..b78549e 100644 --- a/src/routes/torrent.rs +++ b/src/routes/torrent.rs @@ -37,7 +37,7 @@ pub async fn upload_torrent( jar: CookieJar, TypedMultipart(form): TypedMultipart, ) -> Result { - let view = FolderView::from_id(&context.db.content_folder(), id).await?; + let view = FolderView::from_id(&context.db.content_folder(), id, None).await?; if let Err(e) = context .db @@ -64,7 +64,7 @@ pub async fn upload_magnet( jar: CookieJar, Form(form): Form, ) -> Result { - let view = FolderView::from_id(&context.db.content_folder(), id).await?; + let view = FolderView::from_id(&context.db.content_folder(), id, None).await?; if let Err(e) = context .db diff --git a/src/state/flash_message.rs b/src/state/flash_message.rs index 9e443b1..73ea67b 100644 --- a/src/state/flash_message.rs +++ b/src/state/flash_message.rs @@ -150,6 +150,14 @@ impl StatusCookie { status } + pub fn with_success(self, s: String) -> Self { + Self::success(self.cookies, s) + } + + pub fn with_error(self, s: String) -> Self { + Self::error(self.cookies, s) + } + pub fn redirect(self, url: &str) -> FlashRedirect { (self.cookies, Redirect::to(url)) } diff --git a/templates/content_folders/show.html b/templates/content_folders/show.html index d2f97cd..60a10cc 100644 --- a/templates/content_folders/show.html +++ b/templates/content_folders/show.html @@ -1,9 +1,27 @@ {% extends "base.html" %} +{%- macro query_moving_torrent() -%} +{%- if let Some(moving_torrent) = moving_torrent -%}?moving_id={{ moving_torrent.id }}{%- endif -%} +{%- endmacro query_moving_torrent -%} + {% block main %}

    File System

    + {% if let Some(moving_torrent) = moving_torrent %} +
    + You are currently moving the torrent {{ moving_torrent.name }} + + + Validate moving here + + + + Cancel + +
    + {% endif %} +