adding search engine

This commit is contained in:
DioCrafts
2025-03-27 01:13:34 +01:00
parent dacc3ecc4c
commit 7affff379d
24 changed files with 1531 additions and 66 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
use serde::Serialize;
use serde::{Serialize, Deserialize};
use crate::domain::entities::file::File;
/// DTO for file responses
#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDto {
/// File ID
pub id: String,
+1 -1
View File
@@ -26,7 +26,7 @@ pub struct MoveFolderDto {
}
/// DTO for folder responses
#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderDto {
/// Folder ID
pub id: String,
+1
View File
@@ -4,4 +4,5 @@ pub mod i18n_dto;
pub mod pagination;
pub mod user_dto;
pub mod trash_dto;
pub mod search_dto;
+152
View File
@@ -0,0 +1,152 @@
use serde::{Serialize, Deserialize};
/**
* Data Transfer Object for file search criteria.
*
* This structure represents all possible search parameters that can be used
* to filter files and folders in the system. It supports various filter types
* including name matching, file types, date ranges, and size constraints.
*/
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchCriteriaDto {
/// Optional text to search in file/folder names
#[serde(skip_serializing_if = "Option::is_none")]
pub name_contains: Option<String>,
/// Optional list of file extensions to include (e.g., "pdf", "jpg")
#[serde(skip_serializing_if = "Option::is_none")]
pub file_types: Option<Vec<String>>,
/// Optional minimum creation date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")]
pub created_after: Option<u64>,
/// Optional maximum creation date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")]
pub created_before: Option<u64>,
/// Optional minimum modification date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_after: Option<u64>,
/// Optional maximum modification date (seconds since epoch)
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_before: Option<u64>,
/// Optional minimum file size in bytes
#[serde(skip_serializing_if = "Option::is_none")]
pub min_size: Option<u64>,
/// Optional maximum file size in bytes
#[serde(skip_serializing_if = "Option::is_none")]
pub max_size: Option<u64>,
/// Optional folder ID to limit search scope
#[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>,
/// Whether to search recursively within subfolders (default: true)
#[serde(default = "default_recursive")]
pub recursive: bool,
/// Maximum number of results to return
#[serde(default = "default_limit")]
pub limit: usize,
/// Offset for pagination
#[serde(default)]
pub offset: usize,
}
/// Default value for recursive search (true)
fn default_recursive() -> bool {
true
}
/// Default limit for search results (100)
fn default_limit() -> usize {
100
}
impl Default for SearchCriteriaDto {
fn default() -> Self {
Self {
name_contains: None,
file_types: None,
created_after: None,
created_before: None,
modified_after: None,
modified_before: None,
min_size: None,
max_size: None,
folder_id: None,
recursive: default_recursive(),
limit: default_limit(),
offset: 0,
}
}
}
/**
* Data Transfer Object for search results.
*
* This structure encapsulates the results of a search operation, including
* both files and folders that match the search criteria, along with pagination information.
*/
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResultsDto {
/// Files matching the search criteria
pub files: Vec<crate::application::dtos::file_dto::FileDto>,
/// Folders matching the search criteria
pub folders: Vec<crate::application::dtos::folder_dto::FolderDto>,
/// Total count of matching items (for pagination)
pub total_count: Option<usize>,
/// Limit used in the search
pub limit: usize,
/// Offset used in the search
pub offset: usize,
/// Whether there are more results available
pub has_more: bool,
}
impl SearchResultsDto {
/// Creates a new empty search results object
pub fn empty() -> Self {
Self {
files: Vec::new(),
folders: Vec::new(),
total_count: None,
limit: 0,
offset: 0,
has_more: false,
}
}
/// Creates a new search results object from files and folders
pub fn new(
files: Vec<crate::application::dtos::file_dto::FileDto>,
folders: Vec<crate::application::dtos::folder_dto::FolderDto>,
limit: usize,
offset: usize,
total_count: Option<usize>,
) -> Self {
let has_more = match total_count {
Some(total) => (offset + files.len() + folders.len()) < total,
None => false,
};
Self {
files,
folders,
total_count,
limit,
offset,
has_more,
}
}
}
+26
View File
@@ -5,6 +5,7 @@ use futures::Stream;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto};
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::common::errors::DomainError;
/// Puerto primario para operaciones de archivos
@@ -70,8 +71,33 @@ pub trait FolderUseCase: Send + Sync + 'static {
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
}
/**
* Puerto primario para búsqueda de archivos y carpetas
*
* Define las operaciones relacionadas con la búsqueda avanzada de
* archivos y carpetas basándose en diversos criterios.
*/
#[async_trait]
pub trait SearchUseCase: Send + Sync + 'static {
/**
* Realiza una búsqueda basada en los criterios especificados
*
* @param criteria Criterios de búsqueda que incluyen texto, fechas, tamaños, etc.
* @return Resultados de la búsqueda que contienen archivos y carpetas coincidentes
*/
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>;
/**
* Limpia la caché de resultados de búsqueda
*
* @return Resultado indicando éxito o error
*/
async fn clear_search_cache(&self) -> Result<(), DomainError>;
}
/// Factory para crear implementaciones de casos de uso
pub trait UseCaseFactory {
fn create_file_use_case(&self) -> Arc<dyn FileUseCase>;
fn create_folder_use_case(&self) -> Arc<dyn FolderUseCase>;
fn create_search_use_case(&self) -> Arc<dyn SearchUseCase>;
}
+2
View File
@@ -11,6 +11,7 @@ pub mod file_management_service;
pub mod file_use_case_factory;
pub mod auth_application_service;
pub mod trash_service;
pub mod search_service;
#[cfg(test)]
mod trash_service_test;
@@ -21,3 +22,4 @@ pub use file_retrieval_service::FileRetrievalService;
pub use file_management_service::FileManagementService;
pub use file_use_case_factory::AppFileUseCaseFactory;
pub use trash_service::TrashService;
pub use search_service::SearchService;
+494
View File
@@ -0,0 +1,494 @@
use std::sync::Arc;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use std::sync::Mutex;
use async_trait::async_trait;
use tokio::time;
use crate::common::errors::Result;
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::application::ports::inbound::SearchUseCase;
use crate::application::ports::outbound::{FileStoragePort, FolderStoragePort};
/**
* Implementación del servicio de búsqueda para archivos y carpetas.
*
* Este servicio implementa la funcionalidad de búsqueda avanzada que permite
* a los usuarios encontrar archivos y carpetas basados en diversos criterios
* como nombre, tipo, fecha y tamaño. También incluye una caché para mejorar
* el rendimiento de búsquedas repetidas.
*/
pub struct SearchService {
/// Repositorio para operaciones con archivos
file_repository: Arc<dyn FileStoragePort>,
/// Repositorio para operaciones con carpetas
folder_repository: Arc<dyn FolderStoragePort>,
/// Caché de resultados de búsqueda con tiempo de expiración
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
/// Duración de validez de la caché en segundos
cache_ttl: u64,
/// Tamaño máximo de la caché (número de resultados almacenados)
max_cache_size: usize,
}
/// Clave para la caché de búsqueda
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct SearchCacheKey {
/// Representación serializada de los criterios de búsqueda
criteria_hash: String,
/// ID del usuario (para aislar búsquedas entre usuarios)
user_id: String,
}
/// Resultado de búsqueda en caché con tiempo de expiración
struct CachedSearchResult {
/// Resultados de la búsqueda
results: SearchResultsDto,
/// Momento en que se creó la entrada de caché
timestamp: Instant,
}
impl SearchService {
/**
* Crea una nueva instancia del servicio de búsqueda.
*
* @param file_repository Repositorio para operaciones con archivos
* @param folder_repository Repositorio para operaciones con carpetas
* @param cache_ttl Tiempo de vida de la caché en segundos (0 para desactivar)
* @param max_cache_size Tamaño máximo de la caché
*/
pub fn new(
file_repository: Arc<dyn FileStoragePort>,
folder_repository: Arc<dyn FolderStoragePort>,
cache_ttl: u64,
max_cache_size: usize,
) -> Self {
let search_service = Self {
file_repository,
folder_repository,
search_cache: Arc::new(Mutex::new(HashMap::new())),
cache_ttl,
max_cache_size,
};
// Iniciar tarea de limpieza de caché si TTL > 0
if cache_ttl > 0 {
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
}
search_service
}
/**
* Inicia una tarea asíncrona para limpiar entradas expiradas de la caché.
*
* @param cache_ref Referencia a la caché compartida
* @param ttl_seconds TTL en segundos
*/
fn start_cache_cleanup_task(
cache_ref: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
ttl_seconds: u64,
) {
tokio::spawn(async move {
let cleanup_interval = Duration::from_secs(ttl_seconds / 2);
let ttl = Duration::from_secs(ttl_seconds);
loop {
time::sleep(cleanup_interval).await;
// Obtener lock y limpiar entradas expiradas
if let Ok(mut cache) = cache_ref.lock() {
let now = Instant::now();
// Identificar entradas expiradas
let expired_keys: Vec<SearchCacheKey> = cache
.iter()
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
.map(|(key, _)| key.clone())
.collect();
// Eliminar entradas expiradas
for key in expired_keys {
cache.remove(&key);
}
}
}
});
}
/**
* Crea una clave de caché a partir de los criterios de búsqueda.
*
* @param criteria Criterios de búsqueda
* @param user_id ID del usuario (para aislar caché entre usuarios)
* @return Clave para la caché
*/
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
// Serializar criterios para generar un hash
let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
SearchCacheKey {
criteria_hash: criteria_str,
user_id: user_id.to_string(),
}
}
/**
* Intenta obtener resultados de la caché.
*
* @param key Clave de caché
* @return Opcionalmente, los resultados si existen y no han expirado
*/
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
// Si TTL es 0, la caché está desactivada
if self.cache_ttl == 0 {
return None;
}
if let Ok(cache) = self.search_cache.lock() {
if let Some(cached_result) = cache.get(key) {
let now = Instant::now();
let ttl = Duration::from_secs(self.cache_ttl);
// Comprobar si la entrada ha expirado
if now.duration_since(cached_result.timestamp) < ttl {
return Some(cached_result.results.clone());
}
}
}
None
}
/**
* Almacena resultados en la caché.
*
* @param key Clave de caché
* @param results Resultados a almacenar
*/
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) {
// Si TTL es 0, la caché está desactivada
if self.cache_ttl == 0 {
return;
}
if let Ok(mut cache) = self.search_cache.lock() {
// Si la caché está llena, eliminar la entrada más antigua
if cache.len() >= self.max_cache_size {
if let Some((oldest_key, _)) = cache
.iter()
.min_by_key(|(_, result)| result.timestamp) {
let key_to_remove = oldest_key.clone();
cache.remove(&key_to_remove);
}
}
// Almacenar el nuevo resultado
cache.insert(key, CachedSearchResult {
results,
timestamp: Instant::now(),
});
}
}
/**
* Filtra archivos según los criterios de búsqueda.
*
* @param files Lista de archivos a filtrar
* @param criteria Criterios de búsqueda
* @return Archivos que cumplen con los criterios
*/
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
files.into_iter()
.filter(|file| {
// Filtrar por nombre
if let Some(name_query) = &criteria.name_contains {
if !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false;
}
}
// Filtrar por tipo de archivo (extensión)
if let Some(file_types) = &criteria.file_types {
if let Some(extension) = file.name.split('.').last() {
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
return false;
}
} else {
// No tiene extensión
return false;
}
}
// Filtrar por fecha de creación
if let Some(created_after) = criteria.created_after {
if file.created_at < created_after {
return false;
}
}
if let Some(created_before) = criteria.created_before {
if file.created_at > created_before {
return false;
}
}
// Filtrar por fecha de modificación
if let Some(modified_after) = criteria.modified_after {
if file.modified_at < modified_after {
return false;
}
}
if let Some(modified_before) = criteria.modified_before {
if file.modified_at > modified_before {
return false;
}
}
// Filtrar por tamaño
if let Some(min_size) = criteria.min_size {
if file.size < min_size {
return false;
}
}
if let Some(max_size) = criteria.max_size {
if file.size > max_size {
return false;
}
}
true
})
.collect()
}
/**
* Filtra carpetas según los criterios de búsqueda.
*
* @param folders Lista de carpetas a filtrar
* @param criteria Criterios de búsqueda
* @return Carpetas que cumplen con los criterios
*/
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
folders.into_iter()
.filter(|folder| {
// Filtrar por nombre
if let Some(name_query) = &criteria.name_contains {
if !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
return false;
}
}
// Filtrar por fecha de creación
if let Some(created_after) = criteria.created_after {
if folder.created_at < created_after {
return false;
}
}
if let Some(created_before) = criteria.created_before {
if folder.created_at > created_before {
return false;
}
}
// Filtrar por fecha de modificación
if let Some(modified_after) = criteria.modified_after {
if folder.modified_at < modified_after {
return false;
}
}
if let Some(modified_before) = criteria.modified_before {
if folder.modified_at > modified_before {
return false;
}
}
true
})
.collect()
}
/**
* Implementación de la búsqueda recursiva a través de carpetas.
*
* @param current_folder_id ID de la carpeta actual
* @param criteria Criterios de búsqueda
* @param found_files Archivos encontrados hasta ahora
* @param found_folders Carpetas encontradas hasta ahora
*/
async fn search_recursive(
&self,
current_folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
found_files: &mut Vec<FileDto>,
found_folders: &mut Vec<FolderDto>,
) -> Result<()> {
Box::pin(async move {
// Listar archivos en la carpeta actual
let files = self.file_repository.list_files(current_folder_id).await?;
// Filtrar archivos según criterios y agregarlos a los resultados
let filtered_files = self.filter_files(
files.into_iter().map(FileDto::from).collect(),
criteria
);
found_files.extend(filtered_files);
// Si la búsqueda es recursiva, procesar subcarpetas
if criteria.recursive {
// Listar subcarpetas
let folders = self.folder_repository.list_folders(current_folder_id).await?;
// Filtrar carpetas según criterios y agregarlas a los resultados
let filtered_folders: Vec<FolderDto> = self.filter_folders(
folders.into_iter().map(FolderDto::from).collect(),
criteria
);
// Añadir las carpetas filtradas a los resultados
found_folders.extend(filtered_folders.iter().cloned());
// Buscar recursivamente en cada subcarpeta
for folder in filtered_folders {
self.search_recursive(
Some(&folder.id),
criteria,
found_files,
found_folders,
).await?;
}
}
Ok(())
}).await
}
}
#[async_trait]
impl SearchUseCase for SearchService {
/**
* Realiza una búsqueda basada en los criterios especificados.
*
* @param criteria Criterios de búsqueda
* @return Resultados de la búsqueda
*/
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
// TODO: Obtener ID de usuario del contexto de autenticación
let user_id = "default-user";
let cache_key = self.create_cache_key(&criteria, user_id);
// Intentar obtener resultados de la caché
if let Some(cached_results) = self.get_from_cache(&cache_key) {
return Ok(cached_results);
}
// Inicializar colecciones para resultados
let mut found_files: Vec<FileDto> = Vec::new();
let mut found_folders: Vec<FolderDto> = Vec::new();
// Realizar búsqueda en la carpeta especificada o en la raíz
self.search_recursive(
criteria.folder_id.as_deref(),
&criteria,
&mut found_files,
&mut found_folders,
).await?;
// Aplicar paginación
let total_count = found_files.len() + found_folders.len();
// Ordenar por relevancia o fecha según criterios
// Por defecto, ordenamos por fecha de modificación (más reciente primero)
found_files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
found_folders.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
// Aplicar límite y offset para paginación
let start_idx = criteria.offset.min(total_count);
let end_idx = (criteria.offset + criteria.limit).min(total_count);
let paginated_items: Vec<(bool, usize)> = (start_idx..end_idx)
.map(|i| {
if i < found_folders.len() {
(true, i) // Es una carpeta
} else {
(false, i - found_folders.len()) // Es un archivo
}
})
.collect();
// Extraer elementos paginados
let mut paginated_folders = Vec::new();
let mut paginated_files = Vec::new();
for (is_folder, idx) in paginated_items {
if is_folder {
if idx < found_folders.len() {
paginated_folders.push(found_folders[idx].clone());
}
} else {
if idx < found_files.len() {
paginated_files.push(found_files[idx].clone());
}
}
}
// Crear objeto de resultados
let search_results = SearchResultsDto::new(
paginated_files,
paginated_folders,
criteria.limit,
criteria.offset,
Some(total_count),
);
// Almacenar en caché
self.store_in_cache(cache_key, search_results.clone());
Ok(search_results)
}
/**
* Limpia la caché de resultados de búsqueda.
*
* @return Resultado indicando éxito
*/
async fn clear_search_cache(&self) -> Result<()> {
if let Ok(mut cache) = self.search_cache.lock() {
cache.clear();
}
Ok(())
}
}
// Implementar el caso de uso de prueba (stub)
impl SearchService {
/// Crea una versión stub del servicio para pruebas
pub fn new_stub() -> impl SearchUseCase {
struct SearchServiceStub;
#[async_trait]
impl SearchUseCase for SearchServiceStub {
async fn search(&self, _criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
Ok(SearchResultsDto::empty())
}
async fn clear_search_cache(&self) -> Result<()> {
Ok(())
}
}
SearchServiceStub
}
}
+2
View File
@@ -273,6 +273,7 @@ pub struct FeaturesConfig {
pub enable_user_storage_quotas: bool,
pub enable_file_sharing: bool,
pub enable_trash: bool,
pub enable_search: bool,
}
impl Default for FeaturesConfig {
@@ -282,6 +283,7 @@ impl Default for FeaturesConfig {
enable_user_storage_quotas: false,
enable_file_sharing: false,
enable_trash: true, // Enable trash feature
enable_search: true, // Enable search feature
}
}
}
+24 -2
View File
@@ -19,7 +19,7 @@ use crate::application::services::file_service::FileService;
use crate::application::services::i18n_application_service::I18nApplicationService;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator};
use crate::application::ports::inbound::{FileUseCase, FolderUseCase};
use crate::application::ports::inbound::{FileUseCase, FolderUseCase, SearchUseCase};
use crate::application::ports::outbound::{FileStoragePort, FolderStoragePort};
use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory};
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
@@ -226,6 +226,9 @@ impl AppServiceFactory {
// Servicio de papelera (deshabilitado temporalmente)
let trash_service = None; // La función de papelera está deshabilitada por defecto
// Servicio de búsqueda (deshabilitado por defecto)
let search_service = None; // La función de búsqueda se activa según la configuración
ApplicationServices {
folder_service,
file_service,
@@ -235,6 +238,7 @@ impl AppServiceFactory {
file_use_case_factory,
i18n_service,
trash_service,
search_service,
}
}
}
@@ -276,6 +280,7 @@ pub struct ApplicationServices {
pub file_use_case_factory: Arc<dyn FileUseCaseFactory>,
pub i18n_service: Arc<I18nApplicationService>,
pub trash_service: Option<Arc<dyn TrashUseCase>>,
pub search_service: Option<Arc<dyn SearchUseCase>>,
}
/// Contenedor para servicios de autenticación
@@ -698,7 +703,7 @@ impl Default for AppState {
}
}
struct DummyI18nApplicationService {};
struct DummyI18nApplicationService {}
// Need to implement the actual service to match the type signature in DI container
impl DummyI18nApplicationService {
@@ -746,6 +751,22 @@ impl Default for AppState {
trash_repository: None, // No trash repository in minimal mode
};
// Create dummy search use case
struct DummySearchUseCase;
#[async_trait::async_trait]
impl crate::application::ports::inbound::SearchUseCase for DummySearchUseCase {
async fn search(
&self,
_criteria: crate::application::dtos::search_dto::SearchCriteriaDto
) -> Result<crate::application::dtos::search_dto::SearchResultsDto, crate::common::errors::DomainError> {
Ok(crate::application::dtos::search_dto::SearchResultsDto::empty())
}
async fn clear_search_cache(&self) -> Result<(), crate::common::errors::DomainError> {
Ok(())
}
}
// Create application services
let application_services = ApplicationServices {
folder_service,
@@ -756,6 +777,7 @@ impl Default for AppState {
file_use_case_factory,
i18n_service: Arc::new(DummyI18nApplicationService::dummy()),
trash_service: None, // No trash service in minimal mode
search_service: Some(Arc::new(DummySearchUseCase) as Arc<dyn crate::application::ports::inbound::SearchUseCase>),
};
// Return a minimal app state
+1
View File
@@ -4,6 +4,7 @@ pub mod i18n_handler;
pub mod batch_handler;
pub mod auth_handler;
pub mod trash_handler;
pub mod search_handler;
/// Tipo de resultado para controladores de API
pub type ApiResult<T> = Result<T, (axum::http::StatusCode, String)>;
@@ -0,0 +1,225 @@
use axum::{
extract::{State, Query, Json},
response::IntoResponse,
http::StatusCode,
};
use serde_json::json;
use tracing::{info, error};
use crate::application::dtos::search_dto::SearchCriteriaDto;
use crate::common::di::AppState;
/**
* Manejador para las operaciones de búsqueda a través de la API.
*
* Este manejador expone endpoints relacionados con la funcionalidad de búsqueda,
* permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios.
*/
pub struct SearchHandler;
impl SearchHandler {
/**
* Realiza una búsqueda basada en los criterios proporcionados como parámetros de consulta.
*
* Este endpoint permite búsquedas simples directamente con parámetros URL.
*
* @param state Estado de la aplicación con servicios
* @param query_params Parámetros de búsqueda como query string
* @return Respuesta HTTP con los resultados de la búsqueda
*/
pub async fn search_files_get(
State(state): State<AppState>,
Query(params): Query<SearchParams>,
) -> impl IntoResponse {
info!("API: Búsqueda de archivos con parámetros: {:?}", params);
// Extraer el servicio de búsqueda o devolver error si no está disponible
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"error": "Search service is not available"
}))
).into_response();
}
};
// Convertir parámetros de búsqueda a DTO
let search_criteria = SearchCriteriaDto {
name_contains: params.query,
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()),
created_after: params.created_after,
created_before: params.created_before,
modified_after: params.modified_after,
modified_before: params.modified_before,
min_size: params.min_size,
max_size: params.max_size,
folder_id: params.folder_id,
recursive: params.recursive.unwrap_or(true),
limit: params.limit.unwrap_or(100),
offset: params.offset.unwrap_or(0),
};
// Realizar la búsqueda
match search_service.search(search_criteria).await {
Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response()
},
Err(err) => {
error!("Error en búsqueda: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Search error: {}", err)
}))
).into_response()
}
}
}
/**
* Realiza una búsqueda avanzada basada en un objeto de criterios JSON completo.
*
* Este endpoint permite búsquedas más complejas con todos los criterios posibles
* proporcionados en el cuerpo de la solicitud.
*
* @param state Estado de la aplicación con servicios
* @param criteria Criterios de búsqueda completos
* @return Respuesta HTTP con los resultados de la búsqueda
*/
pub async fn search_files_post(
State(state): State<AppState>,
Json(criteria): Json<SearchCriteriaDto>,
) -> impl IntoResponse {
info!("API: Búsqueda avanzada de archivos");
// Extraer el servicio de búsqueda o devolver error si no está disponible
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"error": "Search service is not available"
}))
).into_response();
}
};
// Realizar la búsqueda
match search_service.search(criteria).await {
Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response()
},
Err(err) => {
error!("Error en búsqueda: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Search error: {}", err)
}))
).into_response()
}
}
}
/**
* Limpia la caché de resultados de búsqueda.
*
* Este endpoint es útil para forzar búsquedas frescas después de cambios
* significativos en el sistema de archivos.
*
* @param state Estado de la aplicación con servicios
* @return Respuesta HTTP indicando éxito o error
*/
pub async fn clear_search_cache(
State(state): State<AppState>,
) -> impl IntoResponse {
info!("API: Limpiando caché de búsqueda");
// Extraer el servicio de búsqueda o devolver error si no está disponible
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"error": "Search service is not available"
}))
).into_response();
}
};
// Limpiar la caché
match search_service.clear_search_cache().await {
Ok(_) => {
info!("Caché de búsqueda limpiada correctamente");
(
StatusCode::OK,
Json(json!({
"message": "Search cache cleared successfully"
}))
).into_response()
},
Err(err) => {
error!("Error al limpiar caché de búsqueda: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error clearing search cache: {}", err)
}))
).into_response()
}
}
}
}
/// Parámetros de búsqueda para el endpoint GET
#[derive(Debug, serde::Deserialize)]
pub struct SearchParams {
/// Texto a buscar en nombres de archivos y carpetas
pub query: Option<String>,
/// Filtro por tipos de archivo (extensiones separadas por comas)
#[serde(rename = "type")]
pub type_filter: Option<String>,
/// Filtrar elementos creados después de esta fecha (timestamp)
pub created_after: Option<u64>,
/// Filtrar elementos creados antes de esta fecha (timestamp)
pub created_before: Option<u64>,
/// Filtrar elementos modificados después de esta fecha (timestamp)
pub modified_after: Option<u64>,
/// Filtrar elementos modificados antes de esta fecha (timestamp)
pub modified_before: Option<u64>,
/// Tamaño mínimo en bytes
pub min_size: Option<u64>,
/// Tamaño máximo en bytes
pub max_size: Option<u64>,
/// ID de carpeta para limitar la búsqueda
pub folder_id: Option<String>,
/// Búsqueda recursiva en subcarpetas
pub recursive: Option<bool>,
/// Límite de resultados para paginación
pub limit: Option<usize>,
/// Desplazamiento para paginación
pub offset: Option<usize>,
}
+21 -1
View File
@@ -23,6 +23,7 @@ use crate::application::services::file_service::FileService;
use crate::application::services::i18n_application_service::I18nApplicationService;
use crate::application::services::batch_operations::BatchOperationService;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::ports::inbound::SearchUseCase;
use crate::interfaces::api::handlers::folder_handler::FolderHandler;
use crate::interfaces::api::handlers::file_handler::FileHandler;
@@ -38,6 +39,7 @@ pub fn create_api_routes(
file_service: Arc<FileService>,
i18n_service: Option<Arc<I18nApplicationService>>,
trash_service: Option<Arc<dyn TrashUseCase>>,
search_service: Option<Arc<dyn SearchUseCase>>,
) -> Router<crate::common::di::AppState> {
// Create a simplified AppState for the trash view
// Setup required components for repository construction
@@ -99,6 +101,7 @@ pub fn create_api_routes(
Arc::new(crate::application::services::i18n_application_service::I18nApplicationService::dummy())
),
trash_service: trash_service.clone(), // Include the trash service here too for consistency
search_service: search_service.clone(), // Include the search service
},
db_pool: None,
auth_service: None,
@@ -265,11 +268,28 @@ pub fn create_api_routes(
.route("/folders/get", post(batch_handler::get_folders_batch))
.with_state(batch_handler_state);
// Create search routes if the service is available
let search_router = if search_service.is_some() {
use crate::interfaces::api::handlers::search_handler::SearchHandler;
Router::new()
// Simple search with query parameters
.route("/", get(SearchHandler::search_files_get))
// Advanced search with full criteria object
.route("/advanced", post(SearchHandler::search_files_post))
// Clear search cache
.route("/cache", delete(SearchHandler::clear_search_cache))
.with_state(app_state.clone())
} else {
Router::new()
};
// Create a router without the i18n routes
let mut router = Router::new()
.nest("/folders", folders_router)
.nest("/files", files_router)
.nest("/batch", batch_router);
.nest("/batch", batch_router)
.nest("/search", search_router);
// Re-enable trash routes to make the trash view work
if let Some(trash_service_ref) = trash_service.clone() {
+39 -26
View File
@@ -6,31 +6,29 @@ use axum::Router;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
/**
* OxiCloud - Cloud Storage Platform
*
* OxiCloud is a NextCloud-like file storage system built in Rust with a focus on
* performance, security, and clean architecture. The system provides:
*
* - File and folder management with rich metadata
* - User authentication and authorization
* - File trash system with automatic cleanup
* - Efficient handling of large files through parallel processing
* - Compression capabilities for bandwidth optimization
* - RESTful API and web interface
*
* The architecture follows the Clean/Hexagonal Architecture pattern with:
*
* - Domain Layer: Core business entities and repository interfaces (domain/*)
* - Application Layer: Use cases and service orchestration (application/*)
* - Infrastructure Layer: Technical implementations of repositories (infrastructure/*)
* - Interface Layer: API endpoints and web controllers (interfaces/*)
*
* Dependencies are managed through dependency inversion, with high-level modules
* defining interfaces (ports) that low-level modules implement (adapters).
*
* @author OxiCloud Development Team
*/
/// OxiCloud - Cloud Storage Platform
///
/// OxiCloud is a NextCloud-like file storage system built in Rust with a focus on
/// performance, security, and clean architecture. The system provides:
///
/// - File and folder management with rich metadata
/// - User authentication and authorization
/// - File trash system with automatic cleanup
/// - Efficient handling of large files through parallel processing
/// - Compression capabilities for bandwidth optimization
/// - RESTful API and web interface
///
/// The architecture follows the Clean/Hexagonal Architecture pattern with:
///
/// - Domain Layer: Core business entities and repository interfaces (domain/*)
/// - Application Layer: Use cases and service orchestration (application/*)
/// - Infrastructure Layer: Technical implementations of repositories (infrastructure/*)
/// - Interface Layer: API endpoints and web controllers (interfaces/*)
///
/// Dependencies are managed through dependency inversion, with high-level modules
/// defining interfaces (ports) that low-level modules implement (adapters).
///
/// @author OxiCloud Development Team
/// Common utilities, configuration, and error handling
mod common;
@@ -580,6 +578,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}),
};
// Create the search service
let search_service: Option<Arc<dyn application::ports::inbound::SearchUseCase>> = {
// Create the search service with caching
let search_service = Arc::new(application::services::search_service::SearchService::new(
file_repository.clone(),
folder_repository.clone(),
300, // Cache TTL in seconds (5 minutes)
1000, // Maximum cache entries
));
tracing::info!("Search service initialized with caching (TTL: 300s, max entries: 1000)");
Some(search_service)
};
let application_services = common::di::ApplicationServices {
folder_service: folder_service.clone(),
file_service: file_service.clone(),
@@ -589,6 +601,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
file_use_case_factory: Arc::new(application::services::file_use_case_factory::AppFileUseCaseFactory::default_stub()),
i18n_service: i18n_service.clone(),
trash_service: trash_service.clone(),
search_service: search_service.clone(),
};
// Create the AppState without Arc first
@@ -613,7 +626,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app_state = Arc::new(app_state);
// Build application router
let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service);
let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service, search_service);
let web_routes = create_web_routes();
// Build the app router