2025-03-27 01:13:34 +01:00
|
|
|
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;
|
2026-02-08 13:40:23 +01:00
|
|
|
use crate::application::ports::outbound::FolderStoragePort;
|
|
|
|
|
use crate::application::ports::storage_ports::FileReadPort;
|
2025-03-27 01:13:34 +01:00
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Search service implementation for files and folders.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* This service implements the advanced search functionality that allows
|
|
|
|
|
* users to find files and folders based on various criteria
|
|
|
|
|
* such as name, type, date and size. It also includes a cache to improve
|
|
|
|
|
* the performance of repeated searches.
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
pub struct SearchService {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Repository for file operations
|
2026-02-08 13:40:23 +01:00
|
|
|
file_repository: Arc<dyn FileReadPort>,
|
2025-03-27 01:13:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Repository for folder operations
|
2025-03-27 01:13:34 +01:00
|
|
|
folder_repository: Arc<dyn FolderStoragePort>,
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Search results cache with expiration time
|
2025-03-27 01:13:34 +01:00
|
|
|
search_cache: Arc<Mutex<HashMap<SearchCacheKey, CachedSearchResult>>>,
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Cache validity duration in seconds
|
2025-03-27 01:13:34 +01:00
|
|
|
cache_ttl: u64,
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Maximum cache size (number of stored results)
|
2025-03-27 01:13:34 +01:00
|
|
|
max_cache_size: usize,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Key for the search cache
|
2025-03-27 01:13:34 +01:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
|
|
|
struct SearchCacheKey {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Serialized representation of the search criteria
|
2025-03-27 01:13:34 +01:00
|
|
|
criteria_hash: String,
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// User ID (to isolate searches between users)
|
2025-03-27 01:13:34 +01:00
|
|
|
user_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Cached search result with expiration time
|
2025-03-27 01:13:34 +01:00
|
|
|
struct CachedSearchResult {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Search results
|
2025-03-27 01:13:34 +01:00
|
|
|
results: SearchResultsDto,
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Time when the cache entry was created
|
2025-03-27 01:13:34 +01:00
|
|
|
timestamp: Instant,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SearchService {
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Creates a new instance of the search service.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param file_repository Repository for file operations
|
|
|
|
|
* @param folder_repository Repository for folder operations
|
|
|
|
|
* @param cache_ttl Cache time-to-live in seconds (0 to disable)
|
|
|
|
|
* @param max_cache_size Maximum cache size
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
pub fn new(
|
2026-02-08 13:40:23 +01:00
|
|
|
file_repository: Arc<dyn FileReadPort>,
|
2025-03-27 01:13:34 +01:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Start cache cleanup task if TTL > 0
|
2025-03-27 01:13:34 +01:00
|
|
|
if cache_ttl > 0 {
|
|
|
|
|
Self::start_cache_cleanup_task(search_service.search_cache.clone(), cache_ttl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
search_service
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Starts an asynchronous task to clean up expired cache entries.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param cache_ref Reference to the shared cache
|
|
|
|
|
* @param ttl_seconds TTL in seconds
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Acquire lock and clean up expired entries
|
2025-03-27 01:13:34 +01:00
|
|
|
if let Ok(mut cache) = cache_ref.lock() {
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Identify expired entries
|
2025-03-27 01:13:34 +01:00
|
|
|
let expired_keys: Vec<SearchCacheKey> = cache
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(_, result)| now.duration_since(result.timestamp) > ttl)
|
|
|
|
|
.map(|(key, _)| key.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Remove expired entries
|
2025-03-27 01:13:34 +01:00
|
|
|
for key in expired_keys {
|
|
|
|
|
cache.remove(&key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Creates a cache key from the search criteria.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param criteria Search criteria
|
|
|
|
|
* @param user_id User ID (to isolate cache between users)
|
|
|
|
|
* @return Cache key
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
fn create_cache_key(&self, criteria: &SearchCriteriaDto, user_id: &str) -> SearchCacheKey {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Serialize criteria to generate a hash
|
2025-03-27 01:13:34 +01:00
|
|
|
let criteria_str = serde_json::to_string(criteria).unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
SearchCacheKey {
|
|
|
|
|
criteria_hash: criteria_str,
|
|
|
|
|
user_id: user_id.to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Attempts to retrieve results from the cache.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param key Cache key
|
|
|
|
|
* @return Optionally, the results if they exist and have not expired
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
fn get_from_cache(&self, key: &SearchCacheKey) -> Option<SearchResultsDto> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// If TTL is 0, the cache is disabled
|
2025-03-27 01:13:34 +01:00
|
|
|
if self.cache_ttl == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Ok(cache) = self.search_cache.lock()
|
|
|
|
|
&& let Some(cached_result) = cache.get(key) {
|
2025-03-27 01:13:34 +01:00
|
|
|
let now = Instant::now();
|
|
|
|
|
let ttl = Duration::from_secs(self.cache_ttl);
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Check if the entry has expired
|
2025-03-27 01:13:34 +01:00
|
|
|
if now.duration_since(cached_result.timestamp) < ttl {
|
|
|
|
|
return Some(cached_result.results.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Stores results in the cache.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param key Cache key
|
|
|
|
|
* @param results Results to store
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
fn store_in_cache(&self, key: SearchCacheKey, results: SearchResultsDto) {
|
2026-02-12 09:41:25 +01:00
|
|
|
// If TTL is 0, the cache is disabled
|
2025-03-27 01:13:34 +01:00
|
|
|
if self.cache_ttl == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Ok(mut cache) = self.search_cache.lock() {
|
2026-02-12 09:41:25 +01:00
|
|
|
// If the cache is full, remove the oldest entry
|
2026-02-14 01:26:02 +01:00
|
|
|
if cache.len() >= self.max_cache_size
|
|
|
|
|
&& let Some((oldest_key, _)) = cache
|
2025-03-27 01:13:34 +01:00
|
|
|
.iter()
|
|
|
|
|
.min_by_key(|(_, result)| result.timestamp) {
|
|
|
|
|
let key_to_remove = oldest_key.clone();
|
|
|
|
|
cache.remove(&key_to_remove);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Store the new result
|
2025-03-27 01:13:34 +01:00
|
|
|
cache.insert(key, CachedSearchResult {
|
|
|
|
|
results,
|
|
|
|
|
timestamp: Instant::now(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Filters files according to the search criteria.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param files List of files to filter
|
|
|
|
|
* @param criteria Search criteria
|
|
|
|
|
* @return Files that match the criteria
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
fn filter_files(&self, files: Vec<FileDto>, criteria: &SearchCriteriaDto) -> Vec<FileDto> {
|
|
|
|
|
files.into_iter()
|
|
|
|
|
.filter(|file| {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by name
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(name_query) = &criteria.name_contains
|
|
|
|
|
&& !file.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by file type (extension)
|
2025-03-27 01:13:34 +01:00
|
|
|
if let Some(file_types) = &criteria.file_types {
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(extension) = file.name.split('.').next_back() {
|
2025-03-27 01:13:34 +01:00
|
|
|
if !file_types.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Has no extension
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by creation date
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(created_after) = criteria.created_after
|
|
|
|
|
&& file.created_at < created_after {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(created_before) = criteria.created_before
|
|
|
|
|
&& file.created_at > created_before {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by modification date
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(modified_after) = criteria.modified_after
|
|
|
|
|
&& file.modified_at < modified_after {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(modified_before) = criteria.modified_before
|
|
|
|
|
&& file.modified_at > modified_before {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by size
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(min_size) = criteria.min_size
|
|
|
|
|
&& file.size < min_size {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(max_size) = criteria.max_size
|
|
|
|
|
&& file.size > max_size {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Filters folders according to the search criteria.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param folders List of folders to filter
|
|
|
|
|
* @param criteria Search criteria
|
|
|
|
|
* @return Folders that match the criteria
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
fn filter_folders(&self, folders: Vec<FolderDto>, criteria: &SearchCriteriaDto) -> Vec<FolderDto> {
|
|
|
|
|
folders.into_iter()
|
|
|
|
|
.filter(|folder| {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by name
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(name_query) = &criteria.name_contains
|
|
|
|
|
&& !folder.name.to_lowercase().contains(&name_query.to_lowercase()) {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by creation date
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(created_after) = criteria.created_after
|
|
|
|
|
&& folder.created_at < created_after {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(created_before) = criteria.created_before
|
|
|
|
|
&& folder.created_at > created_before {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter by modification date
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(modified_after) = criteria.modified_after
|
|
|
|
|
&& folder.modified_at < modified_after {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:26:02 +01:00
|
|
|
if let Some(modified_before) = criteria.modified_before
|
|
|
|
|
&& folder.modified_at > modified_before {
|
2025-03-27 01:13:34 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Implementation of recursive search through folders.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param current_folder_id ID of the current folder
|
|
|
|
|
* @param criteria Search criteria
|
|
|
|
|
* @param found_files Files found so far
|
|
|
|
|
* @param found_folders Folders found so far
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
// List files in the current folder
|
2025-03-27 01:13:34 +01:00
|
|
|
let files = self.file_repository.list_files(current_folder_id).await?;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter files according to criteria and add them to the results
|
2025-03-27 01:13:34 +01:00
|
|
|
let filtered_files = self.filter_files(
|
|
|
|
|
files.into_iter().map(FileDto::from).collect(),
|
|
|
|
|
criteria
|
|
|
|
|
);
|
|
|
|
|
found_files.extend(filtered_files);
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// If the search is recursive, process subfolders
|
2025-03-27 01:13:34 +01:00
|
|
|
if criteria.recursive {
|
2026-02-12 09:41:25 +01:00
|
|
|
// List subfolders
|
2025-03-27 01:13:34 +01:00
|
|
|
let folders = self.folder_repository.list_folders(current_folder_id).await?;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Filter folders according to criteria and add them to the results
|
2025-03-27 01:13:34 +01:00
|
|
|
let filtered_folders: Vec<FolderDto> = self.filter_folders(
|
|
|
|
|
folders.into_iter().map(FolderDto::from).collect(),
|
|
|
|
|
criteria
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Add filtered folders to the results
|
2025-03-27 01:13:34 +01:00
|
|
|
found_folders.extend(filtered_folders.iter().cloned());
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Search recursively in each subfolder
|
2025-03-27 01:13:34 +01:00
|
|
|
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 {
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Performs a search based on the specified criteria.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @param criteria Search criteria
|
|
|
|
|
* @return Search results
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// TODO: Get user ID from the authentication context
|
2025-03-27 01:13:34 +01:00
|
|
|
let user_id = "default-user";
|
|
|
|
|
let cache_key = self.create_cache_key(&criteria, user_id);
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Try to get results from the cache
|
2025-03-27 01:13:34 +01:00
|
|
|
if let Some(cached_results) = self.get_from_cache(&cache_key) {
|
|
|
|
|
return Ok(cached_results);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Initialize collections for results
|
2025-03-27 01:13:34 +01:00
|
|
|
let mut found_files: Vec<FileDto> = Vec::new();
|
|
|
|
|
let mut found_folders: Vec<FolderDto> = Vec::new();
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Perform search in the specified folder or at the root
|
2025-03-27 01:13:34 +01:00
|
|
|
self.search_recursive(
|
|
|
|
|
criteria.folder_id.as_deref(),
|
|
|
|
|
&criteria,
|
|
|
|
|
&mut found_files,
|
|
|
|
|
&mut found_folders,
|
|
|
|
|
).await?;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Apply pagination
|
2025-03-27 01:13:34 +01:00
|
|
|
let total_count = found_files.len() + found_folders.len();
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Sort by relevance or date according to criteria
|
|
|
|
|
// By default, sort by modification date (most recent first)
|
2025-03-27 01:13:34 +01:00
|
|
|
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));
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Apply limit and offset for pagination
|
2025-03-27 01:13:34 +01:00
|
|
|
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() {
|
2026-02-12 09:41:25 +01:00
|
|
|
(true, i) // It's a folder
|
2025-03-27 01:13:34 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
(false, i - found_folders.len()) // It's a file
|
2025-03-27 01:13:34 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Extract paginated items
|
2025-03-27 01:13:34 +01:00
|
|
|
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());
|
|
|
|
|
}
|
2026-02-14 01:26:02 +01:00
|
|
|
} else if idx < found_files.len() {
|
|
|
|
|
paginated_files.push(found_files[idx].clone());
|
2025-03-27 01:13:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Create results object
|
2025-03-27 01:13:34 +01:00
|
|
|
let search_results = SearchResultsDto::new(
|
|
|
|
|
paginated_files,
|
|
|
|
|
paginated_folders,
|
|
|
|
|
criteria.limit,
|
|
|
|
|
criteria.offset,
|
|
|
|
|
Some(total_count),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Store in cache
|
2025-03-27 01:13:34 +01:00
|
|
|
self.store_in_cache(cache_key, search_results.clone());
|
|
|
|
|
|
|
|
|
|
Ok(search_results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Clears the search results cache.
|
2025-03-27 01:13:34 +01:00
|
|
|
*
|
2026-02-12 09:41:25 +01:00
|
|
|
* @return Result indicating success
|
2025-03-27 01:13:34 +01:00
|
|
|
*/
|
|
|
|
|
async fn clear_search_cache(&self) -> Result<()> {
|
|
|
|
|
if let Ok(mut cache) = self.search_cache.lock() {
|
|
|
|
|
cache.clear();
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Implement the test use case (stub)
|
2025-03-27 01:13:34 +01:00
|
|
|
impl SearchService {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Creates a stub version of the service for testing
|
2025-03-27 01:13:34 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|