From 27fa11d07440b3ad14af287e4406b97273773f60 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Sat, 21 Feb 2026 17:40:40 +0100 Subject: [PATCH 1/3] refactor: Mutualize breadcrumbs logic --- src/database/content_folder.rs | 75 +++++++++++-------------- src/extractors/folder_request.rs | 14 ++--- src/lib.rs | 1 + src/routes/category.rs | 6 ++ src/routes/content_folder.rs | 7 +-- templates/categories/show.html | 20 ------- templates/content_folders/show.html | 42 -------------- templates/index.html | 8 --- templates/layouts/file_system_base.html | 23 +++++++- 9 files changed, 69 insertions(+), 127 deletions(-) diff --git a/src/database/content_folder.rs b/src/database/content_folder.rs index 0ed1c36..2f37891 100644 --- a/src/database/content_folder.rs +++ b/src/database/content_folder.rs @@ -1,3 +1,4 @@ +use camino::Utf8PathBuf; use chrono::Utc; use sea_orm::entity::prelude::*; use sea_orm::*; @@ -173,54 +174,44 @@ 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?) - }; +#[derive(Clone, Debug, PartialEq)] +pub struct PathBreadcrumb { + pub name: String, + pub path: String, +} - ancestors.breadcrumbs.push(PathBreadcrumb { - name: ancestors.parent.as_ref().unwrap().name.to_string(), - path: ancestors.parent.as_ref().unwrap().path.to_string(), +impl PathBreadcrumb { + /// Produce a list of path ancestors, ordered by descending order (parent -> child). + /// + /// This includes the current category/folder. + /// + /// The path may contain leading/trailing slashes, but any sufficiently + /// weirder path may produce unexpected results. + pub fn for_filesystem_path(path: &str) -> Vec { + let path = path.trim_start_matches("/").trim_end_matches("/"); + let mut breadcrumbs = vec![]; + let mut path = Utf8PathBuf::from(path.to_string()); + + log::info!("{path}"); + breadcrumbs.push(PathBreadcrumb { + name: path.file_name().unwrap().to_string(), + path: 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, + while path.pop() { + if path.as_str().is_empty() { + break; + } + + log::info!("{:?}", path.file_name()); + breadcrumbs.push(PathBreadcrumb { + name: path.file_name().unwrap().to_string(), + path: path.to_string(), }); - 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) + breadcrumbs.into_iter().rev().collect() } } - -#[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 4e73c15..75ba018 100644 --- a/src/extractors/folder_request.rs +++ b/src/extractors/folder_request.rs @@ -12,8 +12,7 @@ pub struct FolderRequest { pub category: category::Model, pub folder: content_folder::Model, pub sub_folders: Vec, - pub ancestors: Vec, - pub parent: Option, + pub breadcrumbs: Vec, } impl FromRequestParts for FolderRequest { @@ -53,17 +52,16 @@ impl FromRequestParts for FolderRequest { .await .context(CategorySnafu)?; - let ancestors = content_folder_operator - .ancestors(¤t_content_folder) - .await - .context(ContentFolderSnafu)?; + let breadcrumbs = PathBreadcrumb::for_filesystem_path(&format!( + "{}{}", + category.name, current_content_folder.path + )); Ok(Self { category, folder: current_content_folder, sub_folders: sub_content_folders, - ancestors: ancestors.breadcrumbs, - parent: ancestors.parent, + breadcrumbs, }) } } diff --git a/src/lib.rs b/src/lib.rs index 448f86a..ea43c89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,7 @@ pub fn router(state: state::AppState) -> Router { "/folders/{category_name}/{*folder_path}", get(routes::content_folder::show), ) + .route("/folders", get(routes::index::index)) .route("/folders", post(routes::content_folder::create)) .route("/logs", get(routes::logs::index)) // Register static assets routes diff --git a/src/routes/category.rs b/src/routes/category.rs index d147374..1f7cdc2 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -10,6 +10,7 @@ use snafu::prelude::*; use crate::database::category; use crate::database::category::CategoryError; use crate::database::content_folder; +use crate::database::content_folder::PathBreadcrumb; use crate::extractors::normalized_path::*; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppStateContext, error::*}; @@ -105,6 +106,8 @@ pub struct CategoryShowTemplate { category: category::Model, /// Operation status for UI confirmation (Cookie) pub flash: Option, + /// Breadcrumbs navigation + pub breadcrumbs: Vec, } pub async fn show( @@ -127,6 +130,8 @@ pub async fn show( let (jar, operation_status) = get_cookie(jar); + let breadcrumbs = PathBreadcrumb::for_filesystem_path(category.name.as_str()); + Ok(( jar, CategoryShowTemplate { @@ -134,6 +139,7 @@ pub async fn show( category, state: context, flash: operation_status, + breadcrumbs, }, )) } diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 69df004..79115bb 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -33,9 +33,7 @@ pub struct ContentFolderShowTemplate { /// Category pub category: category::Model, /// BreadCrumb extract from path - pub breadcrumb_items: Vec, - /// Parent Folder if exist. If None, the parent is category - pub parent_folder: Option, + pub breadcrumbs: Vec, /// Operation status for UI confirmation (Cookie) pub flash: Option, } @@ -50,8 +48,7 @@ pub async fn show( Ok(( jar, ContentFolderShowTemplate { - parent_folder: folder.parent, - breadcrumb_items: folder.ancestors, + breadcrumbs: folder.breadcrumbs, sub_content_folders: folder.sub_folders, current_content_folder: folder.folder, category: folder.category, diff --git a/templates/categories/show.html b/templates/categories/show.html index 124bc6c..0ddb964 100644 --- a/templates/categories/show.html +++ b/templates/categories/show.html @@ -1,29 +1,9 @@ {% extends "layouts/file_system_base.html" %} -{% block breadcrumb %} - - - -{% endblock %} - {% block folder_title %} {{ category.name }} {% endblock%} -{% block additional_buttons %} - - Go up - -{% endblock %} - {% block system_list %} {% if content_folders.is_empty() %} {% include "shared/empty_state.html" %} diff --git a/templates/content_folders/show.html b/templates/content_folders/show.html index 0441874..a40cfe4 100644 --- a/templates/content_folders/show.html +++ b/templates/content_folders/show.html @@ -1,54 +1,13 @@ {% extends "layouts/file_system_base.html" %} -{% block breadcrumb %} - - - - - {% for i in (0..breadcrumb_items.len()) %} - {% let breadcrumb_item = breadcrumb_items[i] %} - - - {% endfor %} -{% endblock %} - {% block folder_title %} {{ current_content_folder.name }} {% endblock%} -{% block additional_buttons %} - {% if let Some(p) = parent_folder %} - - Go up - - {% else %} - - Go up - - {% endif %} -{% endblock %} - {% block alert_message %} {% include "shared/alert_operation_status.html" %} {% endblock %} - {% block system_list %} {% if sub_content_folders.is_empty() %} {% include "shared/empty_state.html" %} @@ -77,7 +36,6 @@
{% endfor %} {% endblock %} - {% block actions_buttons %} {% include "content_folders/dropdown_actions.html" %} {% endblock %} diff --git a/templates/index.html b/templates/index.html index 15da385..ebf5aee 100755 --- a/templates/index.html +++ b/templates/index.html @@ -1,13 +1,5 @@ {% extends "layouts/file_system_base.html" %} -{% block breadcrumb %} - -{% endblock %} - {% block folder_title %} All categories {% endblock%} diff --git a/templates/layouts/file_system_base.html b/templates/layouts/file_system_base.html index 01a8c72..55e79b4 100644 --- a/templates/layouts/file_system_base.html +++ b/templates/layouts/file_system_base.html @@ -16,7 +16,21 @@

File System

@@ -30,7 +44,12 @@

File System

-

{% block folder_title %}{% endblock folder_title %}

{% block additional_buttons %}{% endblock additional_buttons %}
+

{% block folder_title %}{% endblock folder_title %}

+
+ {% if breadcrumbs is defined %} + Go up + {% endif %} +
    From 6f456f7b7c6b8a278df598e7a2de387657b01024 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Sat, 21 Feb 2026 17:44:47 +0100 Subject: [PATCH 2/3] minor: Mutualize operation status flash message --- templates/categories/show.html | 4 ---- templates/content_folders/show.html | 4 ---- templates/index.html | 4 ---- templates/layouts/file_system_base.html | 4 +++- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/templates/categories/show.html b/templates/categories/show.html index 0ddb964..bfdf082 100644 --- a/templates/categories/show.html +++ b/templates/categories/show.html @@ -37,10 +37,6 @@
    {% include "content_folders/dropdown_actions.html" %} {% endblock %} -{% block alert_message %} - {% include "shared/alert_operation_status.html" %} -{% endblock %} - {% block content_folder_form %}
    diff --git a/templates/content_folders/show.html b/templates/content_folders/show.html index a40cfe4..df2b38d 100644 --- a/templates/content_folders/show.html +++ b/templates/content_folders/show.html @@ -4,10 +4,6 @@ {{ current_content_folder.name }} {% endblock%} -{% block alert_message %} - {% include "shared/alert_operation_status.html" %} -{% endblock %} - {% block system_list %} {% if sub_content_folders.is_empty() %} {% include "shared/empty_state.html" %} diff --git a/templates/index.html b/templates/index.html index ebf5aee..22c2bc9 100755 --- a/templates/index.html +++ b/templates/index.html @@ -4,10 +4,6 @@ All categories {% endblock%} -{% block alert_message %} - {% include "shared/alert_operation_status.html" %} -{% endblock %} - {% block actions_buttons %} {% include "categories/dropdown_actions.html" %} {% endblock actions_buttons %} diff --git a/templates/layouts/file_system_base.html b/templates/layouts/file_system_base.html index 55e79b4..51e8605 100644 --- a/templates/layouts/file_system_base.html +++ b/templates/layouts/file_system_base.html @@ -4,7 +4,9 @@

    File System

    - {% block alert_message %}{% endblock alert_message %} + {% block alert_message %} + {% include "shared/alert_operation_status.html" %} + {% endblock alert_message %} {% if state.free_space.free_space_percent < 5 %}
    From 36a4e6066dfc336cce1ad0091c9d10736c808599 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Sat, 21 Feb 2026 18:21:54 +0100 Subject: [PATCH 3/3] refactor: Mutualize children view in filesystem --- Cargo.lock | 34 ++++++++++++------- Cargo.toml | 4 +-- src/extractors/folder_request.rs | 7 ++-- src/filesystem.rs | 43 +++++++++++++++++++++++++ src/lib.rs | 1 + src/routes/category.rs | 8 +++-- src/routes/content_folder.rs | 5 +-- src/routes/index.rs | 7 ++-- templates/categories/show.html | 29 ----------------- templates/content_folders/show.html | 28 ---------------- templates/index.html | 22 +------------ templates/layouts/file_system_base.html | 29 ++++++++++++++++- 12 files changed, 114 insertions(+), 103 deletions(-) create mode 100644 src/filesystem.rs diff --git a/Cargo.lock b/Cargo.lock index 20d1605..13013f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,11 +107,11 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "askama" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57" dependencies = [ - "askama_derive", + "askama_macros", "itoa", "percent-encoding", "serde", @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37" dependencies = [ "askama_parser", "basic-toml", @@ -135,23 +135,33 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "askama_macros" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b" +dependencies = [ + "askama_derive", +] + [[package]] name = "askama_parser" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c" dependencies = [ - "memchr", + "rustc-hash", "serde", "serde_derive", + "unicode-ident", "winnow", ] [[package]] name = "askama_web" -version = "0.14.6" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dcd7d2caaff31b91ef5d112ed10416344e23a33db9e7eea7ba695d2a97a88a" +checksum = "5911a65ac3916ef133167a855d52978f9fbf54680a093e0ef29e20b7e94a4523" dependencies = [ "askama", "askama_web_derive", @@ -162,9 +172,9 @@ dependencies = [ [[package]] name = "askama_web_derive" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b" +checksum = "9767c17d33a63daf6da5872ffaf2ab0c289cd73ce7ed4f41d5ddf9149c004873" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 32a4057..8075f0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,9 @@ name = "torrentmanager" path = "src/main.rs" [dependencies] -askama = "0.14.0" +askama = "0.15" # askama_web::WebTemplate implements axum::IntoResponse -askama_web = { version = "0.14.6", features = ["axum-0.8"] } +askama_web = { version = "0.15", features = ["axum-0.8"] } axum = { version = "0.8.4", features = ["macros"] } axum-extra = { version = "0.12.1", features = ["cookie"] } # UTF-8 paths for easier String/PathBuf interop diff --git a/src/extractors/folder_request.rs b/src/extractors/folder_request.rs index 75ba018..315bce0 100644 --- a/src/extractors/folder_request.rs +++ b/src/extractors/folder_request.rs @@ -4,6 +4,7 @@ use snafu::prelude::*; use crate::database::category::{self, CategoryOperator}; use crate::database::content_folder::{self, ContentFolderOperator, PathBreadcrumb}; +use crate::filesystem::FileSystemEntry; use crate::state::AppState; use crate::state::error::*; @@ -11,7 +12,7 @@ use crate::state::error::*; pub struct FolderRequest { pub category: category::Model, pub folder: content_folder::Model, - pub sub_folders: Vec, + pub children: Vec, pub breadcrumbs: Vec, } @@ -52,6 +53,8 @@ impl FromRequestParts for FolderRequest { .await .context(CategorySnafu)?; + let children = FileSystemEntry::from_content_folders(&category, &sub_content_folders); + let breadcrumbs = PathBreadcrumb::for_filesystem_path(&format!( "{}{}", category.name, current_content_folder.path @@ -60,7 +63,7 @@ impl FromRequestParts for FolderRequest { Ok(Self { category, folder: current_content_folder, - sub_folders: sub_content_folders, + children, breadcrumbs, }) } diff --git a/src/filesystem.rs b/src/filesystem.rs new file mode 100644 index 0000000..3dc453b --- /dev/null +++ b/src/filesystem.rs @@ -0,0 +1,43 @@ +use crate::database::{category, content_folder}; + +#[derive(Clone, Debug)] +pub struct FileSystemEntry { + pub name: String, + pub extra: Option, + pub folder_path: String, +} + +impl FileSystemEntry { + pub fn from_category(category: &category::Model) -> Self { + Self { + name: category.name.to_string(), + extra: Some(category.path.to_string()), + folder_path: category.name.to_string(), + } + } + + pub fn from_categories(categories: &[category::Model]) -> Vec { + categories.iter().map(Self::from_category).collect() + } + + pub fn from_content_folder( + category: &category::Model, + content_folder: &content_folder::Model, + ) -> Self { + Self { + name: content_folder.name.to_string(), + extra: None, + folder_path: format!("{}{}", category.name, content_folder.path), + } + } + + pub fn from_content_folders( + category: &category::Model, + content_folders: &[content_folder::Model], + ) -> Vec { + content_folders + .iter() + .map(|x| Self::from_content_folder(category, x)) + .collect() + } +} diff --git a/src/lib.rs b/src/lib.rs index ea43c89..ae60294 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use static_serve::embed_assets; pub mod config; pub mod database; pub mod extractors; +pub mod filesystem; pub mod middleware; pub mod migration; pub mod routes; diff --git a/src/routes/category.rs b/src/routes/category.rs index 1f7cdc2..a2e1669 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -9,9 +9,9 @@ use snafu::prelude::*; use crate::database::category; use crate::database::category::CategoryError; -use crate::database::content_folder; use crate::database::content_folder::PathBreadcrumb; use crate::extractors::normalized_path::*; +use crate::filesystem::FileSystemEntry; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppStateContext, error::*}; @@ -101,7 +101,7 @@ pub struct CategoryShowTemplate { /// Global application state pub state: AppStateContext, /// Categories found in database - pub content_folders: Vec, + pub children: Vec, /// Category category: category::Model, /// Operation status for UI confirmation (Cookie) @@ -128,6 +128,8 @@ pub async fn show( .await .context(CategorySnafu)?; + let children = FileSystemEntry::from_content_folders(&category, &content_folders); + let (jar, operation_status) = get_cookie(jar); let breadcrumbs = PathBreadcrumb::for_filesystem_path(category.name.as_str()); @@ -135,8 +137,8 @@ pub async fn show( Ok(( jar, CategoryShowTemplate { - content_folders, category, + children, state: context, flash: operation_status, breadcrumbs, diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 79115bb..ff85078 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -10,6 +10,7 @@ use snafu::prelude::*; use crate::database::content_folder::PathBreadcrumb; use crate::database::{category, content_folder}; use crate::extractors::folder_request::FolderRequest; +use crate::filesystem::FileSystemEntry; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppStateContext, error::*}; @@ -29,7 +30,7 @@ pub struct ContentFolderShowTemplate { /// current folder pub current_content_folder: content_folder::Model, /// Folders with parent_id set to current folder - pub sub_content_folders: Vec, + pub children: Vec, /// Category pub category: category::Model, /// BreadCrumb extract from path @@ -49,7 +50,7 @@ pub async fn show( jar, ContentFolderShowTemplate { breadcrumbs: folder.breadcrumbs, - sub_content_folders: folder.sub_folders, + children: folder.children, current_content_folder: folder.folder, category: folder.category, state: context, diff --git a/src/routes/index.rs b/src/routes/index.rs index 7861ab9..c4130f5 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -4,7 +4,7 @@ use axum_extra::extract::CookieJar; use snafu::prelude::*; // TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ -use crate::database::category; +use crate::filesystem::FileSystemEntry; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppStateContext, error::*}; @@ -14,7 +14,7 @@ pub struct IndexTemplate { /// Global application state (errors/warnings) pub state: AppStateContext, /// Categories - pub categories: Vec, + pub children: Vec, /// Operation status for UI confirmation pub flash: Option, } @@ -34,6 +34,7 @@ impl IndexTemplate { jar: CookieJar, ) -> Result<(CookieJar, Self), AppStateError> { let categories = context.db.category().list().await.context(CategorySnafu)?; + let children = FileSystemEntry::from_categories(&categories); let (jar, operation_status) = get_cookie(jar); @@ -41,8 +42,8 @@ impl IndexTemplate { jar, IndexTemplate { state: context, - categories, flash: operation_status, + children, }, )) } diff --git a/templates/categories/show.html b/templates/categories/show.html index bfdf082..8223730 100644 --- a/templates/categories/show.html +++ b/templates/categories/show.html @@ -4,35 +4,6 @@ {{ category.name }} {% endblock%} -{% block system_list %} - {% if content_folders.is_empty() %} - {% include "shared/empty_state.html" %} - {% endif %} - - {% for folder in content_folders %} -
  • - -
    -
    -
    - -
    - {{ folder.name }} -
    -
    -
    -
    -

    - -

    -
    -
    -
    -
  • - {% endfor %} -{% endblock %} - - {% block actions_buttons %} {% include "content_folders/dropdown_actions.html" %} {% endblock %} diff --git a/templates/content_folders/show.html b/templates/content_folders/show.html index df2b38d..280cfaa 100644 --- a/templates/content_folders/show.html +++ b/templates/content_folders/show.html @@ -4,34 +4,6 @@ {{ current_content_folder.name }} {% endblock%} -{% block system_list %} - {% if sub_content_folders.is_empty() %} - {% include "shared/empty_state.html" %} - {% endif %} - - {% for folder in sub_content_folders %} -
  • - -
    -
    -
    - -
    - {{ folder.name }} -
    -
    -
    -
    -

    - -

    -
    -
    -
    -
  • - {% endfor %} -{% endblock %} - {% block actions_buttons %} {% include "content_folders/dropdown_actions.html" %} {% endblock %} diff --git a/templates/index.html b/templates/index.html index 22c2bc9..8756f12 100755 --- a/templates/index.html +++ b/templates/index.html @@ -9,27 +9,7 @@ {% endblock actions_buttons %} {% block system_list %} - {% for category in categories %} -
  • - -
    -
    -
    - -
    - {{ category.name }} {{ category.path }} -
    -
    -
    -
    -

    - -

    -
    -
    -
    -
  • - {% endfor %} + {{ super() }}