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/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..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,9 +12,8 @@ use crate::state::error::*; pub struct FolderRequest { pub category: category::Model, pub folder: content_folder::Model, - pub sub_folders: Vec, - pub ancestors: Vec, - pub parent: Option, + pub children: Vec, + pub breadcrumbs: Vec, } impl FromRequestParts for FolderRequest { @@ -53,17 +53,18 @@ impl FromRequestParts for FolderRequest { .await .context(CategorySnafu)?; - let ancestors = content_folder_operator - .ancestors(¤t_content_folder) - .await - .context(ContentFolderSnafu)?; + let children = FileSystemEntry::from_content_folders(&category, &sub_content_folders); + + 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, + 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 448f86a..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; @@ -30,6 +31,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..a2e1669 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -9,8 +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::*}; @@ -100,11 +101,13 @@ 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) pub flash: Option, + /// Breadcrumbs navigation + pub breadcrumbs: Vec, } pub async fn show( @@ -125,15 +128,20 @@ 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()); + 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 69df004..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,13 +30,11 @@ 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 - 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,9 +49,8 @@ pub async fn show( Ok(( jar, ContentFolderShowTemplate { - parent_folder: folder.parent, - breadcrumb_items: folder.ancestors, - sub_content_folders: folder.sub_folders, + breadcrumbs: folder.breadcrumbs, + 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 124bc6c..8223730 100644 --- a/templates/categories/show.html +++ b/templates/categories/show.html @@ -1,66 +1,13 @@ {% 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" %} - {% endif %} - - {% for folder in content_folders %} -
  • - -
    -
    -
    - -
    - {{ folder.name }} -
    -
    -
    -
    -

    - -

    -
    -
    -
    -
  • - {% endfor %} -{% endblock %} - - {% block actions_buttons %} {% 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 0441874..280cfaa 100644 --- a/templates/content_folders/show.html +++ b/templates/content_folders/show.html @@ -1,83 +1,9 @@ {% 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" %} - {% 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 15da385..8756f12 100755 --- a/templates/index.html +++ b/templates/index.html @@ -1,47 +1,15 @@ {% extends "layouts/file_system_base.html" %} -{% block breadcrumb %} - -{% endblock %} - {% block folder_title %} All categories {% endblock%} -{% block alert_message %} - {% include "shared/alert_operation_status.html" %} -{% endblock %} - {% block actions_buttons %} {% include "categories/dropdown_actions.html" %} {% endblock actions_buttons %} {% block system_list %} - {% for category in categories %} -
  • - -
    -
    -
    - -
    - {{ category.name }} {{ category.path }} -
    -
    -
    -
    -

    - -

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