From 9f00b01b32e08ea49bbf2727e465ee9496bcd41d Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 09:57:24 +0100 Subject: [PATCH 1/2] refactor: Make an extractor for content/folder view --- src/extractors/folder_request.rs | 98 ++++++++++++++++++++++++++++++++ src/extractors/mod.rs | 1 + src/extractors/torrent_list.rs | 2 +- src/routes/content_folder.rs | 69 +++------------------- 4 files changed, 109 insertions(+), 61 deletions(-) create mode 100644 src/extractors/folder_request.rs diff --git a/src/extractors/folder_request.rs b/src/extractors/folder_request.rs new file mode 100644 index 0000000..1416253 --- /dev/null +++ b/src/extractors/folder_request.rs @@ -0,0 +1,98 @@ +use axum::extract::{FromRequestParts, Path}; +use axum::http::request::Parts; +use snafu::prelude::*; + +use crate::database::category::{self, CategoryOperator}; +use crate::database::content_folder::{self, ContentFolderOperator}; +use crate::routes::content_folder::PathBreadcrumb; +use crate::state::AppState; +use crate::state::error::*; + +#[derive(Clone, Debug)] +pub struct FolderRequest { + pub category: category::Model, + pub folder: content_folder::Model, + pub sub_folders: Vec, + pub ancestors: Vec, + pub parent: Option, +} + +impl FromRequestParts for FolderRequest { + type Rejection = AppStateError; + + async fn from_request_parts( + parts: &mut Parts, + app_state: &AppState, + ) -> Result { + // This unwrap will only a category name is set, but no further folder path + // However, that case is handled by a different route (`routes::category::show`). + let Path((_category_name, folder_path)) = as FromRequestParts< + AppState, + >>::from_request_parts(parts, app_state) + .await + .unwrap(); + + // Read-only operators: no need to extract the current user + let category_operator = CategoryOperator::new(app_state.clone(), None); + let content_folder_operator = ContentFolderOperator::new(app_state.clone(), None); + + // get current content folders with Path + let current_content_folder = content_folder_operator + // must format to add "/" in front of path like in DB + .find_by_path(format!("/{}", folder_path)) + .await + .context(ContentFolderSnafu)?; + + // Get all sub content folders of the current folder + let sub_content_folders: Vec = content_folder_operator + .list_child_folders(current_content_folder.id) + .await + .context(ContentFolderSnafu)?; + + // Read-only operator + let category: category::Model = category_operator + .find_by_id(current_content_folder.category_id) + .await + .context(CategorySnafu)?; + + // create breadcrumb with ancestor of current folders + let mut content_folder_ancestors: Vec = Vec::new(); + // To get Current Parent Folder + let mut parent_folder: Option = None; + + content_folder_ancestors.push(PathBreadcrumb { + name: current_content_folder.name.clone(), + path: current_content_folder.path.clone(), + }); + + let mut current_id = current_content_folder.parent_id; + while let Some(id) = current_id { + let folder = content_folder_operator + .find_by_id(id) + .await + .context(ContentFolderSnafu)?; + + if parent_folder.is_none() { + parent_folder = Some(folder.clone()); + } + + content_folder_ancestors.push(PathBreadcrumb { + name: folder.name, + path: folder.path, + }); + + current_id = folder.parent_id; + } + + // Reverse the ancestor to create Breadrumb + content_folder_ancestors.reverse(); + + Ok(Self { + category, + folder: current_content_folder, + sub_folders: sub_content_folders, + ancestors: content_folder_ancestors, + parent: parent_folder, + }) + } +} diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index 87119f1..6537af5 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -1,3 +1,4 @@ +pub mod folder_request; pub mod normalized_path; pub mod torrent_list; pub mod user; diff --git a/src/extractors/torrent_list.rs b/src/extractors/torrent_list.rs index e184024..aa8eff7 100644 --- a/src/extractors/torrent_list.rs +++ b/src/extractors/torrent_list.rs @@ -83,7 +83,7 @@ impl TorrentListFilter { }; // Sort list by the latest added torrent (reverse order date_start) - list.sort_unstable_by(|a, b| b.date_start.cmp(&a.date_start)); + list.sort_unstable_by_key(|b| std::cmp::Reverse(b.date_start)); list } } diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 82f62cb..0a0b654 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -1,7 +1,7 @@ use askama::Template; use askama_web::WebTemplate; use axum::Form; -use axum::extract::{Path, State}; +use axum::extract::State; use axum::response::{IntoResponse, Redirect}; use axum_extra::extract::CookieJar; use camino::Utf8PathBuf; @@ -11,6 +11,7 @@ use snafu::prelude::*; use crate::database::category::CategoryOperator; use crate::database::content_folder::ContentFolderOperator; use crate::database::{category, content_folder}; +use crate::extractors::folder_request::FolderRequest; use crate::extractors::user::User; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppState, AppStateContext, error::*}; @@ -44,6 +45,7 @@ pub struct ContentFolderShowTemplate { pub flash: Option, } +#[derive(Clone, Debug, PartialEq)] pub struct PathBreadcrumb { pub name: String, pub path: String, @@ -51,75 +53,22 @@ pub struct PathBreadcrumb { pub async fn show( State(app_state): State, + folder: FolderRequest, user: Option, - Path((_category_name, folder_path)): Path<(String, String)>, jar: CookieJar, ) -> Result<(CookieJar, ContentFolderShowTemplate), AppStateError> { let app_state_context = app_state.context().await?; - let content_folder_operator = ContentFolderOperator::new(app_state.clone(), user.clone()); - - // get current content folders with Path - let current_content_folder = content_folder_operator - // must format to add "/" in front of path like in DB - .find_by_path(format!("/{}", folder_path)) - .await - .context(ContentFolderSnafu)?; - - // Get all sub content folders of the current folder - let sub_content_folders: Vec = content_folder_operator - .list_child_folders(current_content_folder.id) - .await - .context(ContentFolderSnafu)?; - - // Get current categories - let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) - .find_by_id(current_content_folder.category_id) - .await - .context(CategorySnafu)?; - - // create breadcrumb with ancestor of current folders - let mut content_folder_ancestors: Vec = Vec::new(); - // To get Current Parent Folder - let mut parent_folder: Option = None; - - content_folder_ancestors.push(PathBreadcrumb { - name: current_content_folder.name.clone(), - path: current_content_folder.path.clone(), - }); - - let mut current_id = current_content_folder.parent_id; - while let Some(id) = current_id { - let folder = content_folder_operator - .find_by_id(id) - .await - .context(ContentFolderSnafu)?; - - if parent_folder.is_none() { - parent_folder = Some(folder.clone()); - } - - content_folder_ancestors.push(PathBreadcrumb { - name: folder.name, - path: folder.path, - }); - - current_id = folder.parent_id; - } - - // Reverse the ancestor to create Breadrumb - content_folder_ancestors.reverse(); - let (jar, operation_status) = get_cookie(jar); Ok(( jar, ContentFolderShowTemplate { - parent_folder, - breadcrumb_items: content_folder_ancestors, - sub_content_folders, - current_content_folder, - category, + parent_folder: folder.parent, + breadcrumb_items: folder.ancestors, + sub_content_folders: folder.sub_folders, + current_content_folder: folder.folder, + category: folder.category, state: app_state_context, user, flash: operation_status, From ed5e6a436d51cfaaaac2edcba2b72e6882a26824 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 10:52:27 +0100 Subject: [PATCH 2/2] refactor: Introduce ContentFolderAncestors --- src/database/content_folder.rs | 50 ++++++++++++++++++++++++++++++++ src/extractors/folder_request.rs | 43 +++++---------------------- src/routes/content_folder.rs | 8 +---- 3 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/database/content_folder.rs b/src/database/content_folder.rs index 422bb33..de567e9 100644 --- a/src/database/content_folder.rs +++ b/src/database/content_folder.rs @@ -177,4 +177,54 @@ impl ContentFolderOperator { Ok(model) } + + pub async fn ancestors( + &self, + folder: &Model, + ) -> Result { + let mut ancestors = ContentFolderAncestors::default(); + + // Fetch the parent model + ancestors.parent = { + let Some(parent_id) = folder.parent_id else { + // No parent, no ancestors + return Ok(ancestors); + }; + + Some(self.find_by_id(parent_id).await?) + }; + + ancestors.breadcrumbs.push(PathBreadcrumb { + name: ancestors.parent.as_ref().unwrap().name.to_string(), + path: ancestors.parent.as_ref().unwrap().path.to_string(), + }); + + let mut next_id = ancestors.parent.as_ref().unwrap().parent_id; + while let Some(id) = next_id { + let folder = self.find_by_id(id).await?; + ancestors.breadcrumbs.push(PathBreadcrumb { + name: folder.name, + path: folder.path, + }); + next_id = folder.parent_id; + } + + // We walked from the bottom to the top of the folder hierarchy, + // but we want breadcrumbs navigation the other way around. + ancestors.breadcrumbs.reverse(); + + Ok(ancestors) + } +} + +#[derive(Debug, Default)] +pub struct ContentFolderAncestors { + pub parent: Option, + pub breadcrumbs: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PathBreadcrumb { + pub name: String, + pub path: String, } diff --git a/src/extractors/folder_request.rs b/src/extractors/folder_request.rs index 1416253..4e73c15 100644 --- a/src/extractors/folder_request.rs +++ b/src/extractors/folder_request.rs @@ -3,8 +3,7 @@ use axum::http::request::Parts; use snafu::prelude::*; use crate::database::category::{self, CategoryOperator}; -use crate::database::content_folder::{self, ContentFolderOperator}; -use crate::routes::content_folder::PathBreadcrumb; +use crate::database::content_folder::{self, ContentFolderOperator, PathBreadcrumb}; use crate::state::AppState; use crate::state::error::*; @@ -49,50 +48,22 @@ impl FromRequestParts for FolderRequest { .await .context(ContentFolderSnafu)?; - // Read-only operator let category: category::Model = category_operator .find_by_id(current_content_folder.category_id) .await .context(CategorySnafu)?; - // create breadcrumb with ancestor of current folders - let mut content_folder_ancestors: Vec = Vec::new(); - // To get Current Parent Folder - let mut parent_folder: Option = None; - - content_folder_ancestors.push(PathBreadcrumb { - name: current_content_folder.name.clone(), - path: current_content_folder.path.clone(), - }); - - let mut current_id = current_content_folder.parent_id; - while let Some(id) = current_id { - let folder = content_folder_operator - .find_by_id(id) - .await - .context(ContentFolderSnafu)?; - - if parent_folder.is_none() { - parent_folder = Some(folder.clone()); - } - - content_folder_ancestors.push(PathBreadcrumb { - name: folder.name, - path: folder.path, - }); - - current_id = folder.parent_id; - } - - // Reverse the ancestor to create Breadrumb - content_folder_ancestors.reverse(); + let ancestors = content_folder_operator + .ancestors(¤t_content_folder) + .await + .context(ContentFolderSnafu)?; Ok(Self { category, folder: current_content_folder, sub_folders: sub_content_folders, - ancestors: content_folder_ancestors, - parent: parent_folder, + ancestors: ancestors.breadcrumbs, + parent: ancestors.parent, }) } } diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 0a0b654..ea5984d 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use snafu::prelude::*; use crate::database::category::CategoryOperator; -use crate::database::content_folder::ContentFolderOperator; +use crate::database::content_folder::{ContentFolderOperator, PathBreadcrumb}; use crate::database::{category, content_folder}; use crate::extractors::folder_request::FolderRequest; use crate::extractors::user::User; @@ -45,12 +45,6 @@ pub struct ContentFolderShowTemplate { pub flash: Option, } -#[derive(Clone, Debug, PartialEq)] -pub struct PathBreadcrumb { - pub name: String, - pub path: String, -} - pub async fn show( State(app_state): State, folder: FolderRequest,