use std::path::{Path, PathBuf}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use async_trait::async_trait; use tokio::fs; use uuid::Uuid; use serde::{Serialize, Deserialize}; use crate::domain::entities::folder::Folder; use crate::domain::repositories::folder_repository::{ FolderRepository, FolderRepositoryError, FolderRepositoryResult }; /// Estructura para almacenar la relación entre paths y IDs #[derive(Debug, Serialize, Deserialize, Default)] struct FolderIdMap { path_to_id: HashMap, } /// Filesystem implementation of the FolderRepository interface pub struct FolderFsRepository { root_path: PathBuf, id_map: Arc>, } impl FolderFsRepository { /// Creates a new filesystem-based folder repository pub fn new(root_path: PathBuf) -> Self { // Crear o cargar el mapeo de IDs let id_map = Arc::new(Mutex::new(FolderIdMap::default())); // Intentar cargar el mapeo existente si existe let map_path = root_path.join("folder_ids.json"); if let Ok(contents) = std::fs::read_to_string(map_path) { if let Ok(loaded_map) = serde_json::from_str::(&contents) { let mut map = id_map.lock().unwrap(); *map = loaded_map; tracing::info!("Loaded folder ID map with {} entries", map.path_to_id.len()); } } Self { root_path, id_map } } /// Guarda el mapeo de IDs a disco fn save_id_map(&self) { let map_path = self.root_path.join("folder_ids.json"); let map = self.id_map.lock().unwrap(); if let Ok(json) = serde_json::to_string_pretty(&*map) { std::fs::write(map_path, json).ok(); } } /// Obtiene o genera un ID para un path fn get_or_create_id(&self, path: &Path) -> String { let path_str = path.to_string_lossy().to_string(); let mut map = self.id_map.lock().unwrap(); if let Some(id) = map.path_to_id.get(&path_str) { return id.clone(); } // Si no existe, genera un nuevo ID let id = Uuid::new_v4().to_string(); map.path_to_id.insert(path_str, id.clone()); // Guardar el mapa actualizado drop(map); // Liberar el mutex antes de guardar self.save_id_map(); id } /// Generates a unique ID for a folder #[allow(dead_code)] fn generate_id(&self) -> String { Uuid::new_v4().to_string() } /// Resolves a relative path to an absolute path fn resolve_path(&self, relative_path: &Path) -> PathBuf { self.root_path.join(relative_path) } /// Creates the physical directory on the filesystem async fn create_directory(&self, path: &Path) -> Result<(), std::io::Error> { fs::create_dir_all(path).await } } #[async_trait] impl FolderRepository for FolderFsRepository { async fn create_folder(&self, name: String, parent_path: Option) -> FolderRepositoryResult { // Calculate the new folder path let path = match &parent_path { Some(parent) => parent.join(&name), None => PathBuf::from(&name), }; // Check if folder already exists if self.folder_exists(&path).await? { return Err(FolderRepositoryError::AlreadyExists(path.to_string_lossy().to_string())); } // Create the physical directory let abs_path = self.resolve_path(&path); self.create_directory(&abs_path).await .map_err(FolderRepositoryError::IoError)?; // Determine parent ID if any let parent_id = if let Some(parent) = &parent_path { if !parent.as_os_str().is_empty() { let parent_folder = self.get_folder_by_path(parent).await?; Some(parent_folder.id) } else { None } } else { None }; // Create and return the folder entity with a persisted ID let id = self.get_or_create_id(&path); let folder = Folder::new(id, name, path, parent_id); tracing::debug!("Created folder with ID: {}", folder.id); Ok(folder) } async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult { tracing::debug!("Buscando carpeta con ID: {}", id); // First try to find the path associated with this ID let path_opt = { let map = self.id_map.lock().unwrap(); // Invertir el mapeo para buscar por ID map.path_to_id.iter() .find_map(|(path, folder_id)| if folder_id == id { Some(path.clone()) } else { None }) }; if let Some(path_str) = path_opt { let path = PathBuf::from(path_str); tracing::debug!("Encontrado path para ID {}: {:?}", id, path); return self.get_folder_by_path(&path).await; } // Fallback: buscar a través de todas las carpetas tracing::debug!("ID {} no encontrado en el mapa, buscando a través de todas las carpetas", id); let all_folders = self.list_folders(None).await?; // Imprimir IDs disponibles para depuración for folder in &all_folders { tracing::debug!("Carpeta disponible - ID: {}, Nombre: {}", folder.id, folder.name); } // Find the folder with the matching ID all_folders.into_iter() .find(|folder| folder.id == id) .ok_or_else(|| FolderRepositoryError::NotFound(id.to_string())) } async fn get_folder_by_path(&self, path: &PathBuf) -> FolderRepositoryResult { // Check if the physical directory exists let abs_path = self.resolve_path(path); if !abs_path.exists() || !abs_path.is_dir() { return Err(FolderRepositoryError::NotFound(path.to_string_lossy().to_string())); } // Extract folder name and parent path let name = path.file_name() .ok_or_else(|| FolderRepositoryError::InvalidPath(path.to_string_lossy().to_string()))? .to_string_lossy() .to_string(); let parent_path = path.parent().map(|p| p.to_path_buf()); // Determine parent ID if any let parent_id = if let Some(parent) = &parent_path { if !parent.as_os_str().is_empty() { match self.get_folder_by_path(parent).await { Ok(parent_folder) => Some(parent_folder.id), Err(_) => None, } } else { None } } else { None }; // Get a consistent ID for this path let id = self.get_or_create_id(path); tracing::debug!("Found folder with path: {:?}, assigned ID: {}", path, id); // Get folder metadata for timestamps let metadata = fs::metadata(&abs_path).await .map_err(FolderRepositoryError::IoError)?; let created_at = metadata.created() .map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()) .unwrap_or_else(|_| 0); let modified_at = metadata.modified() .map(|time| time.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()) .unwrap_or_else(|_| 0); // Create and return the folder entity let mut folder = Folder::new(id, name, path.clone(), parent_id); folder.created_at = created_at; folder.modified_at = modified_at; Ok(folder) } async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult> { let parent_path = match parent_id { Some(id) => { let parent = self.get_folder_by_id(id).await?; parent.path }, None => PathBuf::from(""), }; let abs_parent_path = self.resolve_path(&parent_path); let mut folders = Vec::new(); // Read directory entries let mut entries = fs::read_dir(abs_parent_path).await .map_err(FolderRepositoryError::IoError)?; while let Some(entry) = entries.next_entry().await .map_err(FolderRepositoryError::IoError)? { let metadata = entry.metadata().await .map_err(FolderRepositoryError::IoError)?; // Only include directories if metadata.is_dir() { let path = if parent_path.as_os_str().is_empty() { PathBuf::from(entry.file_name()) } else { parent_path.join(entry.file_name()) }; match self.get_folder_by_path(&path).await { Ok(folder) => folders.push(folder), Err(_) => continue, } } } Ok(folders) } async fn rename_folder(&self, id: &str, new_name: String) -> FolderRepositoryResult { let folder = self.get_folder_by_id(id).await?; tracing::debug!("Renombrando carpeta con ID: {}, Nombre: {}", id, folder.name); // Calculate new path let parent_path = folder.path.parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| PathBuf::from("")); let new_path = if parent_path.as_os_str().is_empty() { PathBuf::from(&new_name) } else { parent_path.join(&new_name) }; // Check if target already exists if self.folder_exists(&new_path).await? { return Err(FolderRepositoryError::AlreadyExists(new_path.to_string_lossy().to_string())); } // Rename the physical directory let abs_old_path = self.resolve_path(&folder.path); let abs_new_path = self.resolve_path(&new_path); fs::rename(&abs_old_path, &abs_new_path).await .map_err(FolderRepositoryError::IoError)?; // Actualizar el mapa de IDs - eliminar la entrada antigua y añadir la nueva let path_str = new_path.to_string_lossy().to_string(); { let mut map = self.id_map.lock().unwrap(); let old_path_str = folder.path.to_string_lossy().to_string(); map.path_to_id.remove(&old_path_str); map.path_to_id.insert(path_str.clone(), id.to_string()); } // Guardar el mapa actualizado self.save_id_map(); // Create and return updated folder entity let mut updated_folder = Folder::new( folder.id.clone(), new_name, new_path.clone(), folder.parent_id.clone(), ); updated_folder.created_at = folder.created_at; updated_folder.touch(); tracing::debug!("Carpeta renombrada exitosamente: ID={}, Nuevo nombre={}", id, updated_folder.name); Ok(updated_folder) } async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> FolderRepositoryResult { let folder = self.get_folder_by_id(id).await?; tracing::debug!("Moviendo carpeta con ID: {}, Nombre: {}", id, folder.name); // Get new parent path let new_parent_path = match new_parent_id { Some(parent_id) => { let parent = self.get_folder_by_id(parent_id).await?; parent.path }, None => PathBuf::from(""), }; // Calculate new path let new_path = if new_parent_path.as_os_str().is_empty() { PathBuf::from(&folder.name) } else { new_parent_path.join(&folder.name) }; // Check if target already exists if self.folder_exists(&new_path).await? { return Err(FolderRepositoryError::AlreadyExists(new_path.to_string_lossy().to_string())); } // Move the physical directory let abs_old_path = self.resolve_path(&folder.path); let abs_new_path = self.resolve_path(&new_path); fs::rename(&abs_old_path, &abs_new_path).await .map_err(FolderRepositoryError::IoError)?; // Actualizar el mapa de IDs - eliminar la entrada antigua y añadir la nueva let path_str = new_path.to_string_lossy().to_string(); { let mut map = self.id_map.lock().unwrap(); let old_path_str = folder.path.to_string_lossy().to_string(); map.path_to_id.remove(&old_path_str); map.path_to_id.insert(path_str.clone(), id.to_string()); } // Guardar el mapa actualizado self.save_id_map(); // Create and return updated folder entity let new_parent_id = if let Some(parent_id) = new_parent_id { Some(parent_id.to_string()) } else { None }; let mut updated_folder = Folder::new( folder.id.clone(), folder.name.clone(), new_path.clone(), new_parent_id, ); updated_folder.created_at = folder.created_at; updated_folder.touch(); tracing::debug!("Carpeta movida exitosamente: ID={}, Nueva ruta={:?}", id, new_path); Ok(updated_folder) } async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()> { let folder = self.get_folder_by_id(id).await?; tracing::debug!("Eliminando carpeta con ID: {}, Nombre: {}", id, folder.name); // Delete the physical directory let abs_path = self.resolve_path(&folder.path); fs::remove_dir_all(abs_path).await .map_err(FolderRepositoryError::IoError)?; // Actualizar el mapa de IDs - eliminar la entrada { let mut map = self.id_map.lock().unwrap(); let path_str = folder.path.to_string_lossy().to_string(); map.path_to_id.remove(&path_str); } // Guardar el mapa actualizado self.save_id_map(); tracing::debug!("Carpeta eliminada exitosamente: ID={}", id); Ok(()) } async fn folder_exists(&self, path: &PathBuf) -> FolderRepositoryResult { let abs_path = self.resolve_path(path); Ok(abs_path.exists() && abs_path.is_dir()) } }