adding search engine
This commit is contained in:
+6
-6
@@ -20,12 +20,12 @@ This document contains the task list for the development of OxiCloud, a minimali
|
||||
- [ ] Add text/code preview
|
||||
|
||||
### Enhanced Search
|
||||
- [ ] Implement search by name
|
||||
- [ ] Add filters by file type
|
||||
- [ ] Implement search by date range
|
||||
- [ ] Add filter by file size
|
||||
- [ ] Add search within specific folders
|
||||
- [ ] Implement cache for search results
|
||||
- [x] Implement search by name
|
||||
- [x] Add filters by file type
|
||||
- [x] Implement search by date range
|
||||
- [x] Add filter by file size
|
||||
- [x] Add search within specific folders
|
||||
- [x] Implement cache for search results
|
||||
|
||||
### UI/UX Optimizations
|
||||
- [ ] Improve responsive design for mobile devices
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod i18n_dto;
|
||||
pub mod pagination;
|
||||
pub mod user_dto;
|
||||
pub mod trash_dto;
|
||||
pub mod search_dto;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -144,6 +144,8 @@ body {
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
@@ -165,6 +167,53 @@ body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background-color: #ff5e3a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background-color: #e64a29;
|
||||
}
|
||||
|
||||
/* Estilos para resultados de búsqueda */
|
||||
.search-results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-results-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<script src="/js/fileRenderer.js"></script>
|
||||
<script src="/js/contextMenus.js"></script>
|
||||
<script src="/js/fileOperations.js"></script>
|
||||
<script src="/js/search.js"></script>
|
||||
<script src="/js/ui.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
||||
@@ -82,6 +83,7 @@
|
||||
<div class="search-container">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
|
||||
<button id="search-button" class="search-button"><i class="fas fa-search"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="user-controls">
|
||||
|
||||
+199
-14
@@ -14,6 +14,7 @@ const app = {
|
||||
moveDialogMode: 'file', // Move dialog mode: 'file' or 'folder'
|
||||
isTrashView: false, // Whether we're in trash view
|
||||
currentSection: 'files', // Current section: 'files' or 'trash'
|
||||
isSearchMode: false, // Whether we're in search mode
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
@@ -34,11 +35,6 @@ function initApp() {
|
||||
// Setup event listeners
|
||||
setupEventListeners();
|
||||
|
||||
// Load initial view
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
|
||||
// Initialize file renderer if available
|
||||
if (window.fileRenderer) {
|
||||
console.log('Using optimized file renderer');
|
||||
@@ -46,8 +42,26 @@ function initApp() {
|
||||
console.log('Using standard file rendering');
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
// Wait for translations to load before checking authentication
|
||||
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
|
||||
// Translations already loaded, proceed with authentication
|
||||
checkAuthentication();
|
||||
} else {
|
||||
// Wait for translations to be loaded before proceeding
|
||||
console.log('Waiting for translations to load...');
|
||||
window.addEventListener('translationsLoaded', () => {
|
||||
console.log('Translations loaded, proceeding with authentication');
|
||||
checkAuthentication();
|
||||
});
|
||||
|
||||
// Set a timeout as a fallback in case translations take too long
|
||||
setTimeout(() => {
|
||||
if (!window.i18n || !window.i18n.isLoaded || !window.i18n.isLoaded()) {
|
||||
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
||||
checkAuthentication();
|
||||
}
|
||||
}, 3000); // 3 second timeout
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +82,7 @@ function cacheElements() {
|
||||
elements.actionsBar = document.querySelector('.actions-bar');
|
||||
elements.navItems = document.querySelectorAll('.nav-item');
|
||||
elements.trashBtn = document.querySelector('.nav-item:nth-child(5)'); // The trash nav item
|
||||
elements.searchInput = document.querySelector('.search-container input');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +92,30 @@ function setupEventListeners() {
|
||||
// Set up drag and drop
|
||||
ui.setupDragAndDrop();
|
||||
|
||||
// Search input
|
||||
elements.searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const query = elements.searchInput.value.trim();
|
||||
if (query) {
|
||||
performSearch(query);
|
||||
} else if (app.isSearchMode) {
|
||||
// If search is empty and we're in search mode, return to normal view
|
||||
app.isSearchMode = false;
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Search button
|
||||
document.getElementById('search-button').addEventListener('click', () => {
|
||||
const query = elements.searchInput.value.trim();
|
||||
if (query) {
|
||||
performSearch(query);
|
||||
}
|
||||
});
|
||||
|
||||
// Upload button
|
||||
elements.uploadBtn.addEventListener('click', () => {
|
||||
elements.dropzone.style.display = elements.dropzone.style.display === 'none' ? 'block' : 'none';
|
||||
@@ -328,12 +367,17 @@ async function loadTrashItems() {
|
||||
<div class="list-header">
|
||||
<div data-i18n="files.name">Nombre</div>
|
||||
<div data-i18n="files.type">Tipo</div>
|
||||
<div data-i18n="files.original_location">Ubicación original</div>
|
||||
<div data-i18n="files.deleted_date">Fecha eliminación</div>
|
||||
<div data-i18n="files.actions">Acciones</div>
|
||||
<div data-i18n="trash.original_location">Ubicación original</div>
|
||||
<div data-i18n="trash.deleted_date">Fecha eliminación</div>
|
||||
<div data-i18n="trash.actions">Acciones</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Translate the header if i18n is available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
|
||||
// Update breadcrumb for trash
|
||||
ui.updateBreadcrumb(window.i18n ? window.i18n.t('nav.trash') : 'Papelera');
|
||||
|
||||
@@ -394,10 +438,10 @@ function addTrashItemToView(item) {
|
||||
<div class="file-name">${item.name}</div>
|
||||
<div class="file-info">${typeLabel} - ${formattedDate}</div>
|
||||
<div class="trash-actions">
|
||||
<button class="btn-restore" title="Restaurar">
|
||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
<button class="btn-delete" title="Eliminar permanentemente">
|
||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -438,10 +482,10 @@ function addTrashItemToView(item) {
|
||||
<div class="path-cell">${item.original_path || '--'}</div>
|
||||
<div class="date-cell">${formattedDate}</div>
|
||||
<div class="actions-cell">
|
||||
<button class="btn-restore" title="Restaurar">
|
||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restaurar'}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
<button class="btn-delete" title="Eliminar permanentemente">
|
||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Eliminar permanentemente'}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -465,11 +509,70 @@ function addTrashItemToView(item) {
|
||||
elements.filesListView.appendChild(listElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform search with the given query
|
||||
* @param {string} query - Search query
|
||||
*/
|
||||
async function performSearch(query) {
|
||||
console.log(`Performing search for: "${query}"`);
|
||||
|
||||
try {
|
||||
// Update UI to indicate search mode
|
||||
app.isSearchMode = true;
|
||||
|
||||
// Set breadcrumb for search
|
||||
ui.updateBreadcrumb(`Búsqueda: "${query}"`);
|
||||
|
||||
// Prepare search options
|
||||
const options = {
|
||||
recursive: true, // Search in all subfolders
|
||||
limit: 100 // Limit results for performance
|
||||
};
|
||||
|
||||
// Always restrict search to the user's current folder context
|
||||
// This ensures users can't search outside their personal folder
|
||||
if (!app.isTrashView) {
|
||||
// If we're in a subfolder, search from there, otherwise use the user's home folder
|
||||
options.folder_id = app.currentPath;
|
||||
|
||||
// Always include folder_id even if it's the root of user's home folder
|
||||
// so user cannot search outside their allowed scope
|
||||
if (!options.folder_id || options.folder_id === '') {
|
||||
// Fall back to user's home folder - we should never be here
|
||||
// because findUserHomeFolder should have set app.currentPath
|
||||
console.warn("Search without folder_id - this shouldn't happen with proper user context");
|
||||
|
||||
// Try to get folder from localStorage if available
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||
if (userData.username) {
|
||||
console.log("Retrieving home folder for user before search");
|
||||
await findUserHomeFolder(userData.username);
|
||||
options.folder_id = app.currentPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Searching with options:`, options);
|
||||
|
||||
// Perform the search
|
||||
const searchResults = await window.search.searchFiles(query, options);
|
||||
|
||||
// Display search results
|
||||
window.search.displaySearchResults(searchResults);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
|
||||
}
|
||||
}
|
||||
|
||||
// Expose needed functions to global scope
|
||||
window.app = app;
|
||||
window.loadFiles = loadFiles;
|
||||
window.loadTrashItems = loadTrashItems;
|
||||
window.formatFileSize = formatFileSize;
|
||||
window.performSearch = performSearch;
|
||||
|
||||
// Set up global selectFolder function for navigation
|
||||
window.selectFolder = (id, name) => {
|
||||
@@ -479,7 +582,7 @@ window.selectFolder = (id, name) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
* Check if user is authenticated and load user's home folder
|
||||
*/
|
||||
function checkAuthentication() {
|
||||
// Nombres de variables según auth.js
|
||||
@@ -505,6 +608,88 @@ function checkAuthentication() {
|
||||
if (userAvatar) {
|
||||
userAvatar.textContent = userInitials;
|
||||
}
|
||||
|
||||
// Find and load the user's home folder
|
||||
findUserHomeFolder(userData.username);
|
||||
} else {
|
||||
// If no user data, fallback to standard load
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the user's home folder and load it
|
||||
* @param {string} username - The current user's username
|
||||
*/
|
||||
async function findUserHomeFolder(username) {
|
||||
try {
|
||||
console.log("Finding home folder for user:", username);
|
||||
|
||||
// First, load all folders at the root
|
||||
const response = await fetch('/api/folders');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error loading folders: ${response.status}`);
|
||||
}
|
||||
|
||||
const folders = await response.json();
|
||||
const folderList = Array.isArray(folders) ? folders : [];
|
||||
|
||||
// Look for a folder with a name pattern that matches the user's home folder
|
||||
// Typically named "Mi Carpeta - username"
|
||||
const homeFolderPattern = `Mi Carpeta - ${username}`;
|
||||
let homeFolder = folderList.find(folder => folder.name === homeFolderPattern);
|
||||
|
||||
// If exact match not found, try a more flexible match
|
||||
if (!homeFolder) {
|
||||
homeFolder = folderList.find(folder =>
|
||||
folder.name.toLowerCase().includes(username.toLowerCase()) ||
|
||||
folder.name.startsWith('Mi Carpeta -')
|
||||
);
|
||||
}
|
||||
|
||||
if (homeFolder) {
|
||||
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
|
||||
|
||||
// Store the home folder ID and name in the app state
|
||||
// This is used for breadcrumb navigation and restricting user access
|
||||
app.userHomeFolderId = homeFolder.id;
|
||||
app.userHomeFolderName = homeFolder.name;
|
||||
|
||||
// Set this as the current path and load its contents
|
||||
app.currentPath = homeFolder.id;
|
||||
ui.updateBreadcrumb(homeFolder.name);
|
||||
loadFiles();
|
||||
} else {
|
||||
console.warn("Could not find user's home folder, fallback to first folder or root");
|
||||
|
||||
// If we can't find a specific home folder but there are folders,
|
||||
// use the first folder as the user's home
|
||||
if (folderList.length > 0) {
|
||||
const fallbackFolder = folderList[0];
|
||||
console.log(`Using first folder as fallback: ${fallbackFolder.name} (${fallbackFolder.id})`);
|
||||
|
||||
app.userHomeFolderId = fallbackFolder.id;
|
||||
app.userHomeFolderName = fallbackFolder.name;
|
||||
app.currentPath = fallbackFolder.id;
|
||||
ui.updateBreadcrumb(fallbackFolder.name);
|
||||
loadFiles();
|
||||
} else {
|
||||
// No folders at all - this is an edge case
|
||||
console.warn("No folders found, using root");
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error finding user home folder:', error);
|
||||
|
||||
// Fall back to loading root in case of error
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -419,7 +419,8 @@ const fileOps = {
|
||||
* @returns {Promise<boolean>} - Éxito de la operación
|
||||
*/
|
||||
async emptyTrash() {
|
||||
if (!confirm('¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos y no se puede deshacer.')) {
|
||||
const confirmMsg = window.i18n ? window.i18n.t('trash.empty_confirm') : '¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos.';
|
||||
if (!confirm(confirmMsg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+22
-3
@@ -227,14 +227,33 @@ function getSupportedLocales() {
|
||||
return [...supportedLocales];
|
||||
}
|
||||
|
||||
// Flag to track if translations are loaded
|
||||
let translationsLoaded = false;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initI18n);
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await initI18n();
|
||||
translationsLoaded = true;
|
||||
// Dispatch an event when translations are fully loaded
|
||||
window.dispatchEvent(new Event('translationsLoaded'));
|
||||
});
|
||||
|
||||
// Improved t function with fallback for early calls
|
||||
function safeT(key, params = {}) {
|
||||
if (!translationsLoaded) {
|
||||
console.warn(`Translations for ${currentLocale} not loaded yet`);
|
||||
// Return a default value or the key depending on context
|
||||
return key.split('.').pop() || key;
|
||||
}
|
||||
return t(key, params);
|
||||
}
|
||||
|
||||
// Export functions for use in other modules
|
||||
window.i18n = {
|
||||
t,
|
||||
t: safeT,
|
||||
setLocale,
|
||||
getCurrentLocale,
|
||||
getSupportedLocales,
|
||||
translatePage
|
||||
translatePage,
|
||||
isLoaded: () => translationsLoaded
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* OxiCloud - Search Module
|
||||
* This file handles search functionality for files and folders
|
||||
*/
|
||||
|
||||
const search = {
|
||||
/**
|
||||
* Perform a basic search using query string
|
||||
* @param {string} query - Search query
|
||||
* @param {Object} options - Additional search options
|
||||
* @returns {Promise<Object>} - Search results
|
||||
*/
|
||||
async searchFiles(query, options = {}) {
|
||||
try {
|
||||
// Prepare search parameters
|
||||
const params = new URLSearchParams();
|
||||
params.append('query', query);
|
||||
|
||||
// Add optional parameters
|
||||
if (options.folder_id) params.append('folder_id', options.folder_id);
|
||||
if (options.recursive !== undefined) params.append('recursive', options.recursive);
|
||||
if (options.file_types) params.append('type', options.file_types);
|
||||
if (options.min_size) params.append('min_size', options.min_size);
|
||||
if (options.max_size) params.append('max_size', options.max_size);
|
||||
if (options.created_after) params.append('created_after', options.created_after);
|
||||
if (options.created_before) params.append('created_before', options.created_before);
|
||||
if (options.modified_after) params.append('modified_after', options.modified_after);
|
||||
if (options.modified_before) params.append('modified_before', options.modified_before);
|
||||
if (options.limit) params.append('limit', options.limit);
|
||||
if (options.offset) params.append('offset', options.offset);
|
||||
|
||||
// Create search URL
|
||||
const url = `/api/search?${params.toString()}`;
|
||||
console.log(`Performing search with URL: ${url}`);
|
||||
|
||||
// Perform the search request
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
let errorText = '';
|
||||
try {
|
||||
const errorJson = await response.json();
|
||||
errorText = errorJson.error || response.statusText;
|
||||
} catch (e) {
|
||||
errorText = response.statusText;
|
||||
}
|
||||
|
||||
console.error(`Search error: ${errorText}`);
|
||||
throw new Error(`Search failed: ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error performing search:', error);
|
||||
window.ui.showNotification('Error', 'Error al realizar la búsqueda');
|
||||
return { files: [], folders: [], total_count: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform advanced search with multiple criteria
|
||||
* @param {Object} criteria - Search criteria
|
||||
* @returns {Promise<Object>} - Search results
|
||||
*/
|
||||
async advancedSearch(criteria) {
|
||||
try {
|
||||
console.log('Performing advanced search with criteria:', criteria);
|
||||
|
||||
// Use POST endpoint for advanced search
|
||||
const response = await fetch('/api/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(criteria)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
let errorText = '';
|
||||
try {
|
||||
const errorJson = await response.json();
|
||||
errorText = errorJson.error || response.statusText;
|
||||
} catch (e) {
|
||||
errorText = response.statusText;
|
||||
}
|
||||
|
||||
console.error(`Advanced search error: ${errorText}`);
|
||||
throw new Error(`Advanced search failed: ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error performing advanced search:', error);
|
||||
window.ui.showNotification('Error', 'Error al realizar la búsqueda avanzada');
|
||||
return { files: [], folders: [], total_count: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display search results in the UI
|
||||
* @param {Object} results - Search results object with files and folders arrays
|
||||
*/
|
||||
displaySearchResults(results) {
|
||||
// Get the files grid and list view elements
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
|
||||
// Clear existing content
|
||||
filesGrid.innerHTML = '';
|
||||
filesListView.innerHTML = `
|
||||
<div class="list-header">
|
||||
<div data-i18n="files.name">Nombre</div>
|
||||
<div data-i18n="files.type">Tipo</div>
|
||||
<div data-i18n="files.size">Tamaño</div>
|
||||
<div data-i18n="files.modified">Modificado</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add search results header
|
||||
const searchHeader = document.createElement('div');
|
||||
searchHeader.className = 'search-results-header';
|
||||
searchHeader.innerHTML = `
|
||||
<h3>Resultados de búsqueda (${results.total_count || (results.files.length + results.folders.length)})</h3>
|
||||
<button class="btn btn-secondary" id="clear-search-btn">
|
||||
<i class="fas fa-times"></i> Limpiar búsqueda
|
||||
</button>
|
||||
`;
|
||||
filesGrid.appendChild(searchHeader);
|
||||
|
||||
// Add event listener to clear search button
|
||||
const clearSearchBtn = document.getElementById('clear-search-btn');
|
||||
if (clearSearchBtn) {
|
||||
clearSearchBtn.addEventListener('click', () => {
|
||||
// Clear search input
|
||||
document.querySelector('.search-container input').value = '';
|
||||
|
||||
// Load regular files view
|
||||
window.app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
window.loadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
// If no results, show empty state
|
||||
if (results.files.length === 0 && results.folders.length === 0) {
|
||||
const emptyState = document.createElement('div');
|
||||
emptyState.className = 'empty-state';
|
||||
emptyState.innerHTML = `
|
||||
<i class="fas fa-search" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
||||
<p>No se encontraron resultados para esta búsqueda</p>
|
||||
`;
|
||||
filesGrid.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process folders
|
||||
results.folders.forEach(folder => {
|
||||
window.ui.addFolderToView(folder);
|
||||
});
|
||||
|
||||
// Process files
|
||||
results.files.forEach(file => {
|
||||
window.ui.addFileToView(file);
|
||||
});
|
||||
|
||||
// Update file icons
|
||||
window.ui.updateFileIcons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the search cache on the server
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
async clearSearchCache() {
|
||||
try {
|
||||
const response = await fetch('/api/search/cache', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Caché limpiada', 'Caché de búsqueda limpiada correctamente');
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing search cache:', error);
|
||||
window.ui.showNotification('Error', 'Error al limpiar la caché de búsqueda');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose the search module globally
|
||||
window.search = search;
|
||||
+39
-4
@@ -191,18 +191,53 @@ const ui = {
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb.innerHTML = '';
|
||||
|
||||
// Get user info to help determine home folder
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||
const username = userData.username || '';
|
||||
|
||||
// Create the home item - for users, this is their personal folder
|
||||
const homeItem = document.createElement('span');
|
||||
homeItem.className = 'breadcrumb-item';
|
||||
homeItem.textContent = window.i18n ? window.i18n.t('breadcrumb.home') : 'Home';
|
||||
|
||||
// Helper function to safely get translation text
|
||||
const getTranslatedText = (key, defaultValue) => {
|
||||
if (!window.i18n || !window.i18n.t) return defaultValue;
|
||||
return window.i18n.t(key);
|
||||
};
|
||||
|
||||
// Set appropriate text for home item
|
||||
if (username && folderName && folderName.includes(username)) {
|
||||
// If the current folder is the user's home folder, label it as "Home"
|
||||
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
||||
} else if (folderName && folderName.startsWith('Mi Carpeta')) {
|
||||
// If the current folder is another user's home folder or a special folder, use its name
|
||||
homeItem.textContent = folderName;
|
||||
} else {
|
||||
// Default - use "Home" label
|
||||
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
||||
|
||||
// For searching, we might have a custom breadcrumb text
|
||||
if (folderName && folderName.startsWith('Búsqueda:')) {
|
||||
// We're in search mode - don't add click handler
|
||||
breadcrumb.appendChild(homeItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add click handler - but only if we have a user home folder to return to
|
||||
if (window.app.userHomeFolderId) {
|
||||
homeItem.addEventListener('click', () => {
|
||||
window.app.currentPath = '';
|
||||
this.updateBreadcrumb('');
|
||||
window.app.currentPath = window.app.userHomeFolderId;
|
||||
this.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
|
||||
window.loadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
breadcrumb.appendChild(homeItem);
|
||||
|
||||
if (folderName) {
|
||||
// If we have a subfolder, add it to the breadcrumb
|
||||
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Búsqueda:')) {
|
||||
const separator = document.createElement('span');
|
||||
separator.className = 'breadcrumb-separator';
|
||||
separator.textContent = '>';
|
||||
|
||||
@@ -67,6 +67,16 @@
|
||||
"breadcrumb": {
|
||||
"home": "Home"
|
||||
},
|
||||
"trash": {
|
||||
"empty_trash": "Empty Trash",
|
||||
"empty_state": "Trash is empty",
|
||||
"original_location": "Original location",
|
||||
"deleted_date": "Deletion date",
|
||||
"actions": "Actions",
|
||||
"restore": "Restore",
|
||||
"delete_permanently": "Delete permanently",
|
||||
"empty_confirm": "Are you sure you want to empty the trash? This will permanently delete all items."
|
||||
},
|
||||
"auth": {
|
||||
"login_title": "Sign in",
|
||||
"username": "Username",
|
||||
|
||||
@@ -67,6 +67,16 @@
|
||||
"breadcrumb": {
|
||||
"home": "Inicio"
|
||||
},
|
||||
"trash": {
|
||||
"empty_trash": "Vaciar papelera",
|
||||
"empty_state": "La papelera está vacía",
|
||||
"original_location": "Ubicación original",
|
||||
"deleted_date": "Fecha de eliminación",
|
||||
"actions": "Acciones",
|
||||
"restore": "Restaurar",
|
||||
"delete_permanently": "Eliminar permanentemente",
|
||||
"empty_confirm": "¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos."
|
||||
},
|
||||
"auth": {
|
||||
"login_title": "Iniciar sesión",
|
||||
"username": "Usuario",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user