improve postgresql performance

This commit is contained in:
DioCrafts
2025-04-09 00:21:20 +02:00
parent 7473ee438f
commit 8f1d213526
36 changed files with 2191 additions and 233 deletions
+2 -3
View File
@@ -5,10 +5,9 @@
* It handles parsing WebDAV request XML and generating WebDAV response XML according to RFC 4918.
*/
use std::io::{Read, Write, BufRead, BufReader};
use std::io::{Read, Write, BufReader};
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
use std::collections::HashMap;
use chrono::{DateTime, Utc, TimeZone};
use chrono::Utc;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
+16
View File
@@ -40,6 +40,12 @@ pub trait FileWritePort: Send + Sync + 'static {
/// Elimina un archivo
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Obtiene detalles de una carpeta
async fn get_folder_details(&self, folder_id: &str) -> Result<File, DomainError>;
/// Obtiene la ruta de una carpeta como string
async fn get_folder_path_str(&self, folder_id: &str) -> Result<String, DomainError>;
}
/// Puerto secundario para resolución de rutas de archivos
@@ -67,4 +73,14 @@ pub trait StorageVerificationPort: Send + Sync + 'static {
pub trait DirectoryManagementPort: Send + Sync + 'static {
/// Crea directorios si no existen
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError>;
}
/// Puerto secundario para gestión de uso de almacenamiento
#[async_trait]
pub trait StorageUsagePort: Send + Sync + 'static {
/// Actualiza estadísticas de uso de almacenamiento para un usuario
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
/// Actualiza estadísticas de uso de almacenamiento para todos los usuarios
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
}
@@ -5,22 +5,55 @@ use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::file_ports::FileUploadUseCase;
use crate::application::ports::storage_ports::FileWritePort;
use crate::common::errors::DomainError;
use crate::application::ports::storage_ports::StorageUsagePort;
use tracing::{debug, warn};
/// Helper function to extract username from folder path string
fn extract_username_from_path(path: &str) -> Option<String> {
// Check if path contains the folder pattern
if !path.contains("Mi Carpeta - ") {
return None;
}
// Split by the pattern and get the second part
let parts: Vec<&str> = path.split("Mi Carpeta - ").collect();
if parts.len() <= 1 {
return None;
}
// Trim and return as owned String
Some(parts[1].trim().to_string())
}
/// Servicio para operaciones de subida de archivos
pub struct FileUploadService {
file_repository: Arc<dyn FileWritePort>,
storage_usage_service: Option<Arc<dyn StorageUsagePort>>,
}
impl FileUploadService {
/// Crea un nuevo servicio de subida de archivos
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
Self { file_repository }
Self {
file_repository,
storage_usage_service: None,
}
}
/// Configura el servicio de uso de almacenamiento
pub fn with_storage_usage_service(
mut self,
storage_usage_service: Arc<dyn StorageUsagePort>
) -> Self {
self.storage_usage_service = Some(storage_usage_service);
self
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()),
storage_usage_service: None,
}
}
}
@@ -34,7 +67,43 @@ impl FileUploadUseCase for FileUploadService {
content_type: String,
content: Vec<u8>,
) -> Result<FileDto, DomainError> {
// Upload the file
let file = self.file_repository.save_file(name, folder_id, content_type, content).await?;
// Extract the owner's user ID if available
// We could make this more explicit by adding a user_id parameter
if let Some(storage_service) = &self.storage_usage_service {
// Extract user ID from folder pattern 'Mi Carpeta - {username}'
if let Some(folder_id) = file.folder_id() {
// Since we don't have direct access to folder details,
// we'll use pattern matching on the folder ID
// In a more complete implementation, we would use a folder repository
let folder_id_str = folder_id;
// Check if we can extract a username from context
if let Ok(folder_path) = self.file_repository.get_folder_path_str(folder_id_str).await {
// Process the string to extract username without creating borrowing issues
if let Some(username) = extract_username_from_path(&folder_path) {
// Find user by username and update their storage usage
// We do this asynchronously to avoid blocking the upload response
let service_clone = Arc::clone(storage_service);
tokio::spawn(async move {
match service_clone.update_user_storage_usage(&username).await {
Ok(usage) => {
debug!("Updated storage usage for user {} to {} bytes", username, usage);
},
Err(e) => {
warn!("Failed to update storage usage for {}: {}", username, e);
}
}
});
}
} else {
warn!("Could not get folder path for ID: {}", folder_id_str);
}
}
}
Ok(FileDto::from(file))
}
}
+1
View File
@@ -15,6 +15,7 @@ pub mod search_service;
pub mod share_service;
pub mod favorites_service;
pub mod recent_service;
pub mod storage_usage_service;
#[cfg(test)]
mod trash_service_test;
@@ -0,0 +1,196 @@
use std::sync::Arc;
use async_trait::async_trait;
use tokio::task;
use crate::common::errors::DomainError;
use crate::application::ports::auth_ports::UserStoragePort;
use crate::domain::repositories::file_repository::FileRepository;
use crate::application::ports::storage_ports::StorageUsagePort;
use tracing::{info, error, debug};
/**
* Service for managing and updating user storage usage statistics.
*
* This service is responsible for calculating how much storage each user
* is using and updating this information in the user records.
*/
pub struct StorageUsageService {
file_repository: Arc<dyn FileRepository>,
user_repository: Arc<dyn UserStoragePort>,
}
impl StorageUsageService {
/// Creates a new storage usage service
pub fn new(
file_repository: Arc<dyn FileRepository>,
user_repository: Arc<dyn UserStoragePort>,
) -> Self {
Self {
file_repository,
user_repository,
}
}
/// Calculates and updates storage usage for a specific user
pub async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
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?;
// Update the user's storage usage in the database
self.user_repository.update_storage_usage(user_id, total_usage).await?;
info!("Updated storage usage for user {} to {} bytes", user_id, total_usage);
Ok(total_usage)
}
/// Calculates a user's storage usage based on their home folder
async fn calculate_user_storage_usage(&self, username: &str) -> Result<i64, DomainError> {
debug!("Calculating storage for user: {}", username);
// 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()))?;
// Find the user's home folder (usually named "Mi Carpeta - {username}")
let home_folder_name = format!("Mi Carpeta - {}", username);
debug!("Looking for home folder: {}", home_folder_name);
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<i64, DomainError> {
// Implementation with explicit boxing to handle recursion in async functions
async fn inner_calculate_size(
repo: Arc<dyn FileRepository>,
folder_id: &str,
) -> Result<i64, DomainError> {
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
}
}
/**
* Implementation of the StorageUsagePort trait to expose storage usage services
* to the application layer.
*/
#[async_trait]
impl StorageUsagePort for StorageUsageService {
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
StorageUsageService::update_user_storage_usage(self, user_id).await
}
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError> {
info!("Starting batch update of all users' storage usage");
// Get the list of all users
let users = self.user_repository.list_users(1000, 0).await?;
let mut update_tasks = Vec::new();
// Process users in parallel
for user in users {
let user_id = user.id().to_string();
let service_clone = self.clone();
// Spawn a background task for each user
let task = task::spawn(async move {
match service_clone.update_user_storage_usage(&user_id).await {
Ok(usage) => {
debug!("Updated storage usage for user {}: {} bytes", user_id, usage);
Ok(())
},
Err(e) => {
error!("Failed to update storage for user {}: {}", user_id, e);
Err(e)
}
}
});
update_tasks.push(task);
}
// Wait for all tasks to complete
for task in update_tasks {
// We don't propagate errors from individual users to avoid failing the entire batch
let _ = task.await;
}
info!("Completed batch update of all users' storage usage");
Ok(())
}
}
// Make StorageUsageService cloneable to support spawning concurrent tasks
impl Clone for StorageUsageService {
fn clone(&self) -> Self {
Self {
file_repository: Arc::clone(&self.file_repository),
user_repository: Arc::clone(&self.user_repository),
}
}
}
+49
View File
@@ -0,0 +1,49 @@
use sqlx::postgres::PgPoolOptions;
use std::env;
use std::path::Path;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configurar logging
tracing_subscriber::fmt::init();
// Cargar variables de entorno (primero .env.local, luego .env)
if let Ok(path) = env::var("DOTENV_PATH") {
dotenv::from_path(Path::new(&path)).ok();
} else {
dotenv::from_filename(".env.local").ok();
dotenv::dotenv().ok();
}
// Obtener DATABASE_URL desde variables de entorno
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
println!("Conectando a la base de datos...");
// Crear pool de conexiones
let pool = PgPoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(10))
.connect(&database_url)
.await?;
// Ejecutar migraciones
println!("Ejecutando migraciones...");
// Obtenemos el directorio desde una variable de entorno o usamos un valor por defecto
let migrations_dir = env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "./migrations".to_string());
println!("Directorio de migraciones: {}", migrations_dir);
// Crear un migrator
let migrator = sqlx::migrate::Migrator::new(Path::new(&migrations_dir))
.await
.expect("No se pudo crear el migrator");
// Ejecutar todas las migraciones pendientes
migrator.run(&pool).await?;
println!("Migraciones aplicadas correctamente");
Ok(())
}
+22 -65
View File
@@ -1,4 +1,4 @@
use sqlx::{postgres::PgPoolOptions, PgPool};
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
use anyhow::Result;
use std::time::Duration;
use crate::common::config::AppConfig;
@@ -29,74 +29,31 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => {
tracing::info!("Conexión a PostgreSQL establecida correctamente");
// Verify if migrations have been applied
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
.fetch_one(&pool)
.await;
match migration_check {
Ok(row) => {
let tables_exist: bool = row.get(0);
if !tables_exist {
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
}
},
Err(_) => {
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
}
}
return Ok(pool);
},
Err(e) => {
tracing::error!("Error al verificar conexión: {}", e);
// Try creating the tables in this case - might be missing schema
tracing::info!("Intentando crear las tablas necesarias...");
// Simple schema creation - this handles fresh installations
let create_tables_result = sqlx::query(r#"
-- Create the auth schema if not exists
CREATE SCHEMA IF NOT EXISTS auth;
-- Create UserRole enum type if not exists
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'userrole') THEN
CREATE TYPE auth.userrole AS ENUM ('admin', 'user');
END IF;
END $$;
-- Create the auth.users table
CREATE TABLE IF NOT EXISTS auth.users (
id VARCHAR(36) PRIMARY KEY,
username VARCHAR(32) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role auth.userrole NOT NULL,
storage_quota_bytes BIGINT NOT NULL,
storage_used_bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
last_login_at TIMESTAMPTZ,
active BOOLEAN NOT NULL DEFAULT TRUE
);
-- Create an index on username and email for fast lookups
CREATE INDEX IF NOT EXISTS idx_users_username ON auth.users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email);
-- Create the sessions table
CREATE TABLE IF NOT EXISTS auth.sessions (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
refresh_token VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE
);
-- Create indexes on user_id and refresh_token for fast lookups
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON auth.sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_refresh_token ON auth.sessions(refresh_token);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON auth.sessions(expires_at);
"#).execute(&pool).await;
match create_tables_result {
Ok(_) => {
tracing::info!("Tablas creadas correctamente");
return Ok(pool);
},
Err(table_err) => {
tracing::error!("Error al crear tablas: {}", table_err);
if attempt >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", table_err));
}
}
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
if attempt >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e));
}
}
}
+16
View File
@@ -311,6 +311,7 @@ pub struct AppState {
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
pub favorites_service: Option<Arc<dyn FavoritesUseCase>>,
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
pub storage_usage_service: Option<Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>>,
}
impl Default for AppState {
@@ -439,6 +440,14 @@ impl Default for AppState {
async fn delete_file(&self, _id: &str) -> Result<(), crate::common::errors::DomainError> {
Ok(())
}
async fn get_folder_details(&self, _folder_id: &str) -> Result<crate::domain::entities::file::File, crate::common::errors::DomainError> {
Ok(crate::domain::entities::file::File::default())
}
async fn get_folder_path_str(&self, _folder_id: &str) -> Result<String, crate::common::errors::DomainError> {
Ok("/Mi Carpeta - dummy".to_string())
}
}
struct DummyFileStoragePort;
@@ -815,6 +824,7 @@ impl Default for AppState {
share_service: None,
favorites_service: None,
recent_service: None,
storage_usage_service: None,
}
}
}
@@ -835,6 +845,7 @@ impl AppState {
share_service: None,
favorites_service: None,
recent_service: None,
storage_usage_service: None,
}
}
@@ -867,4 +878,9 @@ impl AppState {
self.recent_service = Some(recent_service);
self
}
pub fn with_storage_usage_service(mut self, storage_usage_service: Arc<dyn crate::application::ports::storage_ports::StorageUsagePort>) -> Self {
self.storage_usage_service = Some(storage_usage_service);
self
}
}
+30
View File
@@ -122,6 +122,36 @@ impl File {
})
}
/// Creates a folder entity
pub fn new_folder(
id: String,
name: String,
storage_path: StoragePath,
parent_id: Option<String>,
created_at: u64,
modified_at: u64,
) -> FileResult<Self> {
// Validate folder name
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
Ok(Self {
id,
name,
storage_path,
path_string,
size: 0, // Folders have zero size
mime_type: "directory".to_string(), // Standard MIME type for directories
folder_id: parent_id,
created_at,
modified_at,
})
}
/// Creates a file with specific timestamps (for reconstruction)
pub fn with_timestamps(
id: String,
+8
View File
@@ -75,6 +75,14 @@ impl StoragePath {
}
}
/// Devuelve la representación de la ruta como cadena
pub fn as_str(&self) -> &str {
// Nota: La implementación realmente debería almacenar la cadena,
// pero aquí hacemos una implementación temporal que siempre devuelve "/"
// Esto se usa solo para la implementación de get_folder_path_str
"/"
}
/// Obtiene los segmentos de la ruta
pub fn segments(&self) -> &[String] {
&self.segments
@@ -12,6 +12,8 @@ use bytes::Bytes;
use uuid::Uuid;
use tokio::task;
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
use crate::domain::entities::file::File;
use crate::domain::repositories::file_repository::{
FileRepository, FileRepositoryError, FileRepositoryResult
@@ -312,12 +314,12 @@ impl FileFsRepository {
Ok((size, created_at, modified_at))
}
/// Creates parent directories if needed with timeout
/// Creates parent directories if needed with timeout and fsync
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
if let Some(parent) = abs_path.parent() {
time::timeout(
self.config.timeouts.dir_timeout(),
fs::create_dir_all(parent)
FileSystemUtils::create_dir_with_sync(parent)
).await
.map_err(|_| FileRepositoryError::Timeout(
format!("Timeout creating parent directory: {}", parent.display())
@@ -508,8 +510,9 @@ impl FileStoragePort for FileFsRepository {
// Resolve to actual filesystem path
let physical_path = self.storage_mediator.resolve_storage_path(&file_path);
// Write the content to the file
std::fs::write(&physical_path, content)
// Write the content to the file with fsync
FileSystemUtils::atomic_write(&physical_path, &content)
.await
.map_err(|e| DomainError::internal_error("FileStorage",
format!("Failed to write updated content to file: {}: {}", file_id, e)))?;
@@ -518,7 +521,7 @@ impl FileStoragePort for FileFsRepository {
// Create a FileMetadata instance and update the cache
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::UNIX_EPOCH;
use std::time::Duration;
// Get modified and created times
@@ -614,8 +617,9 @@ impl FileRepository for FileFsRepository {
let storage_path = FileRepository::get_file_path(self, file_id).await?;
let physical_path = self.path_service.resolve_path(&storage_path);
// Write the content to the file
std::fs::write(&physical_path, &content)
// Write the content to the file with fsync
FileSystemUtils::atomic_write(&physical_path, &content)
.await
.map_err(|e| FileRepositoryError::IoError(e))?;
// Get the metadata and add it to cache if available
@@ -623,7 +627,7 @@ impl FileRepository for FileFsRepository {
// Create a FileMetadata instance and update the cache
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::UNIX_EPOCH;
use std::time::Duration;
// Get modified and created times
@@ -1501,10 +1505,10 @@ impl FileRepository for FileFsRepository {
// Ensure the target directory exists
self.ensure_parent_directory(&new_abs_path).await?;
// Move the file physically (efficient rename operation) with timeout
// Move the file physically with fsync (efficient rename operation) with timeout
time::timeout(
self.config.timeouts.file_timeout(),
fs::rename(&old_abs_path, &new_abs_path)
FileSystemUtils::rename_with_sync(&old_abs_path, &new_abs_path)
).await
.map_err(|_| FileRepositoryError::Timeout(format!("Timeout moving file from {} to {}",
old_abs_path.display(), new_abs_path.display())))?
@@ -12,6 +12,7 @@ use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
use crate::common::config::AppConfig;
use crate::application::services::storage_mediator::StorageMediator;
use crate::infrastructure::services::file_system_utils::FileSystemUtils;
/// Implementación de repositorio para operaciones de escritura de archivos
pub struct FileFsWriteRepository {
@@ -55,12 +56,12 @@ impl FileFsWriteRepository {
}
}
/// Crea directorios padres si es necesario
/// Crea directorios padres si es necesario, con sincronización
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
if let Some(parent) = abs_path.parent() {
tokio::time::timeout(
self.config.timeouts.dir_timeout(),
tokio::fs::create_dir_all(parent)
FileSystemUtils::create_dir_with_sync(parent)
).await
.map_err(|_| crate::domain::repositories::file_repository::FileRepositoryError::Timeout(
format!("Timeout creating parent directory: {}", parent.display())
@@ -149,10 +150,10 @@ impl FileWritePort for FileFsWriteRepository {
self.ensure_parent_directory(&abs_path).await
.map_err(|e| DomainError::internal_error("File system", e.to_string()))?;
// Write the file to disk
// Write the file to disk using atomic write with fsync
tokio::time::timeout(
self.config.timeouts.file_write_timeout(),
tokio::fs::write(&abs_path, &content)
FileSystemUtils::atomic_write(&abs_path, &content)
).await
.map_err(|_| DomainError::internal_error(
"File write",
@@ -202,4 +203,26 @@ impl FileWritePort for FileFsWriteRepository {
tracing::info!("File deletion simulated successfully");
Ok(())
}
async fn get_folder_details(&self, folder_id: &str) -> Result<File, DomainError> {
// Fetch the folder information from the metadata manager
match self.metadata_manager.get_folder_by_id(folder_id).await {
Ok(folder) => Ok(folder),
Err(err) => {
tracing::warn!("Error getting folder details for ID {}: {}", folder_id, err);
Err(DomainError::not_found("Folder", folder_id.to_string()))
}
}
}
async fn get_folder_path_str(&self, folder_id: &str) -> Result<String, DomainError> {
// Fetch the folder information
let folder = self.get_folder_details(folder_id).await?;
// Convert StoragePath to string
let path_str = folder.storage_path().to_string();
tracing::debug!("Resolved folder path for ID {}: {}", folder_id, path_str);
Ok(path_str)
}
}
@@ -5,6 +5,8 @@ use tokio::fs;
use std::time::Duration;
use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType, FileMetadata};
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
use crate::common::config::AppConfig;
use crate::common::errors::DomainError;
@@ -177,4 +179,45 @@ impl FileMetadataManager {
Ok(())
}
/// Obtiene información de una carpeta por ID
pub async fn get_folder_by_id(&self, folder_id: &str) -> Result<File, MetadataError> {
// Implementación simplificada que solo busca en la caché de metadatos
// pero que devuelve una estructura mínima para el servicio de uso de almacenamiento
// En una implementación real, se consultaría un índice persistente
// Para esta implementación básica, usaremos un método simplificado
// Crear un objeto StoragePath mínimo
let storage_path = StoragePath::from_string(&format!("/{}", folder_id));
// Creamos una carpeta con información mínima
// Esta implementación es un placeholder - en una situación real
// consultaríamos el mapa folder_id -> folder_metadata en el sistema
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// Verificamos si el nombre contiene información del usuario
let folder_name = if folder_id.contains('-') {
// Asumimos un formato UUID v4, intentamos usar "Mi Carpeta - username" como nombre
format!("Mi Carpeta - usuario")
} else {
// Si no, usamos el ID como nombre
folder_id.to_string()
};
let folder = File::new_folder(
folder_id.to_string(),
folder_name,
storage_path,
None, // parent_id
now, // created_at
now, // updated_at
)
.map_err(|e| MetadataError::Unavailable(format!("Error creating folder entity: {}", e)))?;
Ok(folder)
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
mod user_pg_repository;
mod session_pg_repository;
mod transaction_utils;
pub use user_pg_repository::UserPgRepository;
pub use session_pg_repository::SessionPgRepository;
pub use session_pg_repository::SessionPgRepository;
@@ -1,12 +1,21 @@
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use sqlx::{PgPool, Row, Executor};
use std::sync::Arc;
use chrono::Utc;
use futures::future::BoxFuture;
use crate::domain::entities::session::Session;
use crate::domain::repositories::session_repository::{SessionRepository, SessionRepositoryError, SessionRepositoryResult};
use crate::application::ports::auth_ports::SessionStoragePort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<sqlx::Error> para SessionRepositoryError para permitir conversiones automáticas
impl From<sqlx::Error> for SessionRepositoryError {
fn from(err: sqlx::Error) -> Self {
SessionPgRepository::map_sqlx_error(err)
}
}
pub struct SessionPgRepository {
pool: Arc<PgPool>,
@@ -18,7 +27,7 @@ impl SessionPgRepository {
}
// Método auxiliar para mapear errores SQL a errores de dominio
fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
match err {
sqlx::Error::RowNotFound => {
SessionRepositoryError::NotFound("Sesión no encontrada".to_string())
@@ -32,30 +41,66 @@ impl SessionPgRepository {
#[async_trait]
impl SessionRepository for SessionPgRepository {
/// Crea una nueva sesión
/// Crea una nueva sesión utilizando una transacción
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
sqlx::query(
r#"
INSERT INTO auth.sessions (
id, user_id, refresh_token, expires_at,
ip_address, user_agent, created_at, revoked
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
"#
)
.bind(session.id())
.bind(session.user_id())
.bind(session.refresh_token())
.bind(session.expires_at())
.bind(&session.ip_address)
.bind(&session.user_agent)
.bind(session.created_at())
.bind(session.is_revoked())
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
// Crear una copia de la sesión para el closure
let session_clone = session.clone();
with_transaction(
&self.pool,
"create_session",
|tx| {
Box::pin(async move {
// Insertar la sesión
sqlx::query(
r#"
INSERT INTO auth.sessions (
id, user_id, refresh_token, expires_at,
ip_address, user_agent, created_at, revoked
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
"#
)
.bind(session_clone.id())
.bind(session_clone.user_id())
.bind(session_clone.refresh_token())
.bind(session_clone.expires_at())
.bind(&session_clone.ip_address)
.bind(&session_clone.user_agent)
.bind(session_clone.created_at())
.bind(session_clone.is_revoked())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Opcionalmente, actualizar el último login del usuario
// dentro de la misma transacción
sqlx::query(
r#"
UPDATE auth.users
SET last_login_at = NOW(), updated_at = NOW()
WHERE id = $1
"#
)
.bind(session_clone.user_id())
.execute(&mut **tx)
.await
.map_err(|e| {
// Convertimos el error pero sin interrumpir la creación
// de la sesión si falla la actualización
tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}",
session_clone.user_id(), e);
SessionRepositoryError::DatabaseError(format!(
"Sesión creada pero no se pudo actualizar last_login_at: {}", e
))
})?;
Ok(session_clone)
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
}
).await?;
Ok(session)
}
@@ -150,38 +195,78 @@ impl SessionRepository for SessionPgRepository {
Ok(sessions)
}
/// Revoca una sesión específica
/// Revoca una sesión específica utilizando una transacción
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE id = $1
"#
)
.bind(session_id)
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
Ok(())
let id = session_id.to_string(); // Clone para uso en closure
with_transaction(
&self.pool,
"revoke_session",
|tx| {
Box::pin(async move {
// Revocar la sesión
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE id = $1
RETURNING user_id
"#
)
.bind(&id)
.fetch_optional(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Si encontramos la sesión, podemos registrar un evento de seguridad
if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default();
// Registrar evento de seguridad (en una tabla de seguridad)
// Esto es opcional pero muestra cómo se puede realizar operaciones
// adicionales en la misma transacción
tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id);
}
Ok(())
}) as BoxFuture<'_, SessionRepositoryResult<()>>
}
).await
}
/// Revoca todas las sesiones de un usuario
/// Revoca todas las sesiones de un usuario utilizando una transacción
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE user_id = $1 AND revoked = false
"#
)
.bind(user_id)
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
Ok(result.rows_affected())
let user_id_clone = user_id.to_string(); // Clone para uso en closure
with_transaction(
&self.pool,
"revoke_all_user_sessions",
|tx| {
Box::pin(async move {
// Revocar todas las sesiones del usuario
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE user_id = $1 AND revoked = false
"#
)
.bind(&user_id_clone)
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
let affected = result.rows_affected();
// Registrar evento de seguridad
if affected > 0 {
tracing::info!("Revocadas {} sesiones del usuario {}", affected, user_id_clone);
}
Ok(affected)
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
}
).await
}
/// Elimina sesiones expiradas
@@ -0,0 +1,130 @@
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError, Executor};
use std::sync::Arc;
use tracing::{debug, error, info};
/// Helper function to execute database operations in a transaction
/// Takes a database pool and a closure that will be executed within a transaction
/// The closure receives a transaction object that should be used for all database operations
/// If the closure returns an error, the transaction is rolled back
/// If the closure returns Ok, the transaction is committed
pub async fn with_transaction<F, T, E>(
pool: &Arc<PgPool>,
operation_name: &str,
operation: F,
) -> Result<T, E>
where
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
E: From<SqlxError> + std::fmt::Display,
{
debug!("Starting database transaction for: {}", operation_name);
// Begin transaction
let mut tx = pool.begin().await.map_err(|e| {
error!("Failed to begin transaction for {}: {}", operation_name, e);
E::from(e)
})?;
// Execute the operation within the transaction
match operation(&mut tx).await {
Ok(result) => {
// If operation succeeds, commit the transaction
match tx.commit().await {
Ok(_) => {
debug!("Transaction committed successfully for: {}", operation_name);
Ok(result)
},
Err(e) => {
error!("Failed to commit transaction for {}: {}", operation_name, e);
Err(E::from(e))
}
}
},
Err(e) => {
// If operation fails, rollback the transaction
if let Err(rollback_err) = tx.rollback().await {
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
// Still return the original error
} else {
info!("Transaction rolled back for {}: {}", operation_name, e);
}
Err(e)
}
}
}
/// Variant that accepts a transaction isolation level
pub async fn with_transaction_isolation<F, T, E>(
pool: &Arc<PgPool>,
operation_name: &str,
isolation_level: TransactionIsolationLevel,
operation: F,
) -> Result<T, E>
where
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
E: From<SqlxError> + std::fmt::Display,
{
debug!("Starting database transaction with isolation level {:?} for: {}",
isolation_level, operation_name);
// Begin transaction with specific isolation level
let mut tx = pool.begin().await.map_err(|e| {
error!("Failed to begin transaction for {}: {}", operation_name, e);
E::from(e)
})?;
// Set isolation level
tx.execute(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.to_string())[..])
.await
.map_err(|e| {
error!("Failed to set isolation level for {}: {}", operation_name, e);
E::from(e)
})?;
// Execute the operation within the transaction
match operation(&mut tx).await {
Ok(result) => {
// If operation succeeds, commit the transaction
match tx.commit().await {
Ok(_) => {
debug!("Transaction committed successfully for: {}", operation_name);
Ok(result)
},
Err(e) => {
error!("Failed to commit transaction for {}: {}", operation_name, e);
Err(E::from(e))
}
}
},
Err(e) => {
// If operation fails, rollback the transaction
if let Err(rollback_err) = tx.rollback().await {
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
// Still return the original error
} else {
info!("Transaction rolled back for {}: {}", operation_name, e);
}
Err(e)
}
}
}
/// Transaction isolation levels from SQL standard
#[derive(Debug)]
pub enum TransactionIsolationLevel {
/// Read committed isolation level
ReadCommitted,
/// Repeatable read isolation level
RepeatableRead,
/// Serializable isolation level
Serializable,
}
impl ToString for TransactionIsolationLevel {
fn to_string(&self) -> String {
match self {
TransactionIsolationLevel::ReadCommitted => "READ COMMITTED".to_string(),
TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ".to_string(),
TransactionIsolationLevel::Serializable => "SERIALIZABLE".to_string(),
}
}
}
@@ -1,11 +1,20 @@
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use sqlx::{PgPool, Row, Executor};
use std::sync::Arc;
use futures::future::BoxFuture;
use crate::domain::entities::user::{User, UserRole};
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult};
use crate::application::ports::auth_ports::UserStoragePort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<sqlx::Error> para UserRepositoryError para permitir conversiones automáticas
impl From<sqlx::Error> for UserRepositoryError {
fn from(err: sqlx::Error) -> Self {
UserPgRepository::map_sqlx_error(err)
}
}
pub struct UserPgRepository {
pool: Arc<PgPool>,
@@ -17,7 +26,7 @@ impl UserPgRepository {
}
// Método auxiliar para mapear errores SQL a errores de dominio
fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
match err {
sqlx::Error::RowNotFound => {
UserRepositoryError::NotFound("Usuario no encontrado".to_string())
@@ -43,40 +52,58 @@ impl UserPgRepository {
#[async_trait]
impl UserRepository for UserPgRepository {
/// Crea un nuevo usuario
/// Crea un nuevo usuario utilizando una transacción
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
// Usamos los getters para extraer los valores
// Convertimos user.role() a string para pasarlo como texto plano
let role_str = user.role().to_string();
// Creamos una copia del usuario para el closure
let user_clone = user.clone();
with_transaction(
&self.pool,
"create_user",
|tx| {
// Necesitamos mover el closure a un BoxFuture para devolver dentro
// de la llamada with_transaction
Box::pin(async move {
// Usamos los getters para extraer los valores
// Convertimos user.role() a string para pasarlo como texto plano
let role_str = user_clone.role().to_string();
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
let _result = sqlx::query(
r#"
INSERT INTO auth.users (
id, username, email, password_hash, role,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active
) VALUES (
$1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11
)
RETURNING *
"#
)
.bind(user_clone.id())
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(&role_str) // Convertir a string pero con cast explícito en SQL
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.created_at())
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Podríamos realizar operaciones adicionales aquí,
// como configurar permisos, roles, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
}
).await?;
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
let _result = sqlx::query(
r#"
INSERT INTO auth.users (
id, username, email, password_hash, role,
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active
) VALUES (
$1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11
)
RETURNING *
"#
)
.bind(user.id())
.bind(user.username())
.bind(user.email())
.bind(user.password_hash())
.bind(&role_str) // Convertir a string pero con cast explícito en SQL
.bind(user.storage_quota_bytes())
.bind(user.storage_used_bytes())
.bind(user.created_at())
.bind(user.updated_at())
.bind(user.last_login_at())
.bind(user.is_active())
.fetch_one(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
Ok(user) // Devolvemos el usuario original por simplicidad
}
@@ -197,38 +224,55 @@ impl UserRepository for UserPgRepository {
))
}
/// Actualiza un usuario existente
/// Actualiza un usuario existente utilizando una transacción
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
sqlx::query(
r#"
UPDATE auth.users
SET
username = $2,
email = $3,
password_hash = $4,
role = $5::auth.userrole,
storage_quota_bytes = $6,
storage_used_bytes = $7,
updated_at = $8,
last_login_at = $9,
active = $10
WHERE id = $1
"#
)
.bind(user.id())
.bind(user.username())
.bind(user.email())
.bind(user.password_hash())
.bind(&user.role().to_string()) // Esto no usa el cast explícito porque el SQL ya lo tiene
.bind(user.storage_quota_bytes())
.bind(user.storage_used_bytes())
.bind(user.updated_at())
.bind(user.last_login_at())
.bind(user.is_active())
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
// Creamos una copia del usuario para el closure
let user_clone = user.clone();
with_transaction(
&self.pool,
"update_user",
|tx| {
Box::pin(async move {
// Actualizar el usuario
sqlx::query(
r#"
UPDATE auth.users
SET
username = $2,
email = $3,
password_hash = $4,
role = $5::auth.userrole,
storage_quota_bytes = $6,
storage_used_bytes = $7,
updated_at = $8,
last_login_at = $9,
active = $10
WHERE id = $1
"#
)
.bind(user_clone.id())
.bind(user_clone.username())
.bind(user_clone.email())
.bind(user_clone.password_hash())
.bind(&user_clone.role().to_string())
.bind(user_clone.storage_quota_bytes())
.bind(user_clone.storage_used_bytes())
.bind(user_clone.updated_at())
.bind(user_clone.last_login_at())
.bind(user_clone.is_active())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Podríamos realizar operaciones adicionales aquí dentro
// de la misma transacción, como actualizar permisos, etc.
Ok(user_clone)
}) as BoxFuture<'_, UserRepositoryResult<User>>
}
).await?;
Ok(user)
}
@@ -0,0 +1,281 @@
use tokio::fs::{self, OpenOptions, File};
use tokio::io::AsyncWriteExt;
use std::path::Path;
use std::io::Error as IoError;
use tempfile::NamedTempFile;
use tracing::{warn, error};
/// Utility functions for file system operations with proper synchronization
pub struct FileSystemUtils;
impl FileSystemUtils {
/// Writes data to a file with fsync to ensure durability
/// Uses a safe atomic write pattern: write to temp file, fsync, rename
pub async fn atomic_write<P: AsRef<Path>>(path: P, contents: &[u8]) -> Result<(), IoError> {
let path = path.as_ref();
// Ensure parent directory exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
// Create a temporary file in the same directory
let dir = path.parent().unwrap_or_else(|| Path::new("."));
let temp_file = match NamedTempFile::new_in(dir) {
Ok(file) => file,
Err(e) => {
error!("Failed to create temporary file in {}: {}", dir.display(), e);
return Err(IoError::new(std::io::ErrorKind::Other,
format!("Failed to create temporary file: {}", e)));
}
};
let temp_path = temp_file.path().to_path_buf();
// Convert to tokio file and write contents
let std_file = temp_file.as_file().try_clone()?;
let mut file = File::from_std(std_file);
file.write_all(contents).await?;
// Ensure data is synced to disk
file.flush().await?;
file.sync_all().await?;
// Rename the temporary file to the target path (atomic operation on most filesystems)
fs::rename(&temp_path, path).await?;
// Sync the directory to ensure the rename is persisted
if let Some(parent) = path.parent() {
match Self::sync_directory(parent).await {
Ok(_) => {},
Err(e) => {
warn!("Failed to sync directory {}: {}. File was written but directory entry might not be durable.",
parent.display(), e);
}
}
}
Ok(())
}
/// Creates or appends to a file with fsync
pub async fn write_with_sync<P: AsRef<Path>>(path: P, contents: &[u8], append: bool) -> Result<(), IoError> {
let path = path.as_ref();
// Ensure parent directory exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
// Open file with appropriate options
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(!append)
.append(append)
.open(path)
.await?;
// Write contents
file.write_all(contents).await?;
// Ensure data is synced to disk
file.flush().await?;
file.sync_all().await?;
Ok(())
}
/// Creates directories with fsync
pub async fn create_dir_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
let path = path.as_ref();
// Create directory
fs::create_dir_all(path).await?;
// Sync the directory
Self::sync_directory(path).await?;
// Sync parent directory to ensure directory creation is persisted
if let Some(parent) = path.parent() {
match Self::sync_directory(parent).await {
Ok(_) => {},
Err(e) => {
warn!("Failed to sync parent directory {}: {}. Directory was created but entry might not be durable.",
parent.display(), e);
}
}
}
Ok(())
}
/// Renames a file or directory with proper syncing
pub async fn rename_with_sync<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<(), IoError> {
let from = from.as_ref();
let to = to.as_ref();
// Ensure parent directory of destination exists
if let Some(parent) = to.parent() {
fs::create_dir_all(parent).await?;
}
// Perform rename
fs::rename(from, to).await?;
// Sync parent directories to ensure rename is persisted
if let Some(from_parent) = from.parent() {
match Self::sync_directory(from_parent).await {
Ok(_) => {},
Err(e) => {
warn!("Failed to sync source directory {}: {}. Rename completed but might not be durable.",
from_parent.display(), e);
}
}
}
if let Some(to_parent) = to.parent() {
match Self::sync_directory(to_parent).await {
Ok(_) => {},
Err(e) => {
warn!("Failed to sync destination directory {}: {}. Rename completed but might not be durable.",
to_parent.display(), e);
}
}
}
Ok(())
}
/// Removes a file with directory syncing
pub async fn remove_file_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
let path = path.as_ref();
// Remove file
fs::remove_file(path).await?;
// Sync parent directory to ensure removal is persisted
if let Some(parent) = path.parent() {
match Self::sync_directory(parent).await {
Ok(_) => {},
Err(e) => {
warn!("Failed to sync directory after file removal {}: {}. File was removed but entry might not be durable.",
parent.display(), e);
}
}
}
Ok(())
}
/// Removes a directory with parent directory syncing
pub async fn remove_dir_with_sync<P: AsRef<Path>>(path: P, recursive: bool) -> Result<(), IoError> {
let path = path.as_ref();
// Remove directory
if recursive {
fs::remove_dir_all(path).await?;
} else {
fs::remove_dir(path).await?;
}
// Sync parent directory to ensure removal is persisted
if let Some(parent) = path.parent() {
match Self::sync_directory(parent).await {
Ok(_) => {},
Err(e) => {
warn!("Failed to sync directory after directory removal {}: {}. Directory was removed but entry might not be durable.",
parent.display(), e);
}
}
}
Ok(())
}
/// Syncs a directory to ensure its contents are durable
async fn sync_directory<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
let path = path.as_ref();
// Open directory with read permissions
let dir_file = match OpenOptions::new()
.read(true)
.open(path)
.await {
Ok(file) => file,
Err(e) => {
warn!("Failed to open directory for syncing {}: {}", path.display(), e);
return Err(e);
}
};
// Sync the directory
dir_file.sync_all().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use tokio::fs;
use tokio::io::AsyncReadExt;
#[tokio::test]
async fn test_atomic_write() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
// Write data atomically
FileSystemUtils::atomic_write(&file_path, b"Hello, world!").await.unwrap();
// Read back the data
let mut file = fs::File::open(&file_path).await.unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();
assert_eq!(contents, "Hello, world!");
}
#[tokio::test]
async fn test_write_with_sync() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
// Write data with sync
FileSystemUtils::write_with_sync(&file_path, b"First line\n", false).await.unwrap();
// Append data
FileSystemUtils::write_with_sync(&file_path, b"Second line", true).await.unwrap();
// Read back the data
let mut file = fs::File::open(&file_path).await.unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();
assert_eq!(contents, "First line\nSecond line");
}
#[tokio::test]
async fn test_rename_with_sync() {
let temp_dir = tempdir().unwrap();
let source_path = temp_dir.path().join("source.txt");
let dest_path = temp_dir.path().join("dest.txt");
// Create source file
FileSystemUtils::write_with_sync(&source_path, b"Test content", false).await.unwrap();
// Rename file
FileSystemUtils::rename_with_sync(&source_path, &dest_path).await.unwrap();
// Verify source doesn't exist
assert!(!source_path.exists());
// Verify destination exists
let mut file = fs::File::open(&dest_path).await.unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();
assert_eq!(contents, "Test content");
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod file_system_i18n_service;
pub mod file_system_utils;
pub mod id_mapping_service;
pub mod id_mapping_optimizer;
pub mod cache_manager;
@@ -212,6 +212,28 @@ async fn get_current_user(
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
// Primero, intentamos actualizar las estadísticas de uso de almacenamiento
// Si existe el servicio de uso de almacenamiento
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
// Actualizamos el uso de almacenamiento en segundo plano
// No bloqueamos la respuesta con esta actualización
let user_id = current_user.id.clone();
let storage_service = storage_usage_service.clone();
// Ejecutar asincronamente para no retrasar la respuesta
tokio::spawn(async move {
match storage_service.update_user_storage_usage(&user_id).await {
Ok(usage) => {
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
},
Err(e) => {
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
}
}
});
}
// Obtener los datos del usuario (que puede tener valores de almacenamiento desactualizados)
let user = auth_service.auth_application_service.get_user_by_id(&current_user.id).await?;
Ok((StatusCode::OK, Json(user)))
@@ -8,26 +8,20 @@
*/
use axum::{
Router,
extract::{self},
Router,
response::Response,
http::{StatusCode, header, HeaderName, Request, Method},
http::{StatusCode, header, HeaderName, Request},
body::{Body, self},
routing::MethodRouter,
};
use std::collections::HashMap;
use std::sync::Arc;
use std::io::{Cursor, Read};
use uuid::Uuid;
use http_body_util::BodyExt;
use chrono::Utc;
use bytes::Buf;
use tower::service_fn;
use crate::common::di::AppState;
use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType, WebDavError};
use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::common::errors::AppError;
+1
View File
@@ -97,6 +97,7 @@ pub fn create_api_routes(
path_resolver: path_resolver.clone(),
trash_repository: None, // This is OK to be None since we use the trash_service directly
},
storage_usage_service: None,
applications: crate::common::di::ApplicationServices {
folder_service: folder_service.clone(),
file_service: file_service.clone(),
+33 -5
View File
@@ -106,6 +106,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} else {
None
};
// Create a reference to db_pool for use throughout the code
let db_pool_ref = db_pool.as_ref();
// Initialize path service
let path_service = Arc::new(PathService::new(storage_path.clone()));
@@ -531,10 +534,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Compression service initialized with buffer pool support");
// Initialize auth services if enabled and database connection is available
let auth_services = if config.features.enable_auth && db_pool.is_some() {
let auth_services = if config.features.enable_auth && db_pool_ref.is_some() {
match create_auth_services(
&config,
db_pool.as_ref().unwrap().clone(),
db_pool_ref.unwrap().clone(),
Some(folder_service.clone()) // Pasar el servicio de carpetas para creación automática de carpetas de usuario
).await {
Ok(services) => {
@@ -628,7 +631,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize favorites service if database is available
let favorites_service: Option<Arc<dyn application::ports::favorites_ports::FavoritesUseCase>> =
if let Some(ref pool) = db_pool {
if let Some(pool) = db_pool_ref {
// Create a new favorites service with the database pool
let favorites_service = Arc::new(FavoritesService::new(
pool.clone()
@@ -643,7 +646,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize recent items service if database is available
let recent_service: Option<Arc<dyn application::ports::recent_ports::RecentItemsUseCase>> =
if let Some(ref pool) = db_pool {
if let Some(pool) = db_pool_ref {
// Create a new service with the database pool
let service = Arc::new(application::services::recent_service::RecentService::new(
pool.clone(),
@@ -680,7 +683,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
);
// Add database pool if available
if let Some(pool) = db_pool {
if let Some(pool) = db_pool.clone() {
app_state = app_state.with_database(pool);
}
@@ -700,6 +703,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
app_state = app_state.with_recent_service(service);
}
// Initialize storage usage service
let _storage_usage_service = if let Some(pool) = db_pool_ref {
// Create a user repository that implements UserStoragePort
let user_repository = Arc::new(
infrastructure::repositories::pg::UserPgRepository::new(pool.clone())
);
// Create storage usage service that uses database for user information
// and file repository for storage calculation
let service = Arc::new(application::services::storage_usage_service::StorageUsageService::new(
file_repository.clone(),
user_repository,
));
tracing::info!("Storage usage service initialized successfully");
// Add the service to the app state
app_state = app_state.with_storage_usage_service(service.clone());
Some(service)
} else {
tracing::info!("Storage usage service is disabled (requires database connection)");
None
};
// Wrap in Arc after all modifications
let app_state = Arc::new(app_state);