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
-60
View File
@@ -1,60 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build/Lint/Test Commands
### Building
- Build debug version: `cargo build`
- Build release version: `cargo build --release`
- Run the application: `cargo run`
### Testing
- Run all tests: `cargo test`
- Run a specific test: `cargo test test_name`
- Run tests for a specific module: `cargo test module_name`
- Run tests with feature flags: `cargo test --features test_utils`
### Linting
- Run clippy linting: `cargo clippy`
- Format code: `cargo fmt`
## Code Style Guidelines
### Architecture
- This project follows a hexagonal/clean architecture pattern:
- `application`: Contains services (use cases), DTOs, and ports
- `domain`: Contains core entities, repositories (interfaces), and domain services
- `infrastructure`: Contains concrete implementations of repository interfaces
- `interfaces`: Contains HTTP/API handlers and routes
### Error Handling
- Use the `DomainError` type for domain-level errors
- Use the `AppError` type for API/HTTP-level errors
- Use the `ErrorContext` trait to add context to errors from external crates
- Follow the error factory pattern for creating common error types
### Naming Conventions
- Types and structs: PascalCase
- Functions and methods: snake_case
- Constants and statics: SCREAMING_SNAKE_CASE
- Modules and files: snake_case
- Use descriptive names that express intent
### Testing
- Use mock objects for dependencies in unit tests
- Use the `#[tokio::test]` attribute for async tests
- Include both positive and negative test cases
- Follow the Arrange-Act-Assert pattern in tests
### Imports
- Group imports by source:
1. Standard library imports
2. External crate imports
3. Local crate imports (with `crate::` prefix)
- Use explicit imports (no glob imports except in tests)
### Documentation
- Document public API functions and types with doc comments
- Include examples where helpful
- Document error cases and conditions
Generated
+568 -649
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -5,45 +5,45 @@ edition = "2021"
[dependencies]
axum = { version = "0.8.3", features = ["multipart", "http1", "tokio", "macros"] }
tokio = { version = "1.44.2", features = ["full"] }
tokio-util = { version = "0.7.14", features = ["io", "codec"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
bytes = "1.10.1"
tempfile = "3.19.1"
tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["fs", "compression-gzip", "trace", "cors", "add-extension", "request-id"] }
flate2 = "1.1.1"
zip = "2.6.1"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
chrono = { version = "0.4.40", features = ["serde"] }
axum = { version = "0.8.8", features = ["multipart", "http1", "tokio", "macros"] }
tokio = { version = "1.49.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
tokio-stream = { version = "0.1.18", features = ["fs"] }
bytes = "1.11.0"
tempfile = "3.24.0"
tower = "0.5.3"
tower-http = { version = "0.6.8", features = ["fs", "compression-gzip", "trace", "cors", "add-extension", "request-id"] }
flate2 = "1.1.8"
zip = "2.1.0"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
chrono = { version = "0.4.43", features = ["serde"] }
http-body = "1.0.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
futures = "0.3.31"
async-stream = "0.3.6"
mime_guess = "2.0.5"
uuid = { version = "1.16.0", features = ["v4", "serde"] }
async-trait = "0.1.88"
thiserror = "2.0.12"
reqwest = { version = "0.12.15", features = ["json", "multipart"] }
mockall = { version = "0.13.1", optional = true }
rand = "0.9.0"
uuid = { version = "1.20.0", features = ["v4", "serde"] }
async-trait = "0.1.89"
thiserror = "2.0.18"
reqwest = { version = "0.12.18", features = ["json", "multipart"] }
mockall = { version = "0.14.0", optional = true }
rand = "0.9.1"
pin-project-lite = "0.2.16"
sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio", "tls-rustls", "chrono", "uuid", "json"] }
anyhow = "1.0.97"
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-rustls", "chrono", "uuid", "json"] }
anyhow = "1.0.100"
jsonwebtoken = "9.3.1"
argon2 = "0.5.3"
rand_core = { version = "0.6.4", features = ["std"] }
time = "0.3.41"
time = "0.3.46"
axum-server = "0.7.2"
hyper = { version = "1.6.0", features = ["full"] }
url = "2.5.4"
quick-xml = "0.37.4"
hyper = { version = "1.8.1", features = ["full"] }
url = "2.5.8"
quick-xml = "0.39.0"
http-body-util = "0.1.3"
openssl = { version = "0.10.72", features = ["vendored"] }
icalendar = "0.16.13"
openssl = { version = "0.10.75", features = ["vendored"] }
icalendar = "0.17.6"
dotenv = "0.15.0"
[features]
+2 -2
View File
@@ -1,5 +1,5 @@
# Stage 1: Cache dependencies
FROM rust:1.85-alpine AS cacher
FROM rust:1.93.0-alpine3.23 AS cacher
WORKDIR /app
RUN apk --no-cache upgrade && \
apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make
@@ -10,7 +10,7 @@ RUN mkdir -p src && \
cargo build --release && \
rm -rf src target/release/deps/oxicloud*
# Stage 2: Build the application
FROM rust:1.85-alpine AS builder
FROM rust:1.93.0-alpine3.23 AS builder
WORKDIR /app
RUN apk --no-cache upgrade && \
apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make
-103
View File
@@ -1,103 +0,0 @@
# Solución para el error "El usuario 'admin' ya existe"
## Problema
Cuando se intenta crear un usuario administrador en una instalación nueva de OxiCloud, aparece el siguiente error:
```
oxicloud-1 | 2025-04-12T10:47:26.643669Z ERROR oxicloud::interfaces::api::handlers::auth_handler: Registration failed for user admin: Already Exists: El usuario 'admin' ya existe
```
Este error ocurre porque las migraciones de la base de datos ya crean un usuario administrador por defecto como parte del proceso de inicialización.
## Solución implementada
Hemos mejorado el sistema para que maneje mejor el registro de usuarios administradores:
1. **En una instalación nueva**:
- Si registras cualquier usuario como administrador (sea cual sea su nombre), el sistema detectará que es una instalación nueva y eliminará automáticamente el usuario admin predeterminado.
- Esto te permite crear tu propio usuario administrador con el nombre que prefieras desde el principio.
2. **En un sistema en uso**:
- No se permite crear nuevos usuarios administradores desde la página de registro una vez que ya existe un administrador en el sistema
- Esto previene la creación no autorizada de usuarios con permisos elevados
- El sistema tampoco permite tener múltiples usuarios con el mismo nombre (incluido "admin")
### Detalles técnicos
La solución implementa:
1. Detección inteligente de instalaciones nuevas basada en:
- Verificación del número total de usuarios en el sistema
- Verificación del número de usuarios administradores
2. Reconocimiento de usuarios administradores:
- Un usuario es administrador si su nombre es "admin"
- Un usuario es administrador si se proporciona un rol "admin" explícitamente
- Los administradores reciben automáticamente una cuota de 100GB
3. Eliminación segura del usuario admin predeterminado:
- Se detecta al inicio del registro si es una instalación nueva
- Se elimina el admin predeterminado antes de continuar con el registro
4. Prevención de creación de múltiples administradores:
- Una vez que existe un usuario administrador en el sistema, no se permite crear más administradores desde la página de registro
- Solo se puede crear un administrador desde la página de registro durante la instalación inicial
- Esto protege el sistema contra la creación no autorizada de usuarios con privilegios elevados
## Cómo usar esta funcionalidad
### En una instalación nueva:
1. Inicia OxiCloud por primera vez (las migraciones crearán automáticamente un usuario admin predeterminado)
2. Ve a la pantalla de registro y crea un usuario con:
- **Nombre de usuario**: Cualquier nombre que prefieras (por ejemplo, "torrefacto")
- **Contraseña**: La que tú quieras
- **Email**: Tu correo electrónico
3. El sistema detectará automáticamente que se trata de una instalación nueva
4. Si es un usuario administrador (porque el nombre es "admin" o porque explícitamente quieres que sea admin), el sistema eliminará el admin predeterminado antes de continuar
5. Tu nuevo usuario se creará y podrás iniciar sesión con él
### Si necesitas restablecer el usuario administrador:
Si ya tienes un sistema en uso y necesitas restablecer el usuario administrador:
#### Opción 1: Usar el script proporcionado
```bash
cat scripts/reset_admin.sql | docker exec -i oxicloud-postgres-1 psql -U postgres -d oxicloud
```
#### Opción 2: Hacerlo manualmente
```bash
docker exec -it oxicloud-postgres-1 psql -U postgres -d oxicloud
```
```sql
SET search_path TO auth;
DELETE FROM auth.users WHERE username = 'admin';
SELECT username, email, role FROM auth.users;
```
Luego registra un nuevo usuario admin a través de la interfaz web.
## Nota técnica
El usuario administrador predeterminado se crea durante las migraciones con estos valores:
```sql
INSERT INTO auth.users (
id,
username,
email,
password_hash,
role,
storage_quota_bytes
) VALUES (
'00000000-0000-0000-0000-000000000000',
'admin',
'admin@oxicloud.local',
'$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$H3VxE8LL2qPT31DM3loTg6D+O4MSc2sD7GjlQ5h7Jkw', -- Admin123!
'admin',
107374182400 -- 100GB for admin
);
```
-708
View File
@@ -1,708 +0,0 @@
# OxiCloud Code Documentation
This document provides a detailed description of each file in the OxiCloud codebase, organized by architectural layers according to the clean hexagonal architecture pattern.
## Domain Layer
The domain layer forms the core of the application, containing business entities and repository interfaces.
### Entities
**File Entity (`src/domain/entities/file.rs`)**
- Core domain entity representing files in the system
- Implements an immutable design pattern for file operations
- Provides validation, creation, and manipulation methods for files
- Maintains both physical storage information and logical metadata
- Includes error handling via `FileError` for validation failures
**Folder Entity (`src/domain/entities/folder.rs`)**
- Represents folders/directories in the domain model
- Supports hierarchical structure with parent-child relationships
- Provides validation, creation, and update operations
- Implements immutable pattern with methods returning new instances
- Handles path resolution for proper folder hierarchy
**User Entity (`src/domain/entities/user.rs`)**
- Manages user accounts and authentication
- Provides secure password handling with Argon2 hashing
- Supports roles (Admin, User) with appropriate permissions
- Tracks storage usage and quotas
- Includes account management functions (activation, deactivation, login tracking)
**Share Entity (`src/domain/entities/share.rs`)**
- Implements file and folder sharing functionality
- Supports various permission levels (read, write, reshare)
- Provides password protection for shared resources
- Handles expiration dates for temporary sharing
- Tracks access statistics for shared resources
**Calendar Entity (`src/domain/entities/calendar.rs`)**
- Supports calendar functionality for CalDAV integration
- Manages calendar properties like name, color, and description
- Provides ownership and access control
- Supports custom properties for extended CalDAV compatibility
- Handles validation of calendar data
**Calendar Event Entity (`src/domain/entities/calendar_event.rs`)**
- Represents calendar events with properties like title, description, location
- Handles date/time management with recurrence rules
- Supports reminders and notifications
- Provides validation for event data
- Implements custom properties for CalDAV compatibility
**Trashed Item Entity (`src/domain/entities/trashed_item.rs`)**
- Manages files and folders in the trash
- Tracks original locations for restoration
- Implements automatic cleanup based on retention policies
- Provides restoration and permanent deletion functionality
### Repositories (Interfaces)
**File Repository (`src/domain/repositories/file_repository.rs`)**
- Defines the contract for file storage operations
- Abstracts storage implementation details from the domain
- Supports file creation, retrieval, updating, and deletion
- Provides methods for content streaming and file movement
- Includes trash functionality for file lifecycle management
**Folder Repository (`src/domain/repositories/folder_repository.rs`)**
- Defines the interface for folder manipulation
- Abstracts storage implementation details for directories
- Handles folder creation, listing, and hierarchy management
- Supports moving folders and retrieving path information
- Includes trash operations for folders
**User Repository (`src/domain/repositories/user_repository.rs`)**
- Defines the interface for user data persistence
- Supports user creation, retrieval, and management
- Provides authentication and session management
- Handles user preferences and settings storage
- Manages user quotas and storage usage tracking
**Share Repository (`src/domain/repositories/share_repository.rs`)**
- Defines the interface for share record management
- Handles creation and validation of share records
- Tracks permissions and expiration settings
- Provides access verification for shared resources
- Manages share revocation and updates
**Trash Repository (`src/domain/repositories/trash_repository.rs`)**
- Defines the interface for trash operations
- Manages soft deletion and restoration of resources
- Handles retention policies and automatic cleanup
- Provides listing of trashed items with metadata
- Supports permanent deletion operations
### Domain Services
**Auth Service (`src/domain/services/auth_service.rs`)**
- Provides domain-level authentication logic
- Implements password validation and hashing
- Defines authentication policies and rules
- Manages token generation and validation
- Handles security-related domain operations
**I18n Service (`src/domain/services/i18n_service.rs`)**
- Defines domain-level internationalization interface
- Provides translation lookup capabilities
- Manages localization strategies
- Supports multiple languages and fallbacks
- Handles format localization for dates, numbers, etc.
**Path Service (`src/domain/services/path_service.rs`)**
- Manages domain-level path abstractions
- Provides path validation and normalization
- Handles path traversal and resolution
- Implements path security measures
- Supports different path formats and conventions
## Application Layer
The application layer orchestrates use cases by coordinating domain objects and providing services to the interfaces layer.
### Services
**File Service (`src/application/services/file_service.rs`)**
- Implements file-related use cases
- Coordinates between repositories for file operations
- Provides file upload, download, and listing functionality
- Handles error translation between layers
- Contains business logic for file operations
**Folder Service (`src/application/services/folder_service.rs`)**
- Implements folder management use cases
- Manages folder creation, listing, and hierarchy
- Coordinates between repositories for folder operations
- Maintains folder structure integrity
- Handles error translation for folder operations
**Auth Application Service (`src/application/services/auth_application_service.rs`)**
- Manages user authentication flows
- Implements login, logout, and session management
- Handles token generation and validation
- Coordinates with user repository for verification
- Manages password reset and account recovery
**File Management Service (`src/application/services/file_management_service.rs`)**
- Provides higher-level file operations
- Manages file uploads, versions, and metadata
- Handles file operations across repositories
- Coordinates transactional file operations
- Provides advanced file searching and filtering
**File Retrieval Service (`src/application/services/file_retrieval_service.rs`)**
- Specialized service for file content retrieval
- Optimizes file reading operations
- Provides streaming and download functionality
- Implements read-specific error handling
- Supports different retrieval patterns (whole file, ranges)
**File Upload Service (`src/application/services/file_upload_service.rs`)**
- Specialized service for handling file uploads
- Manages chunked and multipart uploads
- Provides validation during upload
- Handles large file uploads efficiently
- Supports upload resumption and integrity verification
**Search Service (`src/application/services/search_service.rs`)**
- Implements file and folder search functionality
- Provides text-based content searching
- Handles metadata-based filtering
- Supports sorting and pagination of results
- Optimizes search operations for performance
**Share Service (`src/application/services/share_service.rs`)**
- Implements file and folder sharing functionality
- Creates and manages share links
- Handles permission checking for shared resources
- Manages password protection for shares
- Processes access requests for shared content
**Trash Service (`src/application/services/trash_service.rs`)**
- Implements trash can functionality
- Manages moving items to trash and restoration
- Handles automatic cleanup of expired trash
- Coordinates with repositories for trash operations
- Maintains metadata for trashed items
**Recent Service (`src/application/services/recent_service.rs`)**
- Tracks recently accessed files
- Manages user-specific recent file lists
- Handles expiration of old entries
- Provides sorting and filtering of recent files
- Coordinates with file repository for metadata
**Favorites Service (`src/application/services/favorites_service.rs`)**
- Manages user favorite files and folders
- Provides adding and removing favorites
- Handles listing and sorting of favorites
- Coordinates with repositories for data consistency
- Maintains user-specific favorite lists
**I18n Application Service (`src/application/services/i18n_application_service.rs`)**
- Handles internationalization and localization
- Provides translation lookups for UI components
- Manages locale detection and setting
- Coordinates with i18n domain service
- Supports dynamic language switching
**Storage Mediator (`src/application/services/storage_mediator.rs`)**
- Coordinates between different storage repositories
- Manages transaction coordination
- Handles path resolution between storage layers
- Provides unified view of storage subsystems
- Optimizes operations across storage types
**Batch Operations (`src/application/services/batch_operations.rs`)**
- Implements batch processing for file operations
- Handles atomic multi-file operations
- Provides transaction support for batch operations
- Manages failure handling and partial success
- Optimizes performance for bulk operations
### Ports
**Inbound Ports (`src/application/ports/inbound.rs`)**
- Defines interfaces for external systems to use
- Contains use case interfaces for application services
- Specifies contracts for UI and API interactions
- Provides clear boundaries for application functionality
- Forms the primary API for interfaces layer
**Outbound Ports (`src/application/ports/outbound.rs`)**
- Defines interfaces used by application services
- Specifies contracts that infrastructure must implement
- Allows swapping infrastructure implementations
- Maintains dependency inversion principle
- Protects application layer from external dependencies
**Auth Ports (`src/application/ports/auth_ports.rs`)**
- Defines interfaces for authentication operations
- Specifies contracts for login, validation, and sessions
- Handles token generation and verification
- Provides user identity management
- Supports different authentication methods
**Storage Ports (`src/application/ports/storage_ports.rs`)**
- Defines interfaces for storage operations
- Specifies contracts for accessing persistent storage
- Handles file system and database interactions
- Provides transaction support for storage operations
- Supports different storage backends
**File Ports (`src/application/ports/file_ports.rs`)**
- Defines interfaces for file operations
- Contains file upload and retrieval use cases
- Specifies contracts for file management
- Handles file-specific error conditions
- Supports various file operation patterns
**Favorites Ports (`src/application/ports/favorites_ports.rs`)**
- Defines interfaces for favorites functionality
- Specifies contracts for favorite management
- Handles favorite-specific operations
- Provides user-specific favorite management
- Supports different favorite organization structures
**Recent Ports (`src/application/ports/recent_ports.rs`)**
- Defines interfaces for recent files functionality
- Specifies contracts for recent file tracking
- Handles history and access patterns
- Provides user-specific recent file handling
- Supports different recency algorithms
**Share Ports (`src/application/ports/share_ports.rs`)**
- Defines interfaces for sharing functionality
- Specifies contracts for share creation and access
- Handles permission verification for shares
- Provides link generation and management
- Supports different sharing models
**Trash Ports (`src/application/ports/trash_ports.rs`)**
- Defines interfaces for trash functionality
- Specifies contracts for trash operations
- Handles trash-specific workflows
- Provides retention and cleanup interfaces
- Supports different trash implementation strategies
### DTOs
**File DTO (`src/application/dtos/file_dto.rs`)**
- Data transfer object for file entities
- Provides serialization and API representation
- Translates between domain model and external interfaces
- Includes conversions to/from domain entities
- Contains file metadata for API responses
**Folder DTO (`src/application/dtos/folder_dto.rs`)**
- Data transfer object for folder entities
- Provides folder data for API responses
- Handles serialization and API representation
- Includes conversions to/from domain entities
- Contains folder structure information
**User DTO (`src/application/dtos/user_dto.rs`)**
- Data transfer object for user information
- Provides user data for API responses
- Handles serialization with sensitive data protection
- Includes conversions to/from domain entities
- Contains user profile information
**Share DTO (`src/application/dtos/share_dto.rs`)**
- Data transfer object for share information
- Provides share data for API responses
- Handles serialization of sharing details
- Includes conversions to/from domain entities
- Contains share link and permission data
**Trash DTO (`src/application/dtos/trash_dto.rs`)**
- Data transfer object for trashed items
- Provides trash information for API responses
- Handles serialization of trash metadata
- Includes conversions to/from domain entities
- Contains restoration information
**Pagination DTO (`src/application/dtos/pagination.rs`)**
- Handles pagination for list responses
- Provides page size and number information
- Supports offset and cursor-based pagination
- Includes metadata for total items and pages
- Facilitates consistent pagination across APIs
**Favorites DTO (`src/application/dtos/favorites_dto.rs`)**
- Data transfer object for favorites
- Provides favorites data for API responses
- Handles serialization of favorite items
- Includes conversions to/from domain entities
- Contains favorite metadata and organization
**Recent DTO (`src/application/dtos/recent_dto.rs`)**
- Data transfer object for recent files
- Provides recent items data for API responses
- Handles serialization of access history
- Includes conversions to/from domain entities
- Contains timing and access metadata
**Search DTO (`src/application/dtos/search_dto.rs`)**
- Data transfer object for search results
- Provides search data for API responses
- Handles serialization of search results
- Includes query and result metadata
- Contains relevance and ranking information
**I18n DTO (`src/application/dtos/i18n_dto.rs`)**
- Data transfer object for internationalization
- Provides language and translation data
- Handles serialization of language resources
- Includes locale and preference information
- Contains translation bundle structures
### Adapters
**WebDAV Adapter (`src/application/adapters/webdav_adapter.rs`)**
- Adapts between OxiCloud domain models and WebDAV protocol
- Handles XML parsing and generation for WebDAV operations
- Implements property handling for WebDAV (PROPFIND, PROPPATCH)
- Provides WebDAV-specific error handling
- Translates between file operations and WebDAV methods
### Transactions
**Storage Transaction (`src/application/transactions/storage_transaction.rs`)**
- Manages transactional operations for storage
- Implements transaction boundaries and commits
- Provides rollback capabilities on failure
- Ensures consistency across multiple operations
- Handles transaction isolation levels
## Infrastructure Layer
This layer provides concrete implementations of repository interfaces and technical services.
### Repositories (Implementations)
**File FS Repository (`src/infrastructure/repositories/file_fs_repository.rs`)**
- Implements FileRepository interface for filesystem storage
- Manages physical file operations on disk
- Handles file content reading and writing
- Implements optimized large file handling
- Provides metadata caching for performance
**File FS Read Repository (`src/infrastructure/repositories/file_fs_read_repository.rs`)**
- Specialized repository for read-only file operations
- Optimized for high-performance file retrieval
- Implements caching for frequently accessed files
- Supports streaming of large files
- Handles content type detection and verification
**File FS Write Repository (`src/infrastructure/repositories/file_fs_write_repository.rs`)**
- Specialized repository for file write operations
- Handles atomic file writes with transaction support
- Implements optimized large file writes
- Manages file locking for concurrent writes
- Provides integrity verification for written files
**File FS Repository Trash (`src/infrastructure/repositories/file_fs_repository_trash.rs`)**
- Extends file repository with trash functionality
- Implements soft delete operations for files
- Manages restoration from trash
- Handles automatic cleanup of expired trash
- Maintains metadata for trashed files
**Folder FS Repository (`src/infrastructure/repositories/folder_fs_repository.rs`)**
- Implements FolderRepository interface for filesystem
- Creates and manages directory structures
- Handles folder listing and hierarchy traversal
- Implements folder permissions and ownership
- Provides optimization for deep folder structures
**Folder FS Repository Trash (`src/infrastructure/repositories/folder_fs_repository_trash.rs`)**
- Extends folder repository with trash functionality
- Implements soft delete for directories
- Handles recursive trash operations for folders
- Manages restoration of folder hierarchies
- Maintains metadata for trashed folders
**Share FS Repository (`src/infrastructure/repositories/share_fs_repository.rs`)**
- Implements ShareRepository for filesystem-based sharing
- Manages share records and permissions
- Handles link generation and validation
- Provides access control for shared resources
- Supports share expiration and revocation
**Trash FS Repository (`src/infrastructure/repositories/trash_fs_repository.rs`)**
- Implements TrashRepository for filesystem
- Manages trash directory structure
- Handles metadata for trashed items
- Implements cleanup policies for expired trash
- Supports permanent deletion operations
**Session PG Repository (`src/infrastructure/repositories/pg/session_pg_repository.rs`)**
- Implements session storage using PostgreSQL
- Manages user sessions and authentication state
- Handles session creation, validation, and expiration
- Provides secure token management
- Supports multiple concurrent sessions
**User PG Repository (`src/infrastructure/repositories/pg/user_pg_repository.rs`)**
- Implements UserRepository with PostgreSQL
- Stores user accounts and profile information
- Handles user queries and updates
- Manages user roles and permissions
- Supports user search and filtering
**File Metadata Manager (`src/infrastructure/repositories/file_metadata_manager.rs`)**
- Manages file metadata independently of content
- Handles extended attributes for files
- Provides caching for frequently accessed metadata
- Optimizes metadata operations
- Supports custom metadata fields
**File Path Resolver (`src/infrastructure/repositories/file_path_resolver.rs`)**
- Resolves logical paths to physical storage locations
- Handles path normalization and validation
- Provides path translation between different systems
- Supports virtual paths and redirections
- Optimizes path resolution for nested structures
**Parallel File Processor (`src/infrastructure/repositories/parallel_file_processor.rs`)**
- Implements parallel processing for large files
- Optimizes file operations with multi-threading
- Provides chunked reading and writing
- Handles load balancing for file operations
- Implements backpressure mechanisms
### Services
**ID Mapping Service (`src/infrastructure/services/id_mapping_service.rs`)**
- Manages mapping between UUIDs and filesystem paths
- Provides persistent ID generation and lookup
- Handles path changes while maintaining stable IDs
- Implements caching for frequently accessed mappings
- Ensures consistency between IDs and paths
**Buffer Pool (`src/infrastructure/services/buffer_pool.rs`)**
- Manages memory buffers for file operations
- Implements pooling for optimal memory usage
- Provides buffer recycling to reduce allocations
- Handles buffer sizing for different operations
- Implements thread-safe buffer management
**Cache Manager (`src/infrastructure/services/cache_manager.rs`)**
- Provides application-wide caching services
- Implements multiple cache levels (memory, disk)
- Handles cache invalidation and consistency
- Manages cache size limits and eviction
- Provides statistics for cache performance
**Compression Service (`src/infrastructure/services/compression_service.rs`)**
- Implements data compression for files and responses
- Supports multiple compression algorithms
- Provides on-the-fly compression for API responses
- Handles selective compression based on file types
- Optimizes compression levels for different content
**File System I18n Service (`src/infrastructure/services/file_system_i18n_service.rs`)**
- Implements I18n service using filesystem storage
- Loads translations from JSON files
- Handles language detection and fallbacks
- Provides translation lookups for UI components
- Supports dynamic language switching
**File Metadata Cache (`src/infrastructure/services/file_metadata_cache.rs`)**
- Provides caching for file metadata
- Optimizes repeated metadata access
- Implements cache invalidation strategies
- Handles concurrent access to metadata
- Supports different cache levels (memory, persistent)
**ID Mapping Optimizer (`src/infrastructure/services/id_mapping_optimizer.rs`)**
- Optimizes ID-to-path mapping operations
- Implements batch processing for mapping updates
- Provides preloading for frequently accessed mappings
- Handles compaction of mapping storage
- Optimizes lookup performance for large mappings
**Zip Service (`src/infrastructure/services/zip_service.rs`)**
- Provides ZIP archive creation and extraction
- Supports on-the-fly compression for downloads
- Handles large directory archiving
- Implements streaming ZIP generation
- Provides progress tracking for large operations
**Trash Cleanup Service (`src/infrastructure/services/trash_cleanup_service.rs`)**
- Manages automatic cleanup of expired trash items
- Implements retention policy enforcement
- Provides scheduled cleanup operations
- Handles graceful cleanup with resource limits
- Supports custom cleanup rules
## Interfaces Layer
This layer handles external communication, including API endpoints and web interfaces.
### API Handlers
**File Handler (`src/interfaces/api/handlers/file_handler.rs`)**
- Handles HTTP requests for file operations
- Processes file uploads with multipart support
- Provides file downloads with optional compression
- Implements CRUD operations for files
- Manages error responses and status codes
**Folder Handler (`src/interfaces/api/handlers/folder_handler.rs`)**
- Handles HTTP requests for folder operations
- Processes folder creation and listing
- Implements CRUD operations for directories
- Provides folder hierarchy navigation
- Manages error responses for folder operations
**Auth Handler (`src/interfaces/api/handlers/auth_handler.rs`)**
- Handles authentication-related API endpoints
- Processes login, logout, and registration
- Manages session tokens and refresh
- Implements password reset functionality
- Provides authentication status information
**Share Handler (`src/interfaces/api/handlers/share_handler.rs`)**
- Handles file and folder sharing endpoints
- Processes share creation and management
- Provides access to shared resources
- Handles permission verification
- Manages share links and passwords
**Trash Handler (`src/interfaces/api/handlers/trash_handler.rs`)**
- Handles trash-related API endpoints
- Processes moving items to trash
- Provides trash listing and filtering
- Handles restoration from trash
- Manages permanent deletion operations
**Search Handler (`src/interfaces/api/handlers/search_handler.rs`)**
- Handles search-related API endpoints
- Processes text search queries
- Provides filtering and sorting options
- Handles pagination for search results
- Manages relevance scoring for results
**Recent Handler (`src/interfaces/api/handlers/recent_handler.rs`)**
- Handles recently accessed files endpoints
- Provides listing and filtering of recent files
- Manages user-specific recent history
- Handles pagination for recent items
- Provides sorting options for recent files
**Favorites Handler (`src/interfaces/api/handlers/favorites_handler.rs`)**
- Handles user favorites endpoints
- Processes adding and removing favorites
- Provides listing and filtering of favorites
- Manages user-specific favorite collections
- Handles sorting and organization of favorites
**I18n Handler (`src/interfaces/api/handlers/i18n_handler.rs`)**
- Handles internationalization endpoints
- Provides language selection and detection
- Serves translation resources
- Manages locale settings
- Handles language preference persistence
**Batch Handler (`src/interfaces/api/handlers/batch_handler.rs`)**
- Handles batch operation endpoints
- Processes multiple operations in a single request
- Provides transaction support for batches
- Handles partial success scenarios
- Manages comprehensive error reporting
**WebDAV Handler (`src/interfaces/api/handlers/webdav_handler.rs`)**
- Implements WebDAV protocol (RFC 4918) endpoints
- Handles WebDAV methods (PROPFIND, PROPPATCH, etc.)
- Provides file system access via HTTP
- Manages WebDAV properties and locks
- Supports third-party WebDAV clients
### API Routes
**Routes (`src/interfaces/api/routes.rs`)**
- Defines API routes and URL structure
- Maps endpoints to appropriate handlers
- Configures middleware for routes
- Handles versioning for API endpoints
- Provides documentation integration
### Middleware
**Auth Middleware (`src/interfaces/middleware/auth.rs`)**
- Handles authentication for API requests
- Verifies tokens and sessions
- Provides user context for handlers
- Manages authentication errors
- Supports different authentication methods
**Cache Middleware (`src/interfaces/middleware/cache.rs`)**
- Implements response caching
- Handles cache headers and validation
- Provides conditional request processing
- Manages cache invalidation
- Optimizes for different content types
**Redirect Middleware (`src/interfaces/middleware/redirect.rs`)**
- Handles HTTP redirects
- Manages URL normalization
- Provides permanent and temporary redirects
- Handles protocol upgrades (HTTP to HTTPS)
- Supports path-based redirections
### Web Interface
**Web Module (`src/interfaces/web/mod.rs`)**
- Coordinates web interface components
- Manages static file serving
- Provides web application integration
- Handles web-specific middleware
- Supports single-page application routing
## Common Layer
This layer provides shared utilities and configurations used across the application.
**Config (`src/common/config.rs`)**
- Manages application configuration
- Loads settings from environment and files
- Provides typed configuration access
- Handles configuration validation
- Supports different environments (dev, prod)
**Errors (`src/common/errors.rs`)**
- Defines error types and handling
- Provides consistent error formatting
- Implements error context and wrapping
- Handles error translation between layers
- Supports error categorization and logging
**DI (`src/common/di.rs`)**
- Implements dependency injection
- Manages service lifecycles
- Provides application state container
- Handles service resolution and registration
- Supports scoped service instances
**DB (`src/common/db.rs`)**
- Manages database connections
- Provides connection pooling
- Handles database migrations
- Implements query helpers
- Supports transaction management
**Cache (`src/common/cache.rs`)**
- Provides generic caching facilities
- Implements different cache strategies
- Handles cache key generation
- Manages cache invalidation
- Supports distributed caching
**Auth Factory (`src/common/auth_factory.rs`)**
- Creates authentication components
- Configures auth providers based on settings
- Provides factory methods for auth services
- Handles auth strategy selection
- Supports multiple authentication methods
-44
View File
@@ -1,44 +0,0 @@
# Security Policy
## Supported Versions
The following versions of OxiCloud are currently supported with security updates:
| Version | Supported |
| ------- | ------------------ |
| Latest | :white_check_mark: |
## Reporting a Vulnerability
The OxiCloud team takes security issues seriously. We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
To report a security vulnerability, please follow these steps:
1. **DO NOT** disclose the vulnerability publicly (e.g., in GitHub issues)
2. Email details of the vulnerability to the project maintainers
3. Include as much information as possible, such as:
- A clear description of the vulnerability
- Steps to reproduce the issue
- Potential impact
- Suggested fixes if available
## What to Expect
After submitting a vulnerability report, you can expect the following:
1. **Acknowledgment**: The team will acknowledge receipt of your report within 3 business days
2. **Assessment**: We'll evaluate the vulnerability and determine its impact
3. **Plan**: We'll develop a plan to address the vulnerability
4. **Fix & Release**: Once fixed, we'll release an update
5. **Recognition**: With your permission, we'll acknowledge your contribution in the release notes
## Security Best Practices for OxiCloud Users
- Keep your OxiCloud installation updated to the latest version
- Use strong, unique passwords for all user accounts
- Configure proper file permissions
- Regularly back up your data
- Consider running OxiCloud behind a reverse proxy with HTTPS
- Implement IP restrictions where appropriate
Thank you for helping keep OxiCloud and its users secure!
+3 -6
View File
@@ -48,7 +48,6 @@ impl CalDavAdapter {
let mut in_sync_collection = false;
let mut in_prop = false;
let mut in_filter = false;
let mut in_time_range = false;
let mut start_time: Option<DateTime<Utc>> = None;
let mut end_time: Option<DateTime<Utc>> = None;
let mut props = Vec::new();
@@ -68,8 +67,6 @@ impl CalDavAdapter {
s if s == "prop" || s.ends_with(":prop") => in_prop = true,
s if s == "filter" || s.ends_with(":filter") => in_filter = true,
s if s == "time-range" || s.ends_with(":time-range") => {
in_time_range = true;
// Parse time-range attributes
for attr in e.attributes() {
if let Ok(attr) = attr {
@@ -106,7 +103,7 @@ impl CalDavAdapter {
}
},
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap_or_default();
let text = e.decode().unwrap_or_default();
// Check if we're in sync-token element
if in_sync_collection && !in_prop && !in_filter {
@@ -128,7 +125,7 @@ impl CalDavAdapter {
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = false,
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
s if s == "filter" || s.ends_with(":filter") => in_filter = false,
s if s == "time-range" || s.ends_with(":time-range") => in_time_range = false,
s if s == "time-range" || s.ends_with(":time-range") => { /* time-range end, attributes already parsed */ },
_ => ()
}
},
@@ -771,7 +768,7 @@ impl CalDavAdapter {
}
},
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap_or_default();
let text = e.decode().unwrap_or_default();
if in_displayname {
displayname = text.to_string();
+2 -2
View File
@@ -685,7 +685,7 @@ impl WebDavAdapter {
},
Ok(Event::Text(e)) => {
if current_prop.is_some() {
current_text.push_str(&e.unescape().unwrap_or_default());
current_text.push_str(&e.decode().unwrap_or_default());
}
},
Ok(Event::End(ref e)) => {
@@ -879,7 +879,7 @@ impl WebDavAdapter {
},
Ok(Event::Text(e)) => {
if in_owner {
owner_text.push_str(&e.unescape().unwrap_or_default());
owner_text.push_str(&e.decode().unwrap_or_default());
}
},
Ok(Event::End(ref e)) => {
-1
View File
@@ -1,6 +1,5 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use std::collections::HashMap;
use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent;
+9
View File
@@ -65,4 +65,13 @@ pub struct ChangePasswordDto {
#[derive(Debug, Serialize, Deserialize)]
pub struct RefreshTokenDto {
pub refresh_token: String,
}
/// Datos del usuario autenticado actual (para uso en servicios de application)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: String,
pub username: String,
pub email: String,
pub role: String,
}
+60
View File
@@ -3,6 +3,66 @@ use crate::domain::entities::user::User;
use crate::domain::entities::session::Session;
use crate::common::errors::DomainError;
// ============================================================================
// Cryptography Ports - Extracted from Domain to maintain Clean Architecture
// ============================================================================
/// Port for password hashing operations.
///
/// This trait abstracts cryptographic password operations, allowing the domain
/// layer to remain independent of specific hashing implementations (argon2, bcrypt, etc.)
pub trait PasswordHasherPort: Send + Sync + 'static {
/// Hash a plain text password
fn hash_password(&self, password: &str) -> Result<String, DomainError>;
/// Verify a plain text password against a hash
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError>;
}
/// Claims contained in a JWT token
#[derive(Debug, Clone)]
pub struct TokenClaims {
/// Subject identifier (user ID)
pub sub: String,
/// Expiration timestamp (seconds since Unix epoch)
pub exp: i64,
/// Issued at timestamp (seconds since Unix epoch)
pub iat: i64,
/// JWT unique ID
pub jti: String,
/// Username
pub username: String,
/// User email
pub email: String,
/// User role
pub role: String,
}
/// Port for JWT token operations.
///
/// This trait abstracts token generation and validation, allowing the domain
/// layer to remain independent of specific JWT implementations.
pub trait TokenServicePort: Send + Sync + 'static {
/// Generate an access token for a user
fn generate_access_token(&self, user: &User) -> Result<String, DomainError>;
/// Validate a token and extract its claims
fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError>;
/// Generate a refresh token
fn generate_refresh_token(&self) -> String;
/// Get refresh token expiry in seconds
fn refresh_token_expiry_secs(&self) -> i64;
/// Get refresh token expiry in days
fn refresh_token_expiry_days(&self) -> i64;
}
// ============================================================================
// Storage Ports
// ============================================================================
#[async_trait]
pub trait UserStoragePort: Send + Sync + 'static {
/// Crea un nuevo usuario
@@ -1,8 +1,7 @@
use std::sync::Arc;
use crate::domain::entities::user::{User, UserRole};
use crate::domain::entities::session::Session;
use crate::domain::services::auth_service::AuthService;
use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort};
use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort, PasswordHasherPort, TokenServicePort};
use crate::application::dtos::user_dto::{UserDto, RegisterDto, LoginDto, AuthResponseDto, ChangePasswordDto, RefreshTokenDto};
use crate::application::dtos::folder_dto::CreateFolderDto;
use crate::application::ports::inbound::FolderUseCase;
@@ -11,7 +10,8 @@ use crate::common::errors::{DomainError, ErrorKind};
pub struct AuthApplicationService {
user_storage: Arc<dyn UserStoragePort>,
session_storage: Arc<dyn SessionStoragePort>,
auth_service: Arc<AuthService>,
password_hasher: Arc<dyn PasswordHasherPort>,
token_service: Arc<dyn TokenServicePort>,
folder_service: Option<Arc<dyn FolderUseCase>>,
}
@@ -19,12 +19,14 @@ impl AuthApplicationService {
pub fn new(
user_storage: Arc<dyn UserStoragePort>,
session_storage: Arc<dyn SessionStoragePort>,
auth_service: Arc<AuthService>,
password_hasher: Arc<dyn PasswordHasherPort>,
token_service: Arc<dyn TokenServicePort>,
) -> Self {
Self {
user_storage,
session_storage,
auth_service,
password_hasher,
token_service,
folder_service: None,
}
}
@@ -127,11 +129,23 @@ impl AuthApplicationService {
1024 * 1024 * 1024 // 1GB para usuarios normales
};
// Crear usuario
// Validar longitud de password antes de hashear
if dto.password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Password debe tener al menos 8 caracteres"
));
}
// Hashear el password usando el servicio de infraestructura
let password_hash = self.password_hasher.hash_password(&dto.password)?;
// Crear usuario con el hash pre-generado
let user = User::new(
dto.username.clone(),
dto.email,
dto.password,
password_hash,
role,
quota,
).map_err(|e| DomainError::new(
@@ -203,13 +217,8 @@ impl AuthApplicationService {
));
}
// Verificar contraseña
let is_valid = user.verify_password(&dto.password)
.map_err(|_| DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Credenciales inválidas"
))?;
// Verificar contraseña usando el hasher inyectado
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
@@ -223,11 +232,10 @@ impl AuthApplicationService {
user.register_login();
self.user_storage.update_user(user.clone()).await?;
// Generar tokens
let access_token = self.auth_service.generate_access_token(&user)
.map_err(DomainError::from)?;
// Generar tokens usando el servicio de tokens inyectado
let access_token = self.token_service.generate_access_token(&user)?;
let refresh_token = self.auth_service.generate_refresh_token();
let refresh_token = self.token_service.generate_refresh_token();
// Guardar sesión
let session = Session::new(
@@ -235,7 +243,7 @@ impl AuthApplicationService {
refresh_token.clone(),
None, // IP (se puede añadir desde la capa HTTP)
None, // User-Agent (se puede añadir desde la capa HTTP)
self.auth_service.refresh_token_expiry_days(),
self.token_service.refresh_token_expiry_days(),
);
self.session_storage.create_session(session).await?;
@@ -246,7 +254,7 @@ impl AuthApplicationService {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: self.auth_service.refresh_token_expiry_secs(),
expires_in: self.token_service.refresh_token_expiry_secs(),
})
}
@@ -283,10 +291,9 @@ impl AuthApplicationService {
self.session_storage.revoke_session(session.id()).await?;
// Generar nuevos tokens
let access_token = self.auth_service.generate_access_token(&user)
.map_err(DomainError::from)?;
let access_token = self.token_service.generate_access_token(&user)?;
let new_refresh_token = self.auth_service.generate_refresh_token();
let new_refresh_token = self.token_service.generate_refresh_token();
// Crear nueva sesión
let new_session = Session::new(
@@ -294,7 +301,7 @@ impl AuthApplicationService {
new_refresh_token.clone(),
None,
None,
self.auth_service.refresh_token_expiry_days(),
self.token_service.refresh_token_expiry_days(),
);
self.session_storage.create_session(new_session).await?;
@@ -304,7 +311,7 @@ impl AuthApplicationService {
access_token,
refresh_token: new_refresh_token,
token_type: "Bearer".to_string(),
expires_in: self.auth_service.refresh_token_expiry_secs(),
expires_in: self.token_service.refresh_token_expiry_secs(),
})
}
@@ -342,13 +349,8 @@ impl AuthApplicationService {
// Obtener usuario
let mut user = self.user_storage.get_user_by_id(user_id).await?;
// Verificar contraseña actual
let is_valid = user.verify_password(&dto.current_password)
.map_err(|_| DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Contraseña actual incorrecta"
))?;
// Verificar contraseña actual usando el hasher inyectado
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
@@ -358,13 +360,18 @@ impl AuthApplicationService {
));
}
// Actualizar contraseña
user.update_password(dto.new_password.clone())
.map_err(|e| DomainError::new(
// Validar nueva contraseña
if dto.new_password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
format!("Error al cambiar contraseña: {}", e)
))?;
"Password debe tener al menos 8 caracteres"
));
}
// Hashear nueva contraseña y actualizar usuario
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
user.update_password_hash(new_hash);
// Guardar usuario actualizado
self.user_storage.update_user(user).await?;
+2 -51
View File
@@ -1,12 +1,11 @@
use std::sync::Arc;
use thiserror::Error;
use futures::{future::join_all, Future};
use tokio::sync::Semaphore;
use tracing::{info, error};
use tracing::info;
use thiserror::Error;
use crate::application::services::file_service::FileService;
use crate::application::services::folder_service::FolderService;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
use crate::common::config::AppConfig;
use crate::application::ports::inbound::FolderUseCase;
@@ -15,7 +14,6 @@ use crate::application::dtos::folder_dto::FolderDto;
/// Errores específicos para operaciones por lotes
#[derive(Debug, Error)]
#[allow(dead_code)]
pub enum BatchOperationError {
#[error("Error de dominio: {0}")]
Domain(#[from] DomainError),
@@ -59,52 +57,6 @@ pub struct BatchStats {
pub max_concurrency: usize,
}
/// Tipo de entidad para operaciones por lotes
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum EntityType {
File,
Folder,
}
/// Tipo de operación por lotes
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum BatchOperationType {
Create,
Read,
Update,
Delete,
Copy,
Move,
}
/// Identificador para una entidad (ID o ruta)
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum EntityIdentifier {
Id(String),
Path(StoragePath),
}
impl EntityIdentifier {
#[allow(dead_code)]
pub fn as_id(&self) -> Option<&str> {
match self {
EntityIdentifier::Id(id) => Some(id),
_ => None,
}
}
#[allow(dead_code)]
pub fn as_path(&self) -> Option<&StoragePath> {
match self {
EntityIdentifier::Path(path) => Some(path),
_ => None,
}
}
}
/// Servicio de operaciones por lotes
pub struct BatchOperationService {
file_service: Arc<FileService>,
@@ -494,7 +446,6 @@ impl BatchOperationService {
}
/// Operación genérica de lote para cualquier tipo de función asíncrona
#[allow(dead_code)]
pub async fn generic_batch_operation<T, F, Fut>(
&self,
items: Vec<T>,
@@ -1,14 +1,12 @@
use std::sync::Arc;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto
};
use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::common::errors::{DomainError, ErrorKind};
pub struct CalendarService {
+2 -3
View File
@@ -9,12 +9,11 @@ use crate::application::dtos::address_book_dto::{
};
use crate::application::dtos::contact_dto::{
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto,
EmailDto, PhoneDto, AddressDto
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto
};
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
use crate::application::ports::storage_ports::StorageUseCase;
use crate::common::errors::{DomainError, ErrorContext};
use crate::common::errors::DomainError;
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address};
use crate::domain::repositories::address_book_repository::AddressBookRepository;
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository};
@@ -16,13 +16,6 @@ impl FileManagementService {
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
Self { file_repository }
}
/// Creates a stub for testing
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
}
}
}
#[async_trait]
@@ -18,13 +18,6 @@ impl FileRetrievalService {
pub fn new(file_repository: Arc<dyn FileReadPort>) -> Self {
Self { file_repository }
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub())
}
}
}
#[async_trait]
@@ -48,14 +48,6 @@ impl FileUploadService {
self.storage_usage_service = Some(storage_usage_service);
self
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()),
storage_usage_service: None,
}
}
}
#[async_trait]
@@ -23,14 +23,6 @@ impl AppFileUseCaseFactory {
file_write_repository,
}
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_read_repository: Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()),
file_write_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()),
}
}
}
impl FileUseCaseFactory for AppFileUseCaseFactory {
@@ -51,7 +51,6 @@ impl I18nApplicationService {
}
/// Load translations for all available locales
#[allow(dead_code)]
pub async fn load_all_translations(&self) -> Vec<(Locale, I18nResult<()>)> {
let locales = self.i18n_service.available_locales().await;
let mut results = Vec::new();
@@ -70,7 +69,6 @@ impl I18nApplicationService {
}
/// Check if a locale is supported
#[allow(dead_code)]
pub async fn is_supported(&self, locale: Locale) -> bool {
self.i18n_service.is_supported(locale).await
}
+14 -12
View File
@@ -7,8 +7,8 @@ use thiserror::Error;
use crate::domain::entities::folder::Folder;
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryError};
use crate::domain::repositories::file_repository::FileRepositoryError;
use crate::domain::services::path_service::{PathService, StoragePath};
use crate::application::ports::outbound::IdMappingPort;
use crate::domain::services::path_service::StoragePath;
use crate::application::ports::outbound::{IdMappingPort, StoragePort};
/// Errores específicos del mediador de almacenamiento
#[derive(Debug, Error)]
@@ -99,12 +99,12 @@ pub trait StorageMediator: Send + Sync + 'static {
/// Implementación concreta del mediador de almacenamiento
pub struct FileSystemStorageMediator {
pub folder_repository: Arc<dyn FolderRepository>,
pub path_service: Arc<PathService>,
pub path_service: Arc<dyn StoragePort>,
pub id_mapping: Arc<dyn IdMappingPort>,
}
impl FileSystemStorageMediator {
pub fn new(folder_repository: Arc<dyn FolderRepository>, path_service: Arc<PathService>, id_mapping: Arc<dyn IdMappingPort>) -> Self {
pub fn new(folder_repository: Arc<dyn FolderRepository>, path_service: Arc<dyn StoragePort>, id_mapping: Arc<dyn IdMappingPort>) -> Self {
Self { folder_repository, path_service, id_mapping }
}
@@ -116,7 +116,7 @@ impl FileSystemStorageMediator {
/// Overload para implementar inicialización diferida con repository placeholder
pub fn new_with_lazy_folder(
_folder_repository: Arc<RwLock<Option<Arc<dyn FolderRepository>>>>,
path_service: Arc<PathService>,
path_service: Arc<dyn StoragePort>,
id_mapping: Arc<dyn IdMappingPort>
) -> Self {
// Create temporary stub repository
@@ -208,16 +208,18 @@ impl FolderRepository for FolderRepositoryStub {
}
/// Stub implementation for initialization dependency issues
pub struct StubStorageMediator {
#[allow(dead_code)]
_path_service: Arc<PathService>,
}
/// This is a minimal stub that doesn't require any infrastructure dependencies
pub struct StubStorageMediator;
impl StubStorageMediator {
pub fn new() -> Self {
let root_path = PathBuf::from("/tmp");
let path_service = Arc::new(PathService::new(root_path));
Self { _path_service: path_service }
Self
}
}
impl Default for StubStorageMediator {
fn default() -> Self {
Self::new()
}
}
@@ -3,7 +3,7 @@ use async_trait::async_trait;
use tokio::task;
use crate::common::errors::DomainError;
use crate::application::ports::auth_ports::UserStoragePort;
use crate::domain::repositories::file_repository::FileRepository;
use crate::application::ports::outbound::FileStoragePort;
use crate::application::ports::storage_ports::StorageUsagePort;
use tracing::{info, error, debug};
@@ -14,14 +14,14 @@ use tracing::{info, error, debug};
* is using and updating this information in the user records.
*/
pub struct StorageUsageService {
file_repository: Arc<dyn FileRepository>,
file_repository: Arc<dyn FileStoragePort>,
user_repository: Arc<dyn UserStoragePort>,
}
impl StorageUsageService {
/// Creates a new storage usage service
pub fn new(
file_repository: Arc<dyn FileRepository>,
file_repository: Arc<dyn FileStoragePort>,
user_repository: Arc<dyn UserStoragePort>,
) -> Self {
Self {
@@ -90,7 +90,7 @@ impl StorageUsageService {
async fn calculate_folder_size(&self, folder_id: &str) -> Result<i64, DomainError> {
// Implementation with explicit boxing to handle recursion in async functions
async fn inner_calculate_size(
repo: Arc<dyn FileRepository>,
repo: Arc<dyn FileStoragePort>,
folder_id: &str,
) -> Result<i64, DomainError> {
let mut total_size: i64 = 0;
@@ -37,7 +37,6 @@ impl StorageTransaction {
}
/// Añade una operación sin rollback (para limpieza o logging)
#[allow(dead_code)]
pub fn add_finalizer<F>(&mut self, finalizer: F)
where
F: Future<Output = Result<(), DomainError>> + Send + 'static,
+263
View File
@@ -0,0 +1,263 @@
//! Adaptadores para convertir entre interfaces de dominio y aplicación
//!
//! Este módulo contiene adaptadores que permiten usar repositorios que implementan
//! `FileStoragePort` y `FolderStoragePort` donde se espera `FileRepository` y `FolderRepository`.
use std::sync::Arc;
use async_trait::async_trait;
use crate::application::ports::outbound::{FileStoragePort, FolderStoragePort};
use crate::domain::entities::file::File;
use crate::domain::entities::folder::Folder;
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult, FileRepositoryError};
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult, FolderRepositoryError};
use crate::domain::services::path_service::StoragePath;
/// Adaptador que convierte FileStoragePort a FileRepository
pub struct DomainFileRepoAdapter {
repo: Arc<dyn FileStoragePort>,
}
impl DomainFileRepoAdapter {
pub fn new(repo: Arc<dyn FileStoragePort>) -> Self {
Self { repo }
}
}
#[async_trait]
impl FileRepository for DomainFileRepoAdapter {
async fn save_file_from_bytes(
&self,
name: String,
folder_id: Option<String>,
content_type: String,
content: Vec<u8>,
) -> FileRepositoryResult<File> {
self.repo.save_file(name, folder_id, content_type, content)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn save_file_with_id(
&self,
_id: String,
_name: String,
_folder_id: Option<String>,
_content_type: String,
_content: Vec<u8>,
) -> FileRepositoryResult<File> {
Err(FileRepositoryError::Other("Not implemented".to_string()))
}
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
self.repo.get_file(id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn list_files(&self, folder_id: Option<&str>) -> FileRepositoryResult<Vec<File>> {
self.repo.list_files(folder_id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn delete_file(&self, id: &str) -> FileRepositoryResult<()> {
self.repo.delete_file(id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()> {
self.delete_file(id).await
}
async fn get_file_content(&self, id: &str) -> FileRepositoryResult<Vec<u8>> {
self.repo.get_file_content(id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn get_file_stream(&self, id: &str) -> FileRepositoryResult<Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>> {
self.repo.get_file_stream(id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn move_file(&self, id: &str, target_folder_id: Option<String>) -> FileRepositoryResult<File> {
self.repo.move_file(id, target_folder_id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn get_file_path(&self, id: &str) -> FileRepositoryResult<StoragePath> {
self.repo.get_file_path(id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn move_to_trash(&self, file_id: &str) -> FileRepositoryResult<()> {
self.repo.delete_file(file_id)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> FileRepositoryResult<()> {
tracing::info!("Restoring file from trash: {} to {}", file_id, original_path);
match self.repo.get_file(file_id).await {
Ok(_) => {
let path_components: Vec<&str> = original_path.split('/').collect();
let parent_folder: Option<String> = if path_components.len() > 1 {
tracing::info!("Attempting to restore to parent folder from path: {}", original_path);
None
} else {
None
};
match self.repo.move_file(file_id, parent_folder).await {
Ok(_) => {
tracing::info!("Successfully restored file from trash: {}", file_id);
Ok(())
},
Err(e) => {
tracing::error!("Failed to restore file from trash: {}", e);
Err(FileRepositoryError::Other(format!("Failed to restore file: {}", e)))
}
}
},
Err(e) => {
tracing::error!("File not found in trash: {}", e);
Err(FileRepositoryError::NotFound(file_id.to_string()))
}
}
}
async fn delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()> {
tracing::info!("Permanently deleting file: {}", file_id);
match self.repo.delete_file(file_id).await {
Ok(_) => {
tracing::info!("Successfully deleted file permanently: {}", file_id);
Ok(())
},
Err(e) => {
tracing::error!("Failed to permanently delete file: {}", e);
Err(FileRepositoryError::Other(format!("Failed to delete file permanently: {}", e)))
}
}
}
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> FileRepositoryResult<()> {
tracing::info!("Updating content for file: {}", file_id);
self.repo.update_file_content(file_id, content)
.await
.map_err(|e| {
tracing::error!("Failed to update file content: {}", e);
FileRepositoryError::Other(format!("Failed to update file content: {}", e))
})
}
}
/// Adaptador que convierte FolderStoragePort a FolderRepository
pub struct DomainFolderRepoAdapter {
repo: Arc<dyn FolderStoragePort>,
}
impl DomainFolderRepoAdapter {
pub fn new(repo: Arc<dyn FolderStoragePort>) -> Self {
Self { repo }
}
}
#[async_trait]
impl FolderRepository for DomainFolderRepoAdapter {
async fn create_folder(&self, name: String, parent_id: Option<String>) -> FolderRepositoryResult<Folder> {
self.repo.create_folder(name, parent_id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult<Folder> {
self.repo.get_folder(id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn get_folder_by_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<Folder> {
self.repo.get_folder_by_path(storage_path)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult<Vec<Folder>> {
self.repo.list_folders(parent_id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn list_folders_paginated(
&self,
parent_id: Option<&str>,
offset: usize,
limit: usize,
include_total: bool
) -> FolderRepositoryResult<(Vec<Folder>, Option<usize>)> {
self.repo.list_folders_paginated(parent_id, offset, limit, include_total)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn rename_folder(&self, id: &str, new_name: String) -> FolderRepositoryResult<Folder> {
self.repo.rename_folder(id, new_name)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> FolderRepositoryResult<Folder> {
self.repo.move_folder(id, new_parent_id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()> {
self.repo.delete_folder(id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult<bool> {
self.repo.folder_exists(storage_path)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult<StoragePath> {
self.repo.get_folder_path(id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn folder_exists(&self, _path: &std::path::PathBuf) -> FolderRepositoryResult<bool> {
Err(FolderRepositoryError::Other("Not implemented".to_string()))
}
async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> FolderRepositoryResult<Folder> {
Err(FolderRepositoryError::Other("Not implemented".to_string()))
}
async fn move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> {
self.repo.delete_folder(folder_id)
.await
.map_err(|e| FolderRepositoryError::Other(format!("{}", e)))
}
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> FolderRepositoryResult<()> {
Err(FolderRepositoryError::Other(
"Restore from trash should be handled by TrashService, not through this adapter".to_string()))
}
async fn delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> {
self.delete_folder(folder_id).await
}
}
+11 -5
View File
@@ -2,10 +2,12 @@ use std::sync::Arc;
use anyhow::Result;
use sqlx::PgPool;
use crate::domain::services::auth_service::AuthService;
use crate::application::ports::auth_ports::TokenServicePort;
use crate::application::services::auth_application_service::AuthApplicationService;
use crate::application::services::folder_service::FolderService;
use crate::infrastructure::repositories::{UserPgRepository, SessionPgRepository};
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
use crate::infrastructure::services::jwt_service::JwtTokenService;
use crate::common::config::AppConfig;
use crate::common::di::AuthServices;
@@ -14,13 +16,16 @@ pub async fn create_auth_services(
pool: Arc<PgPool>,
folder_service: Option<Arc<FolderService>>
) -> Result<AuthServices> {
// Crear servicio de dominio de autenticación
let auth_service = Arc::new(AuthService::new(
// Crear servicio de tokens JWT (implementación de TokenServicePort)
let token_service: Arc<dyn TokenServicePort> = Arc::new(JwtTokenService::new(
config.auth.jwt_secret.clone(),
config.auth.access_token_expiry_secs,
config.auth.refresh_token_expiry_secs,
));
// Crear servicio de hashing de contraseñas
let password_hasher = Arc::new(Argon2PasswordHasher::new());
// Crear repositorios PostgreSQL
let user_repository = Arc::new(UserPgRepository::new(pool.clone()));
let session_repository = Arc::new(SessionPgRepository::new(pool.clone()));
@@ -29,7 +34,8 @@ pub async fn create_auth_services(
let mut auth_app_service = AuthApplicationService::new(
user_repository,
session_repository,
auth_service.clone(),
password_hasher,
token_service.clone(),
);
// Configurar servicio de carpetas si está disponible
@@ -41,7 +47,7 @@ pub async fn create_auth_services(
let auth_application_service = Arc::new(auth_app_service);
Ok(AuthServices {
auth_service,
token_service,
auth_application_service,
})
}
-208
View File
@@ -1,208 +0,0 @@
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use crate::common::errors::{DomainError, ErrorKind};
/// Entrada de caché con tiempo de expiración
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct CacheEntry<V> {
value: V,
expiry: Instant,
}
impl<V> CacheEntry<V> {
/// Crea una nueva entrada en la caché
#[allow(dead_code)]
fn new(value: V, ttl: Duration) -> Self {
Self {
value,
expiry: Instant::now() + ttl,
}
}
/// Verifica si la entrada ha expirado
#[allow(dead_code)]
fn is_expired(&self) -> bool {
Instant::now() > self.expiry
}
}
/// Servicio genérico de caché con TTL
#[allow(dead_code)]
pub struct CacheService<K, V> {
cache: Arc<RwLock<HashMap<K, CacheEntry<V>>>>,
ttl: Duration,
max_entries: usize,
}
impl<K, V> CacheService<K, V>
where
K: Hash + Eq + Clone + Send + Sync + 'static + std::fmt::Debug,
V: Clone + Send + Sync + 'static,
{
/// Crea un nuevo servicio de caché
#[allow(dead_code)]
pub fn new(ttl: Duration, max_entries: usize) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
ttl,
max_entries,
}
}
/// Obtiene un valor de la caché o lo inserta si no existe
#[allow(dead_code)]
pub async fn get_or_insert<F, E>(&self, key: K, loader: F) -> Result<V, DomainError>
where
F: FnOnce() -> Result<V, E>,
E: std::error::Error + Send + Sync + 'static,
{
// Intentar leer de la caché primero
{
let cache = self.cache.read().await;
if let Some(entry) = cache.get(&key) {
if !entry.is_expired() {
tracing::debug!("Cache hit for key: {:?}", key);
return Ok(entry.value.clone());
}
}
}
// Cache miss o entrada expirada, obtener valor y actualizar
let value = loader().map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"Cache",
format!("Failed to load value for cache: {}", e),
)
.with_source(e)
})?;
// Insertar en la caché
{
let mut cache = self.cache.write().await;
// Si alcanzamos el límite, eliminar una entrada aleatoria
if cache.len() >= self.max_entries {
if let Some(expired_key) = cache
.iter()
.find(|(_, v)| v.is_expired())
.map(|(k, _)| k.clone())
{
cache.remove(&expired_key);
} else if let Some(random_key) = cache.keys().next().cloned() {
cache.remove(&random_key);
}
}
cache.insert(key.clone(), CacheEntry::new(value.clone(), self.ttl));
}
tracing::debug!("Cache miss for key: {:?}, value loaded and cached", key);
Ok(value)
}
/// Invalida una entrada específica de la caché
#[allow(dead_code)]
pub async fn invalidate(&self, key: &K) {
let mut cache = self.cache.write().await;
cache.remove(key);
tracing::debug!("Cache entry invalidated for key: {:?}", key);
}
/// Invalida todas las entradas de la caché
#[allow(dead_code)]
pub async fn invalidate_all(&self) {
let mut cache = self.cache.write().await;
cache.clear();
tracing::debug!("Cache fully invalidated");
}
/// Obtiene el número de entradas en la caché
#[allow(dead_code)]
pub async fn len(&self) -> usize {
self.cache.read().await.len()
}
/// Limpia las entradas expiradas de la caché
#[allow(dead_code)]
pub async fn cleanup_expired(&self) -> usize {
let mut cache = self.cache.write().await;
let initial_len = cache.len();
cache.retain(|_, v| !v.is_expired());
let removed = initial_len - cache.len();
if removed > 0 {
tracing::debug!("Removed {} expired cache entries", removed);
}
removed
}
}
/// Caché específica para metadatos de archivos
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FileMetadata {
pub size: u64,
pub created_at: u64,
pub modified_at: u64,
pub is_dir: bool,
}
/// Gestor de caché para operaciones comunes de almacenamiento
#[allow(dead_code)]
pub struct CacheManager {
/// Caché para metadatos de archivos/carpetas
metadata_cache: CacheService<std::path::PathBuf, FileMetadata>,
/// Caché para verificación de existencia de archivos
existence_cache: CacheService<std::path::PathBuf, bool>,
}
impl CacheManager {
/// Crea un nuevo gestor de caché
#[allow(dead_code)]
pub fn new(metadata_ttl: Duration, existence_ttl: Duration) -> Self {
Self {
metadata_cache: CacheService::new(metadata_ttl, 10000), // Caché para 10,000 elementos
existence_cache: CacheService::new(existence_ttl, 20000), // Caché para 20,000 elementos
}
}
/// Obtiene o carga los metadatos de un archivo/carpeta
#[allow(dead_code)]
pub async fn get_metadata<F>(&self, path: std::path::PathBuf, loader: F) -> Result<FileMetadata, DomainError>
where
F: FnOnce() -> Result<FileMetadata, std::io::Error>,
{
self.metadata_cache.get_or_insert(path, loader).await
}
/// Verifica o determina si un archivo/carpeta existe
#[allow(dead_code)]
pub async fn check_exists<F>(&self, path: std::path::PathBuf, checker: F) -> Result<bool, DomainError>
where
F: FnOnce() -> Result<bool, std::io::Error>,
{
self.existence_cache.get_or_insert(path, checker).await
}
/// Invalida la caché para una ruta específica
#[allow(dead_code)]
pub async fn invalidate_path(&self, path: &std::path::Path) {
self.metadata_cache.invalidate(&path.to_path_buf()).await;
self.existence_cache.invalidate(&path.to_path_buf()).await;
}
/// Limpia todas las entradas expiradas
#[allow(dead_code)]
pub async fn cleanup(&self) -> (usize, usize) {
let metadata_cleaned = self.metadata_cache.cleanup_expired().await;
let existence_cleaned = self.existence_cache.cleanup_expired().await;
(metadata_cleaned, existence_cleaned)
}
}
-6
View File
@@ -33,7 +33,6 @@ pub struct TimeoutConfig {
/// Timeout para adquisición de locks (ms)
pub lock_acquisition_ms: u64,
/// Timeout para operaciones de red (ms)
#[allow(dead_code)]
pub network_operation_ms: u64,
}
@@ -80,7 +79,6 @@ impl TimeoutConfig {
}
/// Obtiene un Duration para operaciones de red
#[allow(dead_code)]
pub fn network_timeout(&self) -> Duration {
Duration::from_millis(self.network_operation_ms)
}
@@ -92,7 +90,6 @@ pub struct ResourceConfig {
/// Umbral en MB para considerar un archivo como grande
pub large_file_threshold_mb: u64,
/// Umbral de entradas para considerar un directorio como grande
#[allow(dead_code)]
pub large_dir_threshold_entries: usize,
/// Tamaño de chunk para procesamiento de archivos grandes (bytes)
pub chunk_size_bytes: usize,
@@ -133,7 +130,6 @@ impl ResourceConfig {
}
/// Determina si un directorio es considerado grande
#[allow(dead_code)]
pub fn is_large_directory(&self, entry_count: usize) -> bool {
entry_count >= self.large_dir_threshold_entries
}
@@ -170,7 +166,6 @@ pub struct ConcurrencyConfig {
/// Máximo de tareas de archivo concurrentes
pub max_concurrent_files: usize,
/// Máximo de tareas de directorio concurrentes
#[allow(dead_code)]
pub max_concurrent_dirs: usize,
/// Máximo de operaciones de IO concurrentes
pub max_concurrent_io: usize,
@@ -451,7 +446,6 @@ impl AppConfig {
}
/// Obtenemos una configuración global por defecto
#[allow(dead_code)]
pub fn default_config() -> AppConfig {
AppConfig::default()
}
+229 -42
View File
@@ -3,20 +3,29 @@ use std::sync::Arc;
use std::sync::RwLock;
use sqlx::PgPool;
use crate::domain::services::auth_service::AuthService;
use crate::application::services::auth_application_service::AuthApplicationService;
use crate::domain::services::path_service::PathService;
use crate::infrastructure::services::path_service::PathService;
use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository;
use crate::infrastructure::repositories::file_fs_repository::FileFsRepository;
use crate::infrastructure::repositories::trash_fs_repository::TrashFsRepository;
use crate::infrastructure::repositories::share_fs_repository::ShareFsRepository;
use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
use crate::infrastructure::services::file_system_i18n_service::FileSystemI18nService;
use crate::infrastructure::services::id_mapping_service::IdMappingService;
use crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer;
use crate::infrastructure::services::cache_manager::StorageCacheManager;
use crate::infrastructure::services::file_metadata_cache::FileMetadataCache;
use crate::infrastructure::services::buffer_pool::BufferPool;
use crate::infrastructure::services::trash_cleanup_service::TrashCleanupService;
use crate::application::services::folder_service::FolderService;
use crate::application::services::file_service::FileService;
use crate::application::services::i18n_application_service::I18nApplicationService;
use crate::application::services::trash_service::TrashService;
use crate::application::services::search_service::SearchService;
use crate::application::services::share_service::ShareService;
use crate::application::services::favorites_service::FavoritesService;
use crate::application::services::recent_service::RecentService;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
use crate::application::ports::inbound::{FileUseCase, FolderUseCase, SearchUseCase};
@@ -28,11 +37,14 @@ use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::infrastructure::repositories::{FileMetadataManager, FilePathResolver, FileFsReadRepository, FileFsWriteRepository};
use crate::application::services::{FileUploadService, FileRetrievalService, FileManagementService, AppFileUseCaseFactory};
use crate::common::errors::DomainError;
use crate::common::adapters::{DomainFileRepoAdapter, DomainFolderRepoAdapter};
use crate::domain::services::i18n_service::I18nService;
use crate::common::config::AppConfig;
/// Fábrica para los diferentes componentes de la aplicación
#[allow(dead_code)]
///
/// Esta fábrica centraliza la creación de todos los servicios de la aplicación,
/// garantizando el orden correcto de inicialización y resolviendo dependencias circulares.
pub struct AppServiceFactory {
storage_path: PathBuf,
locales_path: PathBuf,
@@ -41,7 +53,6 @@ pub struct AppServiceFactory {
impl AppServiceFactory {
/// Crea una nueva fábrica de servicios
#[allow(dead_code)]
pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self {
Self {
storage_path,
@@ -51,7 +62,6 @@ impl AppServiceFactory {
}
/// Crea una nueva fábrica de servicios con configuración personalizada
#[allow(dead_code)]
pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self {
Self {
storage_path,
@@ -60,17 +70,25 @@ impl AppServiceFactory {
}
}
/// Obtiene la configuración
pub fn config(&self) -> &AppConfig {
&self.config
}
/// Obtiene la ruta de almacenamiento
pub fn storage_path(&self) -> &PathBuf {
&self.storage_path
}
/// Inicializa los servicios base del sistema
#[allow(dead_code)]
pub async fn create_core_services(&self) -> Result<CoreServices, DomainError> {
// Path service
let path_service = Arc::new(PathService::new(self.storage_path.clone()));
// Cache manager
// TTL values in milliseconds and max entries for cache
let file_ttl_ms = 60_000; // 1 minute for files
let dir_ttl_ms = 120_000; // 2 minutes for directories
let max_entries = 10_000; // Maximum cache entries
let file_ttl_ms = self.config.cache.file_ttl_ms;
let dir_ttl_ms = self.config.cache.directory_ttl_ms;
let max_entries = self.config.cache.max_entries;
let cache_manager = Arc::new(StorageCacheManager::new(file_ttl_ms, dir_ttl_ms, max_entries));
// Iniciar tarea de limpieza de caché en segundo plano
@@ -79,22 +97,39 @@ impl AppServiceFactory {
StorageCacheManager::start_cleanup_task(cache_manager_clone).await;
});
// ID mapping service
let id_mapping_path = self.storage_path.join("folder_ids.json");
let id_mapping_service = Arc::new(
IdMappingService::new(id_mapping_path).await?
// ID mapping service para carpetas
let folder_id_mapping_path = self.storage_path.join("folder_ids.json");
let folder_id_mapping_service = Arc::new(
IdMappingService::new(folder_id_mapping_path).await?
);
// ID mapping service para archivos
let file_id_mapping_path = self.storage_path.join("file_ids.json");
let file_id_mapping_service = Arc::new(
IdMappingService::new(file_id_mapping_path).await?
);
// Optimizer con batch processing y caching
let id_mapping_optimizer = Arc::new(
IdMappingOptimizer::new(folder_id_mapping_service.clone())
);
// Iniciar tarea de limpieza del optimizer
IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone());
tracing::info!("Core services initialized: path service, cache manager, ID mapping");
Ok(CoreServices {
path_service,
cache_manager,
id_mapping_service,
id_mapping_service: folder_id_mapping_service,
file_id_mapping_service,
id_mapping_optimizer,
config: self.config.clone(),
})
}
/// Inicializa los servicios de repositorio utilizando el patrón Builder mejorado
#[allow(dead_code)]
/// Inicializa los servicios de repositorio
pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices {
// Storage mediator - con inicialización diferida para folder repository
let folder_repository_holder = Arc::new(RwLock::new(None));
@@ -102,7 +137,7 @@ impl AppServiceFactory {
let storage_mediator = Arc::new(FileSystemStorageMediator::new_with_lazy_folder(
folder_repository_holder.clone(),
core.path_service.clone(),
core.id_mapping_service.clone()
core.id_mapping_optimizer.clone()
));
// Folder repository
@@ -113,7 +148,7 @@ impl AppServiceFactory {
core.path_service.clone(),
));
// Actualizar el holder para el mediador una vez que el repository está creado
// Actualizar el holder para el mediador
if let Ok(mut holder) = folder_repository_holder.write() {
*holder = Some(folder_repository.clone());
}
@@ -123,6 +158,22 @@ impl AppServiceFactory {
FileMetadataCache::default_with_config(core.config.clone())
);
// Iniciar tarea de limpieza de metadata cache
let cache_clone = metadata_cache.clone();
tokio::spawn(async move {
FileMetadataCache::start_cleanup_task(cache_clone).await;
});
// Buffer pool para optimización de memoria
let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL
BufferPool::start_cleaner(buffer_pool.clone());
// Parallel file processor
let parallel_processor = Arc::new(ParallelFileProcessor::new_with_buffer_pool(
core.config.clone(),
buffer_pool.clone()
));
// Componentes refactorizados
let metadata_manager = Arc::new(FileMetadataManager::new(
metadata_cache.clone(),
@@ -141,7 +192,7 @@ impl AppServiceFactory {
metadata_manager.clone(),
path_resolver.clone(),
core.config.clone(),
None // processor will be added later if needed
Some(parallel_processor.clone())
));
let file_write_repository = Arc::new(FileFsWriteRepository::new(
@@ -150,16 +201,17 @@ impl AppServiceFactory {
path_resolver.clone(),
storage_mediator.clone(),
core.config.clone(),
None // processor will be added later if needed
Some(parallel_processor.clone())
));
// Legacy file repository - mantenido por compatibilidad
let file_repository = Arc::new(FileFsRepository::new(
// File repository con procesamiento paralelo
let file_repository = Arc::new(FileFsRepository::new_with_processor(
self.storage_path.clone(),
storage_mediator.clone(),
core.id_mapping_service.clone(),
core.file_id_mapping_service.clone(),
core.path_service.clone(),
metadata_cache,
metadata_cache.clone(),
parallel_processor
));
// I18n repository
@@ -177,6 +229,8 @@ impl AppServiceFactory {
None
};
tracing::info!("Repository services initialized with parallel processing and buffer pool");
RepositoryServices {
folder_repository,
file_repository,
@@ -186,24 +240,23 @@ impl AppServiceFactory {
storage_mediator,
metadata_manager,
path_resolver,
metadata_cache,
trash_repository,
}
}
/// Inicializa los servicios de aplicación
#[allow(dead_code)]
pub fn create_application_services(&self, repos: &RepositoryServices) -> ApplicationServices {
// Servicios principales
let folder_service = Arc::new(FolderService::new(
repos.folder_repository.clone()
));
// Antiguo servicio único
let file_service = Arc::new(FileService::new(
repos.file_repository.clone()
));
// Nuevos servicios refactorizados
// Servicios refactorizados
let file_upload_service = Arc::new(FileUploadService::new(
repos.file_write_repository.clone()
));
@@ -225,13 +278,21 @@ impl AppServiceFactory {
repos.i18n_repository.clone()
));
// Servicio de papelera (deshabilitado temporalmente)
let trash_service = None; // La función de papelera está deshabilitada por defecto
// Search service con caché
let search_service: Option<Arc<dyn SearchUseCase>> = Some(Arc::new(SearchService::new(
repos.file_repository.clone(),
repos.folder_repository.clone(),
300, // Cache TTL in seconds (5 minutes)
1000, // Maximum cache entries
)));
// Servicio de búsqueda (deshabilitado por defecto)
let search_service = None; // La función de búsqueda se activa según la configuración
tracing::info!("Application services initialized");
ApplicationServices {
// Tipos concretos para handlers que los necesitan
folder_service_concrete: folder_service.clone(),
file_service_concrete: file_service.clone(),
// Traits para abstracción
folder_service,
file_service,
file_upload_service,
@@ -239,27 +300,132 @@ impl AppServiceFactory {
file_management_service,
file_use_case_factory,
i18n_service,
trash_service,
trash_service: None, // Se configura después con create_trash_service
search_service,
share_service: None, // No share service by default
favorites_service: None, // No favorites service by default
recent_service: None // No recent service by default
share_service: None, // Se configura después con create_share_service
favorites_service: None, // Se configura después con create_favorites_service
recent_service: None, // Se configura después con create_recent_service
}
}
/// Crea el servicio de papelera
pub async fn create_trash_service(
&self,
repos: &RepositoryServices,
) -> Option<Arc<dyn TrashUseCase>> {
if !self.config.features.enable_trash {
tracing::info!("Trash service is disabled in configuration");
return None;
}
let trash_repo = repos.trash_repository.as_ref()?;
// Crear adaptadores
let file_repo_adapter = Arc::new(DomainFileRepoAdapter::new(repos.file_repository.clone()));
let folder_repo_adapter = Arc::new(DomainFolderRepoAdapter::new(repos.folder_repository.clone()));
let service = Arc::new(TrashService::new(
trash_repo.clone(),
file_repo_adapter,
folder_repo_adapter,
self.config.storage.trash_retention_days,
));
// Inicializar servicio de limpieza
let cleanup_service = TrashCleanupService::new(
service.clone(),
trash_repo.clone(),
24, // Run cleanup every 24 hours
);
cleanup_service.start_cleanup_job().await;
tracing::info!("Trash service initialized with daily cleanup schedule");
Some(service as Arc<dyn TrashUseCase>)
}
/// Crea el servicio de compartición
pub fn create_share_service(
&self,
repos: &RepositoryServices,
) -> Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>> {
if !self.config.features.enable_file_sharing {
tracing::info!("File sharing service is disabled in configuration");
return None;
}
let share_repository = Arc::new(ShareFsRepository::new(
Arc::new(self.config.clone())
));
let service = Arc::new(ShareService::new(
Arc::new(self.config.clone()),
share_repository,
repos.file_repository.clone(),
repos.folder_repository.clone()
));
tracing::info!("File sharing service initialized");
Some(service)
}
/// Crea el servicio de favoritos (requiere base de datos)
pub fn create_favorites_service(
&self,
db_pool: &Arc<PgPool>,
) -> Arc<dyn FavoritesUseCase> {
let service = Arc::new(FavoritesService::new(db_pool.clone()));
tracing::info!("Favorites service initialized");
service
}
/// Crea el servicio de elementos recientes (requiere base de datos)
pub fn create_recent_service(
&self,
db_pool: &Arc<PgPool>,
) -> Arc<dyn RecentItemsUseCase> {
let service = Arc::new(RecentService::new(
db_pool.clone(),
50 // Maximum recent items per user
));
tracing::info!("Recent items service initialized");
service
}
/// Precarga traducciones
pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) {
use crate::domain::services::i18n_service::Locale;
if let Err(e) = i18n_service.load_translations(Locale::English).await {
tracing::warn!("Failed to load English translations: {}", e);
}
if let Err(e) = i18n_service.load_translations(Locale::Spanish).await {
tracing::warn!("Failed to load Spanish translations: {}", e);
}
tracing::info!("Translations preloaded");
}
/// Precarga directorios en caché
pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) {
tracing::info!("Preloading common directories to warm up cache...");
if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await {
tracing::info!("Preloaded {} directory entries into cache", count);
}
}
}
/// Contenedor para servicios base
#[allow(dead_code)]
#[derive(Clone)]
pub struct CoreServices {
pub path_service: Arc<PathService>,
pub cache_manager: Arc<StorageCacheManager>,
pub id_mapping_service: Arc<dyn crate::application::ports::outbound::IdMappingPort>,
pub file_id_mapping_service: Arc<IdMappingService>,
pub id_mapping_optimizer: Arc<IdMappingOptimizer>,
pub config: AppConfig,
}
/// Contenedor para servicios de repositorio
#[allow(dead_code)]
#[derive(Clone)]
pub struct RepositoryServices {
pub folder_repository: Arc<dyn FolderStoragePort>,
@@ -270,13 +436,17 @@ pub struct RepositoryServices {
pub storage_mediator: Arc<dyn StorageMediator>,
pub metadata_manager: Arc<FileMetadataManager>,
pub path_resolver: Arc<FilePathResolver>,
pub metadata_cache: Arc<FileMetadataCache>,
pub trash_repository: Option<Arc<dyn crate::domain::repositories::trash_repository::TrashRepository>>,
}
/// Contenedor para servicios de aplicación
#[allow(dead_code)]
#[derive(Clone)]
pub struct ApplicationServices {
// Tipos concretos para compatibilidad con handlers existentes
pub folder_service_concrete: Arc<FolderService>,
pub file_service_concrete: Arc<FileService>,
// Traits para abstracción
pub folder_service: Arc<dyn FolderUseCase>,
pub file_service: Arc<dyn FileUseCase>,
pub file_upload_service: Arc<dyn FileUploadUseCase>,
@@ -292,10 +462,9 @@ pub struct ApplicationServices {
}
/// Contenedor para servicios de autenticación
#[allow(dead_code)]
#[derive(Clone)]
pub struct AuthServices {
pub auth_service: Arc<AuthService>,
pub token_service: Arc<dyn crate::application::ports::auth_ports::TokenServicePort>,
pub auth_application_service: Arc<AuthApplicationService>,
}
@@ -323,7 +492,7 @@ impl Default for AppState {
let config = crate::common::config::AppConfig::default();
let path_service = Arc::new(
crate::domain::services::path_service::PathService::new(
crate::infrastructure::services::path_service::PathService::new(
std::path::PathBuf::from("./storage")
)
);
@@ -758,14 +927,23 @@ impl Default for AppState {
let file_management_service = Arc::new(DummyFileManagementUseCase) as Arc<dyn crate::application::ports::file_ports::FileManagementUseCase>;
let file_use_case_factory = Arc::new(DummyFileUseCaseFactory) as Arc<dyn crate::application::ports::file_ports::FileUseCaseFactory>;
// Create dummy ID mapping service for files
let dummy_file_id_mapping = Arc::new(IdMappingService::dummy());
let dummy_id_optimizer = Arc::new(IdMappingOptimizer::new(dummy_file_id_mapping.clone()));
// This creates the core services needed for basic functionality
let core_services = CoreServices {
path_service: path_service.clone(),
cache_manager: Arc::new(crate::infrastructure::services::cache_manager::StorageCacheManager::default()),
id_mapping_service: id_mapping_service.clone(),
file_id_mapping_service: dummy_file_id_mapping,
id_mapping_optimizer: dummy_id_optimizer,
config: config.clone(),
};
// Create dummy metadata cache
let dummy_metadata_cache = Arc::new(FileMetadataCache::default_with_config(config.clone()));
// Create empty repository implementations
let repository_services = RepositoryServices {
folder_repository: Arc::new(DummyFolderStoragePort) as Arc<dyn crate::application::ports::outbound::FolderStoragePort>,
@@ -780,6 +958,7 @@ impl Default for AppState {
storage_mediator.clone(),
id_mapping_service.clone()
)),
metadata_cache: dummy_metadata_cache,
trash_repository: None, // No trash repository in minimal mode
};
@@ -798,9 +977,17 @@ impl Default for AppState {
Ok(())
}
}
// Create dummy concrete services for compatibility
let dummy_folder_storage = Arc::new(DummyFolderStoragePort) as Arc<dyn crate::application::ports::outbound::FolderStoragePort>;
let dummy_file_storage = Arc::new(DummyFileStoragePort) as Arc<dyn crate::application::ports::outbound::FileStoragePort>;
let folder_service_concrete = Arc::new(FolderService::new(dummy_folder_storage));
let file_service_concrete = Arc::new(FileService::new(dummy_file_storage));
// Create application services
let application_services = ApplicationServices {
folder_service_concrete: folder_service_concrete.clone(),
file_service_concrete: file_service_concrete.clone(),
folder_service,
file_service,
file_upload_service,
+13 -342
View File
@@ -1,255 +1,22 @@
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::error::Error as StdError;
use thiserror::Error;
//! Errores de la aplicación
//!
//! Este módulo re-exporta los errores del dominio y define utilidades
//! para conversión de errores de infraestructura.
/// Tipo Result común para la aplicación con DomainError como error estándar
pub type Result<T> = std::result::Result<T, DomainError>;
// Re-exportar errores del dominio para compatibilidad
pub use crate::domain::errors::{DomainError, ErrorKind, Result};
/// Tipos de errores comunes en toda la aplicación
#[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,
}
// Re-exportar AppError desde interfaces para compatibilidad hacia atrás
// NOTA: El lugar canónico de AppError es ahora crate::interfaces::errors
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
#[allow(dead_code)]
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;
#[allow(dead_code)]
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)),
}
})
}
}
/// Macro para convertir errores específicos a DomainError
// Macro para convertir errores específicos de infraestructura a DomainError
#[macro_export]
macro_rules! impl_from_error {
($error_type:ty, $entity_type:expr) => {
impl From<$error_type> for DomainError {
impl From<$error_type> for crate::domain::errors::DomainError {
fn from(err: $error_type) -> Self {
DomainError {
kind: ErrorKind::InternalError,
crate::domain::errors::DomainError {
kind: crate::domain::errors::ErrorKind::InternalError,
entity_type: $entity_type,
entity_id: None,
message: format!("{}", err),
@@ -260,102 +27,6 @@ macro_rules! impl_from_error {
};
}
// Implementación para errores estándar comunes
impl_from_error!(std::io::Error, "IO");
// Implementaciones para errores de infraestructura (sqlx, serde_json)
impl_from_error!(serde_json::Error, "Serialization");
impl_from_error!(sqlx::Error, "Database");
impl_from_error!(uuid::Error, "UUID");
// Error para capas HTTP/API
#[derive(Debug)]
pub struct AppError {
pub status_code: axum::http::StatusCode,
pub message: String,
pub error_type: String,
}
// Estructura de respuesta de error
#[derive(serde::Serialize)]
pub struct ErrorResponse {
pub status: String,
pub message: String,
pub error_type: String,
}
impl AppError {
pub fn new(status_code: axum::http::StatusCode, message: impl Into<String>, error_type: impl Into<String>) -> Self {
Self {
status_code,
message: message.into(),
error_type: error_type.into(),
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::BAD_REQUEST, message, "BadRequest")
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::UNAUTHORIZED, message, "Unauthorized")
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::FORBIDDEN, message, "Forbidden")
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::NOT_FOUND, message, "NotFound")
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::INTERNAL_SERVER_ERROR, message, "InternalError")
}
pub fn method_not_allowed(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::METHOD_NOT_ALLOWED, message, "MethodNotAllowed")
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::CONFLICT, message, "Conflict")
}
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType")
}
}
impl From<DomainError> for AppError {
fn from(err: DomainError) -> Self {
let status_code = match err.kind {
ErrorKind::NotFound => axum::http::StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => axum::http::StatusCode::CONFLICT,
ErrorKind::InvalidInput => axum::http::StatusCode::BAD_REQUEST,
ErrorKind::AccessDenied => axum::http::StatusCode::FORBIDDEN,
ErrorKind::Timeout => axum::http::StatusCode::REQUEST_TIMEOUT,
ErrorKind::InternalError => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
ErrorKind::NotImplemented => axum::http::StatusCode::NOT_IMPLEMENTED,
ErrorKind::UnsupportedOperation => axum::http::StatusCode::METHOD_NOT_ALLOWED,
ErrorKind::DatabaseError => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
};
Self {
status_code,
message: err.message,
error_type: err.kind.to_string(),
}
}
}
impl axum::response::IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let status = self.status_code;
let error_response = ErrorResponse {
status: status.to_string(),
message: self.message,
error_type: self.error_type,
};
let body = axum::Json(error_response);
(status, body).into_response()
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
pub mod errors;
pub mod config;
pub mod cache;
pub mod di;
pub mod db;
pub mod auth_factory;
pub mod auth_factory;
pub mod adapters;
+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()));
}
}
@@ -0,0 +1,300 @@
//! Calendar Storage Adapter
//!
//! This adapter implements the `CalendarStoragePort` application port using
//! the `CalendarRepository` and `CalendarEventRepository` domain repositories.
//! It bridges the gap between the application layer and the infrastructure layer.
use std::sync::Arc;
use std::collections::HashMap;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto
};
use crate::application::ports::calendar_ports::CalendarStoragePort;
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent;
use crate::domain::repositories::calendar_repository::CalendarRepository;
use crate::domain::repositories::calendar_event_repository::CalendarEventRepository;
/// Adapter that implements CalendarStoragePort using domain repositories
pub struct CalendarStorageAdapter {
calendar_repository: Arc<dyn CalendarRepository>,
event_repository: Arc<dyn CalendarEventRepository>,
}
impl CalendarStorageAdapter {
/// Creates a new CalendarStorageAdapter with the given repositories
pub fn new(
calendar_repository: Arc<dyn CalendarRepository>,
event_repository: Arc<dyn CalendarEventRepository>,
) -> Self {
Self {
calendar_repository,
event_repository,
}
}
}
#[async_trait]
impl CalendarStoragePort for CalendarStorageAdapter {
// Calendar operations
async fn create_calendar(&self, dto: CreateCalendarDto, owner_id: &str) -> Result<CalendarDto, DomainError> {
let calendar = Calendar::new(
dto.name,
owner_id.to_string(),
dto.description,
dto.color,
)?;
let created = self.calendar_repository.create_calendar(calendar).await?;
Ok(CalendarDto::from(created))
}
async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result<CalendarDto, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
if let Some(name) = update.name {
calendar.update_name(name)?;
}
if let Some(description) = update.description {
calendar.update_description(Some(description));
}
if let Some(color) = update.color {
calendar.update_color(Some(color))?;
}
let updated = self.calendar_repository.update_calendar(calendar).await?;
Ok(CalendarDto::from(updated))
}
async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
// First delete all events in the calendar
self.event_repository.delete_all_events_in_calendar(&uuid).await?;
// Then delete the calendar itself
self.calendar_repository.delete_calendar(&uuid).await
}
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?;
Ok(CalendarDto::from(calendar))
}
async fn list_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
let calendars = self.calendar_repository.list_calendars_by_owner(owner_id).await?;
Ok(calendars.into_iter().map(CalendarDto::from).collect())
}
async fn list_calendars_shared_with_user(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
let calendars = self.calendar_repository.list_calendars_shared_with_user(user_id).await?;
Ok(calendars.into_iter().map(CalendarDto::from).collect())
}
async fn list_public_calendars(&self, limit: i64, offset: i64) -> Result<Vec<CalendarDto>, DomainError> {
let calendars = self.calendar_repository.list_public_calendars(limit, offset).await?;
Ok(calendars.into_iter().map(CalendarDto::from).collect())
}
async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result<bool, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.user_has_calendar_access(&uuid, user_id).await
}
// Calendar sharing
async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.share_calendar(&uuid, user_id, access_level).await
}
async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.remove_calendar_sharing(&uuid, user_id).await
}
async fn get_calendar_shares(&self, calendar_id: &str) -> Result<Vec<(String, String)>, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.get_calendar_shares(&uuid).await
}
// Calendar properties
async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.set_calendar_property(&uuid, property_name, property_value).await
}
async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result<Option<String>, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.get_calendar_property(&uuid, property_name).await
}
async fn get_calendar_properties(&self, calendar_id: &str) -> Result<HashMap<String, String>, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
self.calendar_repository.get_calendar_properties(&uuid).await
}
// Event operations
async fn create_event(&self, dto: CreateEventDto) -> Result<CalendarEventDto, DomainError> {
let calendar_id = Uuid::parse_str(&dto.calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?;
// Verify calendar exists and user has access
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?;
// Generate basic iCal data
let ical_data = format!(
"BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//OxiCloud//EN\nBEGIN:VEVENT\nUID:{}@oxicloud\nDTSTAMP:{}\nDTSTART:{}\nDTEND:{}\nSUMMARY:{}\nEND:VEVENT\nEND:VCALENDAR",
uuid::Uuid::new_v4(),
chrono::Utc::now().format("%Y%m%dT%H%M%SZ"),
dto.start_time.format("%Y%m%dT%H%M%SZ"),
dto.end_time.format("%Y%m%dT%H%M%SZ"),
dto.summary
);
let event = CalendarEvent::new(
calendar_id,
dto.summary,
dto.description,
dto.location,
dto.start_time,
dto.end_time,
dto.all_day.unwrap_or(false),
dto.rrule,
ical_data,
)?;
let created = self.event_repository.create_event(event).await?;
Ok(CalendarEventDto::from(created))
}
async fn create_event_from_ical(&self, dto: CreateEventICalDto) -> Result<CalendarEventDto, DomainError> {
let calendar_id = Uuid::parse_str(&dto.calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?;
// Verify calendar exists
let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?;
// Parse iCal data and create event
let event = CalendarEvent::from_ical(calendar_id, dto.ical_data.clone())?;
let created = self.event_repository.create_event(event).await?;
Ok(CalendarEventDto::from(created))
}
async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result<CalendarEventDto, DomainError> {
let uuid = Uuid::parse_str(event_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?;
let mut event = self.event_repository.find_event_by_id(&uuid).await?;
if let Some(summary) = update.summary {
event.update_summary(summary)?;
}
if let Some(description) = update.description {
event.update_description(Some(description));
}
if let Some(location) = update.location {
event.update_location(Some(location));
}
if let Some(start_time) = update.start_time {
if let Some(end_time) = update.end_time {
event.update_time_range(start_time, end_time)?;
} else {
event.update_time_range(start_time, *event.end_time())?;
}
} else if let Some(end_time) = update.end_time {
event.update_time_range(*event.start_time(), end_time)?;
}
if let Some(all_day) = update.all_day {
event.update_all_day(all_day);
}
if let Some(rrule) = update.rrule {
event.update_rrule(Some(rrule))?;
}
let updated = self.event_repository.update_event(event).await?;
Ok(CalendarEventDto::from(updated))
}
async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(event_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?;
self.event_repository.delete_event(&uuid).await
}
async fn get_event(&self, event_id: &str) -> Result<CalendarEventDto, DomainError> {
let uuid = Uuid::parse_str(event_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?;
let event = self.event_repository.find_event_by_id(&uuid).await?;
Ok(CalendarEventDto::from(event))
}
async fn list_events_by_calendar(&self, calendar_id: &str) -> Result<Vec<CalendarEventDto>, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
let events = self.event_repository.list_events_by_calendar(&uuid).await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect())
}
async fn list_events_by_calendar_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result<Vec<CalendarEventDto>, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
let events = self.event_repository.list_events_by_calendar_paginated(&uuid, limit, offset).await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect())
}
async fn get_events_in_time_range(
&self,
calendar_id: &str,
start: &DateTime<Utc>,
end: &DateTime<Utc>
) -> Result<Vec<CalendarEventDto>, DomainError> {
let uuid = Uuid::parse_str(calendar_id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?;
let events = self.event_repository.get_events_in_time_range(&uuid, start, end).await?;
Ok(events.into_iter().map(CalendarEventDto::from).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
// Tests would go here using mock repositories
}
@@ -0,0 +1,645 @@
//! Contact Storage Adapter
//!
//! This adapter implements the `AddressBookUseCase` and `ContactUseCase` application ports
//! using the domain repositories. It bridges the gap between the application layer
//! and the infrastructure layer for CardDAV functionality.
use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
use crate::application::dtos::address_book_dto::{
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
ShareAddressBookDto, UnshareAddressBookDto
};
use crate::application::dtos::contact_dto::{
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto,
EmailDto, PhoneDto, AddressDto
};
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address};
use crate::domain::repositories::address_book_repository::AddressBookRepository;
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository};
/// Adapter that implements AddressBookUseCase and ContactUseCase using domain repositories
pub struct ContactStorageAdapter {
address_book_repository: Arc<dyn AddressBookRepository>,
contact_repository: Arc<dyn ContactRepository>,
group_repository: Arc<dyn ContactGroupRepository>,
}
impl ContactStorageAdapter {
/// Creates a new ContactStorageAdapter with the given repositories
pub fn new(
address_book_repository: Arc<dyn AddressBookRepository>,
contact_repository: Arc<dyn ContactRepository>,
group_repository: Arc<dyn ContactGroupRepository>,
) -> Self {
Self {
address_book_repository,
contact_repository,
group_repository,
}
}
/// Helper to parse UUID from string
fn parse_uuid(id: &str, entity_name: &'static str) -> Result<Uuid, DomainError> {
Uuid::parse_str(id)
.map_err(|_| DomainError::new(ErrorKind::InvalidInput, entity_name, format!("Invalid {} ID format", entity_name)))
}
/// Helper to check if user has access to an address book
async fn check_address_book_access(&self, address_book_id: &Uuid, user_id: &str) -> Result<AddressBook, DomainError> {
let address_book = self.address_book_repository
.get_address_book_by_id(address_book_id)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
// Check if user is owner
if address_book.owner_id == user_id {
return Ok(address_book);
}
// Check if address book is public
if address_book.is_public {
return Ok(address_book);
}
// Check if address book is shared with user
let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?;
if shares.iter().any(|(shared_user, _)| shared_user == user_id) {
return Ok(address_book);
}
Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Access denied to address book"))
}
/// Helper to check write access
async fn check_write_access(&self, address_book_id: &Uuid, user_id: &str) -> Result<AddressBook, DomainError> {
let address_book = self.address_book_repository
.get_address_book_by_id(address_book_id)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
// Owner always has write access
if address_book.owner_id == user_id {
return Ok(address_book);
}
// Check shares for write permission
let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?;
if shares.iter().any(|(shared_user, can_write)| shared_user == user_id && *can_write) {
return Ok(address_book);
}
Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Write access denied"))
}
/// Convert EmailDto to domain Email
fn dto_to_email(dto: EmailDto) -> Email {
Email {
email: dto.email,
r#type: dto.r#type,
is_primary: dto.is_primary,
}
}
/// Convert PhoneDto to domain Phone
fn dto_to_phone(dto: PhoneDto) -> Phone {
Phone {
number: dto.number,
r#type: dto.r#type,
is_primary: dto.is_primary,
}
}
/// Convert AddressDto to domain Address
fn dto_to_address(dto: AddressDto) -> Address {
Address {
street: dto.street,
city: dto.city,
state: dto.state,
postal_code: dto.postal_code,
country: dto.country,
r#type: dto.r#type,
is_primary: dto.is_primary,
}
}
/// Generate vCard from contact data
fn generate_vcard(contact: &Contact) -> String {
let mut vcard = String::from("BEGIN:VCARD\nVERSION:3.0\n");
if let Some(ref full_name) = contact.full_name {
vcard.push_str(&format!("FN:{}\n", full_name));
}
if contact.first_name.is_some() || contact.last_name.is_some() {
let last = contact.last_name.as_deref().unwrap_or("");
let first = contact.first_name.as_deref().unwrap_or("");
vcard.push_str(&format!("N:{};{};;;\n", last, first));
}
if let Some(ref nickname) = contact.nickname {
vcard.push_str(&format!("NICKNAME:{}\n", nickname));
}
for email in &contact.email {
vcard.push_str(&format!("EMAIL;TYPE={}:{}\n", email.r#type.to_uppercase(), email.email));
}
for phone in &contact.phone {
vcard.push_str(&format!("TEL;TYPE={}:{}\n", phone.r#type.to_uppercase(), phone.number));
}
if let Some(ref org) = contact.organization {
vcard.push_str(&format!("ORG:{}\n", org));
}
if let Some(ref title) = contact.title {
vcard.push_str(&format!("TITLE:{}\n", title));
}
if let Some(ref notes) = contact.notes {
vcard.push_str(&format!("NOTE:{}\n", notes));
}
vcard.push_str(&format!("UID:{}\n", contact.uid));
vcard.push_str("END:VCARD\n");
vcard
}
}
#[async_trait]
impl AddressBookUseCase for ContactStorageAdapter {
async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result<AddressBookDto, DomainError> {
let address_book = AddressBook {
id: Uuid::new_v4(),
name: dto.name,
owner_id: dto.owner_id,
description: dto.description,
color: dto.color,
is_public: dto.is_public.unwrap_or(false),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let created = self.address_book_repository.create_address_book(address_book).await?;
Ok(AddressBookDto::from(created))
}
async fn update_address_book(&self, address_book_id: &str, update: UpdateAddressBookDto) -> Result<AddressBookDto, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Check write access
let mut address_book = self.check_write_access(&uuid, &update.user_id).await?;
if let Some(name) = update.name {
address_book.name = name;
}
if let Some(description) = update.description {
address_book.description = Some(description);
}
if let Some(color) = update.color {
address_book.color = Some(color);
}
if let Some(is_public) = update.is_public {
address_book.is_public = is_public;
}
address_book.updated_at = chrono::Utc::now();
let updated = self.address_book_repository.update_address_book(address_book).await?;
Ok(AddressBookDto::from(updated))
}
async fn delete_address_book(&self, address_book_id: &str, user_id: &str) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Only owner can delete
let address_book = self.address_book_repository
.get_address_book_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
if address_book.owner_id != user_id {
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can delete address book"));
}
self.address_book_repository.delete_address_book(&uuid).await
}
async fn get_address_book(&self, address_book_id: &str, user_id: &str) -> Result<AddressBookDto, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
let address_book = self.check_address_book_access(&uuid, user_id).await?;
Ok(AddressBookDto::from(address_book))
}
async fn list_user_address_books(&self, user_id: &str) -> Result<Vec<AddressBookDto>, DomainError> {
let owned = self.address_book_repository.get_address_books_by_owner(user_id).await?;
let shared = self.address_book_repository.get_shared_address_books(user_id).await?;
let mut all_books: Vec<AddressBook> = owned;
all_books.extend(shared);
Ok(all_books.into_iter().map(AddressBookDto::from).collect())
}
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError> {
let public = self.address_book_repository.get_public_address_books().await?;
Ok(public.into_iter().map(AddressBookDto::from).collect())
}
async fn share_address_book(&self, dto: ShareAddressBookDto, user_id: &str) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Only owner can share
let address_book = self.address_book_repository
.get_address_book_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
if address_book.owner_id != user_id {
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can share"));
}
self.address_book_repository.share_address_book(&uuid, &dto.user_id, dto.can_write).await
}
async fn unshare_address_book(&self, dto: UnshareAddressBookDto, user_id: &str) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Only owner can unshare
let address_book = self.address_book_repository
.get_address_book_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
if address_book.owner_id != user_id {
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can unshare"));
}
self.address_book_repository.unshare_address_book(&uuid, &dto.user_id).await
}
async fn get_address_book_shares(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, bool)>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Only owner can view shares
let address_book = self.address_book_repository
.get_address_book_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?;
if address_book.owner_id != user_id {
return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can view shares"));
}
self.address_book_repository.get_address_book_shares(&uuid).await
}
}
#[async_trait]
impl ContactUseCase for ContactStorageAdapter {
async fn create_contact(&self, dto: CreateContactDto) -> Result<ContactDto, DomainError> {
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Check write access
self.check_write_access(&address_book_id, &dto.user_id).await?;
let contact = Contact {
id: Uuid::new_v4(),
address_book_id,
uid: format!("{}@oxicloud", Uuid::new_v4()),
full_name: dto.full_name,
first_name: dto.first_name,
last_name: dto.last_name,
nickname: dto.nickname,
email: dto.email.into_iter().map(Self::dto_to_email).collect(),
phone: dto.phone.into_iter().map(Self::dto_to_phone).collect(),
address: dto.address.into_iter().map(Self::dto_to_address).collect(),
organization: dto.organization,
title: dto.title,
notes: dto.notes,
photo_url: dto.photo_url,
birthday: dto.birthday,
anniversary: dto.anniversary,
vcard: String::new(),
etag: Uuid::new_v4().to_string(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
// Generate vCard
let mut contact_with_vcard = contact;
contact_with_vcard.vcard = Self::generate_vcard(&contact_with_vcard);
let created = self.contact_repository.create_contact(contact_with_vcard).await?;
Ok(ContactDto::from(created))
}
async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result<ContactDto, DomainError> {
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Check write access
self.check_write_access(&address_book_id, &dto.user_id).await?;
// Parse vCard - for now, create a basic contact with the raw vCard
let contact = Contact {
id: Uuid::new_v4(),
address_book_id,
uid: format!("{}@oxicloud", Uuid::new_v4()),
full_name: Some("Imported Contact".to_string()),
first_name: None,
last_name: None,
nickname: None,
email: Vec::new(),
phone: Vec::new(),
address: Vec::new(),
organization: None,
title: None,
notes: None,
photo_url: None,
birthday: None,
anniversary: None,
vcard: dto.vcard,
etag: Uuid::new_v4().to_string(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let created = self.contact_repository.create_contact(contact).await?;
Ok(ContactDto::from(created))
}
async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result<ContactDto, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
let mut contact = self.contact_repository
.get_contact_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
// Check write access to the address book
self.check_write_access(&contact.address_book_id, &update.user_id).await?;
if let Some(full_name) = update.full_name {
contact.full_name = Some(full_name);
}
if let Some(first_name) = update.first_name {
contact.first_name = Some(first_name);
}
if let Some(last_name) = update.last_name {
contact.last_name = Some(last_name);
}
if let Some(nickname) = update.nickname {
contact.nickname = Some(nickname);
}
if let Some(emails) = update.email {
contact.email = emails.into_iter().map(Self::dto_to_email).collect();
}
if let Some(phones) = update.phone {
contact.phone = phones.into_iter().map(Self::dto_to_phone).collect();
}
if let Some(addresses) = update.address {
contact.address = addresses.into_iter().map(Self::dto_to_address).collect();
}
if let Some(organization) = update.organization {
contact.organization = Some(organization);
}
if let Some(title) = update.title {
contact.title = Some(title);
}
if let Some(notes) = update.notes {
contact.notes = Some(notes);
}
if let Some(photo_url) = update.photo_url {
contact.photo_url = Some(photo_url);
}
if let Some(birthday) = update.birthday {
contact.birthday = Some(birthday);
}
if let Some(anniversary) = update.anniversary {
contact.anniversary = Some(anniversary);
}
contact.updated_at = chrono::Utc::now();
contact.etag = Uuid::new_v4().to_string();
contact.vcard = Self::generate_vcard(&contact);
let updated = self.contact_repository.update_contact(contact).await?;
Ok(ContactDto::from(updated))
}
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
let contact = self.contact_repository
.get_contact_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
// Check write access
self.check_write_access(&contact.address_book_id, user_id).await?;
self.contact_repository.delete_contact(&uuid).await
}
async fn get_contact(&self, contact_id: &str, user_id: &str) -> Result<ContactDto, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
let contact = self.contact_repository
.get_contact_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
// Check read access
self.check_address_book_access(&contact.address_book_id, user_id).await?;
Ok(ContactDto::from(contact))
}
async fn list_contacts(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Check read access
self.check_address_book_access(&uuid, user_id).await?;
let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?;
Ok(contacts.into_iter().map(ContactDto::from).collect())
}
async fn search_contacts(&self, address_book_id: &str, query: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Check read access
self.check_address_book_access(&uuid, user_id).await?;
let contacts = self.contact_repository.search_contacts(&uuid, query).await?;
Ok(contacts.into_iter().map(ContactDto::from).collect())
}
async fn create_group(&self, dto: CreateContactGroupDto) -> Result<ContactGroupDto, DomainError> {
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Check write access
self.check_write_access(&address_book_id, &dto.user_id).await?;
let group = ContactGroup {
id: Uuid::new_v4(),
address_book_id,
name: dto.name,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let created = self.group_repository.create_group(group).await?;
Ok(ContactGroupDto::from(created))
}
async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result<ContactGroupDto, DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
let mut group = self.group_repository
.get_group_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
// Check write access
self.check_write_access(&group.address_book_id, &update.user_id).await?;
group.name = update.name;
group.updated_at = chrono::Utc::now();
let updated = self.group_repository.update_group(group).await?;
Ok(ContactGroupDto::from(updated))
}
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
let group = self.group_repository
.get_group_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
// Check write access
self.check_write_access(&group.address_book_id, user_id).await?;
self.group_repository.delete_group(&uuid).await
}
async fn get_group(&self, group_id: &str, user_id: &str) -> Result<ContactGroupDto, DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
let group = self.group_repository
.get_group_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
// Check read access
self.check_address_book_access(&group.address_book_id, user_id).await?;
Ok(ContactGroupDto::from(group))
}
async fn list_groups(&self, address_book_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Check read access
self.check_address_book_access(&uuid, user_id).await?;
let groups = self.group_repository.get_groups_by_address_book(&uuid).await?;
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
}
async fn add_contact_to_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> {
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
let group = self.group_repository
.get_group_by_id(&group_id)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
// Check write access
self.check_write_access(&group.address_book_id, user_id).await?;
self.group_repository.add_contact_to_group(&group_id, &contact_id).await
}
async fn remove_contact_from_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> {
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
let group = self.group_repository
.get_group_by_id(&group_id)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
// Check write access
self.check_write_access(&group.address_book_id, user_id).await?;
self.group_repository.remove_contact_from_group(&group_id, &contact_id).await
}
async fn list_contacts_in_group(&self, group_id: &str, user_id: &str) -> Result<Vec<ContactDto>, DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
let group = self.group_repository
.get_group_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?;
// Check read access
self.check_address_book_access(&group.address_book_id, user_id).await?;
let contacts = self.group_repository.get_contacts_in_group(&uuid).await?;
Ok(contacts.into_iter().map(ContactDto::from).collect())
}
async fn list_groups_for_contact(&self, contact_id: &str, user_id: &str) -> Result<Vec<ContactGroupDto>, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
let contact = self.contact_repository
.get_contact_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
// Check read access
self.check_address_book_access(&contact.address_book_id, user_id).await?;
let groups = self.group_repository.get_groups_for_contact(&uuid).await?;
Ok(groups.into_iter().map(ContactGroupDto::from).collect())
}
async fn get_contact_vcard(&self, contact_id: &str, user_id: &str) -> Result<String, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
let contact = self.contact_repository
.get_contact_by_id(&uuid)
.await?
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
// Check read access
self.check_address_book_access(&contact.address_book_id, user_id).await?;
Ok(contact.vcard)
}
async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result<Vec<(String, String)>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Check read access
self.check_address_book_access(&uuid, user_id).await?;
let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?;
Ok(contacts
.into_iter()
.map(|c| (c.id.to_string(), c.vcard))
.collect())
}
}
+11
View File
@@ -0,0 +1,11 @@
//! Infrastructure Adapters
//!
//! This module contains adapters that bridge the gap between domain repositories
//! and application ports. These adapters implement the application layer ports
//! using the infrastructure layer repositories.
pub mod calendar_storage_adapter;
pub mod contact_storage_adapter;
pub use calendar_storage_adapter::CalendarStorageAdapter;
pub use contact_storage_adapter::ContactStorageAdapter;
+1
View File
@@ -1,3 +1,4 @@
pub mod adapters;
pub mod repositories;
pub mod services;
@@ -16,39 +16,30 @@ use crate::common::config::AppConfig;
/// Implementación de repositorio para operaciones de lectura de archivos
pub struct FileFsReadRepository {
root_path: PathBuf,
metadata_manager: Arc<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
}
impl FileFsReadRepository {
/// Crea un nuevo repositorio de lectura de archivos
pub fn new(
root_path: PathBuf,
_root_path: PathBuf,
metadata_manager: Arc<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
_config: AppConfig,
_parallel_processor: Option<Arc<ParallelFileProcessor>>,
) -> Self {
Self {
root_path,
metadata_manager,
path_resolver,
config,
parallel_processor,
}
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
root_path: PathBuf::from("./storage"),
metadata_manager: Arc::new(FileMetadataManager::default()),
path_resolver: Arc::new(FilePathResolver::default_stub()),
config: AppConfig::default(),
parallel_processor: None,
}
}
@@ -22,7 +22,8 @@ use crate::application::services::storage_mediator::StorageMediator;
// use crate::application::ports::outbound::IdMappingPort;
use crate::infrastructure::services::id_mapping_service::IdMappingError;
use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType};
use crate::domain::services::path_service::{StoragePath, PathService};
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::services::path_service::PathService;
use crate::common::errors::DomainError;
use crate::common::config::AppConfig;
use crate::application::ports::outbound::FileStoragePort;
@@ -63,7 +64,6 @@ pub struct FileFsRepository {
impl FileFsRepository {
/// Creates a new filesystem-based file repository
#[allow(dead_code)]
pub fn new(
root_path: PathBuf,
storage_mediator: Arc<dyn StorageMediator>,
@@ -389,14 +389,6 @@ impl From<IdMappingError> for FileRepositoryError {
}
}
// Add Timeout variant to FileRepositoryError
impl FileRepositoryError {
#[allow(dead_code)]
fn timeout(message: impl Into<String>) -> Self {
FileRepositoryError::Timeout(message.into())
}
}
// Errors are already defined by the FileRepositoryError interface
// Enable cloning for concurrent operations
@@ -16,43 +16,34 @@ use crate::infrastructure::services::file_system_utils::FileSystemUtils;
/// Implementación de repositorio para operaciones de escritura de archivos
pub struct FileFsWriteRepository {
root_path: PathBuf,
metadata_manager: Arc<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
storage_mediator: Arc<dyn StorageMediator>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
}
impl FileFsWriteRepository {
/// Crea un nuevo repositorio de escritura de archivos
pub fn new(
root_path: PathBuf,
_root_path: PathBuf,
metadata_manager: Arc<FileMetadataManager>,
path_resolver: Arc<FilePathResolver>,
storage_mediator: Arc<dyn StorageMediator>,
_storage_mediator: Arc<dyn StorageMediator>,
config: AppConfig,
parallel_processor: Option<Arc<ParallelFileProcessor>>,
_parallel_processor: Option<Arc<ParallelFileProcessor>>,
) -> Self {
Self {
root_path,
metadata_manager,
path_resolver,
storage_mediator,
config,
parallel_processor,
}
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
root_path: PathBuf::from("./storage"),
metadata_manager: Arc::new(FileMetadataManager::default()),
path_resolver: Arc::new(FilePathResolver::default_stub()),
storage_mediator: Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub()),
config: AppConfig::default(),
parallel_processor: None,
}
}
@@ -108,13 +99,6 @@ impl FileFsWriteRepository {
.map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string()))
}
}
/// Elimina un archivo de forma no bloqueante
async fn delete_file_non_blocking(&self, _abs_path: PathBuf) -> FileRepositoryResult<()> {
// Implementación real debe eliminar el archivo
// Por ahora, devolvemos OK
Ok(())
}
}
#[async_trait]
@@ -2,7 +2,8 @@ use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use crate::domain::services::path_service::{PathService, StoragePath};
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::services::path_service::PathService;
use crate::application::services::storage_mediator::StorageMediator;
// use crate::application::ports::outbound::IdMappingPort;
use crate::domain::repositories::file_repository::FileRepositoryError;
@@ -10,7 +10,8 @@ use crate::domain::entities::folder::{Folder, FolderError};
use crate::domain::repositories::folder_repository::{
FolderRepository, FolderRepositoryError, FolderRepositoryResult
};
use crate::domain::services::path_service::{StoragePath, PathService};
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::services::path_service::PathService;
// use crate::application::ports::outbound::IdMappingPort;
use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMappingError};
use crate::application::services::storage_mediator::StorageMediator;
@@ -51,7 +52,6 @@ impl FolderFsRepository {
/// Creates a stub repository for initialization purposes
/// This is used temporarily during dependency injection setup
#[allow(dead_code)]
pub fn new_stub() -> Self {
let root_path = PathBuf::from("/tmp");
let path_service = Arc::new(PathService::new(root_path.clone()));
@@ -422,22 +422,6 @@ impl ParallelFileProcessor {
info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024));
Ok(())
}
/// Writes a chunk to a file at a specific position
#[allow(dead_code)]
async fn write_chunk_optimized(
file: &mut File,
offset: u64,
data: Bytes
) -> Result<(), std::io::Error> {
// Prepare writing at the correct position
file.seek(SeekFrom::Start(offset)).await?;
// Write data without additional copies
file.write_all(&data).await?;
Ok(())
}
}
#[cfg(test)]
@@ -5,7 +5,7 @@ use std::sync::Arc;
use crate::domain::entities::contact::AddressBook;
use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult};
use crate::common::errors::{DomainError, ErrorContext};
use crate::common::errors::DomainError;
pub struct AddressBookPgRepository {
pool: Arc<PgPool>,
@@ -15,11 +15,6 @@ impl AddressBookPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
// Método auxiliar para mapear errores SQL
fn map_error<T>(err: sqlx::Error) -> Result<T, DomainError> {
Err(DomainError::database_error(err.to_string()))
}
}
#[async_trait]
@@ -409,242 +409,4 @@ impl CalendarEventRepository for CalendarEventPgRepository {
Ok(events)
}
}
// Additional methods not part of the trait
impl CalendarEventPgRepository {
// Helper method to get event by ID
async fn get_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
let row_opt = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
FROM caldav.calendar_events
WHERE id = $1
"#
)
.bind(id)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?;
if let Some(row) = row_opt {
// En una implementación real, construiríamos un objeto CalendarEvent completo
// Este es un ejemplo simplificado
let event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
row.get::<Option<String>, _>("description"),
row.get::<Option<String>, _>("location"),
row.get("start_time"),
row.get("end_time"),
row.get("all_day"),
row.get::<Option<String>, _>("rrule"),
row.get("ical_uid"),
row.get("ical_data"),
row.get("created_at"),
row.get("updated_at")
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
return Ok(Some(event));
}
Ok(None)
}
// Helper method to get event by UID
async fn get_event_by_uid(&self, calendar_id: &Uuid, uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
let row_opt = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
FROM caldav.calendar_events
WHERE calendar_id = $1 AND ical_uid = $2
"#
)
.bind(calendar_id)
.bind(uid)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by UID: {}", e)))?;
if let Some(_row) = row_opt {
// En una implementación real, construiríamos un objeto CalendarEvent a partir de la fila
// Por simplicidad, devolvemos None como ejemplo
return Ok(None);
}
Ok(None)
}
// Helper method to get events by calendar
async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let _rows = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
FROM caldav.calendar_events
WHERE calendar_id = $1
ORDER BY start_time
"#
)
.bind(calendar_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get events by calendar: {}", e)))?;
// En una implementación real, mapearíamos cada fila a un objeto CalendarEvent
// Este es un ejemplo simplificado que devuelve una lista vacía
let events = Vec::new();
// Ejemplo de cómo sería el mapeo real:
// for row in rows {
// let event = CalendarEvent::with_id(
// row.get("id"),
// row.get("calendar_id"),
// row.get("summary"),
// // ... otros campos
// );
// events.push(event);
// }
Ok(events)
}
// Helper method to get changed events
async fn get_changed_events(
&self,
calendar_id: &Uuid,
since: &DateTime<Utc>
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let _rows = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
FROM caldav.calendar_events
WHERE calendar_id = $1 AND updated_at > $2
ORDER BY updated_at
"#
)
.bind(calendar_id)
.bind(since)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get changed events: {}", e)))?;
// En una implementación real, mapearíamos cada fila a un objeto CalendarEvent
// Este es un ejemplo simplificado que devuelve una lista vacía
let events = Vec::new();
// Ejemplo de cómo sería el mapeo real:
// for row in rows {
// let event = CalendarEvent::with_id(
// row.get("id"),
// row.get("calendar_id"),
// row.get("summary"),
// row.get::<Option<String>, _>("description"),
// row.get::<Option<String>, _>("location"),
// row.get("start_time"),
// row.get("end_time"),
// row.get("all_day"),
// row.get::<Option<String>, _>("rrule"),
// row.get("ical_uid"),
// row.get("ical_data"),
// row.get("created_at"),
// row.get("updated_at")
// ).unwrap();
// events.push(event);
// }
Ok(events)
}
// Helper method to add an attendee to an event
async fn add_event_attendee(
&self,
event_id: &Uuid,
email: &str,
name: Option<&str>,
role: &str,
status: &str
) -> CalendarEventRepositoryResult<()> {
sqlx::query(
r#"
INSERT INTO caldav.calendar_event_attendees (event_id, email, name, role, status)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (event_id, email) DO UPDATE
SET name = $3, role = $4, status = $5
"#
)
.bind(event_id)
.bind(email)
.bind(name)
.bind(role)
.bind(status)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to add event attendee: {}", e)))?;
Ok(())
}
// Helper method to remove an attendee from an event
async fn remove_event_attendee(
&self,
event_id: &Uuid,
email: &str
) -> CalendarEventRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM caldav.calendar_event_attendees
WHERE event_id = $1 AND email = $2
"#
)
.bind(event_id)
.bind(email)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to remove event attendee: {}", e)))?;
Ok(())
}
// Helper method to get all attendees for an event
async fn get_event_attendees(
&self,
event_id: &Uuid
) -> CalendarEventRepositoryResult<Vec<(String, Option<String>, String, String)>> {
let rows = sqlx::query(
r#"
SELECT email, name, role, status
FROM caldav.calendar_event_attendees
WHERE event_id = $1
ORDER BY email
"#
)
.bind(event_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get event attendees: {}", e)))?;
let mut attendees = Vec::new();
for row in rows {
let email: String = row.get("email");
let name: Option<String> = row.get("name");
let role: String = row.get("role");
let status: String = row.get("status");
attendees.push((email, name, role, status));
}
Ok(attendees)
}
}
@@ -1,12 +1,11 @@
use async_trait::async_trait;
use chrono::Utc;
use sqlx::{PgPool, query, query_as, Row, types::Uuid};
use sqlx::{PgPool, Row, types::Uuid};
use std::sync::Arc;
use crate::domain::entities::calendar::Calendar;
use crate::domain::repositories::calendar_repository::{CalendarRepository, CalendarRepositoryResult};
use crate::common::errors::{DomainError, ErrorContext};
use sqlx::Transaction;
use crate::common::errors::DomainError;
pub struct CalendarPgRepository {
pool: Arc<PgPool>,
@@ -1,224 +0,0 @@
use async_trait::async_trait;
use sqlx::{PgPool, types::Uuid};
use std::sync::Arc;
use crate::common::errors::DomainError;
use crate::domain::entities::contact::{ContactGroup, Contact};
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepositoryResult};
pub struct ContactGroupPgRepository {
pool: Arc<PgPool>,
}
impl ContactGroupPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl ContactGroupRepository for ContactGroupPgRepository {
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
let _row = sqlx::query(
r#"
INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, address_book_id, name, created_at, updated_at
"#
)
.bind(group.id)
.bind(group.address_book_id)
.bind(&group.name)
.bind(group.created_at)
.bind(group.updated_at)
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?;
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
// Por simplicidad, devolvemos el grupo original
Ok(group)
}
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
let _row = sqlx::query(
r#"
UPDATE carddav.contact_groups
SET name = $3, updated_at = $4
WHERE id = $1 AND address_book_id = $2
RETURNING id, address_book_id, name, created_at, updated_at
"#
)
.bind(group.id)
.bind(group.address_book_id)
.bind(&group.name)
.bind(group.updated_at)
.fetch_one(&*self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => DomainError::not_found("Contact group", group.id.to_string()),
_ => DomainError::database_error(format!("Failed to update contact group: {}", e)),
})?;
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
// Por simplicidad, devolvemos el grupo original
Ok(group)
}
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
// Begin transaction
let mut tx = self.pool.begin().await
.map_err(|e| DomainError::database_error(format!("Failed to begin transaction: {}", e)))?;
// Delete group memberships
sqlx::query(
r#"DELETE FROM carddav.contact_group_members WHERE group_id = $1"#
)
.bind(id)
.execute(&mut *tx)
.await
.map_err(|e| DomainError::database_error(format!("Failed to delete group memberships: {}", e)))?;
// Delete the group
sqlx::query(
r#"DELETE FROM carddav.contact_groups WHERE id = $1"#
)
.bind(id)
.execute(&mut *tx)
.await
.map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?;
// Commit transaction
tx.commit().await
.map_err(|e| DomainError::database_error(format!("Failed to commit transaction: {}", e)))?;
Ok(())
}
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
let row_opt = sqlx::query(
r#"
SELECT id, address_book_id, name, created_at, updated_at
FROM carddav.contact_groups
WHERE id = $1
"#
)
.bind(id)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contact group: {}", e)))?;
if let Some(row) = row_opt {
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
// Para esta demostración, devolvemos un grupo predeterminado con el ID correcto
let mut group = ContactGroup::default();
group.id = id.clone();
return Ok(Some(group));
}
Ok(None)
}
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
let _rows = sqlx::query(
r#"
SELECT id, address_book_id, name, created_at, updated_at
FROM carddav.contact_groups
WHERE address_book_id = $1
ORDER BY name
"#
)
.bind(address_book_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?;
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
// Por simplicidad, devolvemos una lista vacía
let groups = Vec::new();
Ok(groups)
}
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
// Check if the membership already exists
let row_opt = sqlx::query(
r#"
SELECT 1 FROM carddav.contact_group_members
WHERE group_id = $1 AND contact_id = $2
"#
)
.bind(group_id)
.bind(contact_id)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to check group membership: {}", e)))?;
let exists = row_opt.is_some();
if !exists {
sqlx::query(
r#"
INSERT INTO carddav.contact_group_members (group_id, contact_id)
VALUES ($1, $2)
"#
)
.bind(group_id)
.bind(contact_id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?;
}
Ok(())
}
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM carddav.contact_group_members
WHERE group_id = $1 AND contact_id = $2
"#
)
.bind(group_id)
.bind(contact_id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?;
Ok(())
}
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
// En lugar de implementar toda la lógica compleja que requiere query!, simplificamos
// Devolvemos una lista vacía por simplicidad para evitar el uso de macros SQLx
// Para una implementación real, deberíamos convertir cada query! a sqlx::query
// y manejar la conversión de resultados manualmente
Ok(Vec::new())
}
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
let _rows = sqlx::query(
r#"
SELECT
g.id, g.address_book_id, g.name, g.created_at, g.updated_at
FROM carddav.contact_groups g
JOIN carddav.contact_group_members m ON g.id = m.group_id
WHERE m.contact_id = $1
ORDER BY g.name
"#
)
.bind(contact_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?;
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
// Por simplicidad y demostración, devolvemos una lista vacía
let groups = Vec::new();
Ok(groups)
}
}
@@ -0,0 +1,129 @@
//! Persistence DTOs for Contact entities
//!
//! These DTOs are used for JSONB serialization/deserialization in PostgreSQL.
//! They mirror the domain entities but include serde traits required for persistence.
//! This keeps the domain layer free of infrastructure concerns (serde dependency).
use serde::{Deserialize, Serialize};
use crate::domain::entities::contact::{Email, Phone, Address};
/// Persistence DTO for Email - used for JSONB serialization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailPersistenceDto {
pub email: String,
pub r#type: String,
pub is_primary: bool,
}
impl From<&Email> for EmailPersistenceDto {
fn from(email: &Email) -> Self {
Self {
email: email.email.clone(),
r#type: email.r#type.clone(),
is_primary: email.is_primary,
}
}
}
impl From<EmailPersistenceDto> for Email {
fn from(dto: EmailPersistenceDto) -> Self {
Self {
email: dto.email,
r#type: dto.r#type,
is_primary: dto.is_primary,
}
}
}
/// Persistence DTO for Phone - used for JSONB serialization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhonePersistenceDto {
pub number: String,
pub r#type: String,
pub is_primary: bool,
}
impl From<&Phone> for PhonePersistenceDto {
fn from(phone: &Phone) -> Self {
Self {
number: phone.number.clone(),
r#type: phone.r#type.clone(),
is_primary: phone.is_primary,
}
}
}
impl From<PhonePersistenceDto> for Phone {
fn from(dto: PhonePersistenceDto) -> Self {
Self {
number: dto.number,
r#type: dto.r#type,
is_primary: dto.is_primary,
}
}
}
/// Persistence DTO for Address - used for JSONB serialization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddressPersistenceDto {
pub street: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub country: Option<String>,
pub r#type: String,
pub is_primary: bool,
}
impl From<&Address> for AddressPersistenceDto {
fn from(addr: &Address) -> Self {
Self {
street: addr.street.clone(),
city: addr.city.clone(),
state: addr.state.clone(),
postal_code: addr.postal_code.clone(),
country: addr.country.clone(),
r#type: addr.r#type.clone(),
is_primary: addr.is_primary,
}
}
}
impl From<AddressPersistenceDto> for Address {
fn from(dto: AddressPersistenceDto) -> Self {
Self {
street: dto.street,
city: dto.city,
state: dto.state,
postal_code: dto.postal_code,
country: dto.country,
r#type: dto.r#type,
is_primary: dto.is_primary,
}
}
}
/// Helper functions to convert collections
pub fn emails_to_persistence(emails: &[Email]) -> Vec<EmailPersistenceDto> {
emails.iter().map(EmailPersistenceDto::from).collect()
}
pub fn emails_from_persistence(dtos: Vec<EmailPersistenceDto>) -> Vec<Email> {
dtos.into_iter().map(Email::from).collect()
}
pub fn phones_to_persistence(phones: &[Phone]) -> Vec<PhonePersistenceDto> {
phones.iter().map(PhonePersistenceDto::from).collect()
}
pub fn phones_from_persistence(dtos: Vec<PhonePersistenceDto>) -> Vec<Phone> {
dtos.into_iter().map(Phone::from).collect()
}
pub fn addresses_to_persistence(addresses: &[Address]) -> Vec<AddressPersistenceDto> {
addresses.iter().map(AddressPersistenceDto::from).collect()
}
pub fn addresses_from_persistence(dtos: Vec<AddressPersistenceDto>) -> Vec<Address> {
dtos.into_iter().map(Address::from).collect()
}
@@ -1,12 +1,13 @@
use async_trait::async_trait;
use chrono::Utc;
use sqlx::{PgPool, query, query_as, types::Uuid};
use sqlx::{PgPool, types::Uuid};
use std::sync::Arc;
use serde_json::Value as JsonValue;
use crate::domain::entities::contact::{Contact, ContactGroup};
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository, ContactRepositoryResult};
use crate::common::errors::{DomainError, ErrorContext};
use crate::domain::entities::contact::Contact;
use crate::domain::repositories::contact_repository::{ContactRepository, ContactRepositoryResult};
use crate::common::errors::DomainError;
use super::contact_persistence_dto::{emails_to_persistence, phones_to_persistence, addresses_to_persistence};
pub struct ContactPgRepository {
pool: Arc<PgPool>,
@@ -21,12 +22,16 @@ impl ContactPgRepository {
#[async_trait]
impl ContactRepository for ContactPgRepository {
async fn create_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
// Convert complex fields to JSON
let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null);
let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null);
let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null);
// Convert domain entities to persistence DTOs for JSONB serialization
let email_dtos = emails_to_persistence(&contact.email);
let phone_dtos = phones_to_persistence(&contact.phone);
let address_dtos = addresses_to_persistence(&contact.address);
let row = sqlx::query(
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
let _row = sqlx::query(
r#"
INSERT INTO carddav.contacts (
id, address_book_id, uid, full_name, first_name, last_name, nickname,
@@ -74,16 +79,20 @@ impl ContactRepository for ContactPgRepository {
async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult<Contact> {
let now = Utc::now();
// Convert complex fields to JSON
let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null);
let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null);
let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null);
// Convert domain entities to persistence DTOs for JSONB serialization
let email_dtos = emails_to_persistence(&contact.email);
let phone_dtos = phones_to_persistence(&contact.phone);
let address_dtos = addresses_to_persistence(&contact.address);
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
// Create a clone of the contact with the updated timestamp
let mut updated_contact = contact.clone();
updated_contact.updated_at = now;
let row = sqlx::query(
let _row = sqlx::query(
r#"
UPDATE carddav.contacts
SET
@@ -312,205 +321,4 @@ impl ContactRepository for ContactPgRepository {
Ok(contacts)
}
}
pub struct ContactGroupPgRepository {
pool: Arc<PgPool>,
}
impl ContactGroupPgRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl ContactGroupRepository for ContactGroupPgRepository {
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
let _row = sqlx::query(
r#"
INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, address_book_id, name, created_at, updated_at
"#
)
.bind(group.id)
.bind(group.address_book_id)
.bind(&group.name)
.bind(group.created_at)
.bind(group.updated_at)
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?;
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
// Por simplicidad, devolvemos el grupo original
Ok(group)
}
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
let now = Utc::now();
// Create a clone of the group with updated timestamp
let mut updated_group = group.clone();
updated_group.updated_at = now;
let _row = sqlx::query(
r#"
UPDATE carddav.contact_groups
SET name = $1, updated_at = $2
WHERE id = $3
RETURNING id, address_book_id, name, created_at, updated_at
"#
)
.bind(&updated_group.name)
.bind(now)
.bind(updated_group.id)
.fetch_one(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to update contact group: {}", e)))?;
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
// Por simplicidad, devolvemos el grupo con el timestamp actualizado
Ok(updated_group)
}
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM carddav.contact_groups
WHERE id = $1
"#
)
.bind(id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?;
Ok(())
}
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
let row_opt = sqlx::query(
r#"
SELECT id, address_book_id, name, created_at, updated_at
FROM carddav.contact_groups
WHERE id = $1
"#
)
.bind(id)
.fetch_optional(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contact group by id: {}", e)))?;
if let Some(_row) = row_opt {
// En una implementación real, construiríamos un objeto ContactGroup a partir de la fila
// Por simplicidad y demostración, devolvemos una instancia predeterminada
return Ok(Some(ContactGroup::default()));
}
Ok(None)
}
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
let _rows = sqlx::query(
r#"
SELECT id, address_book_id, name, created_at, updated_at
FROM carddav.contact_groups
WHERE address_book_id = $1
ORDER BY name
"#
)
.bind(address_book_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?;
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
// Por simplicidad y demostración, devolvemos una lista vacía
let groups = Vec::new();
Ok(groups)
}
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
sqlx::query(
r#"
INSERT INTO carddav.group_memberships (group_id, contact_id)
VALUES ($1, $2)
ON CONFLICT (group_id, contact_id) DO NOTHING
"#
)
.bind(group_id)
.bind(contact_id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?;
Ok(())
}
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM carddav.group_memberships
WHERE group_id = $1 AND contact_id = $2
"#
)
.bind(group_id)
.bind(contact_id)
.execute(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?;
Ok(())
}
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
let _rows = sqlx::query(
r#"
SELECT
c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname,
c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url,
c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at
FROM carddav.contacts c
INNER JOIN carddav.group_memberships m ON c.id = m.contact_id
WHERE m.group_id = $1
ORDER BY c.full_name, c.first_name, c.last_name
"#
)
.bind(group_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get contacts in group: {}", e)))?;
// En una implementación real, construiríamos objetos Contact a partir de las filas
// Por simplicidad y demostración, devolvemos una lista vacía
let contacts = Vec::new();
Ok(contacts)
}
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
let _rows = sqlx::query(
r#"
SELECT
g.id, g.address_book_id, g.name, g.created_at, g.updated_at
FROM carddav.contact_groups g
INNER JOIN carddav.group_memberships m ON g.id = m.group_id
WHERE m.contact_id = $1
ORDER BY g.name
"#
)
.bind(contact_id)
.fetch_all(&*self.pool)
.await
.map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?;
// En una implementación real, construiríamos objetos ContactGroup a partir de las filas
// Por simplicidad y demostración, devolvemos una lista vacía
let groups = Vec::new();
Ok(groups)
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ mod address_book_pg_repository;
mod calendar_pg_repository;
mod calendar_event_pg_repository;
mod contact_pg_repository;
mod contact_group_pg_repository;
mod contact_persistence_dto;
mod session_pg_repository;
mod transaction_utils;
mod user_pg_repository;
@@ -11,6 +11,6 @@ pub use address_book_pg_repository::AddressBookPgRepository;
pub use calendar_pg_repository::CalendarPgRepository;
pub use calendar_event_pg_repository::CalendarEventPgRepository;
pub use contact_pg_repository::ContactPgRepository;
pub use contact_group_pg_repository::ContactGroupPgRepository;
pub use contact_persistence_dto::*;
pub use session_pg_repository::SessionPgRepository;
pub use user_pg_repository::UserPgRepository;
@@ -1,4 +1,4 @@
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError, Executor};
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError};
use std::sync::Arc;
use tracing::{debug, error, info};
@@ -50,81 +50,4 @@ where
Err(e)
}
}
}
/// Variant that accepts a transaction isolation level
pub async fn with_transaction_isolation<F, T, E>(
pool: &Arc<PgPool>,
operation_name: &str,
isolation_level: TransactionIsolationLevel,
operation: F,
) -> Result<T, E>
where
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
E: From<SqlxError> + std::fmt::Display,
{
debug!("Starting database transaction with isolation level {:?} for: {}",
isolation_level, operation_name);
// Begin transaction with specific isolation level
let mut tx = pool.begin().await.map_err(|e| {
error!("Failed to begin transaction for {}: {}", operation_name, e);
E::from(e)
})?;
// Set isolation level
tx.execute(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.to_string())[..])
.await
.map_err(|e| {
error!("Failed to set isolation level for {}: {}", operation_name, e);
E::from(e)
})?;
// Execute the operation within the transaction
match operation(&mut tx).await {
Ok(result) => {
// If operation succeeds, commit the transaction
match tx.commit().await {
Ok(_) => {
debug!("Transaction committed successfully for: {}", operation_name);
Ok(result)
},
Err(e) => {
error!("Failed to commit transaction for {}: {}", operation_name, e);
Err(E::from(e))
}
}
},
Err(e) => {
// If operation fails, rollback the transaction
if let Err(rollback_err) = tx.rollback().await {
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
// Still return the original error
} else {
info!("Transaction rolled back for {}: {}", operation_name, e);
}
Err(e)
}
}
}
/// Transaction isolation levels from SQL standard
#[derive(Debug)]
pub enum TransactionIsolationLevel {
/// Read committed isolation level
ReadCommitted,
/// Repeatable read isolation level
RepeatableRead,
/// Serializable isolation level
Serializable,
}
impl ToString for TransactionIsolationLevel {
fn to_string(&self) -> String {
match self {
TransactionIsolationLevel::ReadCommitted => "READ COMMITTED".to_string(),
TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ".to_string(),
TransactionIsolationLevel::Serializable => "SERIALIZABLE".to_string(),
}
}
}
@@ -29,13 +29,12 @@ struct TrashedItemEntry {
pub struct TrashFsRepository {
trash_dir: PathBuf,
trash_index_path: PathBuf,
id_mapping_service: Arc<dyn IdMappingPort>,
}
impl TrashFsRepository {
pub fn new(
storage_root: impl AsRef<Path>,
id_mapping_service: Arc<dyn IdMappingPort>,
_id_mapping_service: Arc<dyn IdMappingPort>,
) -> Self {
let trash_dir = storage_root.as_ref().join(".trash");
let trash_index_path = trash_dir.join("trash_index.json");
@@ -43,7 +42,6 @@ impl TrashFsRepository {
Self {
trash_dir,
trash_index_path,
id_mapping_service,
}
}
@@ -224,14 +222,6 @@ impl TrashFsRepository {
deletion_date: item.deletion_date.to_rfc3339(),
}
}
/// Obtiene la ruta de un elemento en la papelera
fn get_trash_path_for_item(&self, user_id: &Uuid, item_id: &Uuid) -> PathBuf {
self.trash_dir
.join("files")
.join(user_id.to_string())
.join(item_id.to_string())
}
}
#[async_trait]
@@ -9,11 +9,9 @@ use tracing::debug;
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
/// Número máximo por defecto de buffers en el pool
#[allow(dead_code)]
pub const DEFAULT_MAX_BUFFERS: usize = 100;
/// Tiempo de vida por defecto de un buffer inactivo (en segundos)
#[allow(dead_code)]
pub const DEFAULT_BUFFER_TTL: u64 = 60;
/// Buffer pooling para optimizar operaciones de lectura/escritura
@@ -83,7 +81,6 @@ impl BufferPool {
}
/// Crea un pool con configuración por defecto
#[allow(dead_code)]
pub fn default() -> Arc<Self> {
Self::new(
DEFAULT_BUFFER_SIZE,
@@ -282,7 +279,6 @@ impl BorrowedBuffer {
}
/// Obtiene una referencia a los datos utilizados
#[allow(dead_code)]
pub fn as_slice(&self) -> &[u8] {
&self.buffer[..self.used_size]
}
@@ -302,7 +298,6 @@ impl BorrowedBuffer {
}
/// Copia datos a este buffer y actualiza el tamaño usado
#[allow(dead_code)]
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
let copy_size = min(data.len(), self.buffer.len());
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
@@ -311,7 +306,6 @@ impl BorrowedBuffer {
}
/// Impide que el buffer se devuelva al pool al destruirse
#[allow(dead_code)]
pub fn do_not_return(mut self) -> Self {
self.return_to_pool = false;
self
@@ -323,7 +317,6 @@ impl BorrowedBuffer {
}
/// Obtiene el tamaño usado del buffer
#[allow(dead_code)]
pub fn used_size(&self) -> usize {
self.used_size
}
@@ -8,7 +8,6 @@ use tokio::sync::RwLock;
/// Representación de metadatos en caché
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CachedMetadata {
/// Si el archivo o directorio existe
pub exists: bool,
@@ -23,7 +22,6 @@ pub struct CachedMetadata {
}
/// Estructura para gestionar la caché de metadatos de archivos y directorios
#[allow(dead_code)]
pub struct StorageCacheManager {
/// Caché de existencia y metadatos
cache: RwLock<HashMap<PathBuf, CachedMetadata>>,
@@ -37,7 +35,6 @@ pub struct StorageCacheManager {
impl StorageCacheManager {
/// Crea una nueva instancia del gestor de caché
#[allow(dead_code)]
pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self {
Self {
cache: RwLock::new(HashMap::with_capacity(max_entries)),
@@ -48,7 +45,6 @@ impl StorageCacheManager {
}
/// Crea una instancia por defecto del gestor de caché
#[allow(dead_code)]
pub fn default() -> Self {
Self::new(
60_000, // 1 minuto para archivos
@@ -58,7 +54,6 @@ impl StorageCacheManager {
}
/// Verifica si un archivo o directorio existe en caché
#[allow(dead_code)]
pub async fn check_exists(&self, path: &PathBuf, _is_dir: bool) -> Result<bool, ()> {
// Intentar obtener de la caché
if let Some(metadata) = self.get_cached_metadata(path).await {
@@ -70,7 +65,6 @@ impl StorageCacheManager {
}
/// Obtiene los metadatos de un path desde la caché
#[allow(dead_code)]
async fn get_cached_metadata(&self, path: &PathBuf) -> Option<CachedMetadata> {
let cache = self.cache.read().await;
@@ -85,7 +79,6 @@ impl StorageCacheManager {
}
/// Actualiza la caché con los metadatos de un path
#[allow(dead_code)]
pub async fn update_cache(&self, path: &PathBuf, exists: bool, size: Option<u64>,
created_at: Option<u64>, modified_at: Option<u64>, is_dir: bool) {
let mut cache = self.cache.write().await;
@@ -115,7 +108,6 @@ impl StorageCacheManager {
}
/// Elimina entradas aleatorias de la caché cuando está llena
#[allow(dead_code)]
async fn evict_entries(&self, cache: &mut HashMap<PathBuf, CachedMetadata>, count: usize) {
// Obtener las entradas más antiguas para eliminar
let mut entries: Vec<_> = cache.keys().cloned().collect();
@@ -136,7 +128,6 @@ impl StorageCacheManager {
}
/// Inicia una tarea de limpieza periódica
#[allow(dead_code)]
pub fn start_cleanup_task(cache_manager: Arc<Self>) -> BoxFuture<'static, ()> {
Box::pin(async move {
let interval = Duration::from_secs(60); // Ejecutar cada minuto
@@ -170,14 +161,12 @@ impl StorageCacheManager {
}
/// Invalida una entrada específica de la caché
#[allow(dead_code)]
pub async fn invalidate(&self, path: &PathBuf) {
let mut cache = self.cache.write().await;
cache.remove(path);
}
/// Invalida todas las entradas de la caché relacionadas con una carpeta
#[allow(dead_code)]
pub async fn invalidate_folder(&self, folder_path: &PathBuf) {
let mut cache = self.cache.write().await;
@@ -204,7 +193,6 @@ impl StorageCacheManager {
}
/// Obtiene el número actual de entradas en la caché
#[allow(dead_code)]
pub async fn cache_size(&self) -> usize {
let cache = self.cache.read().await;
cache.len()
@@ -48,14 +48,12 @@ pub trait CompressionService: Send + Sync {
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
/// Comprime un stream de datos
#[allow(dead_code)]
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
/// Descomprime un stream de datos
#[allow(dead_code)]
fn decompress_stream<S>(&self, compressed_stream: S)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
@@ -47,14 +47,12 @@ pub struct FileMetadata {
/// Ruta absoluta del archivo
pub path: PathBuf,
/// Si el archivo existe físicamente
#[allow(dead_code)]
pub exists: bool,
/// Tipo de entrada (archivo, directorio)
pub entry_type: CacheEntryType,
/// Tamaño en bytes (para archivos)
pub size: Option<u64>,
/// Tipo MIME (para archivos)
#[allow(dead_code)]
pub mime_type: Option<String>,
/// Timestamp de creación (UNIX epoch seconds)
pub created_at: Option<u64>,
@@ -259,7 +257,6 @@ impl FileMetadataCache {
}
/// Verifica si un archivo existe
#[allow(dead_code)]
pub async fn exists(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.exists);
@@ -269,7 +266,6 @@ impl FileMetadataCache {
}
/// Verifica si un path es un directorio
#[allow(dead_code)]
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::Directory);
@@ -288,7 +284,6 @@ impl FileMetadataCache {
}
/// Obtiene el tamaño de un archivo
#[allow(dead_code)]
pub async fn get_size(&self, path: &Path) -> Option<u64> {
if let Some(metadata) = self.get_metadata(path).await {
return metadata.size;
@@ -298,7 +293,6 @@ impl FileMetadataCache {
}
/// Obtiene el tipo MIME de un archivo
#[allow(dead_code)]
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
if let Some(metadata) = self.get_metadata(path).await {
return metadata.mime_type;
@@ -301,7 +301,6 @@ impl IdMappingOptimizer {
}
/// Precargar un conjunto de rutas para obtener sus IDs en batch
#[allow(dead_code)]
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
// Solo proceder si hay rutas para cargar
if paths.is_empty() {
@@ -342,7 +341,6 @@ impl IdMappingOptimizer {
}
/// Precargar un conjunto de IDs para obtener sus rutas en batch
#[allow(dead_code)]
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
// Solo proceder si hay IDs para cargar
if ids.is_empty() {
@@ -1,6 +1,5 @@
use std::path::PathBuf;
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::{RwLock, Mutex};
use tokio::fs;
use tokio::time;
@@ -29,7 +28,6 @@ pub enum IdMappingError {
SerializationError(#[from] serde_json::Error),
#[error("Other error: {0}")]
#[allow(dead_code)]
Other(String),
}
@@ -69,9 +67,6 @@ struct IdMap {
version: u32, // Versión para detectar cambios
}
/// Constantes para configuración
const SAVE_DEBOUNCE_MS: u64 = 0; // Sin debounce para garantizar guardado inmediato
/// Servicio para gestionar mapeos entre rutas y IDs únicos
pub struct IdMappingService {
map_path: PathBuf,
@@ -490,7 +485,6 @@ impl IdMappingPort for IdMappingService {
/// Synchronous helper for contexts where we can't use async
impl IdMappingService {
/// Create a new service synchronously (only for stubs and initialization)
#[allow(dead_code)]
pub fn new_sync(map_path: PathBuf) -> Self {
// Create a minimal implementation for initialization purposes
Self {
+210
View File
@@ -0,0 +1,210 @@
//! JWT-based token service implementation.
//!
//! This module provides JWT token generation and validation functionality,
//! implementing the TokenServicePort trait defined in the application layer.
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use chrono::Utc;
use crate::application::ports::auth_ports::{TokenServicePort, TokenClaims};
use crate::domain::entities::user::User;
use crate::common::errors::{DomainError, ErrorKind};
/// Internal JWT claims structure for serialization.
/// This is the actual JWT payload structure used by jsonwebtoken crate.
#[derive(Debug, Serialize, Deserialize)]
struct JwtClaims {
/// Subject identifier - contains the user ID
pub sub: String,
/// Expiration timestamp (seconds since Unix epoch)
pub exp: i64,
/// Issued at timestamp (seconds since Unix epoch)
pub iat: i64,
/// JWT unique ID for token tracking and revocation
pub jti: String,
/// Username for display and identification purposes
pub username: String,
/// User email for communication and identification
pub email: String,
/// User role for authorization checks
pub role: String,
}
impl From<JwtClaims> for TokenClaims {
fn from(claims: JwtClaims) -> Self {
TokenClaims {
sub: claims.sub,
exp: claims.exp,
iat: claims.iat,
jti: claims.jti,
username: claims.username,
email: claims.email,
role: claims.role,
}
}
}
/// JWT-based implementation of the TokenServicePort.
///
/// This service handles JWT token generation and validation for user authentication.
/// It uses HS256 algorithm for signing tokens.
pub struct JwtTokenService {
/// Secret key used for signing JWT tokens
jwt_secret: String,
/// Expiration time for access tokens in seconds
access_token_expiry: i64,
/// Expiration time for refresh tokens in seconds
refresh_token_expiry: i64,
}
impl JwtTokenService {
/// Create a new JwtTokenService with the specified configuration.
///
/// # Arguments
/// * `jwt_secret` - Secret key for signing tokens (should be at least 32 bytes)
/// * `access_token_expiry_secs` - Lifetime of access tokens in seconds
/// * `refresh_token_expiry_secs` - Lifetime of refresh tokens in seconds
pub fn new(jwt_secret: String, access_token_expiry_secs: i64, refresh_token_expiry_secs: i64) -> Self {
Self {
jwt_secret,
access_token_expiry: access_token_expiry_secs,
refresh_token_expiry: refresh_token_expiry_secs,
}
}
}
impl TokenServicePort for JwtTokenService {
fn generate_access_token(&self, user: &User) -> Result<String, DomainError> {
let now = Utc::now().timestamp();
// Log information for debugging
tracing::debug!(
"Generating token for user: {}, id: {}, role: {}",
user.username(),
user.id(),
user.role()
);
let claims = JwtClaims {
sub: user.id().to_string(),
exp: now + self.access_token_expiry,
iat: now,
jti: Uuid::new_v4().to_string(),
username: user.username().to_string(),
email: user.email().to_string(),
role: format!("{}", user.role()),
};
// Log JWT claims for debugging
tracing::debug!("JWT claims: sub={}, exp={}, iat={}", claims.sub, claims.exp, claims.iat);
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(self.jwt_secret.as_bytes())
)
.map_err(|e| {
tracing::error!("Error generating token: {}", e);
DomainError::new(
ErrorKind::InternalError,
"TokenService",
format!("Error al generar token: {}", e)
)
})
}
fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError> {
let validation = Validation::new(Algorithm::HS256);
let token_data = decode::<JwtClaims>(
token,
&DecodingKey::from_secret(self.jwt_secret.as_bytes()),
&validation
)
.map_err(|e| {
match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado")
},
_ => DomainError::new(
ErrorKind::AccessDenied,
"TokenService",
format!("Token inválido: {}", e)
),
}
})?;
Ok(token_data.claims.into())
}
fn generate_refresh_token(&self) -> String {
Uuid::new_v4().to_string()
}
fn refresh_token_expiry_secs(&self) -> i64 {
self.refresh_token_expiry
}
fn refresh_token_expiry_days(&self) -> i64 {
self.refresh_token_expiry / (24 * 3600)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::entities::user::{User, UserRole};
fn create_test_user() -> User {
User::from_data(
"test-user-id".to_string(),
"testuser".to_string(),
"test@example.com".to_string(),
"hashed_password".to_string(),
UserRole::User,
1024 * 1024 * 1024, // 1GB
0,
chrono::Utc::now(),
chrono::Utc::now(),
None,
true,
)
}
#[test]
fn test_generate_and_validate_token() {
let service = JwtTokenService::new(
"test_secret_key_at_least_32_bytes_long".to_string(),
3600, // 1 hour
86400, // 1 day
);
let user = create_test_user();
let token = service.generate_access_token(&user).expect("Should generate token");
let claims = service.validate_token(&token).expect("Should validate token");
assert_eq!(claims.sub, user.id());
assert_eq!(claims.username, user.username());
assert_eq!(claims.email, user.email());
}
#[test]
fn test_refresh_token_is_unique() {
let service = JwtTokenService::new("secret".to_string(), 3600, 86400);
let token1 = service.generate_refresh_token();
let token2 = service.generate_refresh_token();
assert_ne!(token1, token2);
}
#[test]
fn test_invalid_token() {
let service = JwtTokenService::new("secret".to_string(), 3600, 86400);
let result = service.validate_token("invalid_token");
assert!(result.is_err());
}
}
+4 -1
View File
@@ -7,4 +7,7 @@ pub mod file_metadata_cache;
pub mod compression_service;
pub mod buffer_pool;
pub mod trash_cleanup_service;
pub mod zip_service;
pub mod zip_service;
pub mod path_service;
pub mod password_hasher;
pub mod jwt_service;
@@ -0,0 +1,91 @@
//! Argon2-based password hasher implementation.
//!
//! This module provides a secure password hashing implementation using the Argon2id
//! algorithm, which is the recommended choice for password hashing as of 2023+.
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::password_hash::SaltString;
use rand_core::OsRng;
use crate::application::ports::auth_ports::PasswordHasherPort;
use crate::common::errors::{DomainError, ErrorKind};
/// Argon2-based implementation of the PasswordHasherPort.
///
/// Uses Argon2id algorithm which provides resistance against both side-channel
/// and GPU-based attacks. This is the recommended algorithm for password hashing.
#[derive(Debug, Clone)]
pub struct Argon2PasswordHasher {
/// Argon2 hasher instance - uses default secure parameters
_private: (),
}
impl Argon2PasswordHasher {
/// Create a new Argon2PasswordHasher with default secure parameters.
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for Argon2PasswordHasher {
fn default() -> Self {
Self::new()
}
}
impl PasswordHasherPort for Argon2PasswordHasher {
fn hash_password(&self, password: &str) -> Result<String, DomainError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"PasswordHasher",
format!("Error al generar hash de password: {}", e)
))
}
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"PasswordHasher",
format!("Error al procesar hash: {}", e)
))?;
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_and_verify_password() {
let hasher = Argon2PasswordHasher::new();
let password = "test_password_123";
let hash = hasher.hash_password(password).expect("Should hash password");
assert!(hasher.verify_password(password, &hash).expect("Should verify"));
assert!(!hasher.verify_password("wrong_password", &hash).expect("Should verify"));
}
#[test]
fn test_different_hashes_for_same_password() {
let hasher = Argon2PasswordHasher::new();
let password = "same_password";
let hash1 = hasher.hash_password(password).expect("Should hash");
let hash2 = hasher.hash_password(password).expect("Should hash");
// Hashes should be different due to random salt
assert_ne!(hash1, hash2);
// But both should verify correctly
assert!(hasher.verify_password(password, &hash1).expect("Should verify"));
assert!(hasher.verify_password(password, &hash2).expect("Should verify"));
}
}
+301
View File
@@ -0,0 +1,301 @@
//! PathService - Servicio de infraestructura para manejo de rutas de almacenamiento
//!
//! Este servicio fue movido desde domain/services porque implementa traits de application
//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs).
//!
//! StoragePath (Value Object) permanece en domain/services/path_service.rs
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;
use crate::domain::services::path_service::StoragePath;
/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento
pub struct PathService {
root_path: PathBuf,
}
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
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
let segments: Vec<String> = 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::new(segments)
})
}
/// Crea una ruta de archivo dentro de una carpeta
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
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
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
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);
PathService::resolve_path(self, &storage_path)
}
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
PathService::resolve_path(self, storage_path)
}
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
let abs_path = PathService::resolve_path(self, &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 = PathService::resolve_path(self, 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, &not_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");
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ use crate::application::dtos::user_dto::{
LoginDto, RegisterDto, UserDto, ChangePasswordDto, RefreshTokenDto, AuthResponseDto
};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::common::errors::AppError;
use crate::interfaces::errors::AppError;
pub fn auth_routes() -> Router<Arc<AppState>> {
Router::new()
@@ -39,7 +39,6 @@ pub struct BatchFolderOperationRequest {
pub recursive: bool,
/// ID de la carpeta destino (opcional)
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(dead_code)]
pub target_folder_id: Option<String>,
}
@@ -5,7 +5,6 @@ use axum::{
response::IntoResponse,
Json,
};
use std::sync::Arc;
use serde_json::json;
use crate::common::di::AppState;
@@ -1,362 +0,0 @@
use axum::{
Router,
routing::{get, put, delete, any},
extract::{Path, State, Request},
http::{StatusCode, HeaderMap},
response::{IntoResponse, Response},
body::Body,
Json,
};
use tracing::error;
use std::sync::Arc;
use serde_json::json;
use crate::common::di::AppState;
use crate::application::dtos::calendar_dto::{
CalendarDto, CreateCalendarDto, UpdateCalendarDto,
CalendarEventDto, CreateEventDto as CreateCalendarEventDto,
UpdateEventDto as UpdateCalendarEventDto
};
// CalDAV handler implementation
pub fn caldav_routes() -> Router<AppState> {
Router::new()
// Calendar operations
.route("/calendars", get(list_calendars))
.route("/calendars/:calendar_id",
get(get_calendar)
.put(update_calendar)
.delete(delete_calendar)
)
.route("/calendars/:calendar_id/events",
get(list_events)
.post(create_event)
)
.route("/calendars/:calendar_id/events/:event_id",
get(get_event)
.put(update_event)
.delete(delete_event)
)
}
async fn list_calendars(
State(state): State<AppState>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"user_id": user_id
});
match calendar_service.handle_request("list_user_calendars", params).await {
Ok(result) => {
let calendars: Vec<CalendarDto> = serde_json::from_value(result)
.unwrap_or_else(|_| Vec::new());
(StatusCode::OK, Json(calendars))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to list calendars: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn get_calendar(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"calendar_id": calendar_id,
"user_id": user_id
});
match calendar_service.handle_request("get_calendar", params).await {
Ok(result) => {
let calendar: CalendarDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarDto::default());
(StatusCode::OK, Json(calendar))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to get calendar: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn update_calendar(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
Json(update): Json<UpdateCalendarDto>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
// Set the user ID in the update
let mut update_with_user = update;
update_with_user.user_id = user_id.to_string();
match &state.calendar_service {
Some(calendar_service) => {
match calendar_service.handle_request("update_calendar", json!({
"calendar_id": calendar_id,
"name": update_with_user.name,
"description": update_with_user.description,
"color": update_with_user.color,
"is_public": update_with_user.is_public,
"user_id": update_with_user.user_id
})).await {
Ok(result) => {
let calendar: CalendarDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarDto::default());
(StatusCode::OK, Json(calendar))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to update calendar: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn delete_calendar(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"calendar_id": calendar_id,
"user_id": user_id
});
match calendar_service.handle_request("delete_calendar", params).await {
Ok(_) => {
(StatusCode::NO_CONTENT, Json(json!({})))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to delete calendar: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn list_events(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"calendar_id": calendar_id,
"user_id": user_id
});
match calendar_service.handle_request("list_events", params).await {
Ok(result) => {
let events: Vec<CalendarEventDto> = serde_json::from_value(result)
.unwrap_or_else(|_| Vec::new());
(StatusCode::OK, Json(events))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to list events: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn create_event(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
Json(mut event): Json<CreateCalendarEventDto>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
// Set the calendar ID and user ID in the event
event.calendar_id = calendar_id;
event.user_id = user_id.to_string();
match &state.calendar_service {
Some(calendar_service) => {
match calendar_service.handle_request("create_event", serde_json::to_value(event).unwrap()).await {
Ok(result) => {
let event: CalendarEventDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarEventDto::default());
(StatusCode::CREATED, Json(event))
},
Err(e) => {
let error_dto = CalendarEventDto::default();
error!(
"Failed to create event: {}",
e
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn get_event(
State(state): State<AppState>,
Path((calendar_id, event_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"event_id": event_id,
"user_id": user_id
});
match calendar_service.handle_request("get_event", params).await {
Ok(result) => {
let event: CalendarEventDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarEventDto::default());
(StatusCode::OK, Json(event))
},
Err(e) => {
let error_dto = CalendarEventDto::default();
error!(
"Failed to get event: {}",
e
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn update_event(
State(state): State<AppState>,
Path((calendar_id, event_id)): Path<(String, String)>,
Json(mut update): Json<UpdateCalendarEventDto>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
// Set the user ID in the update
update.user_id = user_id.to_string();
match &state.calendar_service {
Some(calendar_service) => {
let mut params = serde_json::to_value(update).unwrap();
// Add event_id to the params
if let serde_json::Value::Object(ref mut map) = params {
map.insert("event_id".to_string(), serde_json::Value::String(event_id));
}
match calendar_service.handle_request("update_event", params).await {
Ok(result) => {
let event: CalendarEventDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarEventDto::default());
(StatusCode::OK, Json(event))
},
Err(e) => {
let error_dto = CalendarEventDto::default();
error!(
"Failed to update event: {}",
e
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn delete_event(
State(state): State<AppState>,
Path((calendar_id, event_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"event_id": event_id,
"user_id": user_id
});
match calendar_service.handle_request("delete_event", params).await {
Ok(_) => {
(StatusCode::NO_CONTENT, Json(json!({})))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to delete event: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
@@ -1,41 +0,0 @@
--- caldav_handler.rs
+++ caldav_handler.rs
@@ -242,9 +242,9 @@
}
},
None => {
- (StatusCode::NOT_IMPLEMENTED, Json(json\!({
- "error": "Calendar service not available"
- })))
+ let error_dto = CalendarEventDto::default();
+ error\!("Calendar service not available");
+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto))
}
}
}
@@ -277,9 +277,9 @@
}
},
None => {
- (StatusCode::NOT_IMPLEMENTED, Json(json\!({
- "error": "Calendar service not available"
- })))
+ let error_dto = CalendarEventDto::default();
+ error\!("Calendar service not available");
+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto))
}
}
}
@@ -320,9 +320,9 @@
}
},
None => {
- (StatusCode::NOT_IMPLEMENTED, Json(json\!({
- "error": "Calendar service not available"
- })))
+ let error_dto = CalendarEventDto::default();
+ error\!("Calendar service not available");
+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto))
}
}
}
@@ -7,10 +7,6 @@ use axum::{
};
use serde::Deserialize;
use std::collections::HashMap;
use futures::Stream;
// use futures::StreamExt;
use std::task::{Context, Poll};
use std::pin::Pin;
use crate::application::services::file_service::{FileService, FileServiceError};
use crate::infrastructure::services::compression_service::{
@@ -46,35 +42,6 @@ type GlobalState = AppState;
*/
pub struct FileHandler;
// Simpler approach to make streams Unpin - use Pin<Box<dyn Stream>> directly
struct BoxedStream<T> {
inner: Pin<Box<dyn Stream<Item = T> + Send + 'static>>,
}
impl<T> Stream for BoxedStream<T> {
type Item = T;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// Accessing the field directly is safe because BoxedStream is not a structural pinning type
unsafe { self.get_unchecked_mut().inner.as_mut().poll_next(cx) }
}
}
// This is safe because BoxedStream's inner field is already Pin<Box<dyn Stream>>
impl<T> Unpin for BoxedStream<T> {}
impl<T> BoxedStream<T> {
#[allow(dead_code)]
fn new<S>(stream: S) -> Self
where
S: Stream<Item = T> + Send + 'static,
{
BoxedStream {
inner: Box::pin(stream),
}
}
}
impl FileHandler {
/// Uploads a file
pub async fn upload_file(
@@ -22,7 +22,7 @@ use crate::common::di::AppState;
use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::dtos::folder_dto::FolderDto;
use crate::common::errors::AppError;
use crate::interfaces::errors::AppError;
// Create a custom DAV header since it's not in the standard headers
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
+23 -5
View File
@@ -50,7 +50,7 @@ pub fn create_api_routes(
) -> Router<crate::common::di::AppState> {
// Create a simplified AppState for the trash view
// Setup required components for repository construction
let path_service = Arc::new(crate::domain::services::path_service::PathService::new(std::path::PathBuf::from("./storage")));
let path_service = Arc::new(crate::infrastructure::services::path_service::PathService::new(std::path::PathBuf::from("./storage")));
let storage_mediator = Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub());
let id_mapping_service = Arc::new(crate::infrastructure::services::id_mapping_service::IdMappingService::dummy());
let path_resolver = Arc::new(crate::infrastructure::repositories::file_path_resolver::FilePathResolver::new(
@@ -79,11 +79,17 @@ pub fn create_api_routes(
path_service.clone(),
));
// Create concrete id_mapping_service for optimizer
let id_mapping_service_concrete = Arc::new(crate::infrastructure::services::id_mapping_service::IdMappingService::dummy());
let id_mapping_optimizer = Arc::new(crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer::new(id_mapping_service_concrete.clone()));
let mut app_state = crate::common::di::AppState {
core: crate::common::di::CoreServices {
path_service: path_service.clone(),
cache_manager: Arc::new(crate::infrastructure::services::cache_manager::StorageCacheManager::default()),
id_mapping_service: id_mapping_service.clone(),
file_id_mapping_service: id_mapping_service_concrete.clone(),
id_mapping_optimizer: id_mapping_optimizer.clone(),
config: crate::common::config::AppConfig::default(),
},
repositories: crate::common::di::RepositoryServices {
@@ -95,16 +101,28 @@ pub fn create_api_routes(
storage_mediator: storage_mediator.clone(),
metadata_manager: Arc::new(crate::infrastructure::repositories::FileMetadataManager::default()),
path_resolver: path_resolver.clone(),
metadata_cache: metadata_cache.clone(),
trash_repository: None, // This is OK to be None since we use the trash_service directly
},
storage_usage_service: None,
applications: crate::common::di::ApplicationServices {
folder_service_concrete: folder_service.clone(),
file_service_concrete: file_service.clone(),
folder_service: folder_service.clone(),
file_service: file_service.clone(),
file_upload_service: Arc::new(crate::application::services::file_upload_service::FileUploadService::default_stub()),
file_retrieval_service: Arc::new(crate::application::services::file_retrieval_service::FileRetrievalService::default_stub()),
file_management_service: Arc::new(crate::application::services::file_management_service::FileManagementService::default_stub()),
file_use_case_factory: Arc::new(crate::application::services::file_use_case_factory::AppFileUseCaseFactory::default_stub()),
file_upload_service: Arc::new(crate::application::services::file_upload_service::FileUploadService::new(
Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
)),
file_retrieval_service: Arc::new(crate::application::services::file_retrieval_service::FileRetrievalService::new(
Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub())
)),
file_management_service: Arc::new(crate::application::services::file_management_service::FileManagementService::new(
Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
)),
file_use_case_factory: Arc::new(crate::application::services::file_use_case_factory::AppFileUseCaseFactory::new(
Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()),
Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
)),
i18n_service: i18n_service.clone().unwrap_or_else(||
Arc::new(crate::application::services::i18n_application_service::I18nApplicationService::dummy())
),
+117
View File
@@ -0,0 +1,117 @@
//! HTTP/API Error types for the interfaces layer.
//!
//! This module contains error types specific to the HTTP/API layer.
//! These errors handle the conversion from domain errors to HTTP responses.
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
use crate::domain::errors::{DomainError, ErrorKind};
/// Error type for HTTP/API responses.
///
/// This struct represents errors that will be returned to HTTP clients.
/// It contains the HTTP status code, a user-friendly message, and an error type identifier.
#[derive(Debug)]
pub struct AppError {
pub status_code: StatusCode,
pub message: String,
pub error_type: String,
}
/// JSON response structure for errors.
#[derive(Serialize)]
pub struct ErrorResponse {
pub status: String,
pub message: String,
pub error_type: String,
}
impl AppError {
/// Create a new AppError with custom status code, message and error type.
pub fn new(status_code: StatusCode, message: impl Into<String>, error_type: impl Into<String>) -> Self {
Self {
status_code,
message: message.into(),
error_type: error_type.into(),
}
}
/// Create a 400 Bad Request error.
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, message, "BadRequest")
}
/// Create a 401 Unauthorized error.
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNAUTHORIZED, message, "Unauthorized")
}
/// Create a 403 Forbidden error.
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(StatusCode::FORBIDDEN, message, "Forbidden")
}
/// Create a 404 Not Found error.
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, message, "NotFound")
}
/// Create a 500 Internal Server Error.
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, message, "InternalError")
}
/// Create a 405 Method Not Allowed error.
pub fn method_not_allowed(message: impl Into<String>) -> Self {
Self::new(StatusCode::METHOD_NOT_ALLOWED, message, "MethodNotAllowed")
}
/// Create a 409 Conflict error.
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(StatusCode::CONFLICT, message, "Conflict")
}
/// Create a 415 Unsupported Media Type error.
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType")
}
}
impl From<DomainError> for AppError {
fn from(err: DomainError) -> Self {
let status_code = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
ErrorKind::AccessDenied => StatusCode::FORBIDDEN,
ErrorKind::Timeout => StatusCode::REQUEST_TIMEOUT,
ErrorKind::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
ErrorKind::NotImplemented => StatusCode::NOT_IMPLEMENTED,
ErrorKind::UnsupportedOperation => StatusCode::METHOD_NOT_ALLOWED,
ErrorKind::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
};
Self {
status_code,
message: err.message,
error_type: err.kind.to_string(),
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.status_code;
let error_response = ErrorResponse {
status: status.to_string(),
message: self.message,
error_type: self.error_type,
};
let body = Json(error_response);
(status, body).into_response()
}
}
+2 -8
View File
@@ -9,14 +9,8 @@ use axum::{
use crate::common::di::AppState;
// Extensión para almacenar datos del usuario autenticado
#[derive(Clone, Debug)]
pub struct CurrentUser {
pub id: String,
pub username: String,
pub email: String,
pub role: String,
}
// Re-export CurrentUser from application layer for use in handlers
pub use crate::application::dtos::user_dto::CurrentUser;
// Estructura para usar en extractores de Axum
#[derive(Clone, Debug)]
-18
View File
@@ -58,7 +58,6 @@ impl HttpCache {
}
/// Crea una nueva instancia con un tiempo de vida especificado
#[allow(dead_code)]
pub fn with_max_age(max_age: u64) -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
@@ -163,20 +162,6 @@ impl HttpCache {
None
}
/// Calcula el ETag para una respuesta
#[allow(dead_code)]
fn calculate_etag<T: Serialize>(&self, response: &T) -> EntityTag {
// Serializar la respuesta
let json = serde_json::to_string(response).unwrap_or_default();
// Calcular hash
let mut hasher = DefaultHasher::new();
json.hash(&mut hasher);
let hash = hasher.finish();
format!("\"{}\"", hash)
}
/// Genera un ETag simple para un bloque de bytes
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
// Calcular hash
@@ -189,7 +174,6 @@ impl HttpCache {
}
/// Middleware de caché HTTP
#[allow(dead_code)]
pub async fn cache_middleware<T>(
cache: HttpCache,
cache_key: &str,
@@ -328,7 +312,6 @@ pub struct HttpCacheLayer {
impl HttpCacheLayer {
/// Crea una nueva capa de caché
#[allow(dead_code)]
pub fn new(cache: HttpCache) -> Self {
Self {
cache,
@@ -337,7 +320,6 @@ impl HttpCacheLayer {
}
/// Establece el tiempo de vida máximo
#[allow(dead_code)]
pub fn with_max_age(mut self, max_age: u64) -> Self {
self.max_age = Some(max_age);
self
+2 -1
View File
@@ -1,5 +1,6 @@
pub mod api;
pub mod web;
pub mod middleware;
pub mod errors;
pub use api::create_api_routes;
pub use api::create_api_routes;
+2 -1
View File
@@ -10,7 +10,8 @@ pub use application::services::folder_service::FolderService;
pub use application::services::file_service::FileService;
pub use application::services::i18n_application_service::I18nApplicationService;
pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
pub use domain::services::path_service::PathService;
pub use infrastructure::services::path_service::PathService;
pub use domain::services::path_service::StoragePath;
pub use infrastructure::repositories::folder_fs_repository::FolderFsRepository;
pub use infrastructure::repositories::file_fs_repository::FileFsRepository;
pub use infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
+803 -791
View File
File diff suppressed because it is too large Load Diff