fix auth errors and add primigenial paper trash

This commit is contained in:
DioCrafts
2025-03-24 16:47:42 +01:00
parent 838ae6e0d4
commit 38b0e9594b
58 changed files with 3701 additions and 135 deletions
@@ -91,6 +91,21 @@ impl FileFsRepository {
self.storage_mediator.resolve_path(relative_path)
}
/// Returns a reference to the ID mapping service
pub fn id_mapping_service(&self) -> &Arc<dyn crate::application::ports::outbound::IdMappingPort> {
&self.id_mapping_service
}
/// Returns a reference to the metadata cache
pub fn metadata_cache(&self) -> &Arc<FileMetadataCache> {
&self.metadata_cache
}
/// Returns a reference to the root path
pub fn get_root_path(&self) -> &PathBuf {
&self.root_path
}
/// Checks if a file exists at a given storage path
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> FileRepositoryResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
@@ -153,7 +168,7 @@ impl FileFsRepository {
/// Legacy method for checking file existence with PathBuf
#[allow(dead_code)]
async fn file_exists(&self, path: &std::path::Path) -> FileRepositoryResult<bool> {
pub async fn file_exists(&self, path: &std::path::Path) -> FileRepositoryResult<bool> {
let abs_path = self.resolve_legacy_path(path);
// Intentar obtener del caché avanzado primero
@@ -388,37 +403,37 @@ impl FileStoragePort for FileFsRepository {
) -> Result<File, DomainError> {
self.save_file_from_bytes(name, folder_id, content_type, content)
.await
.with_context(|| "Failed to save file")
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to save file: {}", e)))
}
async fn get_file(&self, id: &str) -> Result<File, DomainError> {
self.get_file_by_id(id)
.await
.with_context(|| format!("Failed to get file with ID: {}", id))
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to get file with ID: {}: {}", id, e)))
}
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError> {
FileRepository::list_files(self, folder_id)
.await
.with_context(|| format!("Failed to list files in folder: {:?}", folder_id))
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to list files in folder: {:?}: {}", folder_id, e)))
}
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
FileRepository::delete_file(self, id)
.await
.with_context(|| format!("Failed to delete file with ID: {}", id))
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to delete file with ID: {}: {}", id, e)))
}
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError> {
FileRepository::get_file_content(self, id)
.await
.with_context(|| format!("Failed to get content for file with ID: {}", id))
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to get content for file with ID: {}: {}", id, e)))
}
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
FileRepository::get_file_stream(self, id)
.await
.with_context(|| format!("Failed to get stream for file with ID: {}", id))
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to get stream for file with ID: {}: {}", id, e)))
}
async fn move_file(&self, file_id: &str, target_folder_id: Option<String>) -> Result<File, DomainError> {
@@ -427,18 +442,36 @@ impl FileStoragePort for FileFsRepository {
let result = FileRepository::move_file(self, file_id, target_folder_id)
.await;
result.with_context(|| format!("Failed to move file with ID: {} to folder: {:?}", file_id, cloned_target))
result.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to move file with ID: {} to folder: {:?}: {}", file_id, cloned_target, e)))
}
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError> {
FileRepository::get_file_path(self, id)
.await
.with_context(|| format!("Failed to get path for file with ID: {}", id))
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to get path for file with ID: {}: {}", id, e)))
}
}
#[async_trait]
impl FileRepository for FileFsRepository {
// Temporary stubs for trash functionality
async fn move_to_trash(&self, _file_id: &str) -> FileRepositoryResult<()> {
Err(FileRepositoryError::OperationNotSupported(
"Trash feature temporarily disabled".to_string()
))
}
async fn restore_from_trash(&self, _file_id: &str, _original_path: &str) -> FileRepositoryResult<()> {
Err(FileRepositoryError::OperationNotSupported(
"Trash feature temporarily disabled".to_string()
))
}
async fn delete_file_permanently(&self, _file_id: &str) -> FileRepositoryResult<()> {
Err(FileRepositoryError::OperationNotSupported(
"Trash feature temporarily disabled".to_string()
))
}
async fn save_file_from_bytes(
&self,
name: String,
@@ -0,0 +1,176 @@
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use async_trait::async_trait;
use tracing::{debug, error, instrument};
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult};
use crate::common::errors::ErrorKind;
use crate::infrastructure::repositories::file_fs_repository::FileFsRepository;
// Este archivo contiene la implementación de los métodos relacionados con la papelera
// para el repositorio de archivos FileFsRepository
// Implementación de métodos de papelera para el repositorio de archivos
impl FileFsRepository {
// Obtiene la ruta completa a la papelera
fn get_trash_dir(&self) -> PathBuf {
self.get_root_path().join(".trash").join("files")
}
// Crea una ruta única en la papelera para el archivo
async fn create_trash_file_path(&self, file_id: &str) -> FileRepositoryResult<PathBuf> {
let trash_dir = self.get_trash_dir();
// Asegurarse que el directorio de la papelera existe
if !trash_dir.exists() {
fs::create_dir_all(&trash_dir).await
.map_err(|e| FileRepositoryError::IoError(e))?;
}
// Crear una ruta única para el archivo en la papelera
Ok(trash_dir.join(file_id))
}
}
// Implementación de los métodos públicos del trait FileRepository relacionados con la papelera
// Implementation of internal methods for trash functionality
// These will be enabled when the trash feature is re-enabled
impl FileFsRepository {
/// Helper method that will be used for trash functionality
#[allow(dead_code)]
pub(crate) async fn _trash_move_to_trash(&self, file_id: &str) -> FileRepositoryResult<()> {
debug!("Moviendo archivo a la papelera: {}", file_id);
// Obtener la ruta física del archivo
// Creamos un método independiente para acceder al servicio de mapeo de IDs
let file_path = match self.id_mapping_service().get_file_path(file_id).await {
Ok(path) => path,
Err(e) => {
error!("Error obteniendo ruta del archivo {}: {:?}", file_id, e);
return Err(FileRepositoryError::IdMappingError(format!("Failed to get file path: {}", e)));
}
};
// Verificamos que el archivo existe
if !self.file_exists(&file_path).await? {
return Err(FileRepositoryError::NotFound(format!("File not found: {}", file_id)));
}
// Crear directorio en la papelera si no existe
let trash_file_path = self.create_trash_file_path(file_id).await?;
// Mover el archivo físicamente a la papelera (no actualiza mappings)
match fs::rename(&file_path, &trash_file_path).await {
Ok(_) => {
debug!("Archivo movido a papelera: {} -> {}", file_path.display(), trash_file_path.display());
// Invalidar la caché del archivo original
self.metadata_cache().invalidate(&file_path).await;
// Actualizar el mapeo al nuevo path en la papelera
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &trash_file_path).await {
error!("Error actualizando mapeo de archivo en papelera: {}", e);
return Err(FileRepositoryError::MappingError(format!("Failed to update mapping: {}", e)));
}
Ok(())
},
Err(e) => {
error!("Error moviendo archivo a papelera: {}", e);
Err(FileRepositoryError::IoError(e))
}
}
}
/// Restaura un archivo desde la papelera a su ubicación original
#[allow(dead_code)]
pub(crate) async fn _trash_restore_from_trash(&self, file_id: &str, original_path: &str) -> FileRepositoryResult<()> {
debug!("Restaurando archivo {} a {}", file_id, original_path);
// Obtener la ruta actual en la papelera
let current_path = match self.id_mapping_service().get_file_path(file_id).await {
Ok(path) => path,
Err(e) => {
error!("Error obteniendo ruta actual del archivo {}: {:?}", file_id, e);
return Err(FileRepositoryError::IdMappingError(format!("Failed to get file path: {}", e)));
}
};
// Convertir la ruta original a PathBuf
let original_path_buf = PathBuf::from(original_path);
// Asegurar que el directorio de destino existe
if let Some(parent) = original_path_buf.parent() {
if !parent.exists() {
fs::create_dir_all(parent).await
.map_err(|e| {
error!("Error creando directorio padre para restauración: {}", e);
FileRepositoryError::IoError(e)
})?;
}
}
// Mover el archivo de la papelera a su ubicación original
match fs::rename(&current_path, &original_path_buf).await {
Ok(_) => {
debug!("Archivo restaurado: {} -> {}", current_path.display(), original_path_buf.display());
// Invalidar la caché del archivo en la papelera
self.metadata_cache().invalidate(&current_path).await;
// Actualizar el mapeo a la ruta original
if let Err(e) = self.id_mapping_service().update_file_path(file_id, &original_path_buf).await {
error!("Error actualizando mapeo de archivo restaurado: {}", e);
return Err(FileRepositoryError::MappingError(format!("Failed to update mapping: {}", e)));
}
Ok(())
},
Err(e) => {
error!("Error restaurando archivo: {}", e);
Err(FileRepositoryError::IoError(e))
}
}
}
/// Elimina un archivo permanentemente (usado por la papelera)
#[instrument(skip(self))]
#[allow(dead_code)]
pub(crate) async fn _trash_delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()> {
debug!("Eliminando archivo permanentemente: {}", file_id);
// Este es similar al delete_file pero no verifica permisos ni hace validaciones adicionales
let file_path = match self.id_mapping_service().get_file_path(file_id).await {
Ok(path) => path,
Err(e) => {
error!("Error obteniendo ruta del archivo {}: {:?}", file_id, e);
return Err(FileRepositoryError::IdMappingError(format!("Failed to get file path: {}", e)));
}
};
// Eliminar el archivo físicamente
if let Err(e) = fs::remove_file(&file_path).await {
error!("Error eliminando archivo permanentemente: {}", e);
// No reporte error si el archivo ya no existe
if e.kind() != std::io::ErrorKind::NotFound {
return Err(FileRepositoryError::IoError(e));
}
}
// Invalidar caché
self.metadata_cache().invalidate(&file_path).await;
// Eliminar el mapeo
if let Err(e) = self.id_mapping_service().remove_id(file_id).await {
error!("Error eliminando mapeo del archivo: {}", e);
return Err(FileRepositoryError::MappingError(format!("Failed to remove mapping: {}", e)));
}
debug!("Archivo eliminado permanentemente con éxito: {}", file_id);
Ok(())
}
}
// Re-exportaciones necesarias para el compilador
use crate::domain::repositories::file_repository::FileRepositoryError;
@@ -43,6 +43,11 @@ impl FolderFsRepository {
}
}
/// Returns the root path of the storage
pub fn get_root_path(&self) -> &PathBuf {
&self.root_path
}
/// Creates a stub repository for initialization purposes
/// This is used temporarily during dependency injection setup
#[allow(dead_code)]
@@ -110,6 +115,31 @@ impl FolderFsRepository {
self.storage_mediator.resolve_path(relative_path)
}
/// Returns a reference to the ID mapping service
pub fn id_mapping_service(&self) -> &Arc<dyn crate::application::ports::outbound::IdMappingPort> {
&self.id_mapping_service
}
/// Gets a folder path from the ID mapping service
pub async fn get_mapped_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<String> {
let storage_path = self.id_mapping_service.get_path_by_id(folder_id).await
.map_err(|e| FolderRepositoryError::MappingError(format!("Failed to get folder path: {}", e)))?;
Ok(storage_path.to_string())
}
/// Updates a folder path in the ID mapping service
pub async fn update_mapped_folder_path(&self, folder_id: &str, new_path: &PathBuf) -> FolderRepositoryResult<()> {
let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
self.id_mapping_service.update_path(folder_id, &storage_path).await
.map_err(|e| FolderRepositoryError::MappingError(format!("Failed to update folder path: {}", e)))
}
/// Removes a folder ID from the ID mapping service
pub async fn remove_mapped_folder_id(&self, folder_id: &str) -> FolderRepositoryResult<()> {
self.id_mapping_service.remove_id(folder_id).await
.map_err(|e| FolderRepositoryError::MappingError(format!("Failed to remove folder ID: {}", e)))
}
/// Checks if a folder exists at a given storage path
async fn check_folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<bool> {
let abs_path = self.resolve_storage_path(storage_path);
@@ -222,6 +252,9 @@ impl From<FolderRepositoryError> for DomainError {
FolderRepositoryError::Other(msg) => {
DomainError::internal_error("Folder", msg)
},
FolderRepositoryError::OperationNotSupported(msg) => {
DomainError::operation_not_supported("Folder", msg)
},
FolderRepositoryError::DomainError(e) => e,
}
}
@@ -293,6 +326,24 @@ impl FolderStoragePort for FolderFsRepository {
#[async_trait]
impl FolderRepository for FolderFsRepository {
// Temporary stubs for trash functionality
async fn move_to_trash(&self, _folder_id: &str) -> FolderRepositoryResult<()> {
Err(FolderRepositoryError::OperationNotSupported(
"Trash feature temporarily disabled".to_string()
))
}
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> FolderRepositoryResult<()> {
Err(FolderRepositoryError::OperationNotSupported(
"Trash feature temporarily disabled".to_string()
))
}
async fn delete_folder_permanently(&self, _folder_id: &str) -> FolderRepositoryResult<()> {
Err(FolderRepositoryError::OperationNotSupported(
"Trash feature temporarily disabled".to_string()
))
}
async fn create_folder(&self, name: String, parent_id: Option<String>) -> FolderRepositoryResult<Folder> {
// Get the parent folder path (if any)
let parent_storage_path = match &parent_id {
@@ -0,0 +1,174 @@
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use async_trait::async_trait;
use tracing::{debug, error, instrument};
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult};
use crate::common::errors::ErrorKind;
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
// Este archivo contiene la implementación de los métodos relacionados con la papelera
// para el repositorio de carpetas FolderFsRepository
// Implementación de métodos de papelera para el repositorio de carpetas
impl FolderFsRepository {
// Obtiene la ruta completa a la papelera
fn get_trash_dir(&self) -> PathBuf {
self.get_root_path().join(".trash").join("folders")
}
// Crea una ruta única en la papelera para la carpeta
async fn create_trash_folder_path(&self, folder_id: &str) -> FolderRepositoryResult<PathBuf> {
let trash_dir = self.get_trash_dir();
// Asegurarse que el directorio de la papelera existe
if !trash_dir.exists() {
fs::create_dir_all(&trash_dir).await
.map_err(|e| FolderRepositoryError::IoError(e))?;
}
// Crear una ruta única para la carpeta en la papelera
Ok(trash_dir.join(folder_id))
}
}
// Implementación de los métodos públicos del trait FolderRepository relacionados con la papelera
// Implementation of internal methods for trash functionality
// These will be enabled when the trash feature is re-enabled
impl FolderFsRepository {
/// Helper method that will be used for trash functionality
#[allow(dead_code)]
pub(crate) async fn _trash_move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
debug!("Moviendo carpeta a la papelera: {}", folder_id);
// Obtener la ruta física de la carpeta
let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => path,
Err(e) => {
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e);
return Err(e);
}
};
let folder_path_buf = PathBuf::from(folder_path.to_string());
// Verificamos que la carpeta existe
if !folder_path_buf.exists() {
return Err(FolderRepositoryError::NotFound(format!("Folder not found: {}", folder_id)));
}
// Crear directorio en la papelera
let trash_folder_path = self.create_trash_folder_path(folder_id).await?;
// Mover la carpeta físicamente a la papelera
match fs::rename(&folder_path_buf, &trash_folder_path).await {
Ok(_) => {
debug!("Carpeta movida a papelera: {} -> {}", folder_path_buf.display(), trash_folder_path.display());
// Actualizar el mapeo al nuevo path en la papelera
if let Err(e) = self.update_mapped_folder_path(folder_id, &trash_folder_path).await {
error!("Error actualizando mapeo de carpeta en papelera: {}", e);
return Err(e);
}
Ok(())
},
Err(e) => {
error!("Error moviendo carpeta a papelera: {}", e);
Err(FolderRepositoryError::IoError(e))
}
}
}
/// Restaura una carpeta desde la papelera a su ubicación original
#[allow(dead_code)]
pub(crate) async fn _trash_restore_from_trash(&self, folder_id: &str, original_path: &str) -> FolderRepositoryResult<()> {
debug!("Restaurando carpeta {} a {}", folder_id, original_path);
// Obtener la ruta actual en la papelera
let current_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path),
Err(e) => {
error!("Error obteniendo ruta actual de la carpeta {}: {:?}", folder_id, e);
return Err(e);
}
};
// Convertir la ruta original a PathBuf
let original_path_buf = PathBuf::from(original_path);
// Asegurar que el directorio padre de destino existe
if let Some(parent) = original_path_buf.parent() {
if !parent.exists() {
fs::create_dir_all(parent).await
.map_err(|e| {
error!("Error creando directorio padre para restauración: {}", e);
FolderRepositoryError::IoError(e)
})?;
}
}
// Mover la carpeta de la papelera a su ubicación original
match fs::rename(&current_path, &original_path_buf).await {
Ok(_) => {
debug!("Carpeta restaurada: {} -> {}", current_path.display(), original_path_buf.display());
// Actualizar el mapeo a la ruta original
if let Err(e) = self.update_mapped_folder_path(folder_id, &original_path_buf).await {
error!("Error actualizando mapeo de carpeta restaurada: {}", e);
return Err(e);
}
Ok(())
},
Err(e) => {
error!("Error restaurando carpeta: {}", e);
Err(FolderRepositoryError::IoError(e))
}
}
}
/// Elimina una carpeta permanentemente (usado por la papelera)
#[allow(dead_code)]
pub(crate) async fn _trash_delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
debug!("Eliminando carpeta permanentemente: {}", folder_id);
// Similar a delete_folder pero sin validaciones adicionales
let folder_path = match self.get_mapped_folder_path(folder_id).await {
Ok(path) => PathBuf::from(path),
Err(e) => {
error!("Error obteniendo ruta de la carpeta {}: {:?}", folder_id, e);
return Err(e);
}
};
// Eliminar la carpeta recursivamente
if folder_path.exists() {
match fs::remove_dir_all(&folder_path).await {
Ok(_) => {
debug!("Carpeta eliminada permanentemente: {}", folder_path.display());
},
Err(e) => {
error!("Error eliminando carpeta permanentemente: {}", e);
// No reportar error si la carpeta ya no existe
if e.kind() != std::io::ErrorKind::NotFound {
return Err(FolderRepositoryError::IoError(e));
}
}
}
}
// Eliminar el mapeo
if let Err(e) = self.remove_mapped_folder_id(folder_id).await {
error!("Error eliminando mapeo de la carpeta: {}", e);
return Err(e);
}
debug!("Carpeta eliminada permanentemente con éxito: {}", folder_id);
Ok(())
}
}
// Re-exportaciones necesarias para el compilador
use crate::domain::repositories::folder_repository::FolderRepositoryError;
+4
View File
@@ -7,6 +7,9 @@ pub mod file_metadata_manager;
pub mod file_path_resolver;
pub mod file_fs_read_repository;
pub mod file_fs_write_repository;
pub mod trash_fs_repository;
pub mod file_fs_repository_trash;
pub mod folder_fs_repository_trash;
// Repositorios PostgreSQL
pub mod pg;
@@ -16,4 +19,5 @@ pub use file_metadata_manager::FileMetadataManager;
pub use file_path_resolver::FilePathResolver;
pub use file_fs_read_repository::FileFsReadRepository;
pub use file_fs_write_repository::FileFsWriteRepository;
pub use trash_fs_repository::TrashFsRepository;
pub use pg::{UserPgRepository, SessionPgRepository};
@@ -46,6 +46,10 @@ impl UserRepository for UserPgRepository {
/// Crea un nuevo usuario
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();
// Modificar el SQL para hacer un cast explícito al tipo auth.userrole
let result = sqlx::query(
r#"
INSERT INTO auth.users (
@@ -53,7 +57,7 @@ impl UserRepository for UserPgRepository {
storage_quota_bytes, storage_used_bytes,
created_at, updated_at, last_login_at, active
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
$1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11
)
RETURNING *
"#
@@ -62,7 +66,7 @@ impl UserRepository for UserPgRepository {
.bind(user.username())
.bind(user.email())
.bind(user.password_hash())
.bind(user.role() as UserRole) // sqlx::Type nos permite bind directamente
.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())
@@ -93,12 +97,19 @@ impl UserRepository for UserPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Convert role string to UserRole enum
let role_str: String = row.get("role");
let role = match role_str.as_str() {
"admin" => UserRole::Admin,
_ => UserRole::User,
};
Ok(User::from_data(
row.get("id"),
row.get("username"),
row.get("email"),
row.get("password_hash"),
row.get("role"),
role,
row.get("storage_quota_bytes"),
row.get("storage_used_bytes"),
row.get("created_at"),
@@ -125,12 +136,19 @@ impl UserRepository for UserPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Convert role string to UserRole enum
let role_str: String = row.get("role");
let role = match role_str.as_str() {
"admin" => UserRole::Admin,
_ => UserRole::User,
};
Ok(User::from_data(
row.get("id"),
row.get("username"),
row.get("email"),
row.get("password_hash"),
row.get("role"),
role,
row.get("storage_quota_bytes"),
row.get("storage_used_bytes"),
row.get("created_at"),
@@ -157,12 +175,19 @@ impl UserRepository for UserPgRepository {
.await
.map_err(Self::map_sqlx_error)?;
// Convert role string to UserRole enum
let role_str: String = row.get("role");
let role = match role_str.as_str() {
"admin" => UserRole::Admin,
_ => UserRole::User,
};
Ok(User::from_data(
row.get("id"),
row.get("username"),
row.get("email"),
row.get("password_hash"),
row.get("role"),
role,
row.get("storage_quota_bytes"),
row.get("storage_used_bytes"),
row.get("created_at"),
@@ -181,7 +206,7 @@ impl UserRepository for UserPgRepository {
username = $2,
email = $3,
password_hash = $4,
role = $5,
role = $5::auth.userrole,
storage_quota_bytes = $6,
storage_used_bytes = $7,
updated_at = $8,
@@ -194,7 +219,7 @@ impl UserRepository for UserPgRepository {
.bind(user.username())
.bind(user.email())
.bind(user.password_hash())
.bind(user.role() as UserRole)
.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())
@@ -267,12 +292,19 @@ impl UserRepository for UserPgRepository {
let users = rows.into_iter()
.map(|row| {
// Convert role string to UserRole enum for each row
let role_str: String = row.get("role");
let role = match role_str.as_str() {
"admin" => UserRole::Admin,
_ => UserRole::User,
};
User::from_data(
row.get("id"),
row.get("username"),
row.get("email"),
row.get("password_hash"),
row.get("role"),
role,
row.get("storage_quota_bytes"),
row.get("storage_used_bytes"),
row.get("created_at"),
@@ -328,17 +360,20 @@ impl UserRepository for UserPgRepository {
/// Cambia el rol de un usuario
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
// Convertir el rol a string para el binding
let role_str = role.to_string();
sqlx::query(
r#"
UPDATE auth.users
SET
role = $2,
role = $2::auth.userrole,
updated_at = NOW()
WHERE id = $1
"#
)
.bind(user_id)
.bind(role as UserRole)
.bind(&role_str)
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
@@ -0,0 +1,332 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use async_trait::async_trait;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tokio::fs;
use uuid::Uuid;
use tracing::{debug, error, instrument};
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::outbound::IdMappingPort;
/// Estructura para almacenar elementos en la papelera en formato JSON
#[derive(Debug, Serialize, Deserialize)]
struct TrashedItemEntry {
id: String,
original_id: String,
user_id: String,
item_type: String,
name: String,
original_path: String,
trashed_at: String,
deletion_date: String,
}
/// Implementación del repositorio de papelera usando el sistema de archivos
pub struct TrashFsRepository {
trash_dir: PathBuf,
trash_index_path: PathBuf,
id_mapping_service: Arc<dyn IdMappingPort>,
}
impl TrashFsRepository {
pub fn new(
storage_root: impl AsRef<Path>,
id_mapping_service: Arc<dyn IdMappingPort>,
) -> Self {
let trash_dir = storage_root.as_ref().join(".trash");
let trash_index_path = trash_dir.join("trash_index.json");
Self {
trash_dir,
trash_index_path,
id_mapping_service,
}
}
/// Asegura que existe el directorio de papelera
async fn ensure_trash_dir(&self) -> Result<()> {
if !self.trash_dir.exists() {
fs::create_dir_all(&self.trash_dir).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to create trash directory: {}", e)
))?;
}
Ok(())
}
/// Obtiene todas las entradas del índice de papelera
async fn get_trash_entries(&self) -> Result<Vec<TrashedItemEntry>> {
self.ensure_trash_dir().await?;
if !self.trash_index_path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&self.trash_index_path).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to read trash index: {}", e)
))?;
if content.trim().is_empty() {
return Ok(Vec::new());
}
let entries: Vec<TrashedItemEntry> = serde_json::from_str(&content)
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to parse trash index: {}", e)
))?;
Ok(entries)
}
/// Guarda todas las entradas en el índice de papelera
async fn save_trash_entries(&self, entries: Vec<TrashedItemEntry>) -> Result<()> {
self.ensure_trash_dir().await?;
let json = serde_json::to_string_pretty(&entries)
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to serialize trash index: {}", e)
))?;
fs::write(&self.trash_index_path, json).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to write trash index: {}", e)
))?;
Ok(())
}
/// Convierte una entrada JSON a entidad TrashedItem
fn entry_to_trashed_item(&self, entry: TrashedItemEntry) -> Result<TrashedItem> {
let item_type = match entry.item_type.as_str() {
"file" => TrashedItemType::File,
"folder" => TrashedItemType::Folder,
_ => return Err(DomainError::new(
ErrorKind::InvalidInput,
"Trash",
format!("Invalid trashed item type: {}", entry.item_type)
)),
};
let original_id = Uuid::parse_str(&entry.original_id)
.map_err(|e| DomainError::validation_error(
"Trash",
format!("Invalid original ID format: {}", e)
))?;
let id = Uuid::parse_str(&entry.id)
.map_err(|e| DomainError::validation_error(
"Trash",
format!("Invalid ID format: {}", e)
))?;
let user_id = Uuid::parse_str(&entry.user_id)
.map_err(|e| DomainError::validation_error(
"Trash",
format!("Invalid user ID format: {}", e)
))?;
let trashed_at = chrono::DateTime::parse_from_rfc3339(&entry.trashed_at)
.map_err(|e| DomainError::validation_error(
"Trash",
format!("Invalid trashed_at date: {}", e)
))?
.with_timezone(&Utc);
let deletion_date = chrono::DateTime::parse_from_rfc3339(&entry.deletion_date)
.map_err(|e| DomainError::validation_error(
"Trash",
format!("Invalid deletion_date: {}", e)
))?
.with_timezone(&Utc);
Ok(TrashedItem {
id,
original_id,
user_id,
item_type,
name: entry.name,
original_path: entry.original_path,
trashed_at,
deletion_date,
})
}
/// Convierte una entidad TrashedItem a entrada JSON
fn trashed_item_to_entry(&self, item: &TrashedItem) -> TrashedItemEntry {
TrashedItemEntry {
id: item.id.to_string(),
original_id: item.original_id.to_string(),
user_id: item.user_id.to_string(),
item_type: match item.item_type {
TrashedItemType::File => "file".to_string(),
TrashedItemType::Folder => "folder".to_string(),
},
name: item.name.clone(),
original_path: item.original_path.clone(),
trashed_at: item.trashed_at.to_rfc3339(),
deletion_date: item.deletion_date.to_rfc3339(),
}
}
/// Obtiene la ruta de un elemento en la papelera
fn get_trash_path_for_item(&self, user_id: &Uuid, item_id: &Uuid) -> PathBuf {
self.trash_dir
.join("files")
.join(user_id.to_string())
.join(item_id.to_string())
}
}
#[async_trait]
impl TrashRepository for TrashFsRepository {
#[instrument(skip(self))]
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
debug!("Añadiendo elemento a la papelera: id={}, user={}", item.id, item.user_id);
// Aseguramos que existe el directorio de la papelera para este usuario
let user_trash_dir = self.trash_dir.join("files").join(item.user_id.to_string());
fs::create_dir_all(&user_trash_dir).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Trash",
format!("Failed to create user trash directory: {}", e)
))?;
// Añadimos la entrada al índice
let mut entries = self.get_trash_entries().await?;
entries.push(self.trashed_item_to_entry(item));
self.save_trash_entries(entries).await?;
Ok(())
}
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
debug!("Obteniendo elementos en papelera para usuario: {}", user_id);
let entries = self.get_trash_entries().await?;
let user_id_str = user_id.to_string();
let user_entries = entries.into_iter()
.filter(|entry| entry.user_id == user_id_str)
.collect::<Vec<_>>();
let mut items = Vec::new();
for entry in user_entries {
match self.entry_to_trashed_item(entry) {
Ok(item) => items.push(item),
Err(e) => error!("Error converting trash entry to item: {}", e),
}
}
Ok(items)
}
#[instrument(skip(self))]
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
debug!("Buscando elemento en papelera: id={}, user={}", id, user_id);
let entries = self.get_trash_entries().await?;
let id_str = id.to_string();
let user_id_str = user_id.to_string();
let item_entry = entries.into_iter()
.find(|entry| entry.id == id_str && entry.user_id == user_id_str);
match item_entry {
Some(entry) => {
let item = self.entry_to_trashed_item(entry)?;
Ok(Some(item))
},
None => Ok(None),
}
}
#[instrument(skip(self))]
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
debug!("Restaurando elemento de la papelera: id={}, user={}", id, user_id);
let mut entries = self.get_trash_entries().await?;
let id_str = id.to_string();
let user_id_str = user_id.to_string();
let index = entries.iter().position(|entry|
entry.id == id_str && entry.user_id == user_id_str
);
if let Some(index) = index {
entries.remove(index);
self.save_trash_entries(entries).await?;
Ok(())
} else {
Err(DomainError::not_found("TrashedItem", id.to_string()))
}
}
#[instrument(skip(self))]
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
debug!("Eliminando permanentemente elemento de la papelera: id={}, user={}", id, user_id);
// Simplemente eliminamos la entrada del índice
// Los archivos físicos se eliminarán a través del repositorio correspondiente
self.restore_from_trash(id, user_id).await
}
#[instrument(skip(self))]
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
debug!("Limpiando papelera para usuario: {}", user_id);
let mut entries = self.get_trash_entries().await?;
let user_id_str = user_id.to_string();
entries.retain(|entry| entry.user_id != user_id_str);
self.save_trash_entries(entries).await?;
Ok(())
}
#[instrument(skip(self))]
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
debug!("Buscando elementos de papelera expirados");
let entries = self.get_trash_entries().await?;
let now = Utc::now();
let mut expired_items = Vec::new();
for entry in entries {
match chrono::DateTime::parse_from_rfc3339(&entry.deletion_date) {
Ok(date) => {
let utc_date = date.with_timezone(&Utc);
if utc_date <= now {
match self.entry_to_trashed_item(entry) {
Ok(item) => expired_items.push(item),
Err(e) => error!("Error converting expired trash entry: {}", e),
}
}
},
Err(e) => error!("Invalid date format in trash entry: {}", e),
}
}
Ok(expired_items)
}
}
@@ -115,9 +115,9 @@ impl IdMappingService {
timeouts.lock_timeout(),
fs::read_to_string(map_path)
).await
.with_context(|| format!("Timeout reading ID map from {}", map_path.display()))?;
.map_err(|_| DomainError::timeout("IdMapping", format!("Timeout reading ID map from {}", map_path.display())))?;
let content = read_result.with_context(|| format!("Failed to read ID map from {}", map_path.display()))?;
let content = read_result.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to read ID map from {}: {}", map_path.display(), e)))?;
// Parsear el JSON
match serde_json::from_str::<IdMap>(&content) {
@@ -197,7 +197,7 @@ impl IdMappingService {
self.timeouts.lock_timeout(),
self.save_mutex.lock()
).await
.with_context(|| "Timeout acquiring save lock for ID mapping")?;
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring save lock for ID mapping"))?;
// Crear JSON con el lock de lectura para minimizar el tiempo de bloqueo
let json = {
@@ -205,7 +205,7 @@ impl IdMappingService {
self.timeouts.lock_timeout(),
self.id_map.write()
).await
.with_context(|| "Timeout acquiring write lock for ID mapping")?;
.map_err(|_| DomainError::timeout("IdMapping", "Timeout acquiring write lock for ID mapping"))?;
// Incrementar versión sólo si hay cambios por guardar
let pending = *self.pending_save.read().await;
@@ -216,17 +216,17 @@ impl IdMappingService {
// Use serde with reasonably safe defaults
serde_json::to_string_pretty(&*map)
.with_context(|| "Failed to serialize ID map to JSON")?
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to serialize ID map to JSON: {}", e)))?
};
// Escribir a un archivo temporal primero para evitar corrupción
let temp_path = self.map_path.with_extension("json.tmp");
fs::write(&temp_path, &json).await
.with_context(|| format!("Failed to write temporary ID map to {}", temp_path.display()))?;
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to write temporary ID map to {}: {}", temp_path.display(), e)))?;
// Realizar el rename atómico
fs::rename(&temp_path, &self.map_path).await
.with_context(|| format!("Failed to rename temporary ID map to {}", self.map_path.display()))?;
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to rename temporary ID map to {}: {}", self.map_path.display(), e)))?;
// Resetear flag de pendientes
{
@@ -408,34 +408,36 @@ impl IdMappingPort for IdMappingService {
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
self.get_or_create_id(path).await
.with_context(|| format!("Failed to get or create ID for path: {}", path.to_string()))
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get or create ID for path: {}: {}", path.to_string(), e)))
}
/// Obtiene una ruta por su ID con manejo de timeout
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
self.get_path_by_id(id).await
.with_context(|| format!("Failed to get path for ID: {}", id))
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e)))
}
/// Actualiza el mapeo de un ID existente a una nueva ruta
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
self.update_path(id, new_path).await
.with_context(|| format!("Failed to update path for ID: {} to {}", id, new_path.to_string()))
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to update path for ID: {} to {}: {}", id, new_path.to_string(), e)))
}
/// Elimina un ID del mapa
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
self.remove_id(id).await
.with_context(|| format!("Failed to remove ID: {}", id))
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e)))
}
/// Guarda cambios pendientes al disco
async fn save_changes(&self) -> Result<(), DomainError> {
self.save_pending_changes().await
.with_context(|| "Failed to save pending ID mapping changes")
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e)))
}
}
// The extension methods were moved to the IdMappingPort trait as default implementations
// Implementar Clone para poder usar en tokio::spawn
/// Synchronous helper for contexts where we can't use async
impl IdMappingService {
+2 -1
View File
@@ -4,4 +4,5 @@ pub mod id_mapping_optimizer;
pub mod cache_manager;
pub mod file_metadata_cache;
pub mod compression_service;
pub mod buffer_pool;
pub mod buffer_pool;
pub mod trash_cleanup_service;
@@ -0,0 +1,97 @@
use std::sync::Arc;
use std::time::Duration;
use tokio::time;
use tracing::{debug, error, info, instrument};
use crate::common::errors::Result;
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::ports::trash_ports::TrashUseCase;
/// Servicio para la limpieza automática de elementos expirados en la papelera
pub struct TrashCleanupService {
trash_service: Arc<dyn TrashUseCase>,
trash_repository: Arc<dyn TrashRepository>,
cleanup_interval_hours: u64,
}
impl TrashCleanupService {
pub fn new(
trash_service: Arc<dyn TrashUseCase>,
trash_repository: Arc<dyn TrashRepository>,
cleanup_interval_hours: u64,
) -> Self {
Self {
trash_service,
trash_repository,
cleanup_interval_hours: cleanup_interval_hours.max(1), // Mínimo 1 hora
}
}
/// Inicia el trabajo de limpieza periódica
#[instrument(skip(self))]
pub async fn start_cleanup_job(&self) {
let trash_repository = self.trash_repository.clone();
let trash_service = self.trash_service.clone();
let interval_hours = self.cleanup_interval_hours;
info!("Iniciando trabajo de limpieza de papelera con intervalo de {} horas", interval_hours);
tokio::spawn(async move {
let interval_duration = Duration::from_secs(interval_hours * 60 * 60);
let mut interval = time::interval(interval_duration);
// Primera ejecución inmediata
Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone()).await
.unwrap_or_else(|e| error!("Error en la limpieza inicial de la papelera: {:?}", e));
loop {
interval.tick().await;
debug!("Ejecutando tarea programada de limpieza de papelera");
if let Err(e) = Self::cleanup_expired_items(
trash_repository.clone(),
trash_service.clone()
).await {
error!("Error en la limpieza programada de la papelera: {:?}", e);
}
}
});
}
/// Limpia los elementos expirados en la papelera
#[instrument(skip(trash_repository, trash_service))]
async fn cleanup_expired_items(
trash_repository: Arc<dyn TrashRepository>,
trash_service: Arc<dyn TrashUseCase>,
) -> Result<()> {
debug!("Comenzando limpieza de elementos expirados en la papelera");
// Obtener todos los elementos expirados
let expired_items = trash_repository.get_expired_items().await?;
if expired_items.is_empty() {
debug!("No hay elementos expirados para limpiar");
return Ok(());
}
info!("Encontrados {} elementos expirados para eliminar", expired_items.len());
// Eliminar cada elemento expirado
for item in expired_items {
let trash_id = item.id.to_string();
let user_id = item.user_id.to_string();
debug!("Eliminando elemento expirado: id={}, user={}", trash_id, user_id);
// Si falla una eliminación, continuar con las demás
if let Err(e) = trash_service.delete_permanently(&trash_id, &user_id).await {
error!("Error eliminando elemento expirado {}: {:?}", trash_id, e);
} else {
debug!("Elemento expirado eliminado correctamente: {}", trash_id);
}
}
info!("Limpieza de papelera completada");
Ok(())
}
}