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:
Dionisio
2026-02-02 23:56:40 +01:00
parent 6aceb07f3f
commit 52840e57df
88 changed files with 4286 additions and 4847 deletions
+7 -8
View File
@@ -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,
+3 -9
View File
@@ -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)
+2 -7
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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
+271
View File
@@ -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
View File
@@ -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
-1
View File
@@ -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;
}
+3 -1
View File
@@ -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)
+28 -282
View File
@@ -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, &not_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()));
}
}