refactor: remove serde from domain entities for Clean Architecture compliance
- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities - Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer - Update contact_pg_repository to use persistence DTOs - Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0) - Fix unused variable warnings in main.rs - Move PathService import from domain to infrastructure - Add missing fields to CoreServices and RepositoryServices - Create proper service initialization in main.rs Clean Architecture improvements: - Domain layer no longer depends on serde framework - Persistence concerns isolated to infrastructure layer - TokenClaims in auth_service.rs is only exception (required for JWT)
This commit is contained in:
@@ -1,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
File diff suppressed because it is too large
Load Diff
+29
-29
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
```
|
||||
@@ -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
@@ -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!
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -66,3 +66,12 @@ pub struct ChangePasswordDto {
|
||||
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,
|
||||
}
|
||||
@@ -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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
@@ -799,8 +978,16 @@ impl Default for AppState {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
@@ -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()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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 adapters;
|
||||
@@ -1,8 +1,7 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressBook {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
@@ -29,21 +28,21 @@ impl Default for AddressBook {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Email {
|
||||
pub email: String,
|
||||
pub r#type: String, // home, work, other
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Phone {
|
||||
pub number: String,
|
||||
pub r#type: String, // mobile, home, work, fax, other
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Address {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
@@ -54,7 +53,7 @@ pub struct Address {
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Contact {
|
||||
pub id: Uuid,
|
||||
pub address_book_id: Uuid,
|
||||
@@ -105,7 +104,7 @@ impl Default for Contact {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactGroup {
|
||||
pub id: Uuid,
|
||||
pub address_book_id: Uuid,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/**
|
||||
@@ -15,7 +14,6 @@ pub enum FileError {
|
||||
|
||||
/// Occurs when validation fails for any file entity attribute.
|
||||
#[error("Validation error: {0}")]
|
||||
#[allow(dead_code)]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
@@ -36,7 +34,7 @@ pub type FileResult<T> = Result<T, FileError>;
|
||||
* This entity maintains both physical storage information and logical metadata about files,
|
||||
* serving as the bridge between the storage system and the application.
|
||||
*/
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct File {
|
||||
/// Unique identifier for the file - used throughout the system for file operations
|
||||
id: String,
|
||||
@@ -44,12 +42,10 @@ pub struct File {
|
||||
/// Name of the file including extension
|
||||
name: String,
|
||||
|
||||
/// Path to the file in the domain model - not serialized as it contains internal representation
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
/// Path to the file in the domain model
|
||||
storage_path: StoragePath,
|
||||
|
||||
/// String representation of the path for serialization and API compatibility
|
||||
#[serde(rename = "path")]
|
||||
/// String representation of the path for API compatibility
|
||||
path_string: String,
|
||||
|
||||
/// Size of the file in bytes
|
||||
@@ -253,7 +249,6 @@ impl File {
|
||||
// Methods to create new versions of the file (immutable)
|
||||
|
||||
/// Creates a new version of the file with updated name
|
||||
#[allow(dead_code)]
|
||||
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
|
||||
// Validate file name
|
||||
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
|
||||
@@ -318,7 +313,6 @@ impl File {
|
||||
}
|
||||
|
||||
/// Creates a new version of the file with updated size
|
||||
#[allow(dead_code)]
|
||||
pub fn with_size(&self, new_size: u64) -> Self {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Error in the creation or manipulation of folder entities
|
||||
@@ -8,7 +7,6 @@ pub enum FolderError {
|
||||
InvalidFolderName(String),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
#[allow(dead_code)]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
@@ -16,7 +14,7 @@ pub enum FolderError {
|
||||
pub type FolderResult<T> = Result<T, FolderError>;
|
||||
|
||||
/// Represents a folder entity in the domain
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Folder {
|
||||
/// Unique identifier for the folder
|
||||
id: String,
|
||||
@@ -25,11 +23,9 @@ pub struct Folder {
|
||||
name: String,
|
||||
|
||||
/// Path to the folder in the domain model
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
storage_path: StoragePath,
|
||||
|
||||
/// String representation of the path (for serialization compatibility)
|
||||
#[serde(rename = "path")]
|
||||
/// String representation of the path (for API compatibility)
|
||||
path_string: String,
|
||||
|
||||
/// Parent folder ID (None if it's a root folder)
|
||||
@@ -235,7 +231,6 @@ impl Folder {
|
||||
}
|
||||
|
||||
/// Returns an absolute path for this folder
|
||||
#[allow(dead_code)]
|
||||
pub fn get_absolute_path<P: AsRef<std::path::Path>>(&self, root_path: P) -> std::path::PathBuf {
|
||||
let mut result = std::path::PathBuf::from(root_path.as_ref());
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
|
||||
+23
-40
@@ -1,7 +1,3 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use argon2::password_hash::SaltString;
|
||||
use rand_core::OsRng;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
@@ -22,7 +18,7 @@ pub enum UserError {
|
||||
|
||||
pub type UserResult<T> = Result<T, UserError>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
// We'll handle conversion manually for now until the type is properly set up in the database
|
||||
pub enum UserRole {
|
||||
Admin,
|
||||
@@ -38,12 +34,11 @@ impl std::fmt::Display for UserRole {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
id: String,
|
||||
username: String,
|
||||
email: String,
|
||||
#[serde(skip_serializing)]
|
||||
password_hash: String,
|
||||
role: UserRole,
|
||||
storage_quota_bytes: i64,
|
||||
@@ -55,10 +50,22 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Create a new user with a pre-hashed password.
|
||||
///
|
||||
/// The password hashing should be done externally using PasswordHasherPort
|
||||
/// to maintain clean architecture and keep cryptographic dependencies
|
||||
/// out of the domain layer.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `username` - User's username (3-32 characters)
|
||||
/// * `email` - User's email address
|
||||
/// * `password_hash` - Pre-hashed password (from PasswordHasherPort)
|
||||
/// * `role` - User's role
|
||||
/// * `storage_quota_bytes` - Storage quota in bytes
|
||||
pub fn new(
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
password_hash: String,
|
||||
role: UserRole,
|
||||
storage_quota_bytes: i64,
|
||||
) -> UserResult<Self> {
|
||||
@@ -75,19 +82,12 @@ impl User {
|
||||
)));
|
||||
}
|
||||
|
||||
if password.len() < 8 {
|
||||
if password_hash.is_empty() {
|
||||
return Err(UserError::InvalidPassword(format!(
|
||||
"Password debe tener al menos 8 caracteres"
|
||||
"Password hash no puede estar vacío"
|
||||
)));
|
||||
}
|
||||
|
||||
// Generar hash con Argon2id (recomendado para 2023+)
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2.hash_password(password.as_bytes(), &salt)
|
||||
.map_err(|e| UserError::ValidationError(format!("Error al generar hash: {}", e)))?
|
||||
.to_string();
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
Ok(Self {
|
||||
@@ -179,30 +179,13 @@ impl User {
|
||||
&self.password_hash
|
||||
}
|
||||
|
||||
// Verificación de password
|
||||
pub fn verify_password(&self, password: &str) -> UserResult<bool> {
|
||||
let parsed_hash = PasswordHash::new(&self.password_hash)
|
||||
.map_err(|e| UserError::AuthenticationError(format!("Error al procesar hash: {}", e)))?;
|
||||
|
||||
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
|
||||
}
|
||||
|
||||
// Cambiar contraseña
|
||||
pub fn update_password(&mut self, new_password: String) -> UserResult<()> {
|
||||
if new_password.len() < 8 {
|
||||
return Err(UserError::InvalidPassword(format!(
|
||||
"Password debe tener al menos 8 caracteres"
|
||||
)));
|
||||
}
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
self.password_hash = argon2.hash_password(new_password.as_bytes(), &salt)
|
||||
.map_err(|e| UserError::ValidationError(format!("Error al generar hash: {}", e)))?
|
||||
.to_string();
|
||||
|
||||
/// Update the password hash.
|
||||
///
|
||||
/// The new password should be hashed externally using PasswordHasherPort
|
||||
/// before calling this method.
|
||||
pub fn update_password_hash(&mut self, new_hash: String) {
|
||||
self.password_hash = new_hash;
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Actualizar uso de almacenamiento
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
//! Errores del dominio
|
||||
//!
|
||||
//! Este módulo contiene los tipos de error propios del dominio.
|
||||
//! DomainError es el error base que se usa en toda la capa de dominio.
|
||||
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
use std::error::Error as StdError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Tipo Result común para el dominio con DomainError como error estándar
|
||||
pub type Result<T> = std::result::Result<T, DomainError>;
|
||||
|
||||
/// Tipos de errores del dominio
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ErrorKind {
|
||||
/// Entidad no encontrada
|
||||
NotFound,
|
||||
/// Entidad ya existe
|
||||
AlreadyExists,
|
||||
/// Entrada inválida o validación fallida
|
||||
InvalidInput,
|
||||
/// Error de acceso o permisos
|
||||
AccessDenied,
|
||||
/// Tiempo de espera agotado
|
||||
Timeout,
|
||||
/// Error interno del sistema
|
||||
InternalError,
|
||||
/// Funcionalidad no implementada
|
||||
NotImplemented,
|
||||
/// Operación no soportada
|
||||
UnsupportedOperation,
|
||||
/// Error de base de datos
|
||||
DatabaseError,
|
||||
}
|
||||
|
||||
impl Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
ErrorKind::NotFound => write!(f, "Not Found"),
|
||||
ErrorKind::AlreadyExists => write!(f, "Already Exists"),
|
||||
ErrorKind::InvalidInput => write!(f, "Invalid Input"),
|
||||
ErrorKind::AccessDenied => write!(f, "Access Denied"),
|
||||
ErrorKind::Timeout => write!(f, "Timeout"),
|
||||
ErrorKind::InternalError => write!(f, "Internal Error"),
|
||||
ErrorKind::NotImplemented => write!(f, "Not Implemented"),
|
||||
ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"),
|
||||
ErrorKind::DatabaseError => write!(f, "Database Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error base de dominio que proporciona contexto detallado
|
||||
#[derive(Error, Debug)]
|
||||
#[error("{kind}: {message}")]
|
||||
pub struct DomainError {
|
||||
/// Tipo de error
|
||||
pub kind: ErrorKind,
|
||||
/// Tipo de entidad afectada (ej: "File", "Folder")
|
||||
pub entity_type: &'static str,
|
||||
/// Identificador de la entidad si está disponible
|
||||
pub entity_id: Option<String>,
|
||||
/// Mensaje descriptivo del error
|
||||
pub message: String,
|
||||
/// Error fuente (opcional)
|
||||
#[source]
|
||||
pub source: Option<Box<dyn StdError + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl DomainError {
|
||||
/// Crea un nuevo error de dominio
|
||||
pub fn new<S: Into<String>>(
|
||||
kind: ErrorKind,
|
||||
entity_type: &'static str,
|
||||
message: S,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de entidad no encontrada
|
||||
pub fn not_found<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
||||
let id = entity_id.into();
|
||||
Self {
|
||||
kind: ErrorKind::NotFound,
|
||||
entity_type,
|
||||
entity_id: Some(id.clone()),
|
||||
message: format!("{} not found: {}", entity_type, id),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de entidad ya existente
|
||||
pub fn already_exists<S: Into<String>>(entity_type: &'static str, entity_id: S) -> Self {
|
||||
let id = entity_id.into();
|
||||
Self {
|
||||
kind: ErrorKind::AlreadyExists,
|
||||
entity_type,
|
||||
entity_id: Some(id.clone()),
|
||||
message: format!("{} already exists: {}", entity_type, id),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error para operaciones no soportadas
|
||||
pub fn operation_not_supported<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self::new(
|
||||
ErrorKind::UnsupportedOperation,
|
||||
entity_type,
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
/// Crea un error de tiempo agotado
|
||||
pub fn timeout<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::Timeout,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error interno
|
||||
pub fn internal_error<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de acceso denegado
|
||||
pub fn access_denied<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::AccessDenied,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for access_denied to maintain compatibility
|
||||
pub fn unauthorized<S: Into<String>>(message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::AccessDenied,
|
||||
entity_type: "Authorization",
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de base de datos
|
||||
pub fn database_error<S: Into<String>>(message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::DatabaseError,
|
||||
entity_type: "Database",
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de validación
|
||||
pub fn validation_error<S: Into<String>>(message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::InvalidInput,
|
||||
entity_type: "Validation",
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un error de funcionalidad no implementada
|
||||
pub fn not_implemented<S: Into<String>>(entity_type: &'static str, message: S) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::NotImplemented,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Establece el ID de la entidad
|
||||
pub fn with_id<S: Into<String>>(mut self, entity_id: S) -> Self {
|
||||
self.entity_id = Some(entity_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el error fuente
|
||||
pub fn with_source<E: StdError + Send + Sync + 'static>(mut self, source: E) -> Self {
|
||||
self.source = Some(Box::new(source));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait para añadir contexto a los errores
|
||||
pub trait ErrorContext<T, E> {
|
||||
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
|
||||
where
|
||||
C: Into<String>,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError>;
|
||||
}
|
||||
|
||||
impl<T, E: StdError + Send + Sync + 'static> ErrorContext<T, E> for std::result::Result<T, E> {
|
||||
fn with_context<C, F>(self, context: F) -> std::result::Result<T, DomainError>
|
||||
where
|
||||
C: Into<String>,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
DomainError {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type: "Unknown",
|
||||
entity_id: None,
|
||||
message: context().into(),
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result<T, DomainError> {
|
||||
self.map_err(|e| {
|
||||
DomainError {
|
||||
kind,
|
||||
entity_type,
|
||||
entity_id: None,
|
||||
message: format!("{}", e),
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implementaciones From para errores estándar (sin dependencias externas de infra)
|
||||
impl From<std::io::Error> for DomainError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
DomainError {
|
||||
kind: ErrorKind::InternalError,
|
||||
entity_type: "IO",
|
||||
entity_id: None,
|
||||
message: format!("{}", err),
|
||||
source: Some(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for DomainError {
|
||||
fn from(err: uuid::Error) -> Self {
|
||||
DomainError {
|
||||
kind: ErrorKind::InvalidInput,
|
||||
entity_type: "UUID",
|
||||
entity_id: None,
|
||||
message: format!("{}", err),
|
||||
source: Some(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod entities;
|
||||
pub mod errors;
|
||||
pub mod repositories;
|
||||
pub mod services;
|
||||
|
||||
// Re-export common types
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
use std::result::Result;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
use std::result::Result;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
@@ -12,7 +12,6 @@ use bytes::Bytes;
|
||||
* operations, providing detailed context for error handling across the application.
|
||||
*/
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FileRepositoryError {
|
||||
/// Returned when a requested file cannot be found by ID or path
|
||||
#[error("File not found: {0}")]
|
||||
@@ -109,7 +108,6 @@ pub trait FileRepository: Send + Sync + 'static {
|
||||
* @param content Binary data of the file
|
||||
* @return The created File entity on success, error otherwise
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
async fn save_file_with_id(
|
||||
&self,
|
||||
id: String,
|
||||
@@ -152,7 +150,6 @@ pub trait FileRepository: Send + Sync + 'static {
|
||||
* @param id The unique identifier of the file to delete
|
||||
* @return Success or error
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::common::errors::DomainError;
|
||||
|
||||
/// Error types for folder repository operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FolderRepositoryError {
|
||||
#[error("Folder not found: {0}")]
|
||||
NotFound(String),
|
||||
@@ -84,12 +83,10 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
|
||||
/// Legacy method - checks if a folder exists at the given PathBuf path
|
||||
#[deprecated(note = "Use folder_exists_at_storage_path instead")]
|
||||
#[allow(dead_code)]
|
||||
async fn folder_exists(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<bool>;
|
||||
|
||||
/// Legacy method - gets a folder by its PathBuf path
|
||||
#[deprecated(note = "Use get_folder_by_storage_path instead")]
|
||||
#[allow(dead_code)]
|
||||
async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> FolderRepositoryResult<Folder>;
|
||||
|
||||
/// Moves a folder to trash
|
||||
|
||||
@@ -61,6 +61,5 @@ pub trait I18nService: Send + Sync + 'static {
|
||||
async fn available_locales(&self) -> Vec<Locale>;
|
||||
|
||||
/// Check if a locale is supported
|
||||
#[allow(dead_code)]
|
||||
async fn is_supported(&self, locale: Locale) -> bool;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod i18n_service;
|
||||
pub mod path_service;
|
||||
pub mod auth_service;
|
||||
|
||||
// NOTE: auth_service has been moved to infrastructure/services/jwt_service.rs
|
||||
// The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort)
|
||||
@@ -1,5 +1,12 @@
|
||||
/// Abstracto servicio de dominio para rutas, sin dependencias de sistema de archivos
|
||||
/// Representa una ruta de almacenamiento en el dominio
|
||||
//! StoragePath - Value Object del dominio para representar rutas de almacenamiento
|
||||
//!
|
||||
//! Este módulo contiene solo el Value Object StoragePath que es parte del dominio puro.
|
||||
//! PathService (que implementa StoragePort y StorageMediator) fue movido a
|
||||
//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Representa una ruta de almacenamiento en el dominio (Value Object)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct StoragePath {
|
||||
segments: Vec<String>,
|
||||
@@ -7,7 +14,6 @@ pub struct StoragePath {
|
||||
|
||||
impl StoragePath {
|
||||
/// Crea una nueva ruta de almacenamiento
|
||||
#[allow(dead_code)]
|
||||
pub fn new(segments: Vec<String>) -> Self {
|
||||
Self { segments }
|
||||
}
|
||||
@@ -89,301 +95,41 @@ impl StoragePath {
|
||||
}
|
||||
}
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::application::ports::outbound::StoragePort;
|
||||
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorResult, StorageMediatorError};
|
||||
use crate::domain::entities::folder::Folder;
|
||||
|
||||
/// Servicio de dominio para manejar operaciones con rutas de almacenamiento
|
||||
pub struct PathService {
|
||||
root_path: PathBuf, // Necesario para la implementación
|
||||
}
|
||||
|
||||
impl PathService {
|
||||
/// Crea un nuevo servicio de rutas con una raíz específica
|
||||
pub fn new(root_path: PathBuf) -> Self {
|
||||
Self { root_path }
|
||||
}
|
||||
|
||||
/// Convierte una ruta del dominio a una ruta física absoluta
|
||||
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Convierte una ruta física a una ruta de dominio
|
||||
#[allow(dead_code)]
|
||||
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
|
||||
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
|
||||
let segments = rel_path
|
||||
.components()
|
||||
.filter_map(|c| match c {
|
||||
std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
StoragePath { segments }
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea una ruta de archivo dentro de una carpeta
|
||||
#[allow(dead_code)]
|
||||
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
|
||||
folder_path.join(file_name)
|
||||
}
|
||||
|
||||
/// Verifica si una ruta es directamente hija de otra
|
||||
#[allow(dead_code)]
|
||||
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
|
||||
if let Some(child_parent) = potential_child.parent() {
|
||||
&child_parent == parent_path
|
||||
} else {
|
||||
parent_path.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si una ruta está en la raíz
|
||||
#[allow(dead_code)]
|
||||
pub fn is_in_root(&self, path: &StoragePath) -> bool {
|
||||
path.parent().map_or(true, |p| p.is_empty())
|
||||
}
|
||||
|
||||
/// Gets the root path used by this service
|
||||
#[allow(dead_code)]
|
||||
pub fn get_root_path(&self) -> &Path {
|
||||
&self.root_path
|
||||
}
|
||||
|
||||
/// Valida una ruta para asegurar que no contiene componentes peligrosos
|
||||
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Verificar que no haya segmentos vacíos
|
||||
if path.segments().iter().any(|s| s.is_empty()) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains empty segments: {}", path.to_string())
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no haya caracteres peligrosos
|
||||
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
|
||||
for segment in path.segments() {
|
||||
if segment.contains(&dangerous_chars[..]) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains dangerous characters: {}", segment)
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no empiece con . (oculto en Unix)
|
||||
if segment.starts_with('.') && segment != ".well-known" {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path segments cannot start with dot: {}", segment)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StoragePort for PathService {
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Primero validar la ruta
|
||||
self.validate_path(storage_path)?;
|
||||
|
||||
// Resolver a ruta física
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
// Crear directorios si no existen
|
||||
if !physical_path.exists() {
|
||||
fs::create_dir_all(&physical_path).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Storage",
|
||||
format!("Failed to create directory: {}", physical_path.display())
|
||||
).with_source(e))?;
|
||||
|
||||
tracing::debug!("Created directory: {}", physical_path.display());
|
||||
} else if !physical_path.is_dir() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Storage",
|
||||
format!("Path exists but is not a directory: {}", physical_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_file();
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_dir();
|
||||
Ok(exists)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for PathService {
|
||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> {
|
||||
// This is a simplified implementation since PathService doesn't have direct
|
||||
// access to folder repository. It's typically used through a proxy.
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf {
|
||||
// Convert path to storage path then resolve
|
||||
let path_str = relative_path.to_string_lossy().to_string();
|
||||
let storage_path = StoragePath::from_string(&path_str);
|
||||
self.resolve_path(&storage_path)
|
||||
}
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
self.resolve_path(storage_path)
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let storage_path = StoragePath::from_string("test/file.txt");
|
||||
let absolute = service.resolve_path(&storage_path);
|
||||
|
||||
assert_eq!(absolute, PathBuf::from("/storage/test/file.txt"));
|
||||
fn test_storage_path_from_string() {
|
||||
let path = StoragePath::from_string("folder/subfolder/file.txt");
|
||||
assert_eq!(path.segments(), &["folder", "subfolder", "file.txt"]);
|
||||
assert_eq!(path.to_string(), "/folder/subfolder/file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_storage_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let physical_path = PathBuf::from("/storage/folder/file.txt");
|
||||
let storage_path = service.to_storage_path(&physical_path).unwrap();
|
||||
|
||||
assert_eq!(storage_path.to_string(), "/folder/file.txt");
|
||||
fn test_storage_path_join() {
|
||||
let path = StoragePath::from_string("folder");
|
||||
let joined = path.join("file.txt");
|
||||
assert_eq!(joined.to_string(), "/folder/file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_in_root() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let root_path = StoragePath::from_string("file.txt");
|
||||
let nested_path = StoragePath::from_string("folder/file.txt");
|
||||
|
||||
assert!(service.is_in_root(&root_path));
|
||||
assert!(!service.is_in_root(&nested_path));
|
||||
fn test_storage_path_parent() {
|
||||
let path = StoragePath::from_string("folder/file.txt");
|
||||
let parent = path.parent().unwrap();
|
||||
assert_eq!(parent.to_string(), "/folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_direct_child() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let parent = StoragePath::from_string("folder");
|
||||
let child = StoragePath::from_string("folder/file.txt");
|
||||
let not_child = StoragePath::from_string("folder2/file.txt");
|
||||
|
||||
assert!(service.is_direct_child(&parent, &child));
|
||||
assert!(!service.is_direct_child(&parent, ¬_child));
|
||||
fn test_storage_path_root() {
|
||||
let root = StoragePath::root();
|
||||
assert!(root.is_empty());
|
||||
assert_eq!(root.to_string(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_file_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let folder_path = StoragePath::from_string("folder");
|
||||
let file_path = service.create_file_path(&folder_path, "file.txt");
|
||||
|
||||
assert_eq!(file_path.to_string(), "/folder/file.txt");
|
||||
fn test_storage_path_file_name() {
|
||||
let path = StoragePath::from_string("folder/file.txt");
|
||||
assert_eq!(path.file_name(), Some("file.txt".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,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]
|
||||
|
||||
@@ -410,241 +410,3 @@ 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
|
||||
@@ -313,204 +322,3 @@ 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,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};
|
||||
|
||||
@@ -51,80 +51,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,6 @@ pub mod compression_service;
|
||||
pub mod buffer_pool;
|
||||
pub mod trash_cleanup_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"));
|
||||
}
|
||||
}
|
||||
@@ -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, ¬_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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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())
|
||||
),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod api;
|
||||
pub mod web;
|
||||
pub mod middleware;
|
||||
pub mod errors;
|
||||
|
||||
pub use api::create_api_routes;
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+36
-24
@@ -30,16 +30,12 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
///
|
||||
/// @author OxiCloud Development Team
|
||||
|
||||
/// Common utilities, configuration, and error handling
|
||||
mod common;
|
||||
/// Core domain model, entities, and business rules
|
||||
mod domain;
|
||||
/// Application services, use cases, and DTOs
|
||||
mod application;
|
||||
/// Technical implementations of repositories and services
|
||||
mod infrastructure;
|
||||
/// External interfaces like API endpoints and web controllers
|
||||
mod interfaces;
|
||||
// Use the library crate instead of redefining modules
|
||||
use oxicloud::application;
|
||||
use oxicloud::common;
|
||||
use oxicloud::domain;
|
||||
use oxicloud::infrastructure;
|
||||
use oxicloud::interfaces;
|
||||
|
||||
use application::services::folder_service::FolderService;
|
||||
use application::services::file_service::FileService;
|
||||
@@ -47,7 +43,7 @@ use application::services::i18n_application_service::I18nApplicationService;
|
||||
use application::services::storage_mediator::FileSystemStorageMediator;
|
||||
use application::services::share_service::ShareService;
|
||||
use application::services::favorites_service::FavoritesService;
|
||||
use domain::services::path_service::PathService;
|
||||
use infrastructure::services::path_service::PathService;
|
||||
use infrastructure::repositories::folder_fs_repository::FolderFsRepository;
|
||||
use infrastructure::repositories::file_fs_repository::FileFsRepository;
|
||||
use infrastructure::repositories::parallel_file_processor::ParallelFileProcessor;
|
||||
@@ -245,11 +241,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn save_file_with_id(
|
||||
&self,
|
||||
id: String,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
_id: String,
|
||||
_name: String,
|
||||
_folder_id: Option<String>,
|
||||
_content_type: String,
|
||||
_content: Vec<u8>,
|
||||
) -> domain::repositories::file_repository::FileRepositoryResult<domain::entities::file::File> {
|
||||
Err(domain::repositories::file_repository::FileRepositoryError::Other("Not implemented".to_string()))
|
||||
}
|
||||
@@ -452,11 +448,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e)))
|
||||
}
|
||||
|
||||
async fn folder_exists(&self, path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult<bool> {
|
||||
async fn folder_exists(&self, _path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult<bool> {
|
||||
Err(domain::repositories::folder_repository::FolderRepositoryError::Other("Not implemented".to_string()))
|
||||
}
|
||||
|
||||
async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult<domain::entities::folder::Folder> {
|
||||
async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult<domain::entities::folder::Folder> {
|
||||
Err(domain::repositories::folder_repository::FolderRepositoryError::Other("Not implemented".to_string()))
|
||||
}
|
||||
|
||||
@@ -468,7 +464,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e)))
|
||||
}
|
||||
|
||||
async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> {
|
||||
async fn restore_from_trash(&self, _folder_id: &str, original_path: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> {
|
||||
// Convert the original_path to a StoragePath for the repository
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
let storage_path = StoragePath::from_string(original_path);
|
||||
@@ -557,7 +553,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let core_services = common::di::CoreServices {
|
||||
path_service: path_service.clone(),
|
||||
cache_manager: Arc::new(infrastructure::services::cache_manager::StorageCacheManager::default()),
|
||||
id_mapping_service: base_id_mapping_service.clone(), // We keep using the folder ID mapping service for core services
|
||||
id_mapping_service: base_id_mapping_service.clone(),
|
||||
file_id_mapping_service: file_id_mapping_service.clone(),
|
||||
id_mapping_optimizer: id_mapping_optimizer.clone(),
|
||||
config: config.clone(),
|
||||
};
|
||||
|
||||
@@ -588,6 +586,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
storage_mediator: storage_mediator_stub,
|
||||
metadata_manager,
|
||||
path_resolver: path_resolver_stub,
|
||||
metadata_cache: metadata_cache.clone(),
|
||||
trash_repository: trash_repository.clone().map(|repo| {
|
||||
// Convert Arc<TrashFsRepository> to Arc<dyn TrashRepository>
|
||||
let repo: Arc<dyn crate::domain::repositories::trash_repository::TrashRepository> = repo;
|
||||
@@ -666,12 +665,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let contact_service: Option<Arc<dyn application::ports::storage_ports::StorageUseCase>> = None;
|
||||
|
||||
let application_services = common::di::ApplicationServices {
|
||||
// Concrete types for backward compatibility with handlers
|
||||
folder_service_concrete: folder_service.clone(),
|
||||
file_service_concrete: file_service.clone(),
|
||||
// Trait objects for abstraction
|
||||
folder_service: folder_service.clone(),
|
||||
file_service: file_service.clone(),
|
||||
file_upload_service: Arc::new(application::services::file_upload_service::FileUploadService::default_stub()),
|
||||
file_retrieval_service: Arc::new(application::services::file_retrieval_service::FileRetrievalService::default_stub()),
|
||||
file_management_service: Arc::new(application::services::file_management_service::FileManagementService::default_stub()),
|
||||
file_use_case_factory: Arc::new(application::services::file_use_case_factory::AppFileUseCaseFactory::default_stub()),
|
||||
file_upload_service: Arc::new(application::services::file_upload_service::FileUploadService::new(
|
||||
Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub())
|
||||
)),
|
||||
file_retrieval_service: Arc::new(application::services::file_retrieval_service::FileRetrievalService::new(
|
||||
Arc::new(infrastructure::repositories::FileFsReadRepository::default_stub())
|
||||
)),
|
||||
file_management_service: Arc::new(application::services::file_management_service::FileManagementService::new(
|
||||
Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub())
|
||||
)),
|
||||
file_use_case_factory: Arc::new(application::services::file_use_case_factory::AppFileUseCaseFactory::new(
|
||||
Arc::new(infrastructure::repositories::FileFsReadRepository::default_stub()),
|
||||
Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub())
|
||||
)),
|
||||
i18n_service: i18n_service.clone(),
|
||||
trash_service: trash_service.clone(),
|
||||
search_service: search_service.clone(),
|
||||
|
||||
Reference in New Issue
Block a user