From 6907f0a057fbd6712676e2ab51df9f43e2a99e75 Mon Sep 17 00:00:00 2001 From: George Wu Date: Sat, 21 Feb 2026 17:30:32 -0800 Subject: [PATCH 1/2] fix: correct storage usage calculation by using direct SQL query The original implementation tried to find user's home folder via file_repository.list_files(None), but this only returns files (not folders) and folders are in a separate table. This resulted in storage always showing 0 bytes. Fixed by using a direct SQL query to sum file sizes by user_id from storage.files table, which is more efficient (O(1) vs recursive) and correctly calculates storage for all files owned by the user. --- .../services/storage_usage_service.rs | 143 +++++------------- src/common/di.rs | 4 +- 2 files changed, 38 insertions(+), 109 deletions(-) diff --git a/src/application/services/storage_usage_service.rs b/src/application/services/storage_usage_service.rs index 5c549aed..4413bcba 100644 --- a/src/application/services/storage_usage_service.rs +++ b/src/application/services/storage_usage_service.rs @@ -1,7 +1,8 @@ use crate::application::ports::auth_ports::UserStoragePort; -use crate::application::ports::storage_ports::{FileReadPort, StorageUsagePort}; +use crate::application::ports::storage_ports::StorageUsagePort; use crate::common::errors::DomainError; use async_trait::async_trait; +use sqlx::PgPool; use std::sync::Arc; use tokio::task; use tracing::{debug, error, info}; @@ -11,20 +12,20 @@ use tracing::{debug, error, info}; * * This service is responsible for calculating how much storage each user * is using and updating this information in the user records. + * + * Storage usage is calculated directly from the `storage.files` table + * by summing file sizes for each user (using the `user_id` column). */ pub struct StorageUsageService { - file_repository: Arc, + pool: Arc, user_repository: Arc, } impl StorageUsageService { /// Creates a new storage usage service - pub fn new( - file_repository: Arc, - user_repository: Arc, - ) -> Self { + pub fn new(pool: Arc, user_repository: Arc) -> Self { Self { - file_repository, + pool, user_repository, } } @@ -33,12 +34,8 @@ impl StorageUsageService { pub async fn update_user_storage_usage(&self, user_id: &str) -> Result { info!("Updating storage usage for user: {}", user_id); - // Get user's home folder pattern - let user = self.user_repository.get_user_by_id(user_id).await?; - let username = user.username(); - - // Calculate storage usage for this user - let total_usage = self.calculate_user_storage_usage(username).await?; + // Calculate storage usage directly from database + let total_usage = self.calculate_user_storage_usage(user_id).await?; // Update the user's storage usage in the database self.user_repository @@ -53,101 +50,33 @@ impl StorageUsageService { Ok(total_usage) } - /// Calculates a user's storage usage based on their home folder - async fn calculate_user_storage_usage(&self, username: &str) -> Result { - debug!("Calculating storage for user: {}", username); + /// Calculates a user's storage usage by summing all their file sizes. + /// Uses a direct SQL query for O(1) performance. + async fn calculate_user_storage_usage(&self, user_id: &str) -> Result { + debug!("Calculating storage for user: {}", user_id); - // First, try to find the user's home folder - // List all folders to locate the user's folder - let all_folders = self - .file_repository - .list_files(None) - .await - .map_err(|e| DomainError::internal_error("File repository", e.to_string()))?; + // Direct SQL query to sum all file sizes for this user + // This is much more efficient than recursively walking folders + let total_size: i64 = sqlx::query_scalar( + r#" + SELECT COALESCE(SUM(size), 0)::bigint + FROM storage.files + WHERE user_id = $1::uuid AND NOT is_trashed + "#, + ) + .bind(user_id) + .fetch_one(self.pool.as_ref()) + .await + .map_err(|e| { + DomainError::internal_error("StorageUsage", format!("Failed to calculate usage: {e}")) + })?; - // Find the user's home folder (named "My Folder - {username}") - let home_folder_name = format!("My Folder - {}", username); - debug!("Looking for home folder: {}", home_folder_name); + debug!( + "Calculated storage for user {}: {} bytes", + user_id, total_size + ); - let mut total_usage: i64 = 0; - let mut home_folder_id = None; - - // Find the home folder ID - for folder in &all_folders { - if folder.name() == home_folder_name { - home_folder_id = Some(folder.id().to_string()); - debug!( - "Found home folder for user {}: ID={}", - username, - folder.id() - ); - break; - } - } - - // If we found the home folder, calculate total size - if let Some(folder_id) = home_folder_id { - // Calculate recursively - total_usage = self.calculate_folder_size(&folder_id).await?; - } else { - // If no home folder found, just return 0 - debug!("No home folder found for user: {}", username); - } - - Ok(total_usage) - } - - /// Recursively calculates the size of a folder and all its contents - async fn calculate_folder_size(&self, folder_id: &str) -> Result { - // Implementation with explicit boxing to handle recursion in async functions - async fn inner_calculate_size( - repo: Arc, - folder_id: &str, - ) -> Result { - let mut total_size: i64 = 0; - - // Get files directly in this folder - let files = repo - .list_files(Some(folder_id)) - .await - .map_err(|e| DomainError::internal_error("File repository", e.to_string()))?; - - // Sum the size of all files - for file in &files { - // Skip subdirectories at this level - we'll process them separately - if file.mime_type() == "directory" || file.mime_type() == "application/directory" { - // Recursively calculate subfolder size with explicit boxing - let subfolder_id = file.id().to_string(); // Create owned copy - let repo_clone = repo.clone(); // Clone the repository - - // Use Box::pin to handle recursive async call - let subfolder_size_future = - Box::pin(inner_calculate_size(repo_clone, &subfolder_id)); - - match subfolder_size_future.await { - Ok(size) => { - total_size += size; - } - Err(e) => { - error!( - "Error calculating size for subfolder {}: {}", - subfolder_id, e - ); - // Continue with other folders even if one fails - } - } - } else { - // Add file size to total - total_size += file.size() as i64; - } - } - - Ok(total_size) - } - - // Start the calculation with a clone of our repository reference - let repo_clone = Arc::clone(&self.file_repository); - inner_calculate_size(repo_clone, folder_id).await + Ok(total_size) } /// Calculates and updates storage usage for a user identified by username. @@ -161,7 +90,7 @@ impl StorageUsageService { let user_id = user.id().to_string(); // Reuse the existing calculation logic - let total_usage = self.calculate_user_storage_usage(username).await?; + let total_usage = self.calculate_user_storage_usage(&user_id).await?; // Update the user's storage usage in the database self.user_repository @@ -287,7 +216,7 @@ impl StorageUsagePort for StorageUsageService { impl Clone for StorageUsageService { fn clone(&self) -> Self { Self { - file_repository: Arc::clone(&self.file_repository), + pool: Arc::clone(&self.pool), user_repository: Arc::clone(&self.user_repository), } } diff --git a/src/common/di.rs b/src/common/di.rs index 32543d07..dbe66284 100644 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -398,7 +398,7 @@ impl AppServiceFactory { /// Creates the storage usage service (requires database) pub fn create_storage_usage_service( &self, - repos: &RepositoryServices, + _repos: &RepositoryServices, db_pool: &Arc, ) -> Arc { let user_repository = Arc::new( @@ -406,7 +406,7 @@ impl AppServiceFactory { ); let service = Arc::new( crate::application::services::storage_usage_service::StorageUsageService::new( - repos.file_read_repository.clone(), + db_pool.clone(), user_repository, ), ); From 922f00e339b86bb5091a0aa94d90b6bfe7ef54b9 Mon Sep 17 00:00:00 2001 From: George Wu Date: Sat, 21 Feb 2026 17:49:36 -0800 Subject: [PATCH 2/2] fix: remove uuid cast from storage usage query The user_id column in storage.files is varchar, not uuid. The ::uuid cast was causing a type mismatch error when calculating storage usage. --- src/application/services/storage_usage_service.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/application/services/storage_usage_service.rs b/src/application/services/storage_usage_service.rs index 4413bcba..c2e8d405 100644 --- a/src/application/services/storage_usage_service.rs +++ b/src/application/services/storage_usage_service.rs @@ -57,11 +57,12 @@ impl StorageUsageService { // Direct SQL query to sum all file sizes for this user // This is much more efficient than recursively walking folders + // Note: user_id is stored as varchar, not uuid, so we bind it directly as text let total_size: i64 = sqlx::query_scalar( r#" SELECT COALESCE(SUM(size), 0)::bigint FROM storage.files - WHERE user_id = $1::uuid AND NOT is_trashed + WHERE user_id = $1 AND NOT is_trashed "#, ) .bind(user_id)