refactor: remove serde from domain entities for Clean Architecture compliance
- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities - Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer - Update contact_pg_repository to use persistence DTOs - Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0) - Fix unused variable warnings in main.rs - Move PathService import from domain to infrastructure - Add missing fields to CoreServices and RepositoryServices - Create proper service initialization in main.rs Clean Architecture improvements: - Domain layer no longer depends on serde framework - Persistence concerns isolated to infrastructure layer - TokenClaims in auth_service.rs is only exception (required for JWT)
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressBook {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
@@ -29,21 +28,21 @@ impl Default for AddressBook {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Email {
|
||||
pub email: String,
|
||||
pub r#type: String, // home, work, other
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Phone {
|
||||
pub number: String,
|
||||
pub r#type: String, // mobile, home, work, fax, other
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Address {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
@@ -54,7 +53,7 @@ pub struct Address {
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Contact {
|
||||
pub id: Uuid,
|
||||
pub address_book_id: Uuid,
|
||||
@@ -105,7 +104,7 @@ impl Default for Contact {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactGroup {
|
||||
pub id: Uuid,
|
||||
pub address_book_id: Uuid,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/**
|
||||
@@ -15,7 +14,6 @@ pub enum FileError {
|
||||
|
||||
/// Occurs when validation fails for any file entity attribute.
|
||||
#[error("Validation error: {0}")]
|
||||
#[allow(dead_code)]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
@@ -36,7 +34,7 @@ pub type FileResult<T> = Result<T, FileError>;
|
||||
* This entity maintains both physical storage information and logical metadata about files,
|
||||
* serving as the bridge between the storage system and the application.
|
||||
*/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct File {
|
||||
/// Unique identifier for the file - used throughout the system for file operations
|
||||
id: String,
|
||||
@@ -44,12 +42,10 @@ pub struct File {
|
||||
/// Name of the file including extension
|
||||
name: String,
|
||||
|
||||
/// Path to the file in the domain model - not serialized as it contains internal representation
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
/// Path to the file in the domain model
|
||||
storage_path: StoragePath,
|
||||
|
||||
/// String representation of the path for serialization and API compatibility
|
||||
#[serde(rename = "path")]
|
||||
/// String representation of the path for API compatibility
|
||||
path_string: String,
|
||||
|
||||
/// Size of the file in bytes
|
||||
@@ -253,7 +249,6 @@ impl File {
|
||||
// Methods to create new versions of the file (immutable)
|
||||
|
||||
/// Creates a new version of the file with updated name
|
||||
#[allow(dead_code)]
|
||||
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
|
||||
// Validate file name
|
||||
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
|
||||
@@ -318,7 +313,6 @@ impl File {
|
||||
}
|
||||
|
||||
/// Creates a new version of the file with updated size
|
||||
#[allow(dead_code)]
|
||||
pub fn with_size(&self, new_size: u64) -> Self {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Error in the creation or manipulation of folder entities
|
||||
@@ -8,7 +7,6 @@ pub enum FolderError {
|
||||
InvalidFolderName(String),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
#[allow(dead_code)]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
@@ -16,7 +14,7 @@ pub enum FolderError {
|
||||
pub type FolderResult<T> = Result<T, FolderError>;
|
||||
|
||||
/// Represents a folder entity in the domain
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Folder {
|
||||
/// Unique identifier for the folder
|
||||
id: String,
|
||||
@@ -25,11 +23,9 @@ pub struct Folder {
|
||||
name: String,
|
||||
|
||||
/// 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")]
|
||||
/// String representation of the path (for API compatibility)
|
||||
path_string: String,
|
||||
|
||||
/// Parent folder ID (None if it's a root folder)
|
||||
@@ -235,7 +231,6 @@ impl Folder {
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
|
||||
+23
-40
@@ -1,7 +1,3 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use argon2::password_hash::SaltString;
|
||||
use rand_core::OsRng;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
@@ -22,7 +18,7 @@ pub enum UserError {
|
||||
|
||||
pub type UserResult<T> = Result<T, UserError>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
// We'll handle conversion manually for now until the type is properly set up in the database
|
||||
pub enum UserRole {
|
||||
Admin,
|
||||
@@ -38,12 +34,11 @@ impl std::fmt::Display for UserRole {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
id: String,
|
||||
username: String,
|
||||
email: String,
|
||||
#[serde(skip_serializing)]
|
||||
password_hash: String,
|
||||
role: UserRole,
|
||||
storage_quota_bytes: i64,
|
||||
@@ -55,10 +50,22 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Create a new user with a pre-hashed password.
|
||||
///
|
||||
/// The password hashing should be done externally using PasswordHasherPort
|
||||
/// to maintain clean architecture and keep cryptographic dependencies
|
||||
/// out of the domain layer.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `username` - User's username (3-32 characters)
|
||||
/// * `email` - User's email address
|
||||
/// * `password_hash` - Pre-hashed password (from PasswordHasherPort)
|
||||
/// * `role` - User's role
|
||||
/// * `storage_quota_bytes` - Storage quota in bytes
|
||||
pub fn new(
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
password_hash: String,
|
||||
role: UserRole,
|
||||
storage_quota_bytes: i64,
|
||||
) -> UserResult<Self> {
|
||||
@@ -75,19 +82,12 @@ impl User {
|
||||
)));
|
||||
}
|
||||
|
||||
if password.len() < 8 {
|
||||
if password_hash.is_empty() {
|
||||
return Err(UserError::InvalidPassword(format!(
|
||||
"Password debe tener al menos 8 caracteres"
|
||||
"Password hash no puede estar vacío"
|
||||
)));
|
||||
}
|
||||
|
||||
// Generar hash con Argon2id (recomendado para 2023+)
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2.hash_password(password.as_bytes(), &salt)
|
||||
.map_err(|e| UserError::ValidationError(format!("Error al generar hash: {}", e)))?
|
||||
.to_string();
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
Ok(Self {
|
||||
@@ -179,30 +179,13 @@ impl User {
|
||||
&self.password_hash
|
||||
}
|
||||
|
||||
// Verificación de password
|
||||
pub fn verify_password(&self, password: &str) -> UserResult<bool> {
|
||||
let parsed_hash = PasswordHash::new(&self.password_hash)
|
||||
.map_err(|e| UserError::AuthenticationError(format!("Error al procesar hash: {}", e)))?;
|
||||
|
||||
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
|
||||
}
|
||||
|
||||
// Cambiar contraseña
|
||||
pub fn update_password(&mut self, new_password: String) -> UserResult<()> {
|
||||
if new_password.len() < 8 {
|
||||
return Err(UserError::InvalidPassword(format!(
|
||||
"Password debe tener al menos 8 caracteres"
|
||||
)));
|
||||
}
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
self.password_hash = argon2.hash_password(new_password.as_bytes(), &salt)
|
||||
.map_err(|e| UserError::ValidationError(format!("Error al generar hash: {}", e)))?
|
||||
.to_string();
|
||||
|
||||
/// Update the password hash.
|
||||
///
|
||||
/// The new password should be hashed externally using PasswordHasherPort
|
||||
/// before calling this method.
|
||||
pub fn update_password_hash(&mut self, new_hash: String) {
|
||||
self.password_hash = new_hash;
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Actualizar uso de almacenamiento
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
//! Errores del dominio
|
||||
//!
|
||||
//! Este módulo contiene los tipos de error propios del dominio.
|
||||
//! DomainError es el error base que se usa en toda la capa de dominio.
|
||||
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
use std::error::Error as StdError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Tipo Result común para el dominio con DomainError como error estándar
|
||||
pub type Result<T> = std::result::Result<T, DomainError>;
|
||||
|
||||
/// Tipos de errores del dominio
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ErrorKind {
|
||||
/// Entidad no encontrada
|
||||
NotFound,
|
||||
/// Entidad ya existe
|
||||
AlreadyExists,
|
||||
/// Entrada inválida o validación fallida
|
||||
InvalidInput,
|
||||
/// Error de acceso o permisos
|
||||
AccessDenied,
|
||||
/// Tiempo de espera agotado
|
||||
Timeout,
|
||||
/// Error interno del sistema
|
||||
InternalError,
|
||||
/// Funcionalidad no implementada
|
||||
NotImplemented,
|
||||
/// Operación no soportada
|
||||
UnsupportedOperation,
|
||||
/// Error de base de datos
|
||||
DatabaseError,
|
||||
}
|
||||
|
||||
impl Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
ErrorKind::NotFound => write!(f, "Not Found"),
|
||||
ErrorKind::AlreadyExists => write!(f, "Already Exists"),
|
||||
ErrorKind::InvalidInput => write!(f, "Invalid Input"),
|
||||
ErrorKind::AccessDenied => write!(f, "Access Denied"),
|
||||
ErrorKind::Timeout => write!(f, "Timeout"),
|
||||
ErrorKind::InternalError => write!(f, "Internal Error"),
|
||||
ErrorKind::NotImplemented => write!(f, "Not Implemented"),
|
||||
ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"),
|
||||
ErrorKind::DatabaseError => write!(f, "Database Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error base de dominio que proporciona contexto detallado
|
||||
#[derive(Error, Debug)]
|
||||
#[error("{kind}: {message}")]
|
||||
pub struct DomainError {
|
||||
/// Tipo de error
|
||||
pub kind: ErrorKind,
|
||||
/// Tipo de entidad afectada (ej: "File", "Folder")
|
||||
pub entity_type: &'static str,
|
||||
/// Identificador de la entidad si está disponible
|
||||
pub entity_id: Option<String>,
|
||||
/// Mensaje descriptivo del error
|
||||
pub message: String,
|
||||
/// Error fuente (opcional)
|
||||
#[source]
|
||||
pub source: Option<Box<dyn StdError + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl DomainError {
|
||||
/// Crea un nuevo error de dominio
|
||||
pub fn new<S: Into<String>>(
|
||||
kind: ErrorKind,
|
||||
entity_type: &'static str,
|
||||
message: S,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de entidad no encontrada
|
||||
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
||||
let id = entity_id.into();
|
||||
Self {
|
||||
kind: ErrorKind::NotFound,
|
||||
entity_type,
|
||||
entity_id: Some(id.clone()),
|
||||
message: format!("{} not found: {}", entity_type, id),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de entidad ya existente
|
||||
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
||||
let id = entity_id.into();
|
||||
Self {
|
||||
kind: ErrorKind::AlreadyExists,
|
||||
entity_type,
|
||||
entity_id: Some(id.clone()),
|
||||
message: format!("{} already exists: {}", entity_type, id),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error para operaciones no soportadas
|
||||
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self::new(
|
||||
ErrorKind::UnsupportedOperation,
|
||||
entity_type,
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
/// Crea un error de tiempo agotado
|
||||
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::Timeout,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error interno
|
||||
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de acceso denegado
|
||||
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::AccessDenied,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for access_denied to maintain compatibility
|
||||
pub fn unauthorized<S: Into<String>>(message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::AccessDenied,
|
||||
entity_type: "Authorization",
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de base de datos
|
||||
pub fn database_error<S: Into<String>>(message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::DatabaseError,
|
||||
entity_type: "Database",
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de validación
|
||||
pub fn validation_error<S: Into<String>>(message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::InvalidInput,
|
||||
entity_type: "Validation",
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de funcionalidad no implementada
|
||||
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::NotImplemented,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Establece el ID de la entidad
|
||||
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
|
||||
self.entity_id = Some(entity_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el error fuente
|
||||
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
|
||||
self.source = Some(Box::new(source));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait para añadir contexto a los errores
|
||||
pub trait ErrorContext<T, E> {
|
||||
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
|
||||
where
|
||||
C: Into<String>,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError>;
|
||||
}
|
||||
|
||||
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> {
|
||||
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
|
||||
where
|
||||
C: Into<String>,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
DomainError {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type: "Unknown",
|
||||
entity_id: None,
|
||||
message: context().into(),
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError> {
|
||||
self.map_err(|e| {
|
||||
DomainError {
|
||||
kind,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: format!("{}", e),
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implementaciones From para errores estándar (sin dependencias externas de infra)
|
||||
impl From<std::io::Error> for DomainError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
DomainError {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type: "IO",
|
||||
entity_id: None,
|
||||
message: format!("{}", err),
|
||||
source: Some(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for DomainError {
|
||||
fn from(err: uuid::Error) -> Self {
|
||||
DomainError {
|
||||
kind: ErrorKind::InvalidInput,
|
||||
entity_type: "UUID",
|
||||
entity_id: None,
|
||||
message: format!("{}", err),
|
||||
source: Some(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
pub mod entities;
|
||||
pub mod errors;
|
||||
pub mod repositories;
|
||||
pub mod services;
|
||||
pub mod services;
|
||||
|
||||
// Re-export common types
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
use std::result::Result;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
use std::result::Result;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
@@ -12,7 +12,6 @@ use bytes::Bytes;
|
||||
* operations, providing detailed context for error handling across the application.
|
||||
*/
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FileRepositoryError {
|
||||
/// Returned when a requested file cannot be found by ID or path
|
||||
#[error("File not found: {0}")]
|
||||
@@ -109,7 +108,6 @@ pub trait FileRepository: Send + Sync + 'static {
|
||||
* @param content Binary data of the file
|
||||
* @return The created File entity on success, error otherwise
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
async fn save_file_with_id(
|
||||
&self,
|
||||
id: String,
|
||||
@@ -152,7 +150,6 @@ pub trait FileRepository: Send + Sync + 'static {
|
||||
* @param id The unique identifier of the file to delete
|
||||
* @return Success or error
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::common::errors::DomainError;
|
||||
|
||||
/// Error types for folder repository operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FolderRepositoryError {
|
||||
#[error("Folder not found: {0}")]
|
||||
NotFound(String),
|
||||
@@ -84,12 +83,10 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// Moves a folder to trash
|
||||
|
||||
@@ -61,6 +61,5 @@ pub trait I18nService: Send + Sync + 'static {
|
||||
async fn available_locales(&self) -> Vec<Locale>;
|
||||
|
||||
/// Check if a locale is supported
|
||||
#[allow(dead_code)]
|
||||
async fn is_supported(&self, locale: Locale) -> bool;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod i18n_service;
|
||||
pub mod path_service;
|
||||
pub mod auth_service;
|
||||
|
||||
// NOTE: auth_service has been moved to infrastructure/services/jwt_service.rs
|
||||
// The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort)
|
||||
@@ -1,5 +1,12 @@
|
||||
/// Abstracto servicio de dominio para rutas, sin dependencias de sistema de archivos
|
||||
/// Representa una ruta de almacenamiento en el dominio
|
||||
//! StoragePath - Value Object del dominio para representar rutas de almacenamiento
|
||||
//!
|
||||
//! Este módulo contiene solo el Value Object StoragePath que es parte del dominio puro.
|
||||
//! PathService (que implementa StoragePort y StorageMediator) fue movido a
|
||||
//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Representa una ruta de almacenamiento en el dominio (Value Object)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct StoragePath {
|
||||
segments: Vec<String>,
|
||||
@@ -7,7 +14,6 @@ pub struct StoragePath {
|
||||
|
||||
impl StoragePath {
|
||||
/// Crea una nueva ruta de almacenamiento
|
||||
#[allow(dead_code)]
|
||||
pub fn new(segments: Vec<String>) -> Self {
|
||||
Self { segments }
|
||||
}
|
||||
@@ -89,301 +95,41 @@ impl StoragePath {
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
fn test_storage_path_from_string() {
|
||||
let path = StoragePath::from_string("folder/subfolder/file.txt");
|
||||
assert_eq!(path.segments(), &["folder", "subfolder", "file.txt"]);
|
||||
assert_eq!(path.to_string(), "/folder/subfolder/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");
|
||||
fn test_storage_path_join() {
|
||||
let path = StoragePath::from_string("folder");
|
||||
let joined = path.join("file.txt");
|
||||
assert_eq!(joined.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));
|
||||
fn test_storage_path_parent() {
|
||||
let path = StoragePath::from_string("folder/file.txt");
|
||||
let parent = path.parent().unwrap();
|
||||
assert_eq!(parent.to_string(), "/folder");
|
||||
}
|
||||
|
||||
#[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));
|
||||
fn test_storage_path_root() {
|
||||
let root = StoragePath::root();
|
||||
assert!(root.is_empty());
|
||||
assert_eq!(root.to_string(), "/");
|
||||
}
|
||||
|
||||
#[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");
|
||||
fn test_storage_path_file_name() {
|
||||
let path = StoragePath::from_string("folder/file.txt");
|
||||
assert_eq!(path.file_name(), Some("file.txt".to_string()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user