fix auth errors and add primigenial paper trash
This commit is contained in:
@@ -3,4 +3,5 @@ pub mod folder_dto;
|
||||
pub mod i18n_dto;
|
||||
pub mod pagination;
|
||||
pub mod user_dto;
|
||||
pub mod trash_dto;
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// DTO representing an item in the trash
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TrashedItemDto {
|
||||
pub id: String,
|
||||
pub original_id: String,
|
||||
pub item_type: String, // "file" o "folder"
|
||||
pub name: String,
|
||||
pub original_path: String,
|
||||
pub trashed_at: DateTime<Utc>,
|
||||
pub days_until_deletion: i64,
|
||||
}
|
||||
|
||||
/// Request to move an item to trash
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MoveToTrashRequest {
|
||||
pub item_id: String,
|
||||
pub item_type: String, // "file" o "folder"
|
||||
}
|
||||
|
||||
/// Request to restore an item from trash
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RestoreFromTrashRequest {
|
||||
pub trash_id: String,
|
||||
}
|
||||
|
||||
/// Request to permanently delete an item from trash
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeletePermanentlyRequest {
|
||||
pub trash_id: String,
|
||||
}
|
||||
@@ -2,4 +2,5 @@ pub mod inbound;
|
||||
pub mod outbound;
|
||||
pub mod file_ports;
|
||||
pub mod storage_ports;
|
||||
pub mod auth_ports;
|
||||
pub mod auth_ports;
|
||||
pub mod trash_ports;
|
||||
@@ -115,4 +115,16 @@ pub trait IdMappingPort: Send + Sync + 'static {
|
||||
|
||||
/// Guarda cambios pendientes
|
||||
async fn save_changes(&self) -> Result<(), DomainError>;
|
||||
|
||||
/// Obtiene la ruta de archivo como PathBuf
|
||||
async fn get_file_path(&self, file_id: &str) -> Result<PathBuf, DomainError> {
|
||||
let storage_path = self.get_path_by_id(file_id).await?;
|
||||
Ok(PathBuf::from(storage_path.to_string()))
|
||||
}
|
||||
|
||||
/// Actualiza la ruta de un archivo
|
||||
async fn update_file_path(&self, file_id: &str, new_path: &PathBuf) -> Result<(), DomainError> {
|
||||
let storage_path = StoragePath::from_string(&new_path.to_string_lossy().to_string());
|
||||
self.update_path(file_id, &storage_path).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::dtos::trash_dto::TrashedItemDto;
|
||||
use crate::common::errors::Result;
|
||||
|
||||
/// Port for trash-related use cases
|
||||
#[async_trait]
|
||||
pub trait TrashUseCase: Send + Sync {
|
||||
/// List items in the user's trash
|
||||
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>>;
|
||||
|
||||
/// Move a file or folder to trash
|
||||
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Restore an item from trash to its original location
|
||||
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Permanently delete an item from trash
|
||||
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Empty the trash for a specific user
|
||||
async fn empty_trash(&self, user_id: &str) -> Result<()>;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ impl FolderUseCase for FolderService {
|
||||
// Crear la carpeta
|
||||
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
|
||||
.await
|
||||
.with_context(|| "Failed to create folder")?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
|
||||
|
||||
// Convertir a DTO
|
||||
Ok(FolderDto::from(folder))
|
||||
@@ -104,7 +104,7 @@ impl FolderUseCase for FolderService {
|
||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {}", id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {}: {}", id, e)))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -116,7 +116,7 @@ impl FolderUseCase for FolderService {
|
||||
|
||||
let folder = self.folder_storage.get_folder_by_path(&storage_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder at path: {}", path))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder at path: {}: {}", path, e)))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl FolderUseCase for FolderService {
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
|
||||
let folders = self.folder_storage.list_folders(parent_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to list folders in parent: {:?}", parent_id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?;
|
||||
|
||||
// Convertir a DTOs
|
||||
Ok(folders.into_iter().map(FolderDto::from).collect())
|
||||
@@ -148,7 +148,7 @@ impl FolderUseCase for FolderService {
|
||||
true // Siempre incluir total para mejor UX
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to list folders with pagination in parent: {:?}", parent_id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
|
||||
|
||||
// El total es necesario para calcular la paginación
|
||||
let total = total_items.unwrap_or(folders.len());
|
||||
@@ -178,7 +178,7 @@ impl FolderUseCase for FolderService {
|
||||
// Verificar que la carpeta existe
|
||||
let existing_folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {} for renaming", id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
|
||||
|
||||
// Crear transacción para renombrar
|
||||
let mut transaction = StorageTransaction::new("rename_folder");
|
||||
@@ -220,7 +220,7 @@ impl FolderUseCase for FolderService {
|
||||
// Obtener la carpeta renombrada
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get renamed folder with ID: {}", id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -230,7 +230,7 @@ impl FolderUseCase for FolderService {
|
||||
// Verificar que la carpeta origen existe
|
||||
let source_folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {} for moving", id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
|
||||
|
||||
// Si se especifica un parent_id, verificar que existe
|
||||
if let Some(parent_id) = &dto.parent_id {
|
||||
@@ -295,7 +295,7 @@ impl FolderUseCase for FolderService {
|
||||
// Obtener la carpeta movida
|
||||
let folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get moved folder with ID: {}", id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
@@ -305,13 +305,13 @@ impl FolderUseCase for FolderService {
|
||||
// Verificar que la carpeta existe
|
||||
let _folder = self.folder_storage.get_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to get folder with ID: {} for deletion", id))?;
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
|
||||
|
||||
// En una implementación real, podríamos verificar permisos, dependencias, etc.
|
||||
|
||||
// Eliminar la carpeta
|
||||
self.folder_storage.delete_folder(id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete folder with ID: {}", id))
|
||||
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,14 @@ pub mod file_retrieval_service;
|
||||
pub mod file_management_service;
|
||||
pub mod file_use_case_factory;
|
||||
pub mod auth_application_service;
|
||||
pub mod trash_service;
|
||||
|
||||
#[cfg(test)]
|
||||
mod trash_service_test;
|
||||
|
||||
// Re-exportar para facilitar acceso
|
||||
pub use file_upload_service::FileUploadService;
|
||||
pub use file_retrieval_service::FileRetrievalService;
|
||||
pub use file_management_service::FileManagementService;
|
||||
pub use file_use_case_factory::AppFileUseCaseFactory;
|
||||
pub use trash_service::TrashService;
|
||||
|
||||
@@ -192,6 +192,19 @@ impl FolderRepository for FolderRepositoryStub {
|
||||
async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> Result<Folder, FolderRepositoryError> {
|
||||
Err(FolderRepositoryError::Other("Stub repository".to_string()))
|
||||
}
|
||||
|
||||
// Trash functionality stubs
|
||||
async fn move_to_trash(&self, _folder_id: &str) -> Result<(), FolderRepositoryError> {
|
||||
Err(FolderRepositoryError::OperationNotSupported("Trash feature temporarily disabled".to_string()))
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> Result<(), FolderRepositoryError> {
|
||||
Err(FolderRepositoryError::OperationNotSupported("Trash feature temporarily disabled".to_string()))
|
||||
}
|
||||
|
||||
async fn delete_folder_permanently(&self, _folder_id: &str) -> Result<(), FolderRepositoryError> {
|
||||
Err(FolderRepositoryError::OperationNotSupported("Trash feature temporarily disabled".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation for initialization dependency issues
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
use crate::application::dtos::trash_dto::TrashedItemDto;
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
|
||||
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult};
|
||||
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult};
|
||||
use crate::domain::repositories::trash_repository::TrashRepository;
|
||||
|
||||
/// Servicio de aplicación para operaciones de papelera
|
||||
pub struct TrashService {
|
||||
trash_repository: Arc<dyn TrashRepository>,
|
||||
file_repository: Arc<dyn FileRepository>,
|
||||
folder_repository: Arc<dyn FolderRepository>,
|
||||
retention_days: u32,
|
||||
}
|
||||
|
||||
impl TrashService {
|
||||
pub fn new(
|
||||
trash_repository: Arc<dyn TrashRepository>,
|
||||
file_repository: Arc<dyn FileRepository>,
|
||||
folder_repository: Arc<dyn FolderRepository>,
|
||||
retention_days: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
trash_repository,
|
||||
file_repository,
|
||||
folder_repository,
|
||||
retention_days,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte una entidad TrashedItem a un DTO
|
||||
fn to_dto(&self, item: TrashedItem) -> TrashedItemDto {
|
||||
// Calcular days_until_deletion antes de mover item.original_path
|
||||
let days_until_deletion = item.days_until_deletion();
|
||||
|
||||
TrashedItemDto {
|
||||
id: item.id.to_string(),
|
||||
original_id: item.original_id.to_string(),
|
||||
item_type: match item.item_type {
|
||||
TrashedItemType::File => "file".to_string(),
|
||||
TrashedItemType::Folder => "folder".to_string(),
|
||||
},
|
||||
name: item.name,
|
||||
original_path: item.original_path,
|
||||
trashed_at: item.trashed_at,
|
||||
days_until_deletion,
|
||||
}
|
||||
}
|
||||
|
||||
/// Valida los permisos del usuario sobre un elemento
|
||||
#[instrument(skip(self))]
|
||||
async fn validate_user_ownership(&self, _item_id: &str, _user_id: &str) -> Result<()> {
|
||||
// Aquí implementaríamos la validación de permisos
|
||||
// Por ahora, simplemente devolvemos Ok ya que no tenemos una implementación completa
|
||||
// de permisos por usuario
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TrashUseCase for TrashService {
|
||||
#[instrument(skip(self))]
|
||||
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
|
||||
debug!("Obteniendo elementos en papelera para usuario: {}", user_id);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
||||
|
||||
let dtos = items.into_iter()
|
||||
.map(|item| self.to_dto(item))
|
||||
.collect();
|
||||
|
||||
Ok(dtos)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
|
||||
info!("Moviendo a papelera: tipo={}, id={}, usuario={}", item_type, item_id, user_id);
|
||||
|
||||
self.validate_user_ownership(item_id, user_id).await?;
|
||||
|
||||
let item_uuid = Uuid::parse_str(item_id)
|
||||
.map_err(|e| DomainError::validation_error("Item", format!("Invalid item ID: {}", e)))?;
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
match item_type {
|
||||
"file" => {
|
||||
// Obtener el archivo para verificar que existe y capturar sus datos
|
||||
let file = self.file_repository.get_file_by_id(item_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::NotFound,
|
||||
"File",
|
||||
format!("Error retrieving file {}: {}", item_id, e)
|
||||
))?;
|
||||
|
||||
let original_path = file.storage_path().to_string();
|
||||
|
||||
// Crear el elemento de papelera
|
||||
let trashed_item = TrashedItem::new(
|
||||
item_uuid,
|
||||
user_uuid,
|
||||
TrashedItemType::File,
|
||||
file.name().to_string(),
|
||||
original_path,
|
||||
self.retention_days,
|
||||
);
|
||||
|
||||
// Primero añadimos a la papelera para registrar el elemento
|
||||
self.trash_repository.add_to_trash(&trashed_item).await?;
|
||||
|
||||
// Luego movemos el archivo físicamente a la papelera
|
||||
self.file_repository.move_to_trash(item_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"File",
|
||||
format!("Error moving file {} to trash: {}", item_id, e)
|
||||
))?;
|
||||
|
||||
debug!("Archivo movido a papelera: {}", item_id);
|
||||
Ok(())
|
||||
},
|
||||
"folder" => {
|
||||
// Obtener la carpeta para verificar que existe y capturar sus datos
|
||||
let folder = self.folder_repository.get_folder_by_id(item_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::NotFound,
|
||||
"Folder",
|
||||
format!("Error retrieving folder {}: {}", item_id, e)
|
||||
))?;
|
||||
|
||||
let original_path = folder.storage_path().to_string();
|
||||
|
||||
// Crear el elemento de papelera
|
||||
let trashed_item = TrashedItem::new(
|
||||
item_uuid,
|
||||
user_uuid,
|
||||
TrashedItemType::Folder,
|
||||
folder.name().to_string(),
|
||||
original_path,
|
||||
self.retention_days,
|
||||
);
|
||||
|
||||
// Primero añadimos a la papelera para registrar el elemento
|
||||
self.trash_repository.add_to_trash(&trashed_item).await?;
|
||||
|
||||
// Luego movemos la carpeta físicamente a la papelera
|
||||
self.folder_repository.move_to_trash(item_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Error moving folder {} to trash: {}", item_id, e)
|
||||
))?;
|
||||
|
||||
debug!("Carpeta movida a papelera: {}", item_id);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(DomainError::validation_error("Item", format!("Invalid item type: {}", item_type))),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
||||
info!("Restaurando elemento {} para usuario {}", trash_id, user_id);
|
||||
|
||||
let trash_uuid = Uuid::parse_str(trash_id)
|
||||
.map_err(|e| DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)))?;
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
// Obtener el elemento de la papelera
|
||||
let item = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await?
|
||||
.ok_or_else(|| DomainError::not_found("TrashedItem", trash_id.to_string()))?;
|
||||
|
||||
// Restaurar según tipo
|
||||
match item.item_type {
|
||||
TrashedItemType::File => {
|
||||
// Restaurar el archivo a su ubicación original
|
||||
let file_id = item.original_id.to_string();
|
||||
self.file_repository.restore_from_trash(&file_id, &item.original_path).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"File",
|
||||
format!("Error restoring file {} from trash: {}", file_id, e)
|
||||
))?;
|
||||
debug!("Archivo restaurado desde papelera: {}", file_id);
|
||||
},
|
||||
TrashedItemType::Folder => {
|
||||
// Restaurar la carpeta a su ubicación original
|
||||
let folder_id = item.original_id.to_string();
|
||||
self.folder_repository.restore_from_trash(&folder_id, &item.original_path).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Error restoring folder {} from trash: {}", folder_id, e)
|
||||
))?;
|
||||
debug!("Carpeta restaurada desde papelera: {}", folder_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminar el item de la papelera
|
||||
self.trash_repository.restore_from_trash(&trash_uuid, &user_uuid).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
||||
info!("Eliminando permanentemente elemento {} para usuario {}", trash_id, user_id);
|
||||
|
||||
let trash_uuid = Uuid::parse_str(trash_id)
|
||||
.map_err(|e| DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)))?;
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
// Obtener el elemento de la papelera
|
||||
let item = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await?
|
||||
.ok_or_else(|| DomainError::not_found("TrashedItem", trash_id.to_string()))?;
|
||||
|
||||
// Eliminar permanentemente según tipo
|
||||
match item.item_type {
|
||||
TrashedItemType::File => {
|
||||
// Eliminar el archivo permanentemente
|
||||
let file_id = item.original_id.to_string();
|
||||
self.file_repository.delete_file_permanently(&file_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"File",
|
||||
format!("Error deleting file {} permanently: {}", file_id, e)
|
||||
))?;
|
||||
debug!("Archivo eliminado permanentemente: {}", file_id);
|
||||
},
|
||||
TrashedItemType::Folder => {
|
||||
// Eliminar la carpeta permanentemente
|
||||
let folder_id = item.original_id.to_string();
|
||||
self.folder_repository.delete_folder_permanently(&folder_id).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Folder",
|
||||
format!("Error deleting folder {} permanently: {}", folder_id, e)
|
||||
))?;
|
||||
debug!("Carpeta eliminada permanentemente: {}", folder_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminar el item de la papelera
|
||||
self.trash_repository.delete_permanently(&trash_uuid, &user_uuid).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn empty_trash(&self, user_id: &str) -> Result<()> {
|
||||
info!("Vaciando papelera para usuario {}", user_id);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
// Obtener todos los elementos en la papelera del usuario
|
||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
||||
|
||||
// Eliminar permanentemente cada elemento
|
||||
for item in items {
|
||||
match item.item_type {
|
||||
TrashedItemType::File => {
|
||||
// Eliminar el archivo permanentemente
|
||||
let file_id = item.original_id.to_string();
|
||||
if let Err(e) = self.file_repository.delete_file_permanently(&file_id).await {
|
||||
error!("Error al eliminar archivo {} permanentemente: {}", file_id, e);
|
||||
}
|
||||
},
|
||||
TrashedItemType::Folder => {
|
||||
// Eliminar la carpeta permanentemente
|
||||
let folder_id = item.original_id.to_string();
|
||||
if let Err(e) = self.folder_repository.delete_folder_permanently(&folder_id).await {
|
||||
error!("Error al eliminar carpeta {} permanentemente: {}", folder_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar todos los registros de la papelera para este usuario
|
||||
self.trash_repository.clear_trash(&user_uuid).await?;
|
||||
|
||||
info!("Papelera vaciada completamente para usuario {}", user_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use chrono::Utc;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::errors::{Result, DomainError};
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
|
||||
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult};
|
||||
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult};
|
||||
use crate::domain::repositories::trash_repository::TrashRepository;
|
||||
use crate::application::services::trash_service::TrashService;
|
||||
|
||||
// Mock repositories for testing
|
||||
struct MockTrashRepository {
|
||||
trash_items: Mutex<HashMap<Uuid, TrashedItem>>,
|
||||
}
|
||||
|
||||
impl MockTrashRepository {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
trash_items: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TrashRepository for MockTrashRepository {
|
||||
async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> {
|
||||
let mut items = self.trash_items.lock().unwrap();
|
||||
items.insert(item.id, item.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
|
||||
let items = self.trash_items.lock().unwrap();
|
||||
let user_items = items.values()
|
||||
.filter(|item| item.user_id == *user_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(user_items)
|
||||
}
|
||||
|
||||
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
|
||||
let items = self.trash_items.lock().unwrap();
|
||||
let item = items.get(id)
|
||||
.filter(|item| item.user_id == *user_id)
|
||||
.cloned();
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
||||
let mut items = self.trash_items.lock().unwrap();
|
||||
if let Some(item) = items.get(id) {
|
||||
if item.user_id == *user_id {
|
||||
items.remove(id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> {
|
||||
let mut items = self.trash_items.lock().unwrap();
|
||||
if let Some(item) = items.get(id) {
|
||||
if item.user_id == *user_id {
|
||||
items.remove(id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
|
||||
let mut items = self.trash_items.lock().unwrap();
|
||||
items.retain(|_, item| item.user_id != *user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
|
||||
let items = self.trash_items.lock().unwrap();
|
||||
let now = Utc::now();
|
||||
let expired = items.values()
|
||||
.filter(|item| item.deletion_date <= now)
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(expired)
|
||||
}
|
||||
}
|
||||
|
||||
struct MockFileRepository {
|
||||
files: Mutex<HashMap<String, File>>,
|
||||
trashed_files: Mutex<HashMap<String, File>>,
|
||||
}
|
||||
|
||||
impl MockFileRepository {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
files: Mutex::new(HashMap::new()),
|
||||
trashed_files: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_test_file(&self, id: &str, name: &str, path: &str) {
|
||||
let file = File::new(
|
||||
Uuid::parse_str(id).unwrap(),
|
||||
name.to_string(),
|
||||
path.to_string(),
|
||||
"text/plain".to_string(),
|
||||
100,
|
||||
Uuid::new_v4(),
|
||||
None,
|
||||
).unwrap();
|
||||
|
||||
let mut files = self.files.lock().unwrap();
|
||||
files.insert(id.to_string(), file);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FileRepository for MockFileRepository {
|
||||
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
|
||||
let files = self.files.lock().unwrap();
|
||||
if let Some(file) = files.get(id) {
|
||||
Ok(file.clone())
|
||||
} else {
|
||||
Err("File not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn move_to_trash(&self, id: &str) -> FileRepositoryResult<()> {
|
||||
let mut files = self.files.lock().unwrap();
|
||||
let mut trashed = self.trashed_files.lock().unwrap();
|
||||
|
||||
if let Some(file) = files.remove(id) {
|
||||
trashed.insert(id.to_string(), file);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("File not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, id: &str, original_path: &str) -> FileRepositoryResult<()> {
|
||||
let mut files = self.files.lock().unwrap();
|
||||
let mut trashed = self.trashed_files.lock().unwrap();
|
||||
|
||||
if let Some(file) = trashed.remove(id) {
|
||||
files.insert(id.to_string(), file);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("File not found in trash".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_file_permanently(&self, id: &str) -> FileRepositoryResult<()> {
|
||||
let mut trashed = self.trashed_files.lock().unwrap();
|
||||
if trashed.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("File not found in trash".into())
|
||||
}
|
||||
}
|
||||
|
||||
// Other methods required by the trait (not used in tests)
|
||||
async fn save_file(&self, _file: &File) -> FileRepositoryResult<()> { Ok(()) }
|
||||
async fn delete_file(&self, _id: &str) -> FileRepositoryResult<()> { Ok(()) }
|
||||
async fn get_files_in_folder(&self, _folder_id: Option<&str>) -> FileRepositoryResult<Vec<File>> { Ok(vec![]) }
|
||||
async fn move_file(&self, _id: &str, _new_folder_id: Option<&str>) -> FileRepositoryResult<()> { Ok(()) }
|
||||
async fn update_file_data(&self, _id: &str, _new_data: &[u8]) -> FileRepositoryResult<()> { Ok(()) }
|
||||
async fn get_file_data(&self, _id: &str) -> FileRepositoryResult<Vec<u8>> { Ok(vec![]) }
|
||||
}
|
||||
|
||||
struct MockFolderRepository {
|
||||
folders: Mutex<HashMap<String, Folder>>,
|
||||
trashed_folders: Mutex<HashMap<String, Folder>>,
|
||||
}
|
||||
|
||||
impl MockFolderRepository {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
folders: Mutex::new(HashMap::new()),
|
||||
trashed_folders: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_test_folder(&self, id: &str, name: &str, path: &str) {
|
||||
let folder = Folder::new(
|
||||
Uuid::parse_str(id).unwrap(),
|
||||
name.to_string(),
|
||||
path.to_string(),
|
||||
None,
|
||||
).unwrap();
|
||||
|
||||
let mut folders = self.folders.lock().unwrap();
|
||||
folders.insert(id.to_string(), folder);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderRepository for MockFolderRepository {
|
||||
async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult<Folder> {
|
||||
let folders = self.folders.lock().unwrap();
|
||||
if let Some(folder) = folders.get(id) {
|
||||
Ok(folder.clone())
|
||||
} else {
|
||||
Err("Folder not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn move_to_trash(&self, id: &str) -> FolderRepositoryResult<()> {
|
||||
let mut folders = self.folders.lock().unwrap();
|
||||
let mut trashed = self.trashed_folders.lock().unwrap();
|
||||
|
||||
if let Some(folder) = folders.remove(id) {
|
||||
trashed.insert(id.to_string(), folder);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Folder not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, id: &str, original_path: &str) -> FolderRepositoryResult<()> {
|
||||
let mut folders = self.folders.lock().unwrap();
|
||||
let mut trashed = self.trashed_folders.lock().unwrap();
|
||||
|
||||
if let Some(folder) = trashed.remove(id) {
|
||||
folders.insert(id.to_string(), folder);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Folder not found in trash".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_folder_permanently(&self, id: &str) -> FolderRepositoryResult<()> {
|
||||
let mut trashed = self.trashed_folders.lock().unwrap();
|
||||
if trashed.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Folder not found in trash".into())
|
||||
}
|
||||
}
|
||||
|
||||
// Other methods required by the trait (not used in tests)
|
||||
async fn save_folder(&self, _folder: &Folder) -> FolderRepositoryResult<()> { Ok(()) }
|
||||
async fn delete_folder(&self, _id: &str) -> FolderRepositoryResult<()> { Ok(()) }
|
||||
async fn get_folders_in_folder(&self, _parent_id: Option<&str>) -> FolderRepositoryResult<Vec<Folder>> { Ok(vec![]) }
|
||||
async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> FolderRepositoryResult<()> { Ok(()) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_move_file_to_trash() {
|
||||
// Arrange
|
||||
let trash_repo = Arc::new(MockTrashRepository::new());
|
||||
let file_repo = Arc::new(MockFileRepository::new());
|
||||
let folder_repo = Arc::new(MockFolderRepository::new());
|
||||
|
||||
let service = TrashService::new(
|
||||
trash_repo.clone(),
|
||||
file_repo.clone(),
|
||||
folder_repo.clone(),
|
||||
30, // 30 days retention
|
||||
);
|
||||
|
||||
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||
|
||||
// Add a test file to the repository
|
||||
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
|
||||
|
||||
// Act
|
||||
let result = service.move_to_trash(file_id, "file", user_id).await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok(), "Moving file to trash failed: {:?}", result);
|
||||
|
||||
// Verify the file is in trash
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
|
||||
assert_eq!(trash_items.len(), 1, "Should have exactly one item in trash");
|
||||
let trash_item = &trash_items[0];
|
||||
|
||||
assert_eq!(trash_item.original_id.to_string(), file_id, "Original ID should match file ID");
|
||||
assert_eq!(trash_item.user_id.to_string(), user_id, "User ID should match");
|
||||
assert_eq!(trash_item.item_type, TrashedItemType::File, "Item type should be File");
|
||||
assert_eq!(trash_item.name, "test.txt", "File name should match");
|
||||
|
||||
// Verify file is moved in file repository
|
||||
let files = file_repo.files.lock().unwrap();
|
||||
let trashed_files = file_repo.trashed_files.lock().unwrap();
|
||||
|
||||
assert!(files.get(file_id).is_none(), "File should no longer be in main storage");
|
||||
assert!(trashed_files.get(file_id).is_some(), "File should be in trash storage");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_move_folder_to_trash() {
|
||||
// Arrange
|
||||
let trash_repo = Arc::new(MockTrashRepository::new());
|
||||
let file_repo = Arc::new(MockFileRepository::new());
|
||||
let folder_repo = Arc::new(MockFolderRepository::new());
|
||||
|
||||
let service = TrashService::new(
|
||||
trash_repo.clone(),
|
||||
file_repo.clone(),
|
||||
folder_repo.clone(),
|
||||
30, // 30 days retention
|
||||
);
|
||||
|
||||
let folder_id = "550e8400-e29b-41d4-a716-446655440002";
|
||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||
|
||||
// Add a test folder to the repository
|
||||
folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder");
|
||||
|
||||
// Act
|
||||
let result = service.move_to_trash(folder_id, "folder", user_id).await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok(), "Moving folder to trash failed: {:?}", result);
|
||||
|
||||
// Verify the folder is in trash
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
|
||||
assert_eq!(trash_items.len(), 1, "Should have exactly one item in trash");
|
||||
let trash_item = &trash_items[0];
|
||||
|
||||
assert_eq!(trash_item.original_id.to_string(), folder_id, "Original ID should match folder ID");
|
||||
assert_eq!(trash_item.user_id.to_string(), user_id, "User ID should match");
|
||||
assert_eq!(trash_item.item_type, TrashedItemType::Folder, "Item type should be Folder");
|
||||
assert_eq!(trash_item.name, "test_folder", "Folder name should match");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_restore_file_from_trash() {
|
||||
// Arrange
|
||||
let trash_repo = Arc::new(MockTrashRepository::new());
|
||||
let file_repo = Arc::new(MockFileRepository::new());
|
||||
let folder_repo = Arc::new(MockFolderRepository::new());
|
||||
|
||||
let service = TrashService::new(
|
||||
trash_repo.clone(),
|
||||
file_repo.clone(),
|
||||
folder_repo.clone(),
|
||||
30, // 30 days retention
|
||||
);
|
||||
|
||||
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||
let file_path = "/test/path/test.txt";
|
||||
|
||||
// Add a test file and move it to trash
|
||||
file_repo.add_test_file(file_id, "test.txt", file_path);
|
||||
service.move_to_trash(file_id, "file", user_id).await.unwrap();
|
||||
|
||||
// Get the trash item ID
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
let trash_id = trash_items[0].id.to_string();
|
||||
|
||||
// Act
|
||||
let result = service.restore_item(&trash_id, user_id).await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok(), "Restoring file from trash failed: {:?}", result);
|
||||
|
||||
// Verify the file is restored in file repository
|
||||
let files = file_repo.files.lock().unwrap();
|
||||
let trashed_files = file_repo.trashed_files.lock().unwrap();
|
||||
|
||||
assert!(files.get(file_id).is_some(), "File should be back in main storage");
|
||||
assert!(trashed_files.get(file_id).is_none(), "File should no longer be in trash storage");
|
||||
|
||||
// Verify the trash item is removed
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
assert_eq!(trash_items.len(), 0, "Trash should be empty after restoration");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_permanently() {
|
||||
// Arrange
|
||||
let trash_repo = Arc::new(MockTrashRepository::new());
|
||||
let file_repo = Arc::new(MockFileRepository::new());
|
||||
let folder_repo = Arc::new(MockFolderRepository::new());
|
||||
|
||||
let service = TrashService::new(
|
||||
trash_repo.clone(),
|
||||
file_repo.clone(),
|
||||
folder_repo.clone(),
|
||||
30, // 30 days retention
|
||||
);
|
||||
|
||||
let file_id = "550e8400-e29b-41d4-a716-446655440000";
|
||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||
|
||||
// Add a test file and move it to trash
|
||||
file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt");
|
||||
service.move_to_trash(file_id, "file", user_id).await.unwrap();
|
||||
|
||||
// Get the trash item ID
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
let trash_id = trash_items[0].id.to_string();
|
||||
|
||||
// Act
|
||||
let result = service.delete_permanently(&trash_id, user_id).await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok(), "Deleting file permanently failed: {:?}", result);
|
||||
|
||||
// Verify the file is permanently deleted
|
||||
let files = file_repo.files.lock().unwrap();
|
||||
let trashed_files = file_repo.trashed_files.lock().unwrap();
|
||||
|
||||
assert!(files.get(file_id).is_none(), "File should not be in main storage");
|
||||
assert!(trashed_files.get(file_id).is_none(), "File should not be in trash storage");
|
||||
|
||||
// Verify the trash item is removed
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
assert_eq!(trash_items.len(), 0, "Trash should be empty after permanent deletion");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_trash() {
|
||||
// Arrange
|
||||
let trash_repo = Arc::new(MockTrashRepository::new());
|
||||
let file_repo = Arc::new(MockFileRepository::new());
|
||||
let folder_repo = Arc::new(MockFolderRepository::new());
|
||||
|
||||
let service = TrashService::new(
|
||||
trash_repo.clone(),
|
||||
file_repo.clone(),
|
||||
folder_repo.clone(),
|
||||
30, // 30 days retention
|
||||
);
|
||||
|
||||
let user_id = "550e8400-e29b-41d4-a716-446655440001";
|
||||
|
||||
// Add multiple files and folders to trash
|
||||
let file_ids = [
|
||||
"550e8400-e29b-41d4-a716-446655440010",
|
||||
"550e8400-e29b-41d4-a716-446655440011",
|
||||
];
|
||||
|
||||
let folder_ids = [
|
||||
"550e8400-e29b-41d4-a716-446655440020",
|
||||
"550e8400-e29b-41d4-a716-446655440021",
|
||||
];
|
||||
|
||||
// Add test files and folders
|
||||
for (i, file_id) in file_ids.iter().enumerate() {
|
||||
file_repo.add_test_file(file_id, &format!("test{}.txt", i), &format!("/test/path/test{}.txt", i));
|
||||
service.move_to_trash(file_id, "file", user_id).await.unwrap();
|
||||
}
|
||||
|
||||
for (i, folder_id) in folder_ids.iter().enumerate() {
|
||||
folder_repo.add_test_folder(folder_id, &format!("folder{}", i), &format!("/test/path/folder{}", i));
|
||||
service.move_to_trash(folder_id, "folder", user_id).await.unwrap();
|
||||
}
|
||||
|
||||
// Verify items are in trash
|
||||
let user_uuid = Uuid::parse_str(user_id).unwrap();
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
assert_eq!(trash_items.len(), 4, "Should have 4 items in trash");
|
||||
|
||||
// Act
|
||||
let result = service.empty_trash(user_id).await;
|
||||
|
||||
// Assert
|
||||
assert!(result.is_ok(), "Emptying trash failed: {:?}", result);
|
||||
|
||||
// Verify all items are permanently deleted
|
||||
for file_id in &file_ids {
|
||||
let files = file_repo.files.lock().unwrap();
|
||||
let trashed_files = file_repo.trashed_files.lock().unwrap();
|
||||
assert!(files.get(*file_id).is_none(), "File should not be in main storage");
|
||||
assert!(trashed_files.get(*file_id).is_none(), "File should not be in trash storage");
|
||||
}
|
||||
|
||||
for folder_id in &folder_ids {
|
||||
let folders = folder_repo.folders.lock().unwrap();
|
||||
let trashed_folders = folder_repo.trashed_folders.lock().unwrap();
|
||||
assert!(folders.get(*folder_id).is_none(), "Folder should not be in main storage");
|
||||
assert!(trashed_folders.get(*folder_id).is_none(), "Folder should not be in trash storage");
|
||||
}
|
||||
|
||||
// Verify the trash is empty
|
||||
let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap();
|
||||
assert_eq!(trash_items.len(), 0, "Trash should be empty after emptying");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user