adding several features
This commit is contained in:
+285
-20
@@ -1,67 +1,332 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Error en la creación o manipulación de entidades de archivo
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FileError {
|
||||
#[error("Nombre de archivo inválido: {0}")]
|
||||
InvalidFileName(String),
|
||||
|
||||
#[error("Error en la validación: {0}")]
|
||||
#[allow(dead_code)]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
/// Tipo de resultado para operaciones con entidades de archivo
|
||||
pub type FileResult<T> = Result<T, FileError>;
|
||||
|
||||
/// Represents a file entity in the domain
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct File {
|
||||
/// Unique identifier for the file
|
||||
pub id: String,
|
||||
id: String,
|
||||
|
||||
/// Name of the file
|
||||
pub name: String,
|
||||
name: String,
|
||||
|
||||
/// Path to the file (relative to user's root)
|
||||
pub path: PathBuf,
|
||||
/// Path to the file in the domain model
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
storage_path: StoragePath,
|
||||
|
||||
/// String representation of the path (for serialization compatibility)
|
||||
#[serde(rename = "path")]
|
||||
path_string: String,
|
||||
|
||||
/// Size of the file in bytes
|
||||
pub size: u64,
|
||||
size: u64,
|
||||
|
||||
/// MIME type of the file
|
||||
pub mime_type: String,
|
||||
mime_type: String,
|
||||
|
||||
/// Parent folder ID
|
||||
pub folder_id: Option<String>,
|
||||
folder_id: Option<String>,
|
||||
|
||||
/// Creation timestamp
|
||||
pub created_at: u64,
|
||||
created_at: u64,
|
||||
|
||||
/// Last modification timestamp
|
||||
pub modified_at: u64,
|
||||
modified_at: u64,
|
||||
}
|
||||
|
||||
// Ya no necesitamos este módulo, ahora usamos un String directamente
|
||||
|
||||
impl File {
|
||||
/// Creates a new file
|
||||
/// Crea un nuevo archivo con validación
|
||||
pub fn new(
|
||||
id: String,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
storage_path: StoragePath,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
) -> Self {
|
||||
) -> FileResult<Self> {
|
||||
// Validar nombre de archivo
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
||||
return Err(FileError::InvalidFileName(name));
|
||||
}
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// Almacenamos el string de la ruta para compatibilidad con serialización
|
||||
let path_string = storage_path.to_string();
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
storage_path,
|
||||
path_string,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea un archivo con timestamps específicos (para reconstrucción)
|
||||
pub fn with_timestamps(
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
) -> FileResult<Self> {
|
||||
// Validar nombre de archivo
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
||||
return Err(FileError::InvalidFileName(name));
|
||||
}
|
||||
|
||||
// Almacenamos el string de la ruta para compatibilidad con serialización
|
||||
let path_string = storage_path.to_string();
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
path_string,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn storage_path(&self) -> &StoragePath {
|
||||
&self.storage_path
|
||||
}
|
||||
|
||||
pub fn path_string(&self) -> &str {
|
||||
&self.path_string
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn mime_type(&self) -> &str {
|
||||
&self.mime_type
|
||||
}
|
||||
|
||||
pub fn folder_id(&self) -> Option<&str> {
|
||||
self.folder_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> u64 {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
pub fn modified_at(&self) -> u64 {
|
||||
self.modified_at
|
||||
}
|
||||
|
||||
/// Crea una nueva instancia de File desde un DTO
|
||||
/// Esta función es principalmente para conversiones en los batch handlers
|
||||
pub fn from_dto(
|
||||
id: String,
|
||||
name: String,
|
||||
path: String,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
) -> Self {
|
||||
// Crear storage_path desde el string
|
||||
let storage_path = StoragePath::from_string(&path);
|
||||
|
||||
// Crear directamente sin validación para evitar errores en conversiones DTO
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
path_string: path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates file modification time
|
||||
// Métodos para crear nuevas versiones del archivo (inmutable)
|
||||
|
||||
/// Crea una nueva versión del archivo con nombre actualizado
|
||||
#[allow(dead_code)]
|
||||
pub fn touch(&mut self) {
|
||||
self.modified_at = std::time::SystemTime::now()
|
||||
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
|
||||
// Validar nombre de archivo
|
||||
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
|
||||
return Err(FileError::InvalidFileName(new_name));
|
||||
}
|
||||
|
||||
// Actualizar ruta basada en el nombre
|
||||
let parent_path = self.storage_path.parent();
|
||||
let new_storage_path = match parent_path {
|
||||
Some(parent) => parent.join(&new_name),
|
||||
None => StoragePath::from_string(&new_name),
|
||||
};
|
||||
|
||||
// Actualizar representación en string
|
||||
let new_path_string = new_storage_path.to_string();
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Ok(Self {
|
||||
id: self.id.clone(),
|
||||
name: new_name,
|
||||
storage_path: new_storage_path,
|
||||
path_string: new_path_string,
|
||||
size: self.size,
|
||||
mime_type: self.mime_type.clone(),
|
||||
folder_id: self.folder_id.clone(),
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea una nueva versión del archivo con carpeta actualizada
|
||||
pub fn with_folder(&self, folder_id: Option<String>, folder_path: Option<StoragePath>) -> FileResult<Self> {
|
||||
// Necesitamos una ruta de carpeta para actualizar la ruta del archivo
|
||||
let new_storage_path = match folder_path {
|
||||
Some(path) => path.join(&self.name),
|
||||
None => StoragePath::from_string(&self.name), // Raíz
|
||||
};
|
||||
|
||||
// Actualizar representación en string
|
||||
let new_path_string = new_storage_path.to_string();
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Ok(Self {
|
||||
id: self.id.clone(),
|
||||
name: self.name.clone(),
|
||||
storage_path: new_storage_path,
|
||||
path_string: new_path_string,
|
||||
size: self.size,
|
||||
mime_type: self.mime_type.clone(),
|
||||
folder_id,
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea una nueva versión del archivo con tamaño actualizado
|
||||
#[allow(dead_code)]
|
||||
pub fn with_size(&self, new_size: u64) -> Self {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
name: self.name.clone(),
|
||||
storage_path: self.storage_path.clone(),
|
||||
path_string: self.path_string.clone(),
|
||||
size: new_size,
|
||||
mime_type: self.mime_type.clone(),
|
||||
folder_id: self.folder_id.clone(),
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_file_creation_with_valid_name() {
|
||||
let storage_path = StoragePath::from_string("/test/file.txt");
|
||||
let file = File::new(
|
||||
"123".to_string(),
|
||||
"file.txt".to_string(),
|
||||
storage_path,
|
||||
100,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(file.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_creation_with_invalid_name() {
|
||||
let storage_path = StoragePath::from_string("/test/invalid/file.txt");
|
||||
let file = File::new(
|
||||
"123".to_string(),
|
||||
"file/with/slash.txt".to_string(), // Nombre inválido
|
||||
storage_path,
|
||||
100,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(file.is_err());
|
||||
match file {
|
||||
Err(FileError::InvalidFileName(_)) => (),
|
||||
_ => panic!("Expected InvalidFileName error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_with_name() {
|
||||
let storage_path = StoragePath::from_string("/test/file.txt");
|
||||
let file = File::new(
|
||||
"123".to_string(),
|
||||
"file.txt".to_string(),
|
||||
storage_path,
|
||||
100,
|
||||
"text/plain".to_string(),
|
||||
None,
|
||||
).unwrap();
|
||||
|
||||
let renamed = file.with_name("newname.txt".to_string());
|
||||
assert!(renamed.is_ok());
|
||||
let renamed = renamed.unwrap();
|
||||
assert_eq!(renamed.name(), "newname.txt");
|
||||
assert_eq!(renamed.id(), "123"); // El ID no cambia
|
||||
}
|
||||
}
|
||||
+257
-21
@@ -1,57 +1,293 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Error en la creación o manipulación de entidades de carpeta
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FolderError {
|
||||
#[error("Nombre de carpeta inválido: {0}")]
|
||||
InvalidFolderName(String),
|
||||
|
||||
#[error("Error en la validación: {0}")]
|
||||
#[allow(dead_code)]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
/// Tipo de resultado para operaciones con entidades de carpeta
|
||||
pub type FolderResult<T> = Result<T, FolderError>;
|
||||
|
||||
/// Represents a folder entity in the domain
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Folder {
|
||||
/// Unique identifier for the folder
|
||||
pub id: String,
|
||||
id: String,
|
||||
|
||||
/// Name of the folder
|
||||
pub name: String,
|
||||
name: String,
|
||||
|
||||
/// Path to the folder (relative to user's root)
|
||||
pub path: PathBuf,
|
||||
/// Path to the folder in the domain model
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
storage_path: StoragePath,
|
||||
|
||||
/// String representation of the path (for serialization compatibility)
|
||||
#[serde(rename = "path")]
|
||||
path_string: String,
|
||||
|
||||
/// Parent folder ID (None if it's a root folder)
|
||||
pub parent_id: Option<String>,
|
||||
parent_id: Option<String>,
|
||||
|
||||
/// Creation timestamp
|
||||
pub created_at: u64,
|
||||
created_at: u64,
|
||||
|
||||
/// Last modification timestamp
|
||||
pub modified_at: u64,
|
||||
modified_at: u64,
|
||||
}
|
||||
|
||||
// Ya no necesitamos este módulo, ahora usamos un String directamente
|
||||
|
||||
impl Folder {
|
||||
/// Creates a new folder
|
||||
pub fn new(id: String, name: String, path: PathBuf, parent_id: Option<String>) -> Self {
|
||||
/// Creates a new folder with validation
|
||||
pub fn new(
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
parent_id: Option<String>,
|
||||
) -> FolderResult<Self> {
|
||||
// Validar nombre de carpeta
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
||||
return Err(FolderError::InvalidFolderName(name));
|
||||
}
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// Almacenamos el string de la ruta para compatibilidad con serialización
|
||||
let path_string = storage_path.to_string();
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
storage_path,
|
||||
path_string,
|
||||
parent_id,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a folder with specific timestamps (for reconstruction)
|
||||
pub fn with_timestamps(
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
parent_id: Option<String>,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
) -> FolderResult<Self> {
|
||||
// Validar nombre de carpeta
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
||||
return Err(FolderError::InvalidFolderName(name));
|
||||
}
|
||||
|
||||
// Almacenamos el string de la ruta para compatibilidad con serialización
|
||||
let path_string = storage_path.to_string();
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
path_string,
|
||||
parent_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn storage_path(&self) -> &StoragePath {
|
||||
&self.storage_path
|
||||
}
|
||||
|
||||
pub fn path_string(&self) -> &str {
|
||||
&self.path_string
|
||||
}
|
||||
|
||||
pub fn parent_id(&self) -> Option<&str> {
|
||||
self.parent_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> u64 {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
pub fn modified_at(&self) -> u64 {
|
||||
self.modified_at
|
||||
}
|
||||
|
||||
/// Crea una nueva instancia de Folder desde un DTO
|
||||
/// Esta función es principalmente para conversiones en los batch handlers
|
||||
pub fn from_dto(
|
||||
id: String,
|
||||
name: String,
|
||||
path: String,
|
||||
parent_id: Option<String>,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
) -> Self {
|
||||
// Crear storage_path desde el string
|
||||
let storage_path = StoragePath::from_string(&path);
|
||||
|
||||
// Crear directamente sin validación para evitar errores en conversiones DTO
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
path_string: path,
|
||||
parent_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the absolute path of the folder
|
||||
#[allow(dead_code)]
|
||||
pub fn get_absolute_path(&self, root_path: &PathBuf) -> PathBuf {
|
||||
root_path.join(&self.path)
|
||||
// Métodos para crear nuevas versiones de la carpeta (inmutable)
|
||||
|
||||
/// Creates a new version of the folder with updated name
|
||||
pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
|
||||
// Validar nombre de carpeta
|
||||
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
|
||||
return Err(FolderError::InvalidFolderName(new_name));
|
||||
}
|
||||
|
||||
// Actualizar ruta basada en el nombre
|
||||
let parent_path = self.storage_path.parent();
|
||||
let new_storage_path = match parent_path {
|
||||
Some(parent) => parent.join(&new_name),
|
||||
None => StoragePath::from_string(&new_name),
|
||||
};
|
||||
|
||||
// Actualizar representación en string
|
||||
let new_path_string = new_storage_path.to_string();
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Ok(Self {
|
||||
id: self.id.clone(),
|
||||
name: new_name,
|
||||
storage_path: new_storage_path,
|
||||
path_string: new_path_string,
|
||||
parent_id: self.parent_id.clone(),
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates folder modification time
|
||||
pub fn touch(&mut self) {
|
||||
self.modified_at = std::time::SystemTime::now()
|
||||
/// Creates a new version of the folder with updated parent
|
||||
pub fn with_parent(&self, parent_id: Option<String>, parent_path: Option<StoragePath>) -> FolderResult<Self> {
|
||||
// Necesitamos una ruta de carpeta para actualizar la ruta
|
||||
let new_storage_path = match parent_path {
|
||||
Some(path) => path.join(&self.name),
|
||||
None => StoragePath::from_string(&self.name), // Raíz
|
||||
};
|
||||
|
||||
// Actualizar representación en string
|
||||
let new_path_string = new_storage_path.to_string();
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Ok(Self {
|
||||
id: self.id.clone(),
|
||||
name: self.name.clone(),
|
||||
storage_path: new_storage_path,
|
||||
path_string: new_path_string,
|
||||
parent_id,
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an absolute path for this folder
|
||||
#[allow(dead_code)]
|
||||
pub fn get_absolute_path<P: AsRef<std::path::Path>>(&self, root_path: P) -> std::path::PathBuf {
|
||||
let mut result = std::path::PathBuf::from(root_path.as_ref());
|
||||
|
||||
// Skip leading '/' from path_string to avoid creating absolute path incorrectly
|
||||
let relative_path = if self.path_string.starts_with('/') {
|
||||
&self.path_string[1..]
|
||||
} else {
|
||||
&self.path_string
|
||||
};
|
||||
|
||||
if !relative_path.is_empty() {
|
||||
result.push(relative_path);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_folder_creation_with_valid_name() {
|
||||
let storage_path = StoragePath::from_string("/test/folder");
|
||||
let folder = Folder::new(
|
||||
"123".to_string(),
|
||||
"my_folder".to_string(),
|
||||
storage_path,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(folder.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder_creation_with_invalid_name() {
|
||||
let storage_path = StoragePath::from_string("/test/invalid/folder");
|
||||
let folder = Folder::new(
|
||||
"123".to_string(),
|
||||
"folder/with/slash".to_string(), // Nombre inválido
|
||||
storage_path,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(folder.is_err());
|
||||
match folder {
|
||||
Err(FolderError::InvalidFolderName(_)) => (),
|
||||
_ => panic!("Expected InvalidFolderName error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folder_with_name() {
|
||||
let storage_path = StoragePath::from_string("/test/folder");
|
||||
let folder = Folder::new(
|
||||
"123".to_string(),
|
||||
"old_name".to_string(),
|
||||
storage_path,
|
||||
None,
|
||||
).unwrap();
|
||||
|
||||
let renamed = folder.with_name("new_name".to_string());
|
||||
assert!(renamed.is_ok());
|
||||
let renamed = renamed.unwrap();
|
||||
assert_eq!(renamed.name(), "new_name");
|
||||
assert_eq!(renamed.id(), "123"); // El ID no cambia
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use futures::Stream;
|
||||
use bytes::Bytes;
|
||||
|
||||
/// Error types for file repository operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -18,6 +20,12 @@ pub enum FileRepositoryError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Mapping error: {0}")]
|
||||
MappingError(String),
|
||||
|
||||
#[error("Timeout error: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
@@ -26,11 +34,10 @@ pub enum FileRepositoryError {
|
||||
pub type FileRepositoryResult<T> = Result<T, FileRepositoryError>;
|
||||
|
||||
/// Repository interface for file operations (primary port)
|
||||
/// Esta interfaz define las operaciones de negocio relacionadas con archivos
|
||||
/// sin exponer detalles de implementación como rutas o sistemas de archivos
|
||||
#[async_trait]
|
||||
pub trait FileRepository: Send + Sync + 'static {
|
||||
/// Gets a folder by its ID - helper method for file repository to work with folders
|
||||
#[allow(dead_code)]
|
||||
async fn get_folder_by_id(&self, id: &str) -> FileRepositoryResult<crate::domain::entities::folder::Folder>;
|
||||
/// Saves a file from bytes
|
||||
async fn save_file_from_bytes(
|
||||
&self,
|
||||
@@ -60,13 +67,20 @@ pub trait FileRepository: Send + Sync + 'static {
|
||||
/// Deletes a file
|
||||
async fn delete_file(&self, id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/// Deletes a file and its entry from the map
|
||||
/// Deletes a file and its entry from mapping systems
|
||||
#[allow(dead_code)]
|
||||
async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/// Gets file content as bytes
|
||||
/// Gets file content as bytes - use only for small files
|
||||
async fn get_file_content(&self, id: &str) -> FileRepositoryResult<Vec<u8>>;
|
||||
|
||||
/// Checks if a file exists at the given path
|
||||
async fn file_exists(&self, path: &PathBuf) -> FileRepositoryResult<bool>;
|
||||
/// Gets file content as a stream - better for large files
|
||||
#[allow(clippy::type_complexity)]
|
||||
async fn get_file_stream(&self, id: &str) -> FileRepositoryResult<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>;
|
||||
|
||||
/// Moves a file to a different folder
|
||||
async fn move_file(&self, id: &str, target_folder_id: Option<String>) -> FileRepositoryResult<File>;
|
||||
|
||||
/// Gets the storage path for a file
|
||||
async fn get_file_path(&self, id: &str) -> FileRepositoryResult<StoragePath>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Error types for folder repository operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -18,6 +18,12 @@ pub enum FolderRepositoryError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Mapping error: {0}")]
|
||||
MappingError(String),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
@@ -29,17 +35,31 @@ pub type FolderRepositoryResult<T> = Result<T, FolderRepositoryError>;
|
||||
#[async_trait]
|
||||
pub trait FolderRepository: Send + Sync + 'static {
|
||||
/// Creates a new folder
|
||||
async fn create_folder(&self, name: String, parent_path: Option<PathBuf>) -> FolderRepositoryResult<Folder>;
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> FolderRepositoryResult<Folder>;
|
||||
|
||||
/// Gets a folder by its ID
|
||||
async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult<Folder>;
|
||||
|
||||
/// Gets a folder by its path
|
||||
async fn get_folder_by_path(&self, path: &PathBuf) -> FolderRepositoryResult<Folder>;
|
||||
async fn get_folder_by_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<Folder>;
|
||||
|
||||
/// Lists folders in a parent folder
|
||||
/// Lists all folders in a parent folder (use with caution for large directories)
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult<Vec<Folder>>;
|
||||
|
||||
/// Lists folders in a parent folder with pagination support
|
||||
///
|
||||
/// * `parent_id` - Optional parent folder ID
|
||||
/// * `offset` - Number of folders to skip
|
||||
/// * `limit` - Maximum number of folders to return
|
||||
/// * `include_total` - If true, returns the total count of folders as well
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
) -> FolderRepositoryResult<(Vec<Folder>, Option<usize>)>;
|
||||
|
||||
/// Renames a folder
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> FolderRepositoryResult<Folder>;
|
||||
|
||||
@@ -50,5 +70,18 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()>;
|
||||
|
||||
/// Checks if a folder exists at the given path
|
||||
async fn folder_exists(&self, path: &PathBuf) -> FolderRepositoryResult<bool>;
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<bool>;
|
||||
|
||||
/// Gets the storage path for a folder
|
||||
async fn get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult<StoragePath>;
|
||||
|
||||
/// Legacy method - checks if a folder exists at the given PathBuf path
|
||||
#[deprecated(note = "Use folder_exists_at_storage_path instead")]
|
||||
#[allow(dead_code)]
|
||||
async fn folder_exists(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<bool>;
|
||||
|
||||
/// Legacy method - gets a folder by its PathBuf path
|
||||
#[deprecated(note = "Use get_folder_by_storage_path instead")]
|
||||
#[allow(dead_code)]
|
||||
async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<Folder>;
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod i18n_service;
|
||||
pub mod i18n_service;
|
||||
pub mod path_service;
|
||||
@@ -0,0 +1,381 @@
|
||||
/// Abstracto servicio de dominio para rutas, sin dependencias de sistema de archivos
|
||||
/// Representa una ruta de almacenamiento en el dominio
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct StoragePath {
|
||||
segments: Vec<String>,
|
||||
}
|
||||
|
||||
impl StoragePath {
|
||||
/// Crea una nueva ruta de almacenamiento
|
||||
#[allow(dead_code)]
|
||||
pub fn new(segments: Vec<String>) -> Self {
|
||||
Self { segments }
|
||||
}
|
||||
|
||||
/// Crea una ruta vacía (raíz)
|
||||
pub fn root() -> Self {
|
||||
Self { segments: Vec::new() }
|
||||
}
|
||||
|
||||
/// Crea una ruta a partir de una cadena con segmentos separados por /
|
||||
pub fn from_string(path: &str) -> Self {
|
||||
let segments = path
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
Self { segments }
|
||||
}
|
||||
|
||||
/// Crea una ruta a partir de un PathBuf
|
||||
pub fn from(path_buf: PathBuf) -> Self {
|
||||
let segments = path_buf
|
||||
.components()
|
||||
.filter_map(|c| match c {
|
||||
std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
Self { segments }
|
||||
}
|
||||
|
||||
/// Añade un segmento a la ruta
|
||||
pub fn join(&self, segment: &str) -> Self {
|
||||
let mut new_segments = self.segments.clone();
|
||||
new_segments.push(segment.to_string());
|
||||
Self { segments: new_segments }
|
||||
}
|
||||
|
||||
/// Obtiene el nombre del archivo (último segmento)
|
||||
pub fn file_name(&self) -> Option<String> {
|
||||
self.segments.last().cloned()
|
||||
}
|
||||
|
||||
/// Obtiene la ruta del directorio padre
|
||||
pub fn parent(&self) -> Option<Self> {
|
||||
if self.segments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let parent_segments = self.segments[..self.segments.len() - 1].to_vec();
|
||||
Some(Self { segments: parent_segments })
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si la ruta está vacía (es la raíz)
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.segments.is_empty()
|
||||
}
|
||||
|
||||
/// Convierte la ruta a una cadena con formato "/segment1/segment2/..."
|
||||
pub fn to_string(&self) -> String {
|
||||
if self.segments.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
format!("/{}", self.segments.join("/"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene los segmentos de la ruta
|
||||
pub fn segments(&self) -> &[String] {
|
||||
&self.segments
|
||||
}
|
||||
}
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::application::ports::outbound::StoragePort;
|
||||
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorResult, StorageMediatorError};
|
||||
use crate::domain::entities::folder::Folder;
|
||||
|
||||
/// Servicio de dominio para manejar operaciones con rutas de almacenamiento
|
||||
pub struct PathService {
|
||||
root_path: PathBuf, // Necesario para la implementación
|
||||
}
|
||||
|
||||
impl PathService {
|
||||
/// Crea un nuevo servicio de rutas con una raíz específica
|
||||
pub fn new(root_path: PathBuf) -> Self {
|
||||
Self { root_path }
|
||||
}
|
||||
|
||||
/// Convierte una ruta del dominio a una ruta física absoluta
|
||||
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Convierte una ruta física a una ruta de dominio
|
||||
#[allow(dead_code)]
|
||||
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
|
||||
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
|
||||
let segments = rel_path
|
||||
.components()
|
||||
.filter_map(|c| match c {
|
||||
std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
StoragePath { segments }
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea una ruta de archivo dentro de una carpeta
|
||||
#[allow(dead_code)]
|
||||
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
|
||||
folder_path.join(file_name)
|
||||
}
|
||||
|
||||
/// Verifica si una ruta es directamente hija de otra
|
||||
#[allow(dead_code)]
|
||||
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
|
||||
if let Some(child_parent) = potential_child.parent() {
|
||||
&child_parent == parent_path
|
||||
} else {
|
||||
parent_path.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si una ruta está en la raíz
|
||||
#[allow(dead_code)]
|
||||
pub fn is_in_root(&self, path: &StoragePath) -> bool {
|
||||
path.parent().map_or(true, |p| p.is_empty())
|
||||
}
|
||||
|
||||
/// Gets the root path used by this service
|
||||
#[allow(dead_code)]
|
||||
pub fn get_root_path(&self) -> &Path {
|
||||
&self.root_path
|
||||
}
|
||||
|
||||
/// Valida una ruta para asegurar que no contiene componentes peligrosos
|
||||
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Verificar que no haya segmentos vacíos
|
||||
if path.segments().iter().any(|s| s.is_empty()) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains empty segments: {}", path.to_string())
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no haya caracteres peligrosos
|
||||
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
|
||||
for segment in path.segments() {
|
||||
if segment.contains(&dangerous_chars[..]) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains dangerous characters: {}", segment)
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no empiece con . (oculto en Unix)
|
||||
if segment.starts_with('.') && segment != ".well-known" {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path segments cannot start with dot: {}", segment)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StoragePort for PathService {
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Primero validar la ruta
|
||||
self.validate_path(storage_path)?;
|
||||
|
||||
// Resolver a ruta física
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
// Crear directorios si no existen
|
||||
if !physical_path.exists() {
|
||||
fs::create_dir_all(&physical_path).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Storage",
|
||||
format!("Failed to create directory: {}", physical_path.display())
|
||||
).with_source(e))?;
|
||||
|
||||
tracing::debug!("Created directory: {}", physical_path.display());
|
||||
} else if !physical_path.is_dir() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Storage",
|
||||
format!("Path exists but is not a directory: {}", physical_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_file();
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_dir();
|
||||
Ok(exists)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for PathService {
|
||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> {
|
||||
// This is a simplified implementation since PathService doesn't have direct
|
||||
// access to folder repository. It's typically used through a proxy.
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf {
|
||||
// Convert path to storage path then resolve
|
||||
let path_str = relative_path.to_string_lossy().to_string();
|
||||
let storage_path = StoragePath::from_string(&path_str);
|
||||
self.resolve_path(&storage_path)
|
||||
}
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let storage_path = StoragePath::from_string("test/file.txt");
|
||||
let absolute = service.resolve_path(&storage_path);
|
||||
|
||||
assert_eq!(absolute, PathBuf::from("/storage/test/file.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_storage_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let physical_path = PathBuf::from("/storage/folder/file.txt");
|
||||
let storage_path = service.to_storage_path(&physical_path).unwrap();
|
||||
|
||||
assert_eq!(storage_path.to_string(), "/folder/file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_in_root() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let root_path = StoragePath::from_string("file.txt");
|
||||
let nested_path = StoragePath::from_string("folder/file.txt");
|
||||
|
||||
assert!(service.is_in_root(&root_path));
|
||||
assert!(!service.is_in_root(&nested_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_direct_child() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let parent = StoragePath::from_string("folder");
|
||||
let child = StoragePath::from_string("folder/file.txt");
|
||||
let not_child = StoragePath::from_string("folder2/file.txt");
|
||||
|
||||
assert!(service.is_direct_child(&parent, &child));
|
||||
assert!(!service.is_direct_child(&parent, ¬_child));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_file_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let folder_path = StoragePath::from_string("folder");
|
||||
let file_path = service.create_file_path(&folder_path, "file.txt");
|
||||
|
||||
assert_eq!(file_path.to_string(), "/folder/file.txt");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user