fix auth errors and add primigenial paper trash
This commit is contained in:
@@ -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(¤t_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(¤t_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(¤t_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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user