Merge pull request #188 from zjean/fix/ci-clippy-fmt

fix: resolve clippy and rustfmt CI failures
This commit is contained in:
Dionisio Pozo
2026-03-09 14:48:46 +01:00
committed by GitHub
34 changed files with 370 additions and 336 deletions
Generated
+1 -1
View File
@@ -2527,7 +2527,7 @@ dependencies = [
[[package]]
name = "oxicloud"
version = "0.5.0"
version = "0.5.2"
dependencies = [
"argon2",
"async-compression",
+28 -36
View File
@@ -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<String, String> {
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<String, String> {
}
/// 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<String, String> {
}
/// 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<String> = 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 <link>s with single bundle ────────────────
if t.starts_with("<link") && t.contains("stylesheet") && t.contains("href=\"/css/") {
if !css_done {
out.push(format!(
" <link rel=\"stylesheet\" href=\"{css_path}\">"
));
out.push(format!(" <link rel=\"stylesheet\" href=\"{css_path}\">"));
css_done = true;
}
continue;
}
// ── Replace sync theme-init.js with inline <script> ─────────────────
if t.starts_with("<script")
&& !t.contains("defer")
&& t.contains("theme-init")
{
if t.starts_with("<script") && !t.contains("defer") && t.contains("theme-init") {
out.push(format!(" <script>{inline_theme_js}</script>"));
continue;
}
@@ -443,9 +437,7 @@ fn rewrite_index_html(
// ── Replace all defer <script>s with single bundle ──────────────────
if t.starts_with("<script") && t.contains("defer") && t.contains("src=\"") {
if !defer_done {
out.push(format!(
" <script defer src=\"{js_path}\"></script>"
));
out.push(format!(" <script defer src=\"{js_path}\"></script>"));
defer_done = true;
}
continue;
+1 -5
View File
@@ -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<Bytes>;
async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option<Bytes>;
/// Store an externally-generated thumbnail (e.g. client-side video frame).
///
@@ -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
+2 -6
View File
@@ -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;
+30 -15
View File
@@ -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<Email> = 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(
@@ -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);
@@ -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<HashMap<String, (File, String)>>,
files: Mutex<HashMap<String, (File, Uuid)>>,
}
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<File, DomainError> {
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError> {
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<File>, 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<usize, DomainError> {
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(),
+6 -6
View File
@@ -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<crate::domain::entities::file::File>, 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<usize, DomainError> {
Ok(0)
}
@@ -839,7 +839,7 @@ mod tests {
async fn get_file_for_owner(
&self,
id: &str,
_owner_id: &str,
_owner_id: Uuid,
) -> Result<crate::domain::entities::file::File, DomainError> {
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<Vec<crate::domain::entities::folder::Folder>, 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<crate::domain::entities::folder::Folder, DomainError> {
unimplemented!()
+4 -4
View File
@@ -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 => {
+33 -43
View File
@@ -61,10 +61,8 @@ where
FW: FileWritePort,
FoR: FolderRepository,
{
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
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<Vec<TrashedItemDto>> {
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<File>, 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<usize, DomainError> {
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<File, DomainError> {
// 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<Vec<Folder>, 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<Folder, DomainError> {
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);
+3 -3
View File
@@ -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,
}
}
}
+2 -2
View File
@@ -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,
})
@@ -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?;
+2 -1
View File
@@ -283,7 +283,8 @@ fn split_sql_statements(sql: &str) -> Vec<String> {
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);
@@ -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)
@@ -21,16 +21,7 @@ use crate::domain::services::path_service::StoragePath;
type FolderRow = (String, String, String, Option<String>, Uuid, i64, i64);
/// Type alias for paginated folder rows (includes total_count).
type FolderRowPaginated = (
String,
String,
String,
Option<String>,
Uuid,
i64,
i64,
i64,
);
type FolderRowPaginated = (String, String, String, Option<String>, 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)>(
@@ -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
@@ -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
+3 -2
View File
@@ -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());
}
@@ -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<Bytes> {
pub async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option<Bytes> {
// 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<Vec<u8>, 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<Bytes> {
async fn get_cached_thumbnail(&self, file_id: &str, size: PortThumbnailSize) -> Option<Bytes> {
self.get_cached_thumbnail(file_id, size.into()).await
}
+5 -1
View File
@@ -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
@@ -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))
}
@@ -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();
@@ -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) => {
+7 -2
View File
@@ -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")
@@ -203,7 +203,8 @@ async fn revoke_device(
) -> Result<impl IntoResponse, AppError> {
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)
+37 -47
View File
@@ -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.
+45 -9
View File
@@ -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,
+12 -11
View File
@@ -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(&params.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(
&params.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,
+4 -1
View File
@@ -145,7 +145,10 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
.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());
+1 -6
View File
@@ -91,16 +91,11 @@ where
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(OptionalUserId(
parts
.extensions
.get::<Arc<CurrentUser>>()
.map(|cu| cu.id),
parts.extensions.get::<Arc<CurrentUser>>().map(|cu| cu.id),
))
}
}
// Error for authentication operations
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
+4 -1
View File
@@ -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),
},
+5 -1
View File
@@ -20,7 +20,11 @@ pub fn create_web_routes() -> Router<Arc<AppState>> {
.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()
};