adding recent feature + bug fixed

This commit is contained in:
DioCrafts
2025-04-02 05:08:30 +02:00
parent 21e2eb1ea9
commit a79c335b73
21 changed files with 1112 additions and 15 deletions
+1
View File
@@ -7,4 +7,5 @@ pub mod trash_dto;
pub mod search_dto;
pub mod share_dto;
pub mod favorites_dto;
pub mod recent_dto;
+21
View File
@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// DTO para elementos recientes
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentItemDto {
/// Identificador único para el elemento reciente
pub id: String,
/// ID del usuario propietario
pub user_id: String,
/// ID del elemento (archivo o carpeta)
pub item_id: String,
/// Tipo del elemento ('file' o 'folder')
pub item_type: String,
/// Cuándo se accedió al elemento
pub accessed_at: DateTime<Utc>,
}
+2 -1
View File
@@ -5,4 +5,5 @@ pub mod storage_ports;
pub mod auth_ports;
pub mod trash_ports;
pub mod share_ports;
pub mod favorites_ports;
pub mod favorites_ports;
pub mod recent_ports;
+19
View File
@@ -0,0 +1,19 @@
use async_trait::async_trait;
use crate::common::errors::Result;
use crate::application::dtos::recent_dto::RecentItemDto;
/// Define operaciones para gestionar elementos recientes del usuario
#[async_trait]
pub trait RecentItemsUseCase: Send + Sync {
/// Obtener todos los elementos recientes de un usuario
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>>;
/// Registrar acceso a un elemento
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
/// Eliminar un elemento de recientes
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
/// Limpiar toda la lista de elementos recientes
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
}
+1
View File
@@ -14,6 +14,7 @@ pub mod trash_service;
pub mod search_service;
pub mod share_service;
pub mod favorites_service;
pub mod recent_service;
#[cfg(test)]
mod trash_service_test;
+232
View File
@@ -0,0 +1,232 @@
use std::sync::Arc;
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use tracing::{info, error};
use uuid::Uuid;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::application::ports::recent_ports::RecentItemsUseCase;
use crate::application::dtos::recent_dto::RecentItemDto;
/// Implementación del caso de uso para gestionar elementos recientes
pub struct RecentService {
db_pool: Arc<PgPool>,
max_recent_items: i32, // Número máximo de elementos recientes a mantener por usuario
}
impl RecentService {
/// Crear un nuevo servicio de elementos recientes
pub fn new(db_pool: Arc<PgPool>, max_recent_items: i32) -> Self {
Self {
db_pool,
max_recent_items: max_recent_items.max(1).min(100), // Entre 1 y 100
}
}
}
#[async_trait]
impl RecentItemsUseCase for RecentService {
/// Obtener elementos recientes de un usuario
async fn get_recent_items(&self, user_id: &str, limit: Option<i32>) -> Result<Vec<RecentItemDto>> {
info!("Obteniendo elementos recientes para usuario: {}", user_id);
// Convertir user_id a UUID
let user_uuid = Uuid::parse_str(user_id)?;
// Determinar límite (usar el especificado o el máximo del servicio)
let limit_value = limit.unwrap_or(self.max_recent_items).min(self.max_recent_items);
// Ejecutar consulta SQL
let rows = sqlx::query(
r#"
SELECT
id::TEXT as "id",
user_id::TEXT as "user_id",
item_id as "item_id",
item_type as "item_type",
accessed_at as "accessed_at"
FROM auth.user_recent_files
WHERE user_id = $1
ORDER BY accessed_at DESC
LIMIT $2
"#
)
.bind(user_uuid)
.bind(limit_value)
.fetch_all(&*self.db_pool)
.await
.map_err(|e| {
error!("Error de base de datos al obtener elementos recientes: {}", e);
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Fallo al obtener elementos recientes: {}", e)
)
})?;
// Convertir filas a DTOs
let mut recent_items = Vec::with_capacity(rows.len());
for row in rows {
recent_items.push(RecentItemDto {
id: row.get("id"),
user_id: row.get("user_id"),
item_id: row.get("item_id"),
item_type: row.get("item_type"),
accessed_at: row.get("accessed_at"),
});
}
info!("Recuperados {} elementos recientes para usuario {}", recent_items.len(), user_id);
Ok(recent_items)
}
/// Registrar acceso a un elemento
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
info!("Registrando acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
// Validar tipo de elemento
if item_type != "file" && item_type != "folder" {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"RecentItems",
"El tipo de elemento debe ser 'file' o 'folder'"
));
}
// Convertir user_id a UUID
let user_uuid = Uuid::parse_str(user_id)?;
// Ejecutar consulta SQL con UPSERT para mantener un único registro por elemento
sqlx::query(
r#"
INSERT INTO auth.user_recent_files (user_id, item_id, item_type, accessed_at)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, item_id, item_type)
DO UPDATE SET accessed_at = CURRENT_TIMESTAMP
"#
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Error de base de datos al registrar acceso a elemento: {}", e);
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Fallo al registrar acceso a elemento: {}", e)
)
})?;
// Eliminar elementos antiguos que excedan el límite
self.prune_old_items(user_id).await?;
info!("Registrado correctamente acceso a {} '{}' para usuario {}", item_type, item_id, user_id);
Ok(())
}
/// Eliminar un elemento de recientes
async fn remove_from_recent(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
info!("Eliminando {} '{}' de recientes para usuario {}", item_type, item_id, user_id);
// Convertir user_id a UUID
let user_uuid = Uuid::parse_str(user_id)?;
// Ejecutar consulta SQL
let result = sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE user_id = $1 AND item_id = $2 AND item_type = $3
"#
)
.bind(user_uuid)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Error de base de datos al eliminar elemento de recientes: {}", e);
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Fallo al eliminar de recientes: {}", e)
)
})?;
let removed = result.rows_affected() > 0;
info!(
"{} {} '{}' de recientes para usuario {}",
if removed { "Eliminado correctamente" } else { "No se encontró" },
item_type,
item_id,
user_id
);
Ok(removed)
}
/// Limpiar todos los elementos recientes
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
info!("Limpiando todos los elementos recientes para usuario {}", user_id);
// Convertir user_id a UUID
let user_uuid = Uuid::parse_str(user_id)?;
// Ejecutar consulta SQL
sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE user_id = $1
"#
)
.bind(user_uuid)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Error de base de datos al limpiar elementos recientes: {}", e);
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Fallo al limpiar elementos recientes: {}", e)
)
})?;
info!("Limpiados todos los elementos recientes para usuario {}", user_id);
Ok(())
}
}
impl RecentService {
/// Método auxiliar para eliminar elementos antiguos que excedan el límite
async fn prune_old_items(&self, user_id: &str) -> Result<()> {
// Convertir user_id a UUID
let user_uuid = Uuid::parse_str(user_id)?;
// Eliminar elementos antiguos que excedan el límite
sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE id IN (
SELECT id FROM auth.user_recent_files
WHERE user_id = $1
ORDER BY accessed_at DESC
OFFSET $2
)
"#
)
.bind(user_uuid)
.bind(self.max_recent_items)
.execute(&*self.db_pool)
.await
.map_err(|e| {
error!("Error al podar elementos recientes antiguos: {}", e);
DomainError::new(
ErrorKind::InternalError,
"RecentItems",
format!("Fallo al limpiar elementos recientes antiguos: {}", e)
)
})?;
Ok(())
}
}
+13 -1
View File
@@ -22,6 +22,7 @@ use crate::application::services::storage_mediator::{StorageMediator, FileSystem
use crate::application::ports::inbound::{FileUseCase, FolderUseCase, SearchUseCase};
use crate::application::ports::outbound::{FileStoragePort, FolderStoragePort};
use crate::application::ports::favorites_ports::FavoritesUseCase;
use crate::application::ports::recent_ports::RecentItemsUseCase;
use crate::application::ports::file_ports::{FileUploadUseCase, FileRetrievalUseCase, FileManagementUseCase, FileUseCaseFactory};
use crate::application::ports::storage_ports::{FileReadPort, FileWritePort};
use crate::infrastructure::repositories::{FileMetadataManager, FilePathResolver, FileFsReadRepository, FileFsWriteRepository};
@@ -241,7 +242,8 @@ impl AppServiceFactory {
trash_service,
search_service,
share_service: None, // No share service by default
favorites_service: None // No favorites service by default
favorites_service: None, // No favorites service by default
recent_service: None // No recent service by default
}
}
}
@@ -286,6 +288,7 @@ pub struct ApplicationServices {
pub search_service: Option<Arc<dyn SearchUseCase>>,
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
pub favorites_service: Option<Arc<dyn FavoritesUseCase>>,
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
}
/// Contenedor para servicios de autenticación
@@ -307,6 +310,7 @@ pub struct AppState {
pub trash_service: Option<Arc<dyn TrashUseCase>>,
pub share_service: Option<Arc<dyn crate::application::ports::share_ports::ShareUseCase>>,
pub favorites_service: Option<Arc<dyn FavoritesUseCase>>,
pub recent_service: Option<Arc<dyn RecentItemsUseCase>>,
}
impl Default for AppState {
@@ -777,6 +781,7 @@ impl Default for AppState {
search_service: Some(Arc::new(DummySearchUseCase) as Arc<dyn crate::application::ports::inbound::SearchUseCase>),
share_service: None, // No share service in minimal mode
favorites_service: None, // No favorites service in minimal mode
recent_service: None, // No recent service in minimal mode
};
// Return a minimal app state
@@ -789,6 +794,7 @@ impl Default for AppState {
trash_service: None,
share_service: None,
favorites_service: None,
recent_service: None,
}
}
}
@@ -808,6 +814,7 @@ impl AppState {
trash_service: None,
share_service: None,
favorites_service: None,
recent_service: None,
}
}
@@ -835,4 +842,9 @@ impl AppState {
self.favorites_service = Some(favorites_service);
self
}
pub fn with_recent_service(mut self, recent_service: Arc<dyn RecentItemsUseCase>) -> Self {
self.recent_service = Some(recent_service);
self
}
}
+1
View File
@@ -7,6 +7,7 @@ pub mod trash_handler;
pub mod search_handler;
pub mod share_handler;
pub mod favorites_handler;
pub mod recent_handler;
/// Tipo de resultado para controladores de API
pub type ApiResult<T> = Result<T, (axum::http::StatusCode, String)>;
@@ -0,0 +1,152 @@
use std::sync::Arc;
use axum::{
extract::{Path, State, Query},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use crate::application::ports::recent_ports::RecentItemsUseCase;
/// Parámetros de consulta para obtener elementos recientes
#[derive(Deserialize)]
pub struct GetRecentParams {
#[serde(default)]
limit: Option<i32>,
}
/// Obtener elementos recientes del usuario
pub async fn get_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
Query(params): Query<GetRecentParams>,
) -> impl IntoResponse {
// Para pruebas, usando ID de usuario fijo
let user_id = "00000000-0000-0000-0000-000000000000";
match recent_service.get_recent_items(user_id, params.limit).await {
Ok(items) => {
info!("Recuperados {} elementos recientes para usuario", items.len());
(StatusCode::OK, Json(items)).into_response()
},
Err(err) => {
error!("Error al recuperar elementos recientes: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al recuperar elementos recientes: {}", err)
}))
).into_response()
}
}
}
/// Registrar acceso a un elemento
pub async fn record_item_access(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
Path((item_type, item_id)): Path<(String, String)>,
) -> impl IntoResponse {
// Para pruebas, usando ID de usuario fijo
let user_id = "00000000-0000-0000-0000-000000000000";
// Validar tipo de elemento
if item_type != "file" && item_type != "folder" {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "El tipo de elemento debe ser 'file' o 'folder'"
}))
).into_response();
}
match recent_service.record_item_access(user_id, &item_id, &item_type).await {
Ok(_) => {
info!("Registrado acceso a {} '{}' en recientes", item_type, item_id);
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Acceso registrado correctamente"
}))
).into_response()
},
Err(err) => {
error!("Error al registrar acceso en recientes: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al registrar acceso: {}", err)
}))
).into_response()
}
}
}
/// Eliminar un elemento de recientes
pub async fn remove_from_recent(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
Path((item_type, item_id)): Path<(String, String)>,
) -> impl IntoResponse {
// Para pruebas, usando ID de usuario fijo
let user_id = "00000000-0000-0000-0000-000000000000";
match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
Ok(removed) => {
if removed {
info!("Eliminado {} '{}' de recientes", item_type, item_id);
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Elemento eliminado de recientes"
}))
).into_response()
} else {
info!("Elemento {} '{}' no estaba en recientes", item_type, item_id);
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"message": "Elemento no estaba en recientes"
}))
).into_response()
}
},
Err(err) => {
error!("Error al eliminar de recientes: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al eliminar de recientes: {}", err)
}))
).into_response()
}
}
}
/// Limpiar todos los elementos recientes
pub async fn clear_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
) -> impl IntoResponse {
// Para pruebas, usando ID de usuario fijo
let user_id = "00000000-0000-0000-0000-000000000000";
match recent_service.clear_recent_items(user_id).await {
Ok(_) => {
info!("Limpiados todos los elementos recientes para usuario");
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Elementos recientes limpiados correctamente"
}))
).into_response()
},
Err(err) => {
error!("Error al limpiar elementos recientes: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al limpiar elementos recientes: {}", err)
}))
).into_response()
}
}
}
+20 -1
View File
@@ -26,6 +26,7 @@ use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::ports::inbound::SearchUseCase;
use crate::application::ports::share_ports::ShareUseCase;
use crate::application::ports::favorites_ports::FavoritesUseCase;
use crate::application::ports::recent_ports::RecentItemsUseCase;
use crate::interfaces::api::handlers::folder_handler::FolderHandler;
use crate::interfaces::api::handlers::file_handler::FileHandler;
@@ -45,6 +46,7 @@ pub fn create_api_routes(
search_service: Option<Arc<dyn SearchUseCase>>,
share_service: Option<Arc<dyn ShareUseCase>>,
favorites_service: Option<Arc<dyn FavoritesUseCase>>,
recent_service: Option<Arc<dyn RecentItemsUseCase>>,
) -> Router<crate::common::di::AppState> {
// Create a simplified AppState for the trash view
// Setup required components for repository construction
@@ -109,12 +111,14 @@ pub fn create_api_routes(
search_service: search_service.clone(), // Include the search service
share_service: share_service.clone(), // Include the share service
favorites_service: favorites_service.clone(), // Include the favorites service
recent_service: recent_service.clone(), // Include the recent service
},
db_pool: None,
auth_service: None,
trash_service: trash_service.clone(), // This is the important part - include the trash service
share_service: share_service.clone(), // Include the share service for routes
favorites_service: favorites_service.clone() // Include the favorites service for routes
favorites_service: favorites_service.clone(), // Include the favorites service for routes
recent_service: recent_service.clone() // Include the recent service for routes
};
// Inicializar el servicio de operaciones por lotes
let batch_service = Arc::new(BatchOperationService::default(
@@ -340,6 +344,20 @@ pub fn create_api_routes(
} else {
Router::new()
};
// Create routes for recent items if the service is available
let recent_router = if let Some(recent_service) = recent_service.clone() {
use crate::interfaces::api::handlers::recent_handler;
Router::new()
.route("/", get(recent_handler::get_recent_items))
.route("/{item_type}/{item_id}", post(recent_handler::record_item_access))
.route("/{item_type}/{item_id}", delete(recent_handler::remove_from_recent))
.route("/clear", delete(recent_handler::clear_recent_items))
.with_state(recent_service.clone())
} else {
Router::new()
};
let mut router = Router::new()
.nest("/folders", folders_router)
@@ -349,6 +367,7 @@ pub fn create_api_routes(
.nest("/shares", share_router)
.nest("/s", public_share_router)
.nest("/favorites", favorites_router)
.nest("/recent", recent_router)
;
// Store the share service in app_state for future use
+23 -1
View File
@@ -629,6 +629,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Favorites service is disabled (requires database connection)");
None
};
// Initialize recent items service if database is available
let recent_service: Option<Arc<dyn application::ports::recent_ports::RecentItemsUseCase>> =
if let Some(ref pool) = db_pool {
// Create a new service with the database pool
let service = Arc::new(application::services::recent_service::RecentService::new(
pool.clone(),
50 // Maximum recent items per user
));
tracing::info!("Recent items service initialized successfully");
Some(service)
} else {
tracing::info!("Recent items service is disabled (requires database connection)");
None
};
let application_services = common::di::ApplicationServices {
folder_service: folder_service.clone(),
@@ -642,6 +658,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
search_service: search_service.clone(),
share_service: share_service.clone(),
favorites_service: favorites_service.clone(),
recent_service: recent_service.clone()
};
// Create the AppState without Arc first
@@ -667,11 +684,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
app_state = app_state.with_favorites_service(service);
}
// Add recent service if available
if let Some(service) = recent_service.clone() {
app_state = app_state.with_recent_service(service);
}
// Wrap in Arc after all modifications
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, search_service, share_service, favorites_service);
let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service, search_service, share_service, favorites_service, recent_service);
let web_routes = create_web_routes();
// Build the app router