Initial commit

This commit is contained in:
root
2025-03-17 21:28:08 +01:00
commit fe19bc8505
58 changed files with 7202 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
pub mod repositories;
pub mod services;
@@ -0,0 +1,723 @@
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use async_trait::async_trait;
use tokio::{fs, io::AsyncWriteExt};
use uuid::Uuid;
use mime_guess::from_path;
use serde::{Serialize, Deserialize};
use crate::domain::entities::file::File;
use crate::domain::repositories::file_repository::{
FileRepository, FileRepositoryError, FileRepositoryResult
};
use crate::domain::repositories::folder_repository::FolderRepository;
/// Structure to store file IDs mapped to their paths
#[derive(Serialize, Deserialize, Debug, Default)]
struct FileIdMap {
path_to_id: HashMap<String, String>,
}
/// Filesystem implementation of the FileRepository interface
pub struct FileFsRepository {
root_path: PathBuf,
folder_repository: Arc<dyn FolderRepository>,
id_map: Mutex<FileIdMap>,
}
impl FileFsRepository {
/// Creates a new filesystem-based file repository
pub fn new(root_path: PathBuf, folder_repository: Arc<dyn FolderRepository>) -> Self {
let id_map = Mutex::new(Self::load_id_map(&root_path));
Self { root_path, folder_repository, id_map }
}
/// Loads the file ID map from disk
fn load_id_map(root_path: &PathBuf) -> FileIdMap {
let map_path = root_path.join("file_ids.json");
if map_path.exists() {
match std::fs::read_to_string(&map_path) {
Ok(content) => {
match serde_json::from_str::<FileIdMap>(&content) {
Ok(map) => {
tracing::info!("Loaded file ID map with {} entries", map.path_to_id.len());
return map;
},
Err(e) => {
tracing::error!("Error parsing file ID map: {}", e);
}
}
},
Err(e) => {
tracing::error!("Error reading file ID map: {}", e);
}
}
}
// Return empty map if file doesn't exist or there was an error
FileIdMap::default()
}
/// Saves the file ID map to disk
fn save_id_map(&self) {
let map_path = self.root_path.join("file_ids.json");
let map = self.id_map.lock().unwrap();
match serde_json::to_string_pretty(&*map) {
Ok(json) => {
match std::fs::write(&map_path, json) {
Ok(_) => {
tracing::info!("Saved file ID map with {} entries", map.path_to_id.len());
},
Err(e) => {
tracing::error!("Error writing file ID map: {}", e);
}
}
},
Err(e) => {
tracing::error!("Error serializing file ID map: {}", e);
}
}
}
/// Generates a unique ID for a file
fn generate_id(&self) -> String {
Uuid::new_v4().to_string()
}
/// Gets ID for a file path or generates a new one
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();
// Check if we already have an ID for this path
if let Some(id) = map.path_to_id.get(&path_str) {
return id.clone();
}
// Generate a new ID
let id = self.generate_id();
map.path_to_id.insert(path_str, id.clone());
// No need to save immediately - we'll save when file operations complete
id
}
/// Resolves a relative path to an absolute path
fn resolve_path(&self, relative_path: &Path) -> PathBuf {
self.root_path.join(relative_path)
}
}
#[async_trait]
impl FileRepository for FileFsRepository {
async fn get_folder_by_id(&self, id: &str) -> FileRepositoryResult<crate::domain::entities::folder::Folder> {
match self.folder_repository.get_folder_by_id(id).await {
Ok(folder) => Ok(folder),
Err(e) => Err(FileRepositoryError::Other(format!("Folder not found: {}", e))),
}
}
async fn save_file_from_bytes(
&self,
name: String,
folder_id: Option<String>,
content_type: String,
content: Vec<u8>,
) -> FileRepositoryResult<File>
{
// Get the folder path
let folder_path = match &folder_id {
Some(id) => {
match self.folder_repository.get_folder_by_id(id).await {
Ok(folder) => {
tracing::info!("Using folder path: {:?} for folder_id: {:?}", folder.path, id);
folder.path
},
Err(e) => {
tracing::error!("Error getting folder: {}", e);
PathBuf::new()
},
}
},
None => PathBuf::new(),
};
// Create the file path
let file_path = if folder_path.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
folder_path.join(&name)
};
tracing::info!("Created file path: {:?}", file_path);
// Check if file already exists
let exists = self.file_exists(&file_path).await?;
tracing::info!("File exists check: {} for path: {:?}", exists, file_path);
if exists {
tracing::warn!("File already exists at path: {:?}", file_path);
return Err(FileRepositoryError::AlreadyExists(file_path.to_string_lossy().to_string()));
}
// Create parent directories if they don't exist
let abs_path = self.resolve_path(&file_path);
if let Some(parent) = abs_path.parent() {
fs::create_dir_all(parent).await
.map_err(FileRepositoryError::IoError)?;
}
// Write the file
let mut file = fs::File::create(&abs_path).await
.map_err(FileRepositoryError::IoError)?;
file.write_all(&content).await
.map_err(FileRepositoryError::IoError)?;
// Get file metadata
let metadata = fs::metadata(&abs_path).await
.map_err(FileRepositoryError::IoError)?;
// Determine the MIME type
let mime_type = if content_type.is_empty() {
from_path(&file_path)
.first_or_octet_stream()
.to_string()
} else {
content_type
};
// Create and return the file entity with a persistent ID
let id = self.get_or_create_id(&file_path);
let file = File::new(
id,
name,
file_path.clone(),
metadata.len(),
mime_type,
folder_id,
);
// Save the ID map
self.save_id_map();
tracing::info!("Saved file: {} with ID: {}", file_path.display(), file.id);
Ok(file)
}
async fn save_file_with_id(
&self,
id: String,
name: String,
folder_id: Option<String>,
content_type: String,
content: Vec<u8>,
) -> FileRepositoryResult<File>
{
// Get the folder path
let folder_path = match &folder_id {
Some(fid) => {
match self.folder_repository.get_folder_by_id(fid).await {
Ok(folder) => {
tracing::info!("Using folder path: {:?} for folder_id: {:?}", folder.path, fid);
folder.path
},
Err(e) => {
tracing::error!("Error getting folder: {}", e);
PathBuf::new()
},
}
},
None => PathBuf::new(),
};
// Create the file path
let file_path = if folder_path.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
folder_path.join(&name)
};
tracing::info!("Created file path with ID: {:?} for file: {}", file_path, id);
// Check if file already exists (different from the one we're moving)
let exists = self.file_exists(&file_path).await?;
tracing::info!("File exists check: {} for path: {:?}", exists, file_path);
// For save_file_with_id, we'll force overwrite if needed
if exists {
tracing::warn!("File already exists at path: {:?} - will overwrite", file_path);
// Delete the existing file
let abs_path = self.resolve_path(&file_path);
if let Err(e) = fs::remove_file(&abs_path).await {
tracing::error!("Failed to delete existing file: {} - {}", abs_path.display(), e);
return Err(FileRepositoryError::IoError(e));
}
}
// Create parent directories if they don't exist
let abs_path = self.resolve_path(&file_path);
if let Some(parent) = abs_path.parent() {
fs::create_dir_all(parent).await
.map_err(FileRepositoryError::IoError)?;
}
// Write the file
let mut file = fs::File::create(&abs_path).await
.map_err(FileRepositoryError::IoError)?;
file.write_all(&content).await
.map_err(FileRepositoryError::IoError)?;
// Get file metadata
let metadata = fs::metadata(&abs_path).await
.map_err(FileRepositoryError::IoError)?;
// Determine the MIME type
let mime_type = if content_type.is_empty() {
from_path(&file_path)
.first_or_octet_stream()
.to_string()
} else {
content_type
};
// Create the file entity with the provided ID
let file_entity = File::new(
id.clone(),
name,
file_path.clone(),
metadata.len(),
mime_type,
folder_id,
);
// Update the ID map
{
let mut map = self.id_map.lock().unwrap();
// First, remove any existing entries for this ID
let entries_to_remove: Vec<String> = map.path_to_id.iter()
.filter(|(_, v)| **v == id)
.map(|(k, _)| k.clone())
.collect();
for key in entries_to_remove {
tracing::info!("Removing old mapping: {} -> {}", key, id);
map.path_to_id.remove(&key);
}
// Then add the new entry
let path_str = file_path.to_string_lossy().to_string();
tracing::info!("Adding new mapping: {} -> {}", path_str, id);
map.path_to_id.insert(path_str, id);
}
// Save the ID map
self.save_id_map();
tracing::info!("Saved file with specific ID: {} at path: {}", file_entity.id, file_path.display());
Ok(file_entity)
}
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
// Find path by ID in the map
let path_str = {
let map = self.id_map.lock().unwrap();
match map.path_to_id.iter().find(|(_, v)| v == &id) {
Some((path, _)) => path.clone(),
None => {
tracing::error!("No file found with ID: {}", id);
return Err(FileRepositoryError::NotFound(id.to_string()));
}
}
};
// Convert path string to PathBuf
let file_path = PathBuf::from(path_str);
// Check if file exists
let abs_path = self.resolve_path(&file_path);
if !abs_path.exists() || !abs_path.is_file() {
tracing::error!("File not found at path: {}", file_path.display());
return Err(FileRepositoryError::NotFound(format!("File {} not found at {}", id, file_path.display())));
}
// Get file metadata
let metadata = fs::metadata(&abs_path).await
.map_err(|e| {
tracing::error!("Error getting metadata: {}", e);
FileRepositoryError::IoError(e)
})?;
// Get file name
let name = match file_path.file_name() {
Some(os_str) => os_str.to_string_lossy().to_string(),
None => {
tracing::error!("Invalid file path: {}", file_path.display());
return Err(FileRepositoryError::InvalidPath(file_path.to_string_lossy().to_string()));
}
};
// Determine parent folder ID
let parent_dir = file_path.parent().unwrap_or(Path::new(""));
let folder_id = if parent_dir.as_os_str().is_empty() {
None
} else {
let parent_path_buf = PathBuf::from(parent_dir);
match self.folder_repository.get_folder_by_path(&parent_path_buf).await {
Ok(folder) => Some(folder.id),
Err(_) => None,
}
};
// Determine MIME type
let mime_type = from_path(&file_path)
.first_or_octet_stream()
.to_string();
// Create file entity
let mut file = File::new(
id.to_string(),
name,
file_path,
metadata.len(),
mime_type,
folder_id,
);
// Set timestamps if available
if let Ok(created) = metadata.created() {
if let Ok(since_epoch) = created.duration_since(std::time::UNIX_EPOCH) {
file.created_at = since_epoch.as_secs();
}
}
if let Ok(modified) = metadata.modified() {
if let Ok(since_epoch) = modified.duration_since(std::time::UNIX_EPOCH) {
file.modified_at = since_epoch.as_secs();
}
}
Ok(file)
}
async fn list_files(&self, folder_id: Option<&str>) -> FileRepositoryResult<Vec<File>> {
let mut files = Vec::new();
tracing::info!("Listing files in folder_id: {:?}", folder_id);
// Get the folder path
let folder_path = match folder_id {
Some(id) => {
match self.folder_repository.get_folder_by_id(id).await {
Ok(folder) => {
tracing::info!("Found folder with path: {:?}", folder.path);
folder.path
},
Err(e) => {
tracing::error!("Error getting folder by ID: {}: {}", id, e);
return Ok(Vec::new());
},
}
},
None => PathBuf::new(),
};
// Get the absolute folder path
let abs_folder_path = self.resolve_path(&folder_path);
tracing::info!("Absolute folder path: {:?}", abs_folder_path);
// Ensure the directory exists
if !abs_folder_path.exists() || !abs_folder_path.is_dir() {
tracing::error!("Directory does not exist or is not a directory: {:?}", abs_folder_path);
return Ok(Vec::new());
}
tracing::info!("Directory exists, reading contents");
// Alternative approach - check files in the map that belong to this folder_id
let file_candidates: Vec<(String, String)> = {
let map = self.id_map.lock().unwrap();
tracing::info!("Checking map with {} entries for files in folder_id: {:?}", map.path_to_id.len(), folder_id);
// Filter by folder path prefix and collect filtered entries
let candidates = map.path_to_id.iter()
.filter(|(path_str, _)| {
let path = PathBuf::from(path_str);
// Check if this file belongs to the requested folder
match &folder_id {
Some(_) => {
// For specific folder, check if path starts with folder path
let parent_path = path.parent().unwrap_or_else(|| Path::new(""));
parent_path == folder_path
},
None => {
// For root folder, check if file is directly in root (no parent or parent is empty)
let parent = path.parent().unwrap_or_else(|| Path::new(""));
parent.as_os_str().is_empty() || parent == Path::new(".")
}
}
})
.map(|(path, id)| (path.clone(), id.clone()))
.collect();
candidates
};
// Process candidates after releasing the mutex lock
for (path_str, file_id) in file_candidates {
let path = PathBuf::from(&path_str);
tracing::info!("Found file in target folder: {} with ID: {}", path_str, file_id);
// Verify file exists physically
let abs_path = self.resolve_path(&path);
if !abs_path.exists() || !abs_path.is_file() {
tracing::warn!("File in map doesn't exist physically: {} (ID: {})", path_str, file_id);
continue;
}
// Get file info
match fs::metadata(&abs_path).await {
Ok(metadata) => {
let file_name = path.file_name()
.map(|os_str| os_str.to_string_lossy().to_string())
.unwrap_or_else(|| "unnamed".to_string());
// Determine MIME type
let mime_type = from_path(&path)
.first_or_octet_stream()
.to_string();
// Get timestamps
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);
let mut file = File::new(
file_id.clone(),
file_name.clone(),
path.clone(),
metadata.len(),
mime_type,
folder_id.map(String::from),
);
file.created_at = created_at;
file.modified_at = modified_at;
tracing::info!("Adding file to result list: {} (path: {:?})", file.name, path);
files.push(file);
},
Err(e) => {
tracing::warn!("Failed to get metadata for file: {} - {}", path_str, e);
}
}
}
if !files.is_empty() {
tracing::info!("Found {} files in folder {:?} from map", files.len(), folder_id);
return Ok(files);
}
// If we didn't find files in the map or the list is empty, fall back to directory scan
tracing::info!("Scanning directory for files: {:?}", abs_folder_path);
// Read directory entries
let mut entries = fs::read_dir(abs_folder_path).await
.map_err(FileRepositoryError::IoError)?;
while let Some(entry) = entries.next_entry().await
.map_err(FileRepositoryError::IoError)? {
let path = entry.path();
tracing::info!("Found entry: {:?}", path);
let metadata = entry.metadata().await
.map_err(FileRepositoryError::IoError)?;
// Only include files, not directories
if metadata.is_file() {
let file_name = entry.file_name().to_string_lossy().to_string();
tracing::info!("Found file: {}", file_name);
let file_path = if folder_path.as_os_str().is_empty() {
PathBuf::from(&file_name)
} else {
folder_path.join(&file_name)
};
tracing::info!("File path (relative to root): {:?}", file_path);
// Determine MIME type
let mime_type = from_path(&file_path)
.first_or_octet_stream()
.to_string();
// Get timestamps
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);
// Check if this file is already in our list (could happen with case-insensitive filesystems)
let duplicate = files.iter().any(|f: &File| f.name.to_lowercase() == file_name.to_lowercase());
if duplicate {
tracing::warn!("Skipping duplicate file with name: {} (case-insensitive match)", file_name);
continue;
}
// Create file entity with persistent ID
let id = self.get_or_create_id(&file_path);
tracing::info!("Using ID for file: {} for path: {:?}", id, file_path);
// Check if file is a PDF - just for debugging
if file_name.to_lowercase().ends_with(".pdf") {
tracing::info!("PDF file detected: {} with ID: {}", file_name, id);
}
let mut file = File::new(
id,
file_name,
file_path.clone(),
metadata.len(),
mime_type,
folder_id.map(String::from),
);
file.created_at = created_at;
file.modified_at = modified_at;
tracing::info!("Adding file to result list: {} (path: {:?})", file.name, file_path);
files.push(file);
} else {
tracing::info!("Skipping directory: {:?}", path);
}
}
tracing::info!("Found {} files in folder {:?}", files.len(), folder_id);
// Let's see what's in the map
{
let map = self.id_map.lock().unwrap();
tracing::info!("ID map has {} entries", map.path_to_id.len());
for (path, id) in &map.path_to_id {
tracing::info!("Map entry: {} -> {}", path, id);
}
}
Ok(files)
}
async fn delete_file(&self, id: &str) -> FileRepositoryResult<()> {
let file = self.get_file_by_id(id).await?;
// Delete the physical file
let abs_path = self.resolve_path(&file.path);
tracing::info!("Deleting physical file: {}", abs_path.display());
fs::remove_file(abs_path).await
.map_err(FileRepositoryError::IoError)?;
tracing::info!("Physical file deleted successfully: {}", file.path.display());
Ok(())
}
async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()> {
let file = self.get_file_by_id(id).await?;
// Delete the physical file
let abs_path = self.resolve_path(&file.path);
tracing::info!("Deleting physical file and entry for ID: {}", id);
// Try to delete the file, but continue even if it fails
let delete_result = fs::remove_file(&abs_path).await;
match &delete_result {
Ok(_) => tracing::info!("Physical file deleted successfully: {}", file.path.display()),
Err(e) => tracing::warn!("Failed to delete physical file: {} - {}", file.path.display(), e),
};
// Remove all entries for this ID from the map
{
let mut map = self.id_map.lock().unwrap();
// We don't need the path string anymore since we're finding all entries for this ID
// Find all paths that map to this ID
let paths_to_remove: Vec<String> = map.path_to_id.iter()
.filter(|(_, v)| **v == id)
.map(|(k, _)| k.clone())
.collect();
// Remove each path
for path in &paths_to_remove {
tracing::info!("Removing map entry: {} -> {}", path, id);
map.path_to_id.remove(path);
}
tracing::info!("Removed {} map entries for ID: {}", paths_to_remove.len(), id);
}
// Save the updated map
self.save_id_map();
// Return success if we deleted the file, otherwise propagate the error
if delete_result.is_ok() {
Ok(())
} else {
// Still return Ok - we've removed the entry from the map,
// and we want the operation to continue even if the file deletion failed
Ok(())
}
}
async fn get_file_content(&self, id: &str) -> FileRepositoryResult<Vec<u8>> {
let file = self.get_file_by_id(id).await?;
// Read the file content
let abs_path = self.resolve_path(&file.path);
let content = fs::read(abs_path).await
.map_err(FileRepositoryError::IoError)?;
Ok(content)
}
async fn file_exists(&self, path: &PathBuf) -> FileRepositoryResult<bool> {
let abs_path = self.resolve_path(path);
// Check if file exists and is a file (not a directory)
let exists = abs_path.exists() && abs_path.is_file();
tracing::info!("Checking if file exists: {} - path: {}", exists, abs_path.display());
// If it exists, try to get metadata to verify it's accessible
if exists {
match fs::metadata(&abs_path).await {
Ok(metadata) => {
if metadata.is_file() {
tracing::info!("File exists and is accessible: {}", abs_path.display());
return Ok(true);
} else {
tracing::warn!("Path exists but is not a file: {}", abs_path.display());
return Ok(false);
}
},
Err(e) => {
tracing::warn!("File exists but metadata check failed: {} - {}", abs_path.display(), e);
return Ok(false);
}
}
}
Ok(false)
}
}
@@ -0,0 +1,399 @@
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<String, String>,
}
/// Filesystem implementation of the FolderRepository interface
pub struct FolderFsRepository {
root_path: PathBuf,
id_map: Arc<Mutex<FolderIdMap>>,
}
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::<FolderIdMap>(&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<PathBuf>) -> FolderRepositoryResult<Folder> {
// 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<Folder> {
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<Folder> {
// 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<Vec<Folder>> {
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<Folder> {
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<Folder> {
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<bool> {
let abs_path = self.resolve_path(path);
Ok(abs_path.exists() && abs_path.is_dir())
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod file_fs_repository;
pub mod folder_fs_repository;
@@ -0,0 +1,141 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::RwLock;
use async_trait::async_trait;
use serde_json::Value;
use tokio::fs;
use crate::domain::services::i18n_service::{I18nService, I18nError, I18nResult, Locale};
/// File system implementation of the I18nService
pub struct FileSystemI18nService {
/// Base directory containing translation files
translations_dir: PathBuf,
/// Cached translations (locale code -> JSON data)
cache: RwLock<HashMap<Locale, Value>>,
}
impl FileSystemI18nService {
/// Creates a new file system i18n service
pub fn new(translations_dir: PathBuf) -> Self {
Self {
translations_dir,
cache: RwLock::new(HashMap::new()),
}
}
/// Get translation file path for a locale
fn get_locale_file_path(&self, locale: Locale) -> PathBuf {
self.translations_dir.join(format!("{}.json", locale.as_str()))
}
/// Get a nested key from JSON data
fn get_nested_value(&self, data: &Value, key: &str) -> Option<String> {
let parts: Vec<&str> = key.split('.').collect();
let mut current = data;
for part in &parts[0..parts.len() - 1] {
if let Some(next) = current.get(part) {
current = next;
} else {
return None;
}
}
if let Some(last_part) = parts.last() {
if let Some(value) = current.get(last_part) {
if value.is_string() {
return value.as_str().map(|s| s.to_string());
}
}
}
None
}
}
#[async_trait]
impl I18nService for FileSystemI18nService {
async fn translate(&self, key: &str, locale: Locale) -> I18nResult<String> {
// Check if translations are cached
{
let cache = self.cache.read().unwrap();
if let Some(translations) = cache.get(&locale) {
if let Some(value) = self.get_nested_value(translations, key) {
return Ok(value);
}
// Try to use English as fallback if we couldn't find the key
if locale != Locale::English {
if let Some(english_translations) = cache.get(&Locale::English) {
if let Some(value) = self.get_nested_value(english_translations, key) {
return Ok(value);
}
}
}
return Err(I18nError::KeyNotFound(key.to_string()));
}
}
// If not cached, load translations and try again
self.load_translations(locale).await?;
{
let cache = self.cache.read().unwrap();
if let Some(translations) = cache.get(&locale) {
if let Some(value) = self.get_nested_value(translations, key) {
return Ok(value);
}
// Try to use English as fallback
if locale != Locale::English {
if let Some(english_translations) = cache.get(&Locale::English) {
if let Some(value) = self.get_nested_value(english_translations, key) {
return Ok(value);
}
}
}
}
}
Err(I18nError::KeyNotFound(key.to_string()))
}
async fn load_translations(&self, locale: Locale) -> I18nResult<()> {
let file_path = self.get_locale_file_path(locale);
tracing::info!("Loading translations for locale {} from {:?}", locale.as_str(), file_path);
// Check if file exists
if !file_path.exists() {
return Err(I18nError::InvalidLocale(locale.as_str().to_string()));
}
// Read and parse file
let content = fs::read_to_string(&file_path)
.await
.map_err(|e| I18nError::LoadError(format!("Failed to read translation file: {}", e)))?;
let translations: Value = serde_json::from_str(&content)
.map_err(|e| I18nError::LoadError(format!("Failed to parse translation file: {}", e)))?;
// Update cache
{
let mut cache = self.cache.write().unwrap();
cache.insert(locale, translations);
}
tracing::info!("Translations loaded for locale {}", locale.as_str());
Ok(())
}
async fn available_locales(&self) -> Vec<Locale> {
vec![Locale::English, Locale::Spanish]
}
async fn is_supported(&self, locale: Locale) -> bool {
let file_path = self.get_locale_file_path(locale);
file_path.exists()
}
}
+1
View File
@@ -0,0 +1 @@
pub mod file_system_i18n_service;