refactoring hexagonal and clean architecture

This commit is contained in:
Diocrafts
2026-02-08 13:40:23 +01:00
parent 3e4fb67c19
commit a82faa5eaf
101 changed files with 7433 additions and 9721 deletions
-292
View File
@@ -1,292 +0,0 @@
//! 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_from_stream(
&self,
name: String,
folder_id: Option<String>,
content_type: String,
stream: std::pin::Pin<Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>>,
) -> FileRepositoryResult<File> {
self.repo.save_file_from_stream(name, folder_id, content_type, stream)
.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 get_file_range_stream(
&self,
id: &str,
start: u64,
end: Option<u64>
) -> FileRepositoryResult<Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send>> {
self.repo.get_file_range_stream(id, start, end)
.await
.map_err(|e| FileRepositoryError::Other(format!("{}", e)))
}
async fn get_file_mmap(&self, id: &str) -> FileRepositoryResult<bytes::Bytes> {
self.repo.get_file_mmap(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
}
}
-53
View File
@@ -1,53 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use sqlx::PgPool;
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;
pub async fn create_auth_services(
config: &AppConfig,
pool: Arc<PgPool>,
folder_service: Option<Arc<FolderService>>
) -> Result<AuthServices> {
// 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()));
// Crear servicio de aplicación de autenticación
let mut auth_app_service = AuthApplicationService::new(
user_repository,
session_repository,
password_hasher,
token_service.clone(),
);
// Configurar servicio de carpetas si está disponible
if let Some(folder_svc) = folder_service {
auth_app_service = auth_app_service.with_folder_service(folder_svc);
}
// Empaquetar servicio en Arc
let auth_application_service = Arc::new(auth_app_service);
Ok(AuthServices {
token_service,
auth_application_service,
})
}
+21 -1
View File
@@ -252,7 +252,10 @@ pub struct AuthConfig {
impl Default for AuthConfig {
fn default() -> Self {
Self {
jwt_secret: "ox1cl0ud-sup3r-s3cr3t-k3y-f0r-t0k3n-s1gn1ng".to_string(),
// SECURITY: This default is intentionally insecure to force operators
// to set OXICLOUD_JWT_SECRET in production. The from_env() method
// will validate this and warn/panic if not configured.
jwt_secret: String::new(),
access_token_expiry_secs: 3600, // 1 hora
refresh_token_expiry_secs: 2592000, // 30 días
hash_memory_cost: 65536, // 64MB
@@ -377,6 +380,23 @@ impl AppConfig {
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
config.auth.jwt_secret = jwt_secret;
}
// SECURITY: Validate JWT secret when auth is enabled
if config.features.enable_auth && config.auth.jwt_secret.is_empty() {
// Generate a random secret for this session and warn loudly
use rand_core::{OsRng, RngCore};
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
let generated_secret: String = key.iter().map(|b| format!("{:02x}", b)).collect();
config.auth.jwt_secret = generated_secret;
tracing::error!("==========================================================");
tracing::error!("SECURITY WARNING: OXICLOUD_JWT_SECRET is not set!");
tracing::error!("A random secret has been generated for this session.");
tracing::error!("All tokens will be INVALIDATED on restart.");
tracing::error!("Set OXICLOUD_JWT_SECRET env var for production use.");
tracing::error!("==========================================================");
}
if let Ok(access_token_expiry) = env::var("OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS")
.map(|v| v.parse::<i64>()) {
-72
View File
@@ -1,72 +0,0 @@
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
use anyhow::Result;
use std::time::Duration;
use crate::common::config::AppConfig;
pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
tracing::info!("Inicializando conexión a PostgreSQL con URL: {}",
config.database.connection_string.replace("postgres://", "postgres://[user]:[pass]@"));
// Add a more robust connection attempt with retries
let mut attempt = 0;
const MAX_ATTEMPTS: usize = 3;
while attempt < MAX_ATTEMPTS {
attempt += 1;
tracing::info!("Intento de conexión a PostgreSQL #{}", attempt);
// Crear el pool de conexiones con las opciones de configuración
match PgPoolOptions::new()
.max_connections(config.database.max_connections)
.min_connections(config.database.min_connections)
.acquire_timeout(Duration::from_secs(config.database.connect_timeout_secs))
.idle_timeout(Duration::from_secs(config.database.idle_timeout_secs))
.max_lifetime(Duration::from_secs(config.database.max_lifetime_secs))
.connect(&config.database.connection_string)
.await {
Ok(pool) => {
// Verificar la conexión
match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => {
tracing::info!("Conexión a PostgreSQL establecida correctamente");
// Verify if migrations have been applied
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
.fetch_one(&pool)
.await;
match migration_check {
Ok(row) => {
let tables_exist: bool = row.get(0);
if !tables_exist {
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
}
},
Err(_) => {
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
}
}
return Ok(pool);
},
Err(e) => {
tracing::error!("Error al verificar conexión: {}", e);
tracing::warn!("La base de datos parece no estar configurada. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
if attempt >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("Error al verificar la conexión a PostgreSQL: {}", e));
}
}
}
},
Err(e) => {
tracing::error!("Error al conectar a PostgreSQL: {}", e);
if attempt >= MAX_ATTEMPTS {
return Err(anyhow::anyhow!("Error en la conexión a PostgreSQL: {}", e));
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
Err(anyhow::anyhow!("No se pudo establecer la conexión a PostgreSQL después de {} intentos", MAX_ATTEMPTS))
}
+332 -639
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -1,6 +1,4 @@
pub mod errors;
pub mod config;
pub mod di;
pub mod db;
pub mod auth_factory;
pub mod adapters;
pub mod stubs;
+752
View File
@@ -0,0 +1,752 @@
//! Stub/Dummy implementations for dependency injection.
//!
//! These no-op implementations are used exclusively by `AppState::default()`
//! to provide a minimal, valid state for the auth middleware and route
//! construction before the real services are wired in `main.rs`.
//!
//! **None of these stubs should ever handle real user requests.**
use std::path::{Path, PathBuf};
use std::sync::Arc;
use async_trait::async_trait;
use bytes::Bytes;
use futures::Stream;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto};
use crate::application::dtos::pagination::{PaginatedResponseDto, PaginationRequestDto};
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort};
use crate::application::ports::file_ports::{
FileManagementUseCase, FileRetrievalUseCase, FileUploadUseCase, FileUseCaseFactory,
UploadStrategy, OptimizedFileContent,
};
use crate::application::ports::inbound::{FolderUseCase, SearchUseCase};
use crate::application::ports::outbound::IdMappingPort;
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::application::ports::zip_ports::ZipPort;
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorError};
use crate::common::errors::DomainError;
use crate::domain::entities::file::File;
use crate::domain::entities::folder::Folder;
use crate::domain::services::i18n_service::{I18nResult, I18nService, Locale};
use crate::domain::services::path_service::StoragePath;
// ---------------------------------------------------------------------------
// ZipPort
// ---------------------------------------------------------------------------
/// Placeholder ZipPort that always errors. Replaced after application services
/// are fully initialised.
pub struct StubZipPort;
#[async_trait]
impl ZipPort for StubZipPort {
async fn create_folder_zip(
&self,
_folder_id: &str,
_folder_name: &str,
) -> Result<Vec<u8>, DomainError> {
Err(DomainError::internal_error(
"ZipService",
"ZipService not initialized",
))
}
}
// ---------------------------------------------------------------------------
// CompressionPort
// ---------------------------------------------------------------------------
pub struct StubCompressionPort;
#[async_trait]
impl CompressionPort for StubCompressionPort {
async fn compress_data(
&self,
_data: &[u8],
_level: CompressionLevel,
) -> Result<Vec<u8>, DomainError> {
Ok(Vec::new())
}
async fn decompress_data(&self, _compressed_data: &[u8]) -> Result<Vec<u8>, DomainError> {
Ok(Vec::new())
}
fn should_compress(&self, _mime_type: &str, _size: u64) -> bool {
false
}
}
// ---------------------------------------------------------------------------
// IdMappingPort
// ---------------------------------------------------------------------------
pub struct StubIdMappingService;
#[async_trait]
impl IdMappingPort for StubIdMappingService {
async fn get_or_create_id(
&self,
_path: &StoragePath,
) -> Result<String, DomainError> {
Ok("dummy-id".to_string())
}
async fn get_path_by_id(&self, _id: &str) -> Result<StoragePath, DomainError> {
Ok(StoragePath::from_string("/"))
}
async fn update_path(
&self,
_id: &str,
_new_path: &StoragePath,
) -> Result<(), DomainError> {
Ok(())
}
async fn remove_id(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn save_changes(&self) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// StorageMediator
// ---------------------------------------------------------------------------
pub struct StubStorageMediator;
#[async_trait]
impl StorageMediator for StubStorageMediator {
async fn get_folder_path(
&self,
_folder_id: &str,
) -> Result<PathBuf, StorageMediatorError> {
Ok(PathBuf::from("/tmp"))
}
async fn get_folder_storage_path(
&self,
_folder_id: &str,
) -> Result<StoragePath, StorageMediatorError> {
Ok(StoragePath::root())
}
async fn get_folder(
&self,
_folder_id: &str,
) -> Result<Folder, StorageMediatorError> {
Err(StorageMediatorError::NotFound(
"Stub not implemented".to_string(),
))
}
async fn file_exists_at_path(
&self,
_path: &Path,
) -> Result<bool, StorageMediatorError> {
Ok(false)
}
async fn file_exists_at_storage_path(
&self,
_storage_path: &StoragePath,
) -> Result<bool, StorageMediatorError> {
Ok(false)
}
async fn folder_exists_at_path(
&self,
_path: &Path,
) -> Result<bool, StorageMediatorError> {
Ok(false)
}
async fn folder_exists_at_storage_path(
&self,
_storage_path: &StoragePath,
) -> Result<bool, StorageMediatorError> {
Ok(false)
}
fn resolve_path(&self, _relative_path: &Path) -> PathBuf {
PathBuf::from("/tmp")
}
fn resolve_storage_path(&self, _storage_path: &StoragePath) -> PathBuf {
PathBuf::from("/tmp")
}
async fn ensure_directory(
&self,
_path: &Path,
) -> Result<(), StorageMediatorError> {
Ok(())
}
async fn ensure_storage_directory(
&self,
_storage_path: &StoragePath,
) -> Result<(), StorageMediatorError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// IdMappingPort
// ---------------------------------------------------------------------------
pub struct StubIdMappingPort;
#[async_trait]
impl IdMappingPort for StubIdMappingPort {
async fn get_or_create_id(&self, _path: &StoragePath) -> Result<String, DomainError> {
Ok("stub-id".to_string())
}
async fn get_path_by_id(&self, _id: &str) -> Result<StoragePath, DomainError> {
Ok(StoragePath::from_string("/"))
}
async fn update_path(&self, _id: &str, _new_path: &StoragePath) -> Result<(), DomainError> {
Ok(())
}
async fn remove_id(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn save_changes(&self) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// FileReadPort
// ---------------------------------------------------------------------------
pub struct StubFileReadPort;
#[async_trait]
impl FileReadPort for StubFileReadPort {
async fn get_file(&self, _id: &str) -> Result<File, DomainError> {
Ok(File::default())
}
async fn list_files(
&self,
_folder_id: Option<&str>,
) -> Result<Vec<File>, DomainError> {
Ok(Vec::new())
}
async fn get_file_content(&self, _id: &str) -> Result<Vec<u8>, DomainError> {
Ok(Vec::new())
}
async fn get_file_stream(
&self,
_id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
Ok(Box::new(empty_stream))
}
async fn get_file_range_stream(
&self,
_id: &str,
_start: u64,
_end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
Ok(Box::new(empty_stream))
}
async fn get_file_mmap(&self, _id: &str) -> Result<Bytes, DomainError> {
Ok(Bytes::new())
}
async fn get_file_path(&self, _id: &str) -> Result<StoragePath, DomainError> {
Ok(StoragePath::from_string("/"))
}
async fn get_parent_folder_id(&self, _path: &str) -> Result<String, DomainError> {
Ok("root".to_string())
}
}
// ---------------------------------------------------------------------------
// FileWritePort
// ---------------------------------------------------------------------------
pub struct StubFileWritePort;
#[async_trait]
impl FileWritePort for StubFileWritePort {
async fn save_file(
&self,
_name: String,
_folder_id: Option<String>,
_content_type: String,
_content: Vec<u8>,
) -> Result<File, DomainError> {
Ok(File::default())
}
async fn save_file_from_stream(
&self,
_name: String,
_folder_id: Option<String>,
_content_type: String,
_stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<File, DomainError> {
Ok(File::default())
}
async fn move_file(
&self,
_file_id: &str,
_target_folder_id: Option<String>,
) -> Result<File, DomainError> {
Ok(File::default())
}
async fn delete_file(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn update_file_content(
&self,
_file_id: &str,
_content: Vec<u8>,
) -> Result<(), DomainError> {
Ok(())
}
async fn register_file_deferred(
&self,
_name: String,
_folder_id: Option<String>,
_content_type: String,
_size: u64,
) -> Result<(File, PathBuf), DomainError> {
Ok((File::default(), PathBuf::from("/tmp/dummy")))
}
async fn move_to_trash(&self, _file_id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn restore_from_trash(&self, _file_id: &str, _original_path: &str) -> Result<(), DomainError> {
Ok(())
}
async fn delete_file_permanently(&self, _file_id: &str) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// FolderStoragePort
// ---------------------------------------------------------------------------
pub struct StubFolderStoragePort;
#[async_trait]
impl FolderRepository for StubFolderStoragePort {
async fn create_folder(
&self,
_name: String,
_parent_id: Option<String>,
) -> Result<Folder, DomainError> {
Ok(Folder::default())
}
async fn get_folder(&self, _id: &str) -> Result<Folder, DomainError> {
Ok(Folder::default())
}
async fn get_folder_by_path(
&self,
_storage_path: &StoragePath,
) -> Result<Folder, DomainError> {
Ok(Folder::default())
}
async fn list_folders(
&self,
_parent_id: Option<&str>,
) -> Result<Vec<Folder>, DomainError> {
Ok(Vec::new())
}
async fn list_folders_paginated(
&self,
_parent_id: Option<&str>,
_offset: usize,
_limit: usize,
_include_total: bool,
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
Ok((Vec::new(), Some(0)))
}
async fn rename_folder(
&self,
_id: &str,
_new_name: String,
) -> Result<Folder, DomainError> {
Ok(Folder::default())
}
async fn move_folder(
&self,
_id: &str,
_new_parent_id: Option<&str>,
) -> Result<Folder, DomainError> {
Ok(Folder::default())
}
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn folder_exists(
&self,
_storage_path: &StoragePath,
) -> Result<bool, DomainError> {
Ok(false)
}
async fn get_folder_path(
&self,
_id: &str,
) -> Result<StoragePath, DomainError> {
Ok(StoragePath::from_string("/"))
}
async fn move_to_trash(&self, _folder_id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> Result<(), DomainError> {
Ok(())
}
async fn delete_folder_permanently(&self, _folder_id: &str) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// I18nService
// ---------------------------------------------------------------------------
pub struct StubI18nService;
#[async_trait]
impl I18nService for StubI18nService {
async fn translate(&self, _key: &str, _locale: Locale) -> I18nResult<String> {
Ok(String::new())
}
async fn load_translations(&self, _locale: Locale) -> I18nResult<()> {
Ok(())
}
async fn available_locales(&self) -> Vec<Locale> {
vec![Locale::default()]
}
async fn is_supported(&self, _locale: Locale) -> bool {
true
}
}
// ---------------------------------------------------------------------------
// FolderUseCase
// ---------------------------------------------------------------------------
pub struct StubFolderUseCase;
#[async_trait]
impl FolderUseCase for StubFolderUseCase {
async fn create_folder(
&self,
_dto: CreateFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
async fn get_folder(&self, _id: &str) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
async fn get_folder_by_path(
&self,
_path: &str,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
async fn list_folders(
&self,
_parent_id: Option<&str>,
) -> Result<Vec<FolderDto>, DomainError> {
Ok(Vec::new())
}
async fn list_folders_paginated(
&self,
_parent_id: Option<&str>,
_pagination: &PaginationRequestDto,
) -> Result<PaginatedResponseDto<FolderDto>, DomainError> {
Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0))
}
async fn rename_folder(
&self,
_id: &str,
_dto: RenameFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
async fn move_folder(
&self,
_id: &str,
_dto: MoveFolderDto,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// FileUploadUseCase
// ---------------------------------------------------------------------------
pub struct StubFileUploadUseCase;
#[async_trait]
impl FileUploadUseCase for StubFileUploadUseCase {
async fn upload_file(
&self,
_name: String,
_folder_id: Option<String>,
_content_type: String,
_content: Vec<u8>,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
}
async fn smart_upload(
&self,
_name: String,
_folder_id: Option<String>,
_content_type: String,
_chunks: Vec<Bytes>,
_total_size: usize,
) -> Result<(FileDto, UploadStrategy), DomainError> {
Ok((FileDto::default(), UploadStrategy::Buffered))
}
async fn create_file(&self, _parent_path: &str, _filename: &str, _content: &[u8], _content_type: &str) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
}
async fn update_file(&self, _path: &str, _content: &[u8]) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// FileRetrievalUseCase
// ---------------------------------------------------------------------------
pub struct StubFileRetrievalUseCase;
#[async_trait]
impl FileRetrievalUseCase for StubFileRetrievalUseCase {
async fn get_file(&self, _id: &str) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
}
async fn list_files(
&self,
_folder_id: Option<&str>,
) -> Result<Vec<FileDto>, DomainError> {
Ok(Vec::new())
}
async fn get_file_content(&self, _id: &str) -> Result<Vec<u8>, DomainError> {
Ok(Vec::new())
}
async fn get_file_stream(
&self,
_id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
Ok(Box::new(empty_stream))
}
async fn get_file_optimized(
&self,
_id: &str,
_accept_webp: bool,
_prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
Ok((FileDto::default(), OptimizedFileContent::Bytes {
data: Bytes::new(),
mime_type: String::new(),
was_transcoded: false,
}))
}
async fn get_file_range_stream(
&self,
_id: &str,
_start: u64,
_end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
Ok(Box::new(empty_stream))
}
async fn get_file_by_path(&self, _path: &str) -> Result<FileDto, DomainError> {
Err(DomainError::not_found("File", "stub"))
}
}
// ---------------------------------------------------------------------------
// FileManagementUseCase
// ---------------------------------------------------------------------------
pub struct StubFileManagementUseCase;
#[async_trait]
impl FileManagementUseCase for StubFileManagementUseCase {
async fn move_file(
&self,
_file_id: &str,
_folder_id: Option<String>,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
}
async fn delete_file(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn delete_with_cleanup(
&self,
_id: &str,
_user_id: &str,
) -> Result<bool, DomainError> {
Ok(false)
}
}
// ---------------------------------------------------------------------------
// FileUseCaseFactory
// ---------------------------------------------------------------------------
pub struct StubFileUseCaseFactory;
impl FileUseCaseFactory for StubFileUseCaseFactory {
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase> {
Arc::new(StubFileUploadUseCase)
}
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase> {
Arc::new(StubFileRetrievalUseCase)
}
fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase> {
Arc::new(StubFileManagementUseCase)
}
}
// ---------------------------------------------------------------------------
// SearchUseCase
// ---------------------------------------------------------------------------
pub struct StubSearchUseCase;
#[async_trait]
impl SearchUseCase for StubSearchUseCase {
async fn search(
&self,
_criteria: SearchCriteriaDto,
) -> Result<SearchResultsDto, DomainError> {
Ok(SearchResultsDto::empty())
}
async fn clear_search_cache(&self) -> Result<(), DomainError> {
Ok(())
}
}
// ---------------------------------------------------------------------------
// MetadataCachePort
// ---------------------------------------------------------------------------
use crate::application::ports::cache_ports::{MetadataCachePort, CachedMetadataDto, ContentCachePort};
pub struct StubMetadataCachePort;
#[async_trait]
impl MetadataCachePort for StubMetadataCachePort {
async fn get_metadata(&self, _path: &Path) -> Option<CachedMetadataDto> {
None
}
async fn is_file(&self, _path: &Path) -> Option<bool> {
None
}
async fn refresh_metadata(&self, path: &Path) -> Result<CachedMetadataDto, DomainError> {
Ok(CachedMetadataDto {
path: path.to_path_buf(),
exists: false,
is_file: false,
size: None,
mime_type: None,
created_at: None,
modified_at: None,
})
}
async fn invalidate(&self, _path: &Path) {}
async fn invalidate_directory(&self, _dir_path: &Path) {}
}
// ---------------------------------------------------------------------------
// ContentCachePort
// ---------------------------------------------------------------------------
pub struct StubContentCachePort;
#[async_trait]
impl ContentCachePort for StubContentCachePort {
fn should_cache(&self, _size: usize) -> bool {
false
}
async fn get(&self, _file_id: &str) -> Option<(Bytes, String, String)> {
None
}
async fn put(&self, _file_id: String, _content: Bytes, _etag: String, _content_type: String) {}
async fn invalidate(&self, _file_id: &str) {}
async fn clear(&self) {}
}