From 11af2a2808424a4b4b0acaa4b2ee2d40eaa01262 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 11:53:54 +0100 Subject: [PATCH 1/5] refactor: Operations don't ask for redundant Operator info --- src/database/category.rs | 12 ++++-------- src/database/content_folder.rs | 8 ++------ src/routes/category.rs | 4 ++-- src/routes/content_folder.rs | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/database/category.rs b/src/database/category.rs index e48beb8..04548a7 100644 --- a/src/database/category.rs +++ b/src/database/category.rs @@ -128,7 +128,7 @@ impl CategoryOperator { } /// Delete a category - pub async fn delete(&self, id: i32, user: Option) -> Result { + pub async fn delete(&self, id: i32) -> Result { let db = &self.state.database; let category: Option = Entity::find_by_id(id).one(db).await.context(DBSnafu)?; @@ -138,7 +138,7 @@ impl CategoryOperator { category.delete(db).await.context(DBSnafu)?; let operation_log = OperationLog { - user, + user: self.user.clone(), date: Utc::now(), table: Table::Category, operation: OperationType::Delete, @@ -167,11 +167,7 @@ impl CategoryOperator { /// /// - name or path is already taken (they should be unique) /// - path parent directory does not exist (to avoid completely wrong paths) - pub async fn create( - &self, - f: &CategoryForm, - user: Option, - ) -> Result { + pub async fn create(&self, f: &CategoryForm) -> Result { let dir = Utf8PathBuf::from(&f.path); let parent = dir.parent().unwrap(); @@ -208,7 +204,7 @@ impl CategoryOperator { let model = model.try_into_model().unwrap(); let operation_log = OperationLog { - user, + user: self.user.clone(), date: Utc::now(), table: Table::Category, operation: OperationType::Create, diff --git a/src/database/content_folder.rs b/src/database/content_folder.rs index de567e9..0ed1c36 100644 --- a/src/database/content_folder.rs +++ b/src/database/content_folder.rs @@ -115,11 +115,7 @@ impl ContentFolderOperator { /// /// - name or path is already taken (they should be unique in one folder) /// - path parent directory does not exist (to avoid completely wrong paths) - pub async fn create( - &self, - f: &ContentFolderForm, - user: Option, - ) -> Result { + pub async fn create(&self, f: &ContentFolderForm) -> Result { // Check duplicates in same folder let list = if let Some(parent_id) = f.parent_id { self.list_child_folders(parent_id).await? @@ -158,7 +154,7 @@ impl ContentFolderOperator { let model = model.try_into_model().unwrap(); let operation_log = OperationLog { - user, + user: self.user.clone(), date: Utc::now(), table: Table::ContentFolder, operation: OperationType::Create, diff --git a/src/routes/category.rs b/src/routes/category.rs index 92f8c12..d96be2d 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -57,7 +57,7 @@ pub async fn delete( // let app_state_context = app_state.context().await?; let categories = CategoryOperator::new(app_state.clone(), user.clone()); - let deleted = categories.delete(id, user.clone()).await; + let deleted = categories.delete(id).await; let operation_status = match deleted { Ok(name) => OperationStatus { @@ -83,7 +83,7 @@ pub async fn create( ) -> Result { let categories = CategoryOperator::new(app_state.clone(), user.clone()); - let created = categories.create(&form, user.clone()).await; + let created = categories.create(&form).await; match created { Ok(created) => { diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index ea5984d..a0fe9b6 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -115,7 +115,7 @@ pub async fn create( // build final path with parent_path and path of form form.path = format!("{}/{}", parent_path, form.name); - let created = content_folder.create(&form, user.clone()).await; + let created = content_folder.create(&form).await; match created { Ok(created) => { From 5448011810f63de2df5d8bdc6cf19bc8bfe9f354 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 13:08:59 +0100 Subject: [PATCH 2/5] refactor: Move user to AppStateContext --- src/extractors/user.rs | 16 ++++++-------- src/routes/category.rs | 14 ++---------- src/routes/content_folder.rs | 8 +------ src/routes/index.rs | 43 ++++++++++++++++-------------------- src/routes/logs.rs | 6 +---- src/routes/progress.rs | 6 +---- src/state/error.rs | 2 ++ src/state/mod.rs | 19 ++++++++++------ templates/menus/footer.html | 2 +- 9 files changed, 46 insertions(+), 70 deletions(-) diff --git a/src/extractors/user.rs b/src/extractors/user.rs index c062712..70213ab 100644 --- a/src/extractors/user.rs +++ b/src/extractors/user.rs @@ -1,10 +1,9 @@ -use axum::{ - extract::OptionalFromRequestParts, - http::{StatusCode, request::Parts}, -}; +use axum::{extract::OptionalFromRequestParts, http::request::Parts}; use derive_more::Display; use serde::{Deserialize, Serialize}; +use crate::state::error::AppStateError; + /// A logged-in user, as expressed by the Remote-User header. /// /// Cannot be produced outside of header extraction. @@ -16,7 +15,7 @@ impl OptionalFromRequestParts for User where S: Send + Sync, { - type Rejection = (StatusCode, &'static str); + type Rejection = AppStateError; async fn from_request_parts( parts: &mut Parts, @@ -25,10 +24,9 @@ where if let Some(username) = parts.headers.get("remote-user") { match username.to_str() { Ok(username) => Ok(Some(User(String::from(username)))), - Err(_e) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "The remote-user header returned by the reverse proxy is invalid.", - )), + Err(_e) => Err(AppStateError::Static { + reason: "The remote-user header returned by the reverse proxy is invalid.", + }), } } else { Ok(None) diff --git a/src/routes/category.rs b/src/routes/category.rs index d96be2d..6d51c9b 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -26,8 +26,6 @@ pub struct CategoryForm { pub struct NewCategoryTemplate { /// Global application state pub state: AppStateContext, - /// Logged-in user. - pub user: Option, /// Error pub error: Option, /// Default form with value @@ -35,14 +33,10 @@ pub struct NewCategoryTemplate { } pub async fn new( - State(app_state): State, - user: Option, + app_state_context: AppStateContext, ) -> Result { - let app_state_context = app_state.context().await?; - Ok(NewCategoryTemplate { state: app_state_context, - user, category_form: None, error: None, }) @@ -119,8 +113,6 @@ pub struct CategoryShowTemplate { pub state: AppStateContext, /// Categories found in database pub content_folders: Vec, - /// Logged-in user. - pub user: Option, /// Category category: category::Model, /// Operation status for UI confirmation (Cookie) @@ -128,13 +120,12 @@ pub struct CategoryShowTemplate { } pub async fn show( + app_state_context: AppStateContext, State(app_state): State, user: Option, Path(category_name): Path, jar: CookieJar, ) -> Result { - let app_state_context = app_state.context().await?; - let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) .find_by_name(category_name.to_string()) .await @@ -155,7 +146,6 @@ pub async fn show( content_folders, category, state: app_state_context, - user, flash: operation_status, }, )) diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index a0fe9b6..df01095 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -33,8 +33,6 @@ pub struct ContentFolderShowTemplate { pub current_content_folder: content_folder::Model, /// Folders with parent_id set to current folder pub sub_content_folders: Vec, - /// Logged-in user. - pub user: Option, /// Category pub category: category::Model, /// BreadCrumb extract from path @@ -46,13 +44,10 @@ pub struct ContentFolderShowTemplate { } pub async fn show( - State(app_state): State, + app_state_context: AppStateContext, folder: FolderRequest, - user: Option, jar: CookieJar, ) -> Result<(CookieJar, ContentFolderShowTemplate), AppStateError> { - let app_state_context = app_state.context().await?; - let (jar, operation_status) = get_cookie(jar); Ok(( @@ -64,7 +59,6 @@ pub async fn show( current_content_folder: folder.folder, category: folder.category, state: app_state_context, - user, flash: operation_status, }, )) diff --git a/src/routes/index.rs b/src/routes/index.rs index d20d7e3..2a94a6a 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -6,7 +6,6 @@ use snafu::prelude::*; // TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ use crate::database::category::{self, CategoryOperator}; -use crate::extractors::user::User; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppState, AppStateContext, error::*}; @@ -15,8 +14,6 @@ use crate::state::{AppState, AppStateContext, error::*}; pub struct IndexTemplate { /// Global application state (errors/warnings) pub state: AppStateContext, - /// Logged-in user. - pub user: Option, /// Categories pub categories: Vec, /// Operation status for UI confirmation @@ -28,21 +25,17 @@ pub struct IndexTemplate { pub struct UploadTemplate { /// Global application state (errors/warnings) pub state: AppStateContext, - /// Logged-in user. - pub user: Option, /// Categories pub categories: Vec, } impl IndexTemplate { pub async fn new( + app_state_context: AppStateContext, app_state: AppState, - user: Option, jar: CookieJar, ) -> Result<(CookieJar, Self), AppStateError> { - let app_state_context = app_state.context().await?; - - let categories = CategoryOperator::new(app_state.clone(), user.clone()) + let categories = CategoryOperator::new(app_state.clone(), app_state_context.user.clone()) .list() .await .context(CategorySnafu)?; @@ -53,7 +46,6 @@ impl IndexTemplate { jar, IndexTemplate { state: app_state_context, - user, categories, flash: operation_status, }, @@ -62,34 +54,37 @@ impl IndexTemplate { } impl UploadTemplate { - pub async fn new(app_state: AppState, user: Option) -> Result { - let categories: Vec = CategoryOperator::new(app_state.clone(), user.clone()) - .list() - .await - .context(CategorySnafu)? - .into_iter() - .map(|x| x.name.to_string()) - .collect(); + pub async fn new( + app_state_context: AppStateContext, + app_state: AppState, + ) -> Result { + let categories: Vec = + CategoryOperator::new(app_state.clone(), app_state_context.user.clone()) + .list() + .await + .context(CategorySnafu)? + .into_iter() + .map(|x| x.name.to_string()) + .collect(); Ok(UploadTemplate { - state: app_state.context().await?, - user, + state: app_state_context, categories, }) } } pub async fn index( + app_state_context: AppStateContext, State(app_state): State, - user: Option, jar: CookieJar, ) -> Result<(CookieJar, IndexTemplate), AppStateError> { - IndexTemplate::new(app_state, user, jar).await + IndexTemplate::new(app_state_context, app_state, jar).await } pub async fn upload( + app_state_context: AppStateContext, State(app_state): State, - user: Option, ) -> Result { - UploadTemplate::new(app_state, user).await + UploadTemplate::new(app_state_context, app_state).await } diff --git a/src/routes/logs.rs b/src/routes/logs.rs index 20a8445..29e7932 100644 --- a/src/routes/logs.rs +++ b/src/routes/logs.rs @@ -5,7 +5,6 @@ use snafu::prelude::*; use crate::database::operation::OperationLog; use crate::database::operation::OperationType; -use crate::extractors::user::User; use crate::state::{AppState, AppStateContext, error::*}; #[derive(Template, WebTemplate)] @@ -13,19 +12,16 @@ use crate::state::{AppState, AppStateContext, error::*}; pub struct LogTemplate { pub state: AppStateContext, pub logs: Vec, - pub user: Option, } pub async fn index( + app_state_context: AppStateContext, State(app_state): State, - user: Option, ) -> Result { - let app_state_context = app_state.context().await?; let logs = app_state.logger.read().await.context(LoggerSnafu)?; Ok(LogTemplate { state: app_state_context, logs, - user, }) } diff --git a/src/routes/progress.rs b/src/routes/progress.rs index ac4b807..cb39fd8 100644 --- a/src/routes/progress.rs +++ b/src/routes/progress.rs @@ -17,8 +17,6 @@ pub struct TorrentListTemplate { torrent_list: TorrentListContext, // Filter object filter: TorrentListViewRequest, - /// Logged-in user. - user: Option, } #[derive(Debug)] @@ -36,11 +34,10 @@ pub struct TorrentListContext { } pub async fn progress( + app_state_context: AppStateContext, State(app_state): State, Path(view_request): Path, ) -> Result { - let app_state_context = app_state.context().await?; - // Failing to load the TorrentListView is a fatal error let TorrentListView { counter, @@ -67,6 +64,5 @@ pub async fn progress( files, torrents: filtered_list, }, - user: None, }) } diff --git a/src/state/error.rs b/src/state/error.rs index 21d58a3..a2a3876 100644 --- a/src/state/error.rs +++ b/src/state/error.rs @@ -36,6 +36,8 @@ pub enum AppStateError { ContentFolder { source: ContentFolderError }, #[snafu(display("IO error"))] IO { source: std::io::Error }, + #[snafu(display("{reason}"))] + Static { reason: &'static str }, } impl AppStateError { diff --git a/src/state/mod.rs b/src/state/mod.rs index 1550674..abce54e 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,9 +1,12 @@ +use axum::extract::{FromRequestParts, OptionalFromRequestParts}; +use axum::http::request::Parts; use hightorrent_api::hightorrent::{SingleTarget, TorrentContent, TorrentList}; use hightorrent_api::{Api, QBittorrentClient}; use sea_orm::*; use snafu::prelude::*; use crate::config::AppConfig; +use crate::extractors::user::User; use crate::migration::{Migrator, MigratorTrait}; pub mod error; @@ -41,24 +44,26 @@ pub struct AppState { /// by rendering the AppStateError into an axum Response. pub struct AppStateContext { pub errors: Vec, - // pub errors: Vec, pub free_space: FreeSpace, + pub user: Option, } -impl AppStateContext { - fn from_app_state(state: &AppState) -> Result { +impl FromRequestParts for AppStateContext { + type Rejection = AppStateError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { Ok(Self { errors: vec![], free_space: state.free_space()?, + user: User::from_request_parts(parts, state).await?, }) } } impl AppState { - pub async fn context(&self) -> Result { - AppStateContext::from_app_state(self) - } - pub async fn new(config: AppConfig) -> Result { // TODO: config for torrent backend diff --git a/templates/menus/footer.html b/templates/menus/footer.html index 68ea83c..36707a9 100755 --- a/templates/menus/footer.html +++ b/templates/menus/footer.html @@ -1,6 +1,6 @@
- {% if let Some(user) = user %} + {% if let Some(user) = state.user %}

Logged in as {{ user|safe }} {% else %}

Not logged in.

From 4972b890989c96d6eac9c9a6edfd9f9a275d14f9 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 13:14:17 +0100 Subject: [PATCH 3/5] minor: Move AppStateContext to dedicated module --- src/state/context.rs | 30 ++++++++++++++++++++++++++++++ src/state/mod.rs | 30 ++---------------------------- 2 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 src/state/context.rs diff --git a/src/state/context.rs b/src/state/context.rs new file mode 100644 index 0000000..1109c54 --- /dev/null +++ b/src/state/context.rs @@ -0,0 +1,30 @@ +use axum::extract::{FromRequestParts, OptionalFromRequestParts}; +use axum::http::request::Parts; + +use super::*; + +/// Basic templating context used across pages. +/// +/// Loading it may fail for some reasons, but it's so rare +/// and unrecoverable that it will trigger a global error +/// by rendering the AppStateError into an axum Response. +pub struct AppStateContext { + pub errors: Vec, + pub free_space: FreeSpace, + pub user: Option, +} + +impl FromRequestParts for AppStateContext { + type Rejection = AppStateError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + Ok(Self { + errors: vec![], + free_space: state.free_space()?, + user: User::from_request_parts(parts, state).await?, + }) + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index abce54e..7fddd15 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,5 +1,3 @@ -use axum::extract::{FromRequestParts, OptionalFromRequestParts}; -use axum::http::request::Parts; use hightorrent_api::hightorrent::{SingleTarget, TorrentContent, TorrentList}; use hightorrent_api::{Api, QBittorrentClient}; use sea_orm::*; @@ -9,6 +7,8 @@ use crate::config::AppConfig; use crate::extractors::user::User; use crate::migration::{Migrator, MigratorTrait}; +mod context; +pub use context::AppStateContext; pub mod error; pub mod flash_message; pub mod free_space; @@ -37,32 +37,6 @@ pub struct AppState { pub torrent_client: QBittorrentClient, } -/// Basic templating context used across pages. -/// -/// Loading it may fail for some reasons, but it's so rare -/// and unrecoverable that it will trigger a global error -/// by rendering the AppStateError into an axum Response. -pub struct AppStateContext { - pub errors: Vec, - pub free_space: FreeSpace, - pub user: Option, -} - -impl FromRequestParts for AppStateContext { - type Rejection = AppStateError; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - Ok(Self { - errors: vec![], - free_space: state.free_space()?, - user: User::from_request_parts(parts, state).await?, - }) - } -} - impl AppState { pub async fn new(config: AppConfig) -> Result { // TODO: config for torrent backend From 138b516e07aa8cfb43e80550fb4c82c5f328a295 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 13:29:01 +0100 Subject: [PATCH 4/5] refactor: clone State into AppStateContext (less route boilerplate) --- src/routes/category.rs | 35 +++++++++++++++-------------------- src/routes/content_folder.rs | 22 ++++++++++------------ src/routes/index.rs | 31 +++++++++++-------------------- src/routes/logs.rs | 12 ++++-------- src/routes/progress.rs | 14 +++++++------- src/state/context.rs | 2 ++ 6 files changed, 49 insertions(+), 67 deletions(-) diff --git a/src/routes/category.rs b/src/routes/category.rs index 6d51c9b..8ed11c8 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -1,7 +1,7 @@ use askama::Template; use askama_web::WebTemplate; use axum::Form; -use axum::extract::{Path, State}; +use axum::extract::Path; use axum::response::{IntoResponse, Redirect}; use axum_extra::extract::CookieJar; use serde::{Deserialize, Serialize}; @@ -11,9 +11,8 @@ use crate::database::category::CategoryError; use crate::database::content_folder; use crate::database::{category, category::CategoryOperator}; use crate::extractors::normalized_path::*; -use crate::extractors::user::User; use crate::state::flash_message::{OperationStatus, get_cookie}; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CategoryForm { @@ -43,13 +42,11 @@ pub async fn new( } pub async fn delete( - State(app_state): State, - user: Option, + context: AppStateContext, Path(id): Path, jar: CookieJar, ) -> Result { - // let app_state_context = app_state.context().await?; - let categories = CategoryOperator::new(app_state.clone(), user.clone()); + let categories = CategoryOperator::new(context.state.clone(), context.user.clone()); let deleted = categories.delete(id).await; @@ -70,12 +67,11 @@ pub async fn delete( } pub async fn create( - State(app_state): State, - user: Option, + context: AppStateContext, jar: CookieJar, Form(form): Form, ) -> Result { - let categories = CategoryOperator::new(app_state.clone(), user.clone()); + let categories = CategoryOperator::new(context.state.clone(), context.user.clone()); let created = categories.create(&form).await; @@ -120,23 +116,22 @@ pub struct CategoryShowTemplate { } pub async fn show( - app_state_context: AppStateContext, - State(app_state): State, - user: Option, + context: AppStateContext, Path(category_name): Path, jar: CookieJar, ) -> Result { - let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) + let categories = CategoryOperator::new(context.state.clone(), context.user.clone()); + + let category = categories .find_by_name(category_name.to_string()) .await .context(CategorySnafu)?; // get all content folders in this category - let content_folders: Vec = - CategoryOperator::new(app_state.clone(), user.clone()) - .list_folders(category.id) - .await - .context(CategorySnafu)?; + let content_folders = categories + .list_folders(category.id) + .await + .context(CategorySnafu)?; let (jar, operation_status) = get_cookie(jar); @@ -145,7 +140,7 @@ pub async fn show( CategoryShowTemplate { content_folders, category, - state: app_state_context, + state: context, flash: operation_status, }, )) diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index df01095..77768aa 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -1,7 +1,6 @@ use askama::Template; use askama_web::WebTemplate; use axum::Form; -use axum::extract::State; use axum::response::{IntoResponse, Redirect}; use axum_extra::extract::CookieJar; use camino::Utf8PathBuf; @@ -12,9 +11,8 @@ use crate::database::category::CategoryOperator; use crate::database::content_folder::{ContentFolderOperator, PathBreadcrumb}; 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::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ContentFolderForm { @@ -44,7 +42,7 @@ pub struct ContentFolderShowTemplate { } pub async fn show( - app_state_context: AppStateContext, + context: AppStateContext, folder: FolderRequest, jar: CookieJar, ) -> Result<(CookieJar, ContentFolderShowTemplate), AppStateError> { @@ -58,20 +56,19 @@ pub async fn show( sub_content_folders: folder.sub_folders, current_content_folder: folder.folder, category: folder.category, - state: app_state_context, + state: context, flash: operation_status, }, )) } pub async fn create( - State(app_state): State, - user: Option, + context: AppStateContext, jar: CookieJar, Form(mut form): Form, ) -> Result { // let app_state_context = app_state.context().await?; - let content_folder = ContentFolderOperator::new(app_state.clone(), user.clone()); + let content_folder = ContentFolderOperator::new(context.state.clone(), context.user.clone()); // build path with Parent folder path (or category path if parent is None) + folder.name let parent_path = if let Some(parent_id) = form.parent_id { @@ -85,10 +82,11 @@ pub async fn create( }; // Get folder category - let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) - .find_by_id(form.category_id) - .await - .context(CategorySnafu)?; + let category: category::Model = + CategoryOperator::new(context.state.clone(), context.user.clone()) + .find_by_id(form.category_id) + .await + .context(CategorySnafu)?; // If name contains "/" returns an error if form.name.contains("/") { diff --git a/src/routes/index.rs b/src/routes/index.rs index 2a94a6a..1bee0e1 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -1,13 +1,12 @@ use askama::Template; use askama_web::WebTemplate; -use axum::extract::State; use axum_extra::extract::CookieJar; use snafu::prelude::*; // TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ use crate::database::category::{self, CategoryOperator}; use crate::state::flash_message::{OperationStatus, get_cookie}; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Template, WebTemplate)] #[template(path = "index.html")] @@ -31,11 +30,10 @@ pub struct UploadTemplate { impl IndexTemplate { pub async fn new( - app_state_context: AppStateContext, - app_state: AppState, + context: AppStateContext, jar: CookieJar, ) -> Result<(CookieJar, Self), AppStateError> { - let categories = CategoryOperator::new(app_state.clone(), app_state_context.user.clone()) + let categories = CategoryOperator::new(context.state.clone(), context.user.clone()) .list() .await .context(CategorySnafu)?; @@ -45,7 +43,7 @@ impl IndexTemplate { Ok(( jar, IndexTemplate { - state: app_state_context, + state: context, categories, flash: operation_status, }, @@ -54,12 +52,9 @@ impl IndexTemplate { } impl UploadTemplate { - pub async fn new( - app_state_context: AppStateContext, - app_state: AppState, - ) -> Result { + pub async fn new(context: AppStateContext) -> Result { let categories: Vec = - CategoryOperator::new(app_state.clone(), app_state_context.user.clone()) + CategoryOperator::new(context.state.clone(), context.user.clone()) .list() .await .context(CategorySnafu)? @@ -68,23 +63,19 @@ impl UploadTemplate { .collect(); Ok(UploadTemplate { - state: app_state_context, + state: context, categories, }) } } pub async fn index( - app_state_context: AppStateContext, - State(app_state): State, + context: AppStateContext, jar: CookieJar, ) -> Result<(CookieJar, IndexTemplate), AppStateError> { - IndexTemplate::new(app_state_context, app_state, jar).await + IndexTemplate::new(context, jar).await } -pub async fn upload( - app_state_context: AppStateContext, - State(app_state): State, -) -> Result { - UploadTemplate::new(app_state_context, app_state).await +pub async fn upload(context: AppStateContext) -> Result { + UploadTemplate::new(context).await } diff --git a/src/routes/logs.rs b/src/routes/logs.rs index 29e7932..5533f96 100644 --- a/src/routes/logs.rs +++ b/src/routes/logs.rs @@ -1,11 +1,10 @@ use askama::Template; use askama_web::WebTemplate; -use axum::extract::State; use snafu::prelude::*; use crate::database::operation::OperationLog; use crate::database::operation::OperationType; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Template, WebTemplate)] #[template(path = "logs/index.html")] @@ -14,14 +13,11 @@ pub struct LogTemplate { pub logs: Vec, } -pub async fn index( - app_state_context: AppStateContext, - State(app_state): State, -) -> Result { - let logs = app_state.logger.read().await.context(LoggerSnafu)?; +pub async fn index(context: AppStateContext) -> Result { + let logs = context.state.logger.read().await.context(LoggerSnafu)?; Ok(LogTemplate { - state: app_state_context, + state: context, logs, }) } diff --git a/src/routes/progress.rs b/src/routes/progress.rs index cb39fd8..6095297 100644 --- a/src/routes/progress.rs +++ b/src/routes/progress.rs @@ -1,12 +1,12 @@ use askama::Template; use askama_web::WebTemplate; -use axum::extract::{Path, State}; +use axum::extract::Path; use hightorrent_api::hightorrent::{SingleTarget, Torrent, TorrentContent}; use crate::extractors::torrent_list::{ TorrentListCounter, TorrentListFilter, TorrentListView, TorrentListViewRequest, }; -use crate::state::{AppState, AppStateContext, error::AppStateError}; +use crate::state::{AppStateContext, error::AppStateError}; #[derive(Template, WebTemplate)] #[template(path = "progress.html")] @@ -34,21 +34,21 @@ pub struct TorrentListContext { } pub async fn progress( - app_state_context: AppStateContext, - State(app_state): State, + context: AppStateContext, Path(view_request): Path, ) -> Result { // Failing to load the TorrentListView is a fatal error let TorrentListView { counter, filtered_list, - } = TorrentListView::apply_request(view_request.clone(), &app_state).await?; + } = TorrentListView::apply_request(view_request.clone(), &context.state).await?; // If only one torrent is inspected, display the content files let files = if filtered_list.len() == 1 { let torrent_id = &filtered_list.first().unwrap().id; Some( - app_state + context + .state .torrent_get_files(&SingleTarget::from(torrent_id)) .await?, ) @@ -57,7 +57,7 @@ pub async fn progress( }; Ok(TorrentListTemplate { - state: app_state_context, + state: context, filter: view_request, torrent_list: TorrentListContext { counter, diff --git a/src/state/context.rs b/src/state/context.rs index 1109c54..ae94d54 100644 --- a/src/state/context.rs +++ b/src/state/context.rs @@ -11,6 +11,7 @@ use super::*; pub struct AppStateContext { pub errors: Vec, pub free_space: FreeSpace, + pub state: AppState, pub user: Option, } @@ -24,6 +25,7 @@ impl FromRequestParts for AppStateContext { Ok(Self { errors: vec![], free_space: state.free_space()?, + state: state.clone(), user: User::from_request_parts(parts, state).await?, }) } From 863b477de9d6364342db55c7a323751ca2ef48a9 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Tue, 17 Feb 2026 13:38:01 +0100 Subject: [PATCH 5/5] refactor: Call table operators from AppStateContext.db --- src/database/mod.rs | 1 + src/database/operator.rs | 29 +++++++++++++++++++++++++++++ src/routes/category.rs | 16 ++++------------ src/routes/content_folder.rs | 20 +++++++++----------- src/routes/index.rs | 24 +++++++++++------------- src/state/context.rs | 8 +++++++- 6 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 src/database/operator.rs diff --git a/src/database/mod.rs b/src/database/mod.rs index d795369..bf0b773 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -2,3 +2,4 @@ pub mod category; pub mod content_folder; pub mod operation; +pub mod operator; diff --git a/src/database/operator.rs b/src/database/operator.rs new file mode 100644 index 0000000..566aaf8 --- /dev/null +++ b/src/database/operator.rs @@ -0,0 +1,29 @@ +use crate::database::{category::CategoryOperator, content_folder::ContentFolderOperator}; +use crate::extractors::user::User; +use crate::state::AppState; + +#[derive(Clone, Debug)] +pub struct DatabaseOperator { + pub state: AppState, + pub user: Option, +} + +impl DatabaseOperator { + pub fn new(state: AppState, user: Option) -> Self { + Self { state, user } + } + + pub fn category(&self) -> CategoryOperator { + CategoryOperator { + state: self.state.clone(), + user: self.user.clone(), + } + } + + pub fn content_folder(&self) -> ContentFolderOperator { + ContentFolderOperator { + state: self.state.clone(), + user: self.user.clone(), + } + } +} diff --git a/src/routes/category.rs b/src/routes/category.rs index 8ed11c8..d147374 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -7,9 +7,9 @@ use axum_extra::extract::CookieJar; use serde::{Deserialize, Serialize}; use snafu::prelude::*; +use crate::database::category; use crate::database::category::CategoryError; use crate::database::content_folder; -use crate::database::{category, category::CategoryOperator}; use crate::extractors::normalized_path::*; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppStateContext, error::*}; @@ -46,11 +46,7 @@ pub async fn delete( Path(id): Path, jar: CookieJar, ) -> Result { - let categories = CategoryOperator::new(context.state.clone(), context.user.clone()); - - let deleted = categories.delete(id).await; - - let operation_status = match deleted { + let operation_status = match context.db.category().delete(id).await { Ok(name) => OperationStatus { success: true, message: format!("The category {} has been successfully deleted", name), @@ -71,11 +67,7 @@ pub async fn create( jar: CookieJar, Form(form): Form, ) -> Result { - let categories = CategoryOperator::new(context.state.clone(), context.user.clone()); - - let created = categories.create(&form).await; - - match created { + match context.db.category().create(&form).await { Ok(created) => { let operation_status = OperationStatus { success: true, @@ -120,7 +112,7 @@ pub async fn show( Path(category_name): Path, jar: CookieJar, ) -> Result { - let categories = CategoryOperator::new(context.state.clone(), context.user.clone()); + let categories = context.db.category(); let category = categories .find_by_name(category_name.to_string()) diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index 77768aa..69df004 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -7,8 +7,7 @@ use camino::Utf8PathBuf; use serde::{Deserialize, Serialize}; use snafu::prelude::*; -use crate::database::category::CategoryOperator; -use crate::database::content_folder::{ContentFolderOperator, PathBreadcrumb}; +use crate::database::content_folder::PathBreadcrumb; use crate::database::{category, content_folder}; use crate::extractors::folder_request::FolderRequest; use crate::state::flash_message::{OperationStatus, get_cookie}; @@ -67,12 +66,12 @@ pub async fn create( jar: CookieJar, Form(mut form): Form, ) -> Result { - // let app_state_context = app_state.context().await?; - let content_folder = ContentFolderOperator::new(context.state.clone(), context.user.clone()); + let categories = context.db.category(); + let content_folders = context.db.content_folder(); // build path with Parent folder path (or category path if parent is None) + folder.name let parent_path = if let Some(parent_id) = form.parent_id { - let parent_folder = content_folder + let parent_folder = content_folders .find_by_id(parent_id) .await .context(ContentFolderSnafu)?; @@ -82,11 +81,10 @@ pub async fn create( }; // Get folder category - let category: category::Model = - CategoryOperator::new(context.state.clone(), context.user.clone()) - .find_by_id(form.category_id) - .await - .context(CategorySnafu)?; + let category: category::Model = categories + .find_by_id(form.category_id) + .await + .context(CategorySnafu)?; // If name contains "/" returns an error if form.name.contains("/") { @@ -107,7 +105,7 @@ pub async fn create( // build final path with parent_path and path of form form.path = format!("{}/{}", parent_path, form.name); - let created = content_folder.create(&form).await; + let created = content_folders.create(&form).await; match created { Ok(created) => { diff --git a/src/routes/index.rs b/src/routes/index.rs index 1bee0e1..7861ab9 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::{self, CategoryOperator}; +use crate::database::category; use crate::state::flash_message::{OperationStatus, get_cookie}; use crate::state::{AppStateContext, error::*}; @@ -33,10 +33,7 @@ impl IndexTemplate { context: AppStateContext, jar: CookieJar, ) -> Result<(CookieJar, Self), AppStateError> { - let categories = CategoryOperator::new(context.state.clone(), context.user.clone()) - .list() - .await - .context(CategorySnafu)?; + let categories = context.db.category().list().await.context(CategorySnafu)?; let (jar, operation_status) = get_cookie(jar); @@ -53,14 +50,15 @@ impl IndexTemplate { impl UploadTemplate { pub async fn new(context: AppStateContext) -> Result { - let categories: Vec = - CategoryOperator::new(context.state.clone(), context.user.clone()) - .list() - .await - .context(CategorySnafu)? - .into_iter() - .map(|x| x.name.to_string()) - .collect(); + let categories: Vec = context + .db + .category() + .list() + .await + .context(CategorySnafu)? + .into_iter() + .map(|x| x.name.to_string()) + .collect(); Ok(UploadTemplate { state: context, diff --git a/src/state/context.rs b/src/state/context.rs index ae94d54..e075e9a 100644 --- a/src/state/context.rs +++ b/src/state/context.rs @@ -1,6 +1,8 @@ use axum::extract::{FromRequestParts, OptionalFromRequestParts}; use axum::http::request::Parts; +use crate::database::operator::DatabaseOperator; + use super::*; /// Basic templating context used across pages. @@ -9,6 +11,7 @@ use super::*; /// and unrecoverable that it will trigger a global error /// by rendering the AppStateError into an axum Response. pub struct AppStateContext { + pub db: DatabaseOperator, pub errors: Vec, pub free_space: FreeSpace, pub state: AppState, @@ -22,11 +25,14 @@ impl FromRequestParts for AppStateContext { parts: &mut Parts, state: &AppState, ) -> Result { + let user = User::from_request_parts(parts, state).await?; + Ok(Self { + db: DatabaseOperator::new(state.clone(), user.clone()), errors: vec![], free_space: state.free_space()?, state: state.clone(), - user: User::from_request_parts(parts, state).await?, + user, }) } }