From 18518bedaf6a0e89379ca2d414cefed814e6c8bb Mon Sep 17 00:00:00 2001 From: zjean Date: Mon, 9 Mar 2026 14:34:07 +0100 Subject: [PATCH] fix: resolve clippy warnings and rustfmt issues for CI compliance Fix all clippy lints (collapsible if, clone on Copy, needless borrow, redundant bindings, unused params) and apply rustfmt across the codebase. Update test mocks to match Uuid-based trait signatures. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 2 +- build.rs | 64 +++++++------- src/application/ports/thumbnail_ports.rs | 6 +- .../services/auth_application_service.rs | 2 +- src/application/services/batch_operations.rs | 8 +- src/application/services/contact_service.rs | 45 ++++++---- .../services/file_management_service.rs | 16 ++-- .../services/idor_protection_test.rs | 65 ++++++++------ src/application/services/share_service.rs | 12 +-- src/application/services/trash_service.rs | 8 +- .../services/trash_service_test.rs | 76 ++++++++--------- src/domain/entities/file.rs | 6 +- src/domain/entities/folder.rs | 4 +- .../adapters/contact_storage_adapter.rs | 44 +++++++--- src/infrastructure/db.rs | 3 +- .../pg/app_password_pg_repository.rs | 4 +- .../repositories/pg/folder_db_repository.rs | 39 +++------ .../repositories/pg/share_pg_repository.rs | 19 ++--- .../repositories/pg/trash_db_repository.rs | 6 +- src/infrastructure/services/jwt_service.rs | 5 +- .../services/thumbnail_service.rs | 44 ++++------ src/interfaces/api/handlers/admin_handler.rs | 6 +- .../api/handlers/app_password_handler.rs | 5 +- src/interfaces/api/handlers/caldav_handler.rs | 13 ++- .../api/handlers/chunked_upload_handler.rs | 8 +- src/interfaces/api/handlers/dedup_handler.rs | 9 +- .../api/handlers/device_auth_handler.rs | 3 +- src/interfaces/api/handlers/file_handler.rs | 84 ++++++++----------- src/interfaces/api/handlers/webdav_handler.rs | 54 ++++++++++-- src/interfaces/api/handlers/wopi_handler.rs | 23 ++--- src/interfaces/api/routes.rs | 5 +- src/interfaces/middleware/auth.rs | 7 +- src/interfaces/nextcloud/ocs_handler.rs | 5 +- src/interfaces/web/mod.rs | 6 +- 34 files changed, 370 insertions(+), 336 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ada4a018..76996ad8 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2527,7 +2527,7 @@ dependencies = [ [[package]] name = "oxicloud" -version = "0.5.0" +version = "0.5.2" dependencies = [ "argon2", "async-compression", diff --git a/build.rs b/build.rs index 0eddd050..553952c4 100644 --- a/build.rs +++ b/build.rs @@ -109,7 +109,7 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { fs::write(dist_dir.join("css").join(&css_name), &css_bundle).expect("write css bundle"); // ── 4. Minify ALL individual CSS in static-dist/ ───────────────────────── - minify_tree_css(&dist_dir.join("css"), &css_name); + minify_tree_css(&dist_dir.join("css")); // ── 5. Build JS bundle for index.html ──────────────────────────────────── let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html"); @@ -120,11 +120,11 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { fs::write(dist_dir.join("js").join(&js_name), &js_bundle).expect("write js bundle"); // ── 6. Minify ALL individual JS in static-dist/ ────────────────────────── - minify_tree_js(&dist_dir.join("js"), &js_name); + minify_tree_js(&dist_dir.join("js")); // ── 7. Inline theme-init.js & rewrite index.html ────────────────────── - let theme_init = fs::read_to_string(static_dir.join("js/core/theme-init.js")) - .unwrap_or_default(); + let theme_init = + fs::read_to_string(static_dir.join("js/core/theme-init.js")).unwrap_or_default(); let theme_init_min = js_minify_safe(&theme_init); let rewritten_index = rewrite_index_html( &index_html, @@ -153,9 +153,7 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { // index.html too (future use / embedded route) fs::write(out_dir.join("index.html"), &rewritten_index).expect("write out index.html"); - eprintln!( - "cargo:warning=OxiCloud static-dist built ✓ CSS: {css_name} JS: {js_name}" - ); + eprintln!("cargo:warning=OxiCloud static-dist built ✓ CSS: {css_name} JS: {js_name}"); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -206,8 +204,8 @@ fn css_minify_safe(source: &str) -> String { fn css_minify(source: &str) -> Result { use lightningcss::stylesheet::{ParserOptions, PrinterOptions, StyleSheet}; - let mut sheet = StyleSheet::parse(source, ParserOptions::default()) - .map_err(|e| format!("{e}"))?; + let mut sheet = + StyleSheet::parse(source, ParserOptions::default()).map_err(|e| format!("{e}"))?; sheet .minify(Default::default()) @@ -224,12 +222,14 @@ fn css_minify(source: &str) -> Result { } /// Walk a directory and minify every `.css` in-place (skips generated bundles). -fn minify_tree_css(dir: &Path, skip_prefix: &str) { - let Ok(entries) = fs::read_dir(dir) else { return }; +fn minify_tree_css(dir: &Path) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; for entry in entries.flatten() { let p = entry.path(); if p.is_dir() { - minify_tree_css(&p, skip_prefix); + minify_tree_css(&p); } else if p.extension().is_some_and(|e| e == "css") { let fname = p.file_name().unwrap().to_string_lossy(); // Skip the generated bundle and already-processed main.css @@ -336,12 +336,14 @@ fn js_minify(source: &str) -> Result { } /// Walk a directory and minify every `.js` in-place (skips generated bundles). -fn minify_tree_js(dir: &Path, skip_prefix: &str) { - let Ok(entries) = fs::read_dir(dir) else { return }; +fn minify_tree_js(dir: &Path) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; for entry in entries.flatten() { let p = entry.path(); if p.is_dir() { - minify_tree_js(&p, skip_prefix); + minify_tree_js(&p); } else if p.extension().is_some_and(|e| e == "js") { let fname = p.file_name().unwrap().to_string_lossy(); if fname.starts_with("app.") { @@ -359,13 +361,15 @@ fn minify_tree_js(dir: &Path, skip_prefix: &str) { // ═══════════════════════════════════════════════════════════════════════════════ fn minify_tree_json(dir: &Path) { - let Ok(entries) = fs::read_dir(dir) else { return }; + let Ok(entries) = fs::read_dir(dir) else { + return; + }; for entry in entries.flatten() { let p = entry.path(); - if p.extension().is_some_and(|e| e == "json") { - if let Ok(src) = fs::read_to_string(&p) { - let _ = fs::write(&p, json_minify(&src)); - } + if p.extension().is_some_and(|e| e == "json") + && let Ok(src) = fs::read_to_string(&p) + { + let _ = fs::write(&p, json_minify(&src)); } } } @@ -407,12 +411,7 @@ fn json_minify(source: &str) -> String { // ═══════════════════════════════════════════════════════════════════════════════ /// Rewrite index.html: single CSS bundle, inline theme-init, single JS bundle. -fn rewrite_index_html( - html: &str, - css_path: &str, - js_path: &str, - inline_theme_js: &str, -) -> String { +fn rewrite_index_html(html: &str, css_path: &str, js_path: &str, inline_theme_js: &str) -> String { let mut out: Vec = Vec::with_capacity(html.lines().count()); let mut css_done = false; let mut defer_done = false; @@ -423,19 +422,14 @@ fn rewrite_index_html( // ── Replace all stylesheet s with single bundle ──────────────── if t.starts_with("" - )); + out.push(format!(" ")); css_done = true; } continue; } // ── Replace sync theme-init.js with inline ")); continue; } @@ -443,9 +437,7 @@ fn rewrite_index_html( // ── Replace all defer " - )); + out.push(format!(" ")); defer_done = true; } continue; diff --git a/src/application/ports/thumbnail_ports.rs b/src/application/ports/thumbnail_ports.rs index 8dcc04b7..5d5b5fa7 100755 --- a/src/application/ports/thumbnail_ports.rs +++ b/src/application/ports/thumbnail_ports.rs @@ -89,11 +89,7 @@ pub trait ThumbnailPort: Send + Sync + 'static { /// Returns `None` if no cached thumbnail exists on disk or in memory. /// Used for non-image file types (videos) where thumbnails are /// generated client-side and uploaded. - async fn get_cached_thumbnail( - &self, - file_id: &str, - size: ThumbnailSize, - ) -> Option; + async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option; /// Store an externally-generated thumbnail (e.g. client-side video frame). /// diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 7c77b104..6b888e06 100755 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -17,11 +17,11 @@ use crate::infrastructure::services::jwt_service::JwtTokenService; use crate::infrastructure::services::oidc_service::OidcService; use crate::infrastructure::services::password_hasher::Argon2PasswordHasher; use moka::sync::Cache; -use uuid::Uuid; use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; use std::time::Duration; +use uuid::Uuid; /// Result of a successful OIDC callback. The handler layer inspects this to /// decide whether to redirect to the regular frontend or complete a Nextcloud diff --git a/src/application/services/batch_operations.rs b/src/application/services/batch_operations.rs index 9881bc63..a149ff6c 100755 --- a/src/application/services/batch_operations.rs +++ b/src/application/services/batch_operations.rs @@ -448,11 +448,9 @@ impl BatchOperationService { }, }; - let uid = user_id; - let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| { let trash = trash_service.clone(); - let uid = uid; + let uid = user_id; async move { let trash_result = trash.move_to_trash(&file_id, "file", uid).await; @@ -513,11 +511,9 @@ impl BatchOperationService { }, }; - let uid = user_id; - let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| { let trash = trash_service.clone(); - let uid = uid; + let uid = user_id; async move { let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await; diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index 0a7904a7..f95e9406 100755 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -1,6 +1,6 @@ use chrono::Utc; -use uuid::Uuid; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::address_book_dto::{ AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto, @@ -296,8 +296,11 @@ impl AddressBookUseCase for ContactService { // Check if user has write access to the address book let address_book = self - .check_address_book_write_access(&id, &Uuid::parse_str(&update.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) + .check_address_book_write_access( + &id, + &Uuid::parse_str(&update.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) .await?; // Apply updates @@ -526,9 +529,12 @@ impl ContactUseCase for ContactService { .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has write access to the address book - self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + &address_book_id, + &Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; // Convert DTOs to domain entities let email: Vec = dto @@ -604,9 +610,12 @@ impl ContactUseCase for ContactService { .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has write access to the address book - self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + &address_book_id, + &Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; // Parse vCard data let mut contact = self.parse_vcard(&dto.vcard)?; @@ -819,9 +828,12 @@ impl ContactUseCase for ContactService { .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has write access to the address book - self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + &address_book_id, + &Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; let group = ContactGroup::new(address_book_id, dto.name); @@ -845,9 +857,12 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact group", "not found"))?; // Check if user has write access to the address book - self.check_address_book_write_access(group.address_book_id(), &Uuid::parse_str(&update.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + group.address_book_id(), + &Uuid::parse_str(&update.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; // Update the group let updated_group = ContactGroup::from_raw( diff --git a/src/application/services/file_management_service.rs b/src/application/services/file_management_service.rs index 06ba7ff5..c08ff9ce 100755 --- a/src/application/services/file_management_service.rs +++ b/src/application/services/file_management_service.rs @@ -178,10 +178,10 @@ impl FileManagementUseCase for FileManagementService { async fn delete_file(&self, id: &str) -> Result<(), DomainError> { self.file_repository.delete_file(id).await?; // Best-effort thumbnail cleanup - if let Some(thumb) = &self.thumbnail_service { - if let Err(e) = thumb.delete_thumbnails(id).await { - warn!("Failed to delete thumbnails for file {}: {}", id, e); - } + if let Some(thumb) = &self.thumbnail_service + && let Err(e) = thumb.delete_thumbnails(id).await + { + warn!("Failed to delete thumbnails for file {}: {}", id, e); } Ok(()) } @@ -223,10 +223,10 @@ impl FileManagementUseCase for FileManagementService { warn!("Permanently deleting file: {}", id); self.file_repository.delete_file(id).await?; // Best-effort thumbnail cleanup - if let Some(thumb) = &self.thumbnail_service { - if let Err(e) = thumb.delete_thumbnails(id).await { - warn!("Failed to delete thumbnails for file {}: {}", id, e); - } + if let Some(thumb) = &self.thumbnail_service + && let Err(e) = thumb.delete_thumbnails(id).await + { + warn!("Failed to delete thumbnails for file {}: {}", id, e); } info!("File permanently deleted: {}", id); diff --git a/src/application/services/idor_protection_test.rs b/src/application/services/idor_protection_test.rs index fef54a17..a221e5b2 100755 --- a/src/application/services/idor_protection_test.rs +++ b/src/application/services/idor_protection_test.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Mutex; +use uuid::Uuid; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; use crate::common::errors::DomainError; @@ -22,7 +23,7 @@ use crate::domain::services::path_service::StoragePath; /// A simple in-memory mock that maps (file_id → (File, owner_id)). struct MockFileReadPort { /// file_id → (File, owner_id) - files: Mutex>, + files: Mutex>, } impl MockFileReadPort { @@ -33,7 +34,7 @@ impl MockFileReadPort { } /// Insert a test file owned by `owner_id`. - fn insert(&self, id: &str, name: &str, owner_id: &str) { + fn insert(&self, id: &str, name: &str, owner_id: Uuid) { let file = File::new( id.to_string(), name.to_string(), @@ -46,7 +47,7 @@ impl MockFileReadPort { self.files .lock() .unwrap() - .insert(id.to_string(), (file, owner_id.to_string())); + .insert(id.to_string(), (file, owner_id)); } } @@ -59,10 +60,10 @@ impl FileReadPort for MockFileReadPort { .ok_or_else(|| DomainError::not_found("File", id.to_string())) } - async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result { + async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result { let files = self.files.lock().unwrap(); match files.get(id) { - Some((file, actual_owner)) if actual_owner == owner_id => Ok(file.clone()), + Some((file, actual_owner)) if *actual_owner == owner_id => Ok(file.clone()), // Return NotFound regardless — do not leak existence _ => Err(DomainError::not_found("File", id.to_string())), } @@ -104,7 +105,7 @@ impl FileReadPort for MockFileReadPort { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -113,7 +114,7 @@ impl FileReadPort for MockFileReadPort { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result { Ok(0) } @@ -248,20 +249,23 @@ impl FileWritePort for MockFileWritePort { #[tokio::test] async fn get_file_for_owner_returns_file_for_correct_owner() { + let alice_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - repo.insert("file-1", "secret.txt", "alice"); + repo.insert("file-1", "secret.txt", alice_id); - let result = repo.get_file_for_owner("file-1", "alice").await; + let result = repo.get_file_for_owner("file-1", alice_id).await; assert!(result.is_ok(), "owner should be able to read own file"); assert_eq!(result.unwrap().id(), "file-1"); } #[tokio::test] async fn get_file_for_owner_rejects_wrong_owner() { + let alice_id = Uuid::new_v4(); + let bob_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - repo.insert("file-1", "secret.txt", "alice"); + repo.insert("file-1", "secret.txt", alice_id); - let result = repo.get_file_for_owner("file-1", "bob").await; + let result = repo.get_file_for_owner("file-1", bob_id).await; assert!(result.is_err(), "non-owner should be rejected"); // Must be NotFound, NOT Forbidden — avoids leaking existence @@ -276,20 +280,23 @@ async fn get_file_for_owner_rejects_wrong_owner() { #[tokio::test] async fn get_file_for_owner_returns_not_found_for_missing_file() { + let alice_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - let result = repo.get_file_for_owner("nonexistent", "alice").await; + let result = repo.get_file_for_owner("nonexistent", alice_id).await; assert!(result.is_err()); } #[tokio::test] async fn verify_file_owner_uses_default_impl() { + let alice_id = Uuid::new_v4(); + let bob_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - repo.insert("file-1", "secret.txt", "alice"); + repo.insert("file-1", "secret.txt", alice_id); // Default impl delegates to get_file_for_owner and maps to () - assert!(repo.verify_file_owner("file-1", "alice").await.is_ok()); - assert!(repo.verify_file_owner("file-1", "bob").await.is_err()); + assert!(repo.verify_file_owner("file-1", alice_id).await.is_ok()); + assert!(repo.verify_file_owner("file-1", bob_id).await.is_err()); } // ═══════════════════════════════════════════════════════════════════════════ @@ -310,15 +317,17 @@ async fn verify_file_owner_uses_default_impl() { async fn verify_file_owner_delegates_to_read_port() { // This test verifies the FileReadPort contract that verify_file_owner // returns Ok for the correct owner and Err for others. + let user_id = Uuid::new_v4(); + let attacker_id = Uuid::new_v4(); let read = MockFileReadPort::new(); - read.insert("abc-123", "report.pdf", "user-42"); + read.insert("abc-123", "report.pdf", user_id); // Same user → Ok - let ok = read.verify_file_owner("abc-123", "user-42").await; + let ok = read.verify_file_owner("abc-123", user_id).await; assert!(ok.is_ok(), "correct owner should pass verify_file_owner"); // Different user → Err - let err = read.verify_file_owner("abc-123", "attacker-99").await; + let err = read.verify_file_owner("abc-123", attacker_id).await; assert!(err.is_err(), "wrong owner should fail verify_file_owner"); } @@ -326,15 +335,17 @@ async fn verify_file_owner_delegates_to_read_port() { async fn owned_methods_require_ownership_check_first() { // Simulate what the _owned methods do: verify_owner then delegate. // We test with the mock read port to prove the sequence. + let owner_id = Uuid::new_v4(); + let attacker_id = Uuid::new_v4(); let read = MockFileReadPort::new(); - read.insert("file-1", "data.csv", "owner-a"); + read.insert("file-1", "data.csv", owner_id); // Step 1: verify_owner for correct owner → Ok - let step1 = read.verify_file_owner("file-1", "owner-a").await; + let step1 = read.verify_file_owner("file-1", owner_id).await; assert!(step1.is_ok()); // Step 2: verify_owner for attacker → Err, so the move/rename never executes - let step2 = read.verify_file_owner("file-1", "attacker").await; + let step2 = read.verify_file_owner("file-1", attacker_id).await; assert!(step2.is_err()); } @@ -347,18 +358,20 @@ use crate::common::stubs::StubFileManagementUseCase; #[tokio::test] async fn stub_move_file_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileManagementUseCase; let result = stub - .move_file_owned("file-1", "user-1", Some("folder-2".to_string())) + .move_file_owned("file-1", user_id, Some("folder-2".to_string())) .await; assert!(result.is_ok(), "stub should return Ok for move_file_owned"); } #[tokio::test] async fn stub_rename_file_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileManagementUseCase; let result = stub - .rename_file_owned("file-1", "user-1", "new-name.txt") + .rename_file_owned("file-1", user_id, "new-name.txt") .await; assert!( result.is_ok(), @@ -371,16 +384,18 @@ use crate::common::stubs::StubFileRetrievalUseCase; #[tokio::test] async fn stub_get_file_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileRetrievalUseCase; - let result = stub.get_file_owned("file-1", "user-1").await; + let result = stub.get_file_owned("file-1", user_id).await; assert!(result.is_ok(), "stub should return Ok for get_file_owned"); } #[tokio::test] async fn stub_get_file_optimized_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileRetrievalUseCase; let result = stub - .get_file_optimized_owned("file-1", "user-1", true, false) + .get_file_optimized_owned("file-1", user_id, true, false) .await; assert!( result.is_ok(), diff --git a/src/application/services/share_service.rs b/src/application/services/share_service.rs index 1d88f22f..14e25800 100755 --- a/src/application/services/share_service.rs +++ b/src/application/services/share_service.rs @@ -806,7 +806,7 @@ mod tests { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -815,7 +815,7 @@ mod tests { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result { Ok(0) } @@ -839,7 +839,7 @@ mod tests { async fn get_file_for_owner( &self, id: &str, - _owner_id: &str, + _owner_id: Uuid, ) -> Result { self.get_file(id).await } @@ -891,7 +891,7 @@ mod tests { async fn list_folders_by_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> Result, DomainError> { unimplemented!() } @@ -910,7 +910,7 @@ mod tests { async fn list_folders_by_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _offset: usize, _limit: usize, _include_total: bool, @@ -971,7 +971,7 @@ mod tests { async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> Result { unimplemented!() diff --git a/src/application/services/trash_service.rs b/src/application/services/trash_service.rs index 881bd4e5..c3323e23 100755 --- a/src/application/services/trash_service.rs +++ b/src/application/services/trash_service.rs @@ -566,10 +566,10 @@ impl TrashUseCase for TrashService { // Best-effort thumbnail cleanup — thumbnails are cache // artifacts, so failure must not block file deletion. - if let Some(thumb) = &self.thumbnail_service { - if let Err(e) = thumb.delete_thumbnails(&file_id).await { - warn!("Failed to delete thumbnails for file {}: {}", file_id, e); - } + if let Some(thumb) = &self.thumbnail_service + && let Err(e) = thumb.delete_thumbnails(&file_id).await + { + warn!("Failed to delete thumbnails for file {}: {}", file_id, e); } } TrashedItemType::Folder => { diff --git a/src/application/services/trash_service_test.rs b/src/application/services/trash_service_test.rs index cd20fded..620c2828 100755 --- a/src/application/services/trash_service_test.rs +++ b/src/application/services/trash_service_test.rs @@ -61,10 +61,8 @@ where FW: FileWritePort, FoR: FolderRepository, { - async fn get_trash_items(&self, user_id: &str) -> Result> { - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; - let items = self.trash_repository.get_trash_items(&user_uuid).await?; + async fn get_trash_items(&self, user_id: Uuid) -> Result> { + let items = self.trash_repository.get_trash_items(&user_id).await?; Ok(items .into_iter() .map(|item| { @@ -85,11 +83,9 @@ where .collect()) } - async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> { + async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> { let item_uuid = Uuid::parse_str(item_id) .map_err(|e| DomainError::validation_error(format!("Invalid item ID: {}", e)))?; - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; match item_type { "file" => { @@ -103,7 +99,7 @@ where let original_path = file.storage_path().to_string(); let trashed_item = TrashedItem::new( item_uuid, - user_uuid, + user_id, TrashedItemType::File, file.name().to_string(), original_path, @@ -145,7 +141,7 @@ where let original_path = folder.storage_path().to_string(); let trashed_item = TrashedItem::new( item_uuid, - user_uuid, + user_id, TrashedItemType::Folder, folder.name().to_string(), original_path, @@ -179,15 +175,13 @@ where } } - async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> { + async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> { let trash_uuid = Uuid::parse_str(trash_id) .map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?; - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; let item = self .trash_repository - .get_trash_item(&trash_uuid, &user_uuid) + .get_trash_item(&trash_uuid, &user_id) .await?; match item { Some(item) => { @@ -228,7 +222,7 @@ where } } self.trash_repository - .restore_from_trash(&trash_uuid, &user_uuid) + .restore_from_trash(&trash_uuid, &user_id) .await .map_err(|e| { DomainError::new( @@ -243,15 +237,13 @@ where } } - async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> { + async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> { let trash_uuid = Uuid::parse_str(trash_id) .map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?; - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; let item = self .trash_repository - .get_trash_item(&trash_uuid, &user_uuid) + .get_trash_item(&trash_uuid, &user_id) .await?; match item { Some(item) => { @@ -287,7 +279,7 @@ where } } self.trash_repository - .delete_permanently(&trash_uuid, &user_uuid) + .delete_permanently(&trash_uuid, &user_id) .await .map_err(|e| { DomainError::new( @@ -302,10 +294,8 @@ where } } - async fn empty_trash(&self, user_id: &str) -> Result<()> { - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; - self.trash_repository.clear_trash(&user_uuid).await + async fn empty_trash(&self, user_id: Uuid) -> Result<()> { + self.trash_repository.clear_trash(&user_id).await } } @@ -492,7 +482,7 @@ impl FileReadPort for MockFileRepository { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> std::result::Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -501,7 +491,7 @@ impl FileReadPort for MockFileRepository { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> std::result::Result { Ok(0) } @@ -519,7 +509,7 @@ impl FileReadPort for MockFileRepository { async fn get_file_for_owner( &self, id: &str, - _owner_id: &str, + _owner_id: Uuid, ) -> std::result::Result { // In this mock, ignore ownership — trash tests don't focus on ownership self.get_file(id).await @@ -697,7 +687,7 @@ impl FolderRepository for MockFolderRepository { async fn list_folders_by_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> std::result::Result, DomainError> { Ok(vec![]) } @@ -715,7 +705,7 @@ impl FolderRepository for MockFolderRepository { async fn list_folders_by_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _offset: usize, _limit: usize, _include_total: bool, @@ -799,7 +789,7 @@ impl FolderRepository for MockFolderRepository { async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> std::result::Result { Ok(Folder::default()) @@ -839,18 +829,18 @@ mod tests { let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test file to the repository file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); // Act - let result = service.move_to_trash(file_id, "file", user_id).await; + let result = service.move_to_trash(file_id, "file", user_uuid).await; // Assert assert!(result.is_ok(), "Moving file to trash failed: {:?}", result); // Verify the file is in trash - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( @@ -913,12 +903,13 @@ mod tests { let folder_id = "550e8400-e29b-41d4-a716-446655440002"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test folder to the repository folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder"); // Act - let result = service.move_to_trash(folder_id, "folder", user_id).await; + let result = service.move_to_trash(folder_id, "folder", user_uuid).await; // Assert assert!( @@ -928,7 +919,6 @@ mod tests { ); // Verify the folder is in trash - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( @@ -978,22 +968,22 @@ mod tests { let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); let file_path = "/test/path/test.txt"; // Add a test file and move it to trash file_repo.add_test_file(file_id, "test.txt", file_path); service - .move_to_trash(file_id, "file", user_id) + .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); // Get the trash item ID - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_id = trash_items[0].id().to_string(); // Act - let result = service.restore_item(&trash_id, user_id).await; + let result = service.restore_item(&trash_id, user_uuid).await; // Assert assert!( @@ -1048,21 +1038,21 @@ mod tests { let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test file and move it to trash file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); service - .move_to_trash(file_id, "file", user_id) + .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); // Get the trash item ID - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_id = trash_items[0].id().to_string(); // Act - let result = service.delete_permanently(&trash_id, user_id).await; + let result = service.delete_permanently(&trash_id, user_uuid).await; // Assert assert!( @@ -1116,6 +1106,7 @@ mod tests { ); let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add multiple files and folders to trash let file_ids = [ @@ -1136,7 +1127,7 @@ mod tests { &format!("/test/path/test{}.txt", i), ); service - .move_to_trash(file_id, "file", user_id) + .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); } @@ -1148,18 +1139,17 @@ mod tests { &format!("/test/path/folder{}", i), ); service - .move_to_trash(folder_id, "folder", user_id) + .move_to_trash(folder_id, "folder", user_uuid) .await .unwrap(); } // Verify items are in trash - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!(trash_items.len(), 4, "Should have 4 items in trash"); // Act - let result = service.empty_trash(user_id).await; + let result = service.empty_trash(user_uuid).await; // Assert assert!(result.is_ok(), "Emptying trash failed: {:?}", result); diff --git a/src/domain/entities/file.rs b/src/domain/entities/file.rs index 5dac1fc5..6129969e 100755 --- a/src/domain/entities/file.rs +++ b/src/domain/entities/file.rs @@ -310,7 +310,7 @@ impl File { folder_id: self.folder_id.clone(), created_at: self.created_at, modified_at: now, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, }) } @@ -344,7 +344,7 @@ impl File { folder_id, created_at: self.created_at, modified_at: now, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, }) } @@ -365,7 +365,7 @@ impl File { folder_id: self.folder_id.clone(), created_at: self.created_at, modified_at: now, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, } } } diff --git a/src/domain/entities/folder.rs b/src/domain/entities/folder.rs index 67d08035..fdf82bb4 100755 --- a/src/domain/entities/folder.rs +++ b/src/domain/entities/folder.rs @@ -234,7 +234,7 @@ impl Folder { storage_path: new_storage_path, path_string: new_path_string, parent_id: self.parent_id.clone(), - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, created_at: self.created_at, modified_at: now, }) @@ -266,7 +266,7 @@ impl Folder { storage_path: new_storage_path, path_string: new_path_string, parent_id, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, created_at: self.created_at, modified_at: now, }) diff --git a/src/infrastructure/adapters/contact_storage_adapter.rs b/src/infrastructure/adapters/contact_storage_adapter.rs index b80f3b89..d629be2c 100755 --- a/src/infrastructure/adapters/contact_storage_adapter.rs +++ b/src/infrastructure/adapters/contact_storage_adapter.rs @@ -86,7 +86,10 @@ impl ContactStorageAdapter { .address_book_repository .get_address_book_shares(address_book_id) .await?; - if shares.iter().any(|(shared_user, _)| shared_user == &user_id.to_string()) { + if shares + .iter() + .any(|(shared_user, _)| shared_user == &user_id.to_string()) + { return Ok(address_book); } @@ -248,7 +251,11 @@ impl AddressBookUseCase for ContactStorageAdapter { // Check write access let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "AddressBook", + "Invalid user ID format", + ) })?; let mut address_book = self.check_write_access(&uuid, user_id).await?; @@ -364,7 +371,11 @@ impl AddressBookUseCase for ContactStorageAdapter { } let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "AddressBook", + "Invalid target user ID format", + ) })?; self.address_book_repository @@ -397,7 +408,11 @@ impl AddressBookUseCase for ContactStorageAdapter { } let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "AddressBook", + "Invalid target user ID format", + ) })?; self.address_book_repository @@ -443,8 +458,7 @@ impl ContactUseCase for ContactStorageAdapter { let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format") })?; - self.check_write_access(&address_book_id, user_id) - .await?; + self.check_write_access(&address_book_id, user_id).await?; let now = chrono::Utc::now(); let mut contact = Contact::from_raw( @@ -488,8 +502,7 @@ impl ContactUseCase for ContactStorageAdapter { let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format") })?; - self.check_write_access(&address_book_id, user_id) - .await?; + self.check_write_access(&address_book_id, user_id).await?; // Parse vCard fields let now = chrono::Utc::now(); @@ -742,10 +755,13 @@ impl ContactUseCase for ContactStorageAdapter { // Check write access let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "ContactGroup", + "Invalid user ID format", + ) })?; - self.check_write_access(&address_book_id, user_id) - .await?; + self.check_write_access(&address_book_id, user_id).await?; let group = ContactGroup::new(address_book_id, dto.name); @@ -770,7 +786,11 @@ impl ContactUseCase for ContactStorageAdapter { // Check write access let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "ContactGroup", + "Invalid user ID format", + ) })?; self.check_write_access(group.address_book_id(), user_id) .await?; diff --git a/src/infrastructure/db.rs b/src/infrastructure/db.rs index 1b2ee68d..b7d1c1d5 100755 --- a/src/infrastructure/db.rs +++ b/src/infrastructure/db.rs @@ -283,7 +283,8 @@ fn split_sql_statements(sql: &str) -> Vec { if i >= len { break; } - if bytes[i] == b'$' && i + tag_bytes.len() <= len + if bytes[i] == b'$' + && i + tag_bytes.len() <= len && &bytes[i..i + tag_bytes.len()] == tag_bytes { current.push_str(&tag); diff --git a/src/infrastructure/repositories/pg/app_password_pg_repository.rs b/src/infrastructure/repositories/pg/app_password_pg_repository.rs index 8c1b2635..15fffa73 100755 --- a/src/infrastructure/repositories/pg/app_password_pg_repository.rs +++ b/src/infrastructure/repositories/pg/app_password_pg_repository.rs @@ -32,8 +32,8 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) "#, ) - .bind(&ap.id) - .bind(&ap.user_id) + .bind(ap.id) + .bind(ap.user_id) .bind(&ap.label) .bind(&ap.password_hash) .bind(&ap.prefix) diff --git a/src/infrastructure/repositories/pg/folder_db_repository.rs b/src/infrastructure/repositories/pg/folder_db_repository.rs index 7bf76a85..ba346322 100755 --- a/src/infrastructure/repositories/pg/folder_db_repository.rs +++ b/src/infrastructure/repositories/pg/folder_db_repository.rs @@ -21,16 +21,7 @@ use crate::domain::services::path_service::StoragePath; type FolderRow = (String, String, String, Option, Uuid, i64, i64); /// Type alias for paginated folder rows (includes total_count). -type FolderRowPaginated = ( - String, - String, - String, - Option, - Uuid, - i64, - i64, - i64, -); +type FolderRowPaginated = (String, String, String, Option, Uuid, i64, i64, i64); /// Type alias for folder rows with optional user_id. type FolderRowOptUser = ( @@ -108,14 +99,14 @@ impl FolderRepository for FolderDbRepository { // caller to have set up the home folder beforehand (done during user // registration). let user_id: Uuid = if let Some(ref pid) = parent_id { - sqlx::query_scalar::<_, Uuid>( - "SELECT user_id FROM storage.folders WHERE id = $1::uuid", - ) - .bind(pid) - .fetch_optional(self.pool()) - .await - .map_err(|e| DomainError::internal_error("FolderDb", format!("parent lookup: {e}")))? - .ok_or_else(|| DomainError::not_found("Folder", pid))? + sqlx::query_scalar::<_, Uuid>("SELECT user_id FROM storage.folders WHERE id = $1::uuid") + .bind(pid) + .fetch_optional(self.pool()) + .await + .map_err(|e| { + DomainError::internal_error("FolderDb", format!("parent lookup: {e}")) + })? + .ok_or_else(|| DomainError::not_found("Folder", pid))? } else { return Err(DomainError::internal_error( "FolderDb", @@ -647,15 +638,9 @@ impl FolderRepository for FolderDbRepository { .map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?; match row { - Some((id, path, ca, ma)) => Self::row_to_folder( - id, - name.clone(), - path, - None, - Some(user_id), - ca, - ma, - ), + Some((id, path, ca, ma)) => { + Self::row_to_folder(id, name.clone(), path, None, Some(user_id), ca, ma) + } None => { // Already exists — fetch it let existing = sqlx::query_as::<_, (String, String, i64, i64)>( diff --git a/src/infrastructure/repositories/pg/share_pg_repository.rs b/src/infrastructure/repositories/pg/share_pg_repository.rs index bfdff9ba..592ae040 100755 --- a/src/infrastructure/repositories/pg/share_pg_repository.rs +++ b/src/infrastructure/repositories/pg/share_pg_repository.rs @@ -195,16 +195,15 @@ impl ShareStoragePort for SharePgRepository { } async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> { - let result = - sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2") - .bind(id) - .bind(user_id) - .execute(&*self.db_pool) - .await - .map_err(|e| { - tracing::error!("Database error deleting share for user: {}", e); - DomainError::internal_error("Share", format!("Failed to delete share: {e}")) - })?; + let result = sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2") + .bind(id) + .bind(user_id) + .execute(&*self.db_pool) + .await + .map_err(|e| { + tracing::error!("Database error deleting share for user: {}", e); + DomainError::internal_error("Share", format!("Failed to delete share: {e}")) + })?; if result.rows_affected() == 0 { // SECURITY: could be non-existent or owned by another user — same 404 diff --git a/src/infrastructure/repositories/pg/trash_db_repository.rs b/src/infrastructure/repositories/pg/trash_db_repository.rs index e8cc3ec9..9c0ed8a0 100755 --- a/src/infrastructure/repositories/pg/trash_db_repository.rs +++ b/src/infrastructure/repositories/pg/trash_db_repository.rs @@ -64,9 +64,9 @@ impl TrashDbRepository { // In the soft-delete model, the trash entry ID is the same as the // original item ID since there is no separate trash table. TrashedItem::from_raw( - id, // trash entry id (same as original) - id, // original item id - user_id, // owner + id, // trash entry id (same as original) + id, // original item id + user_id, // owner item_type_enum, name.clone(), String::new(), // original_path — not stored separately in soft-delete model diff --git a/src/infrastructure/services/jwt_service.rs b/src/infrastructure/services/jwt_service.rs index 930c2ba2..7570c70f 100755 --- a/src/infrastructure/services/jwt_service.rs +++ b/src/infrastructure/services/jwt_service.rs @@ -262,10 +262,11 @@ impl TokenServicePort for JwtTokenService { mod tests { use super::*; use crate::domain::entities::user::{User, UserRole}; + use uuid::Uuid; fn create_test_user() -> User { User::from_data( - "test-user-id".to_string(), + Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), "testuser".to_string(), "test@example.com".to_string(), "hashed_password".to_string(), @@ -295,7 +296,7 @@ mod tests { let claims = service .validate_token(&token) .expect("Should validate token"); - assert_eq!(claims.sub, user.id()); + assert_eq!(claims.sub, user.id().to_string()); assert_eq!(claims.username, user.username()); assert_eq!(claims.email, user.email()); } diff --git a/src/infrastructure/services/thumbnail_service.rs b/src/infrastructure/services/thumbnail_service.rs index 8de8610b..fa696295 100755 --- a/src/infrastructure/services/thumbnail_service.rs +++ b/src/infrastructure/services/thumbnail_service.rs @@ -1,6 +1,6 @@ use bytes::Bytes; -use image::imageops::FilterType; use image::codecs::jpeg::JpegEncoder; +use image::imageops::FilterType; /** * Thumbnail Generation Service * @@ -236,20 +236,16 @@ impl ThumbnailService { /// Unlike `get_thumbnail`, this does **not** generate a new thumbnail. /// Useful for non-image file types (videos) where a client-generated /// thumbnail may have been uploaded previously. - pub async fn get_cached_thumbnail( - &self, - file_id: &str, - size: ThumbnailSize, - ) -> Option { + pub async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option { // 1. Check in-memory cache let cache_key = ThumbnailCacheKey { file_id: file_id.to_string(), size, }; - if let Some(bytes) = self.cache.get(&cache_key).await { - if !bytes.is_empty() { - return Some(bytes); - } + if let Some(bytes) = self.cache.get(&cache_key).await + && !bytes.is_empty() + { + return Some(bytes); } // 2. Check disk @@ -285,17 +281,17 @@ impl ThumbnailService { let jpeg_bytes = tokio::task::spawn_blocking(move || -> Result, ThumbnailError> { // ── Fast path: already a correctly-sized JPEG ───────────── // JPEG files start with SOI marker 0xFF 0xD8. - if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 { - if let Ok(reader) = image::ImageReader::new(std::io::Cursor::new(&data)) - .with_guessed_format() - { - if let Ok((w, h)) = reader.into_dimensions() { - if w <= max_dim && h <= max_dim { - // Already JPEG at correct size — zero-copy store - return Ok(data.to_vec()); - } - } - } + if data.len() >= 2 + && data[0] == 0xFF + && data[1] == 0xD8 + && let Ok(reader) = + image::ImageReader::new(std::io::Cursor::new(&data)).with_guessed_format() + && let Ok((w, h)) = reader.into_dimensions() + && w <= max_dim + && h <= max_dim + { + // Already JPEG at correct size — zero-copy store + return Ok(data.to_vec()); } // ── Slow path: decode, resize, re-encode to JPEG ───────── @@ -637,11 +633,7 @@ impl ThumbnailPort for ThumbnailService { .map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string())) } - async fn get_cached_thumbnail( - &self, - file_id: &str, - size: PortThumbnailSize, - ) -> Option { + async fn get_cached_thumbnail(&self, file_id: &str, size: PortThumbnailSize) -> Option { self.get_cached_thumbnail(file_id, size.into()).await } diff --git a/src/interfaces/api/handlers/admin_handler.rs b/src/interfaces/api/handlers/admin_handler.rs index 7f37ed0c..6cf5a458 100755 --- a/src/interfaces/api/handlers/admin_handler.rs +++ b/src/interfaces/api/handlers/admin_handler.rs @@ -73,7 +73,11 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, Str )); } - Ok((Uuid::parse_str(&claims.sub).map_err(|_| AppError::internal_error("Invalid user ID in token"))?, claims.role)) + Ok(( + Uuid::parse_str(&claims.sub) + .map_err(|_| AppError::internal_error("Invalid user ID in token"))?, + claims.role, + )) } /// GET /api/admin/settings/oidc — get OIDC settings for the admin panel diff --git a/src/interfaces/api/handlers/app_password_handler.rs b/src/interfaces/api/handlers/app_password_handler.rs index 91ac4a8c..c3bc702f 100755 --- a/src/interfaces/api/handlers/app_password_handler.rs +++ b/src/interfaces/api/handlers/app_password_handler.rs @@ -75,10 +75,7 @@ async fn revoke_app_password( let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; - let response = service - .revoke(user.id, id) - .await - .map_err(AppError::from)?; + let response = service.revoke(user.id, id).await.map_err(AppError::from)?; Ok(Json(response)) } diff --git a/src/interfaces/api/handlers/caldav_handler.rs b/src/interfaces/api/handlers/caldav_handler.rs index 8d2c35fc..ac18fac3 100755 --- a/src/interfaces/api/handlers/caldav_handler.rs +++ b/src/interfaces/api/handlers/caldav_handler.rs @@ -299,13 +299,12 @@ async fn handle_propfind( } else { // Not a calendar ID — treat as user calendar home (e.g. /caldav/{username}/) // List all calendars for this user - let calendars = - calendar_service - .list_my_calendars(user.id) - .await - .map_err(|e| { - AppError::internal_error(format!("Failed to list calendars: {}", e)) - })?; + let calendars = calendar_service + .list_my_calendars(user.id) + .await + .map_err(|e| { + AppError::internal_error(format!("Failed to list calendars: {}", e)) + })?; let base_href = &format!("/caldav/{}/", first_segment); let mut response_body = Vec::new(); diff --git a/src/interfaces/api/handlers/chunked_upload_handler.rs b/src/interfaces/api/handlers/chunked_upload_handler.rs index afbf387a..8fce8b93 100755 --- a/src/interfaces/api/handlers/chunked_upload_handler.rs +++ b/src/interfaces/api/handlers/chunked_upload_handler.rs @@ -190,13 +190,7 @@ impl ChunkedUploadHandler { }); match chunked_service - .upload_chunk( - &upload_id, - auth_user.id, - params.chunk_index, - body, - checksum, - ) + .upload_chunk(&upload_id, auth_user.id, params.chunk_index, body, checksum) .await { Ok(response) => { diff --git a/src/interfaces/api/handlers/dedup_handler.rs b/src/interfaces/api/handlers/dedup_handler.rs index aaa52bb2..d7def5b3 100755 --- a/src/interfaces/api/handlers/dedup_handler.rs +++ b/src/interfaces/api/handlers/dedup_handler.rs @@ -99,7 +99,9 @@ impl DedupHandler { } // Only reveal whether THIS user has the blob — no global oracle - let user_has_it = dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await; + let user_has_it = dedup + .user_owns_blob_reference(&hash, &auth_user.id.to_string()) + .await; if user_has_it { // Fetch size from metadata (safe — user owns a reference) @@ -346,7 +348,10 @@ impl DedupHandler { } // Verify the user owns at least one file referencing this blob - if !dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await { + if !dedup + .user_owns_blob_reference(&hash, &auth_user.id.to_string()) + .await + { return Response::builder() .status(StatusCode::NOT_FOUND) .header(header::CONTENT_TYPE, "application/json") diff --git a/src/interfaces/api/handlers/device_auth_handler.rs b/src/interfaces/api/handlers/device_auth_handler.rs index c83b1d7d..1544ae61 100755 --- a/src/interfaces/api/handlers/device_auth_handler.rs +++ b/src/interfaces/api/handlers/device_auth_handler.rs @@ -203,7 +203,8 @@ async fn revoke_device( ) -> Result { let device_service = get_device_service(&state)?; - let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?; + let device_id = + Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?; device_service .revoke_device(device_id, auth_user.id) diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index c5c3ecea..cc010bbc 100755 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -109,7 +109,11 @@ impl FileHandler { if let Some(ref fid) = folder_id { use crate::application::ports::inbound::FolderUseCase; let folder_service = &state.applications.folder_service; - if folder_service.get_folder_owned(fid, auth_user.id).await.is_err() { + if folder_service + .get_folder_owned(fid, auth_user.id) + .await + .is_err() + { tracing::warn!( "⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user", auth_user.username, @@ -336,18 +340,17 @@ impl FileHandler { // (file_id, size) pair. If the browser already has it, return 304 // with zero I/O or DB work. let etag = format!("\"thumb-{}-{:?}\"", id, thumb_size); - if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) { - if let Ok(val) = if_none_match.to_str() { - if val == etag || val == "*" { - return Response::builder() - .status(StatusCode::NOT_MODIFIED) - .header(header::ETAG, &etag) - .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") - .body(Body::empty()) - .unwrap() - .into_response(); - } - } + if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) + && let Ok(val) = if_none_match.to_str() + && (val == etag || val == "*") + { + return Response::builder() + .status(StatusCode::NOT_MODIFIED) + .header(header::ETAG, &etag) + .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") + .body(Body::empty()) + .unwrap() + .into_response(); } // ── Cache-first path (Solution A) ──────────────────────────── @@ -409,21 +412,17 @@ impl FileHandler { .get_thumbnail(&id, thumb_size.into(), &file_path) .await { - Ok(data) => { - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "image/jpeg") - .header(header::CONTENT_LENGTH, data.len()) - .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") - .header(header::ETAG, &etag) - .body(Body::from(data)) - .unwrap() - .into_response() - } - Err(err) => { - AppError::internal_error(format!("Thumbnail generation failed: {}", err)) - .into_response() - } + Ok(data) => Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "image/jpeg") + .header(header::CONTENT_LENGTH, data.len()) + .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") + .header(header::ETAG, &etag) + .body(Body::from(data)) + .unwrap() + .into_response(), + Err(err) => AppError::internal_error(format!("Thumbnail generation failed: {}", err)) + .into_response(), } } @@ -487,10 +486,8 @@ impl FileHandler { .await { Ok(_) => StatusCode::CREATED.into_response(), - Err(err) => { - AppError::internal_error(format!("Failed to store thumbnail: {}", err)) - .into_response() - } + Err(err) => AppError::internal_error(format!("Failed to store thumbnail: {}", err)) + .into_response(), } } @@ -663,9 +660,7 @@ impl FileHandler { .unwrap() .into_response(), }, - Err(err) => { - AppError::from(err).into_response() - } + Err(err) => AppError::from(err).into_response(), } } @@ -715,9 +710,7 @@ impl FileHandler { .insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap()); resp } - Err(err) => { - AppError::from(err).into_response() - } + Err(err) => AppError::from(err).into_response(), } } @@ -750,8 +743,7 @@ impl FileHandler { tokio::spawn(async move { tracing::info!("🖼️ Generating thumbnails for: {}", file_id); - thumbnail_service - .generate_all_sizes_background(file_id, file_path); + thumbnail_service.generate_all_sizes_background(file_id, file_path); }); } @@ -832,7 +824,7 @@ impl FileHandler { match result { Ok(_) => StatusCode::NO_CONTENT.into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -864,7 +856,7 @@ impl FileHandler { let mgmt = &state.applications.file_management_service; match mgmt.rename_file_owned(&id, auth_user.id, &new_name).await { Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -884,7 +876,7 @@ impl FileHandler { .await { Ok(file) => (StatusCode::OK, Json(file)).into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -903,7 +895,7 @@ impl FileHandler { let mgmt = &state.applications.file_management_service; match mgmt.move_file_owned(&id, auth_user.id, folder_id).await { Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -962,9 +954,7 @@ impl FileHandler { }) .collect(); - format!( - "{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}" - ) + format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}") } /// Build a 201 Created JSON response. diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index 9d95df22..4fd31229 100755 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -1251,7 +1251,11 @@ async fn handle_move( && let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await { - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; } file_management_service .move_file(&file.id, Some(dest_parent_path.to_string())) @@ -1281,7 +1285,11 @@ async fn handle_move( let folder_result = folder_service.get_folder_by_path(&source_path).await; if let Ok(folder) = folder_result { - assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?; + assert_owner( + folder.owner_id.as_deref(), + &user.id.to_string(), + &source_path, + )?; let dest_folder_name = destination_path .split('/') .next_back() @@ -1299,7 +1307,11 @@ async fn handle_move( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1352,7 +1364,11 @@ async fn handle_move( if !dest_parent_path.is_empty() && let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await { - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; } file_management_service .move_file(&file.id, Some(dest_parent_path.to_string())) @@ -1480,7 +1496,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1528,7 +1548,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1553,7 +1577,11 @@ async fn handle_copy( let folder_result = folder_service.get_folder_by_path(&source_path).await; if let Ok(folder) = folder_result { - assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?; + assert_owner( + folder.owner_id.as_deref(), + &user.id.to_string(), + &source_path, + )?; let recursive = depth != "0"; let dest_folder_name = destination_path @@ -1572,7 +1600,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1627,7 +1659,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, diff --git a/src/interfaces/api/handlers/wopi_handler.rs b/src/interfaces/api/handlers/wopi_handler.rs index b3dcd7fc..97403a05 100755 --- a/src/interfaces/api/handlers/wopi_handler.rs +++ b/src/interfaces/api/handlers/wopi_handler.rs @@ -459,17 +459,18 @@ pub async fn get_editor_url( }; // Generate WOPI access token - let (access_token, access_token_ttl) = - match state - .token_service - .generate_token(¶ms.file_id, &user_id.to_string(), &username, can_write) - { - Ok(t) => t, - Err(e) => { - tracing::error!("Failed to generate WOPI token: {}", e); - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - }; + let (access_token, access_token_ttl) = match state.token_service.generate_token( + ¶ms.file_id, + &user_id.to_string(), + username, + can_write, + ) { + Ok(t) => t, + Err(e) => { + tracing::error!("Failed to generate WOPI token: {}", e); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; axum::Json(EditorUrlResponse { editor_url, diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index aa10a6dd..1c9cc287 100755 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -145,7 +145,10 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { .route("/", get(FileHandler::list_files_query)) .route("/upload", post(FileHandler::upload_file_with_thumbnails)) .route("/{id}", get(FileHandler::download_file)) - .route("/{id}/thumbnail/{size}", get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail)) + .route( + "/{id}/thumbnail/{size}", + get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail), + ) .route("/{id}/metadata", get(FileHandler::get_file_metadata)) .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) // 10 GB for file uploads .with_state(app_state.clone()); diff --git a/src/interfaces/middleware/auth.rs b/src/interfaces/middleware/auth.rs index 2a72ae00..90f56f68 100755 --- a/src/interfaces/middleware/auth.rs +++ b/src/interfaces/middleware/auth.rs @@ -91,16 +91,11 @@ where async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { Ok(OptionalUserId( - parts - .extensions - .get::>() - .map(|cu| cu.id), + parts.extensions.get::>().map(|cu| cu.id), )) } } - - // Error for authentication operations #[derive(Debug, thiserror::Error)] pub enum AuthError { diff --git a/src/interfaces/nextcloud/ocs_handler.rs b/src/interfaces/nextcloud/ocs_handler.rs index a576de58..4cfadada 100755 --- a/src/interfaces/nextcloud/ocs_handler.rs +++ b/src/interfaces/nextcloud/ocs_handler.rs @@ -151,7 +151,10 @@ async fn user_provisioning_response( // Fetch quota from storage usage service let quota: (i64, i64) = match state.storage_usage_service.as_ref() { - Some(service) => match service.get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default()).await { + Some(service) => match service + .get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default()) + .await + { Ok((used, total)) => (used, total), Err(_) => (0, 0), }, diff --git a/src/interfaces/web/mod.rs b/src/interfaces/web/mod.rs index a31377c0..c3f43369 100755 --- a/src/interfaces/web/mod.rs +++ b/src/interfaces/web/mod.rs @@ -20,7 +20,11 @@ pub fn create_web_routes() -> Router> { .parent() .unwrap_or(std::path::Path::new(".")) .join("static-dist"); - if dist.exists() { dist } else { config.static_path.clone() } + if dist.exists() { + dist + } else { + config.static_path.clone() + } } else { config.static_path.clone() };